History

Fix merge conflicts

Jasper van Merle committed 7 years ago

Commit 1c007d82 parents c6f0f16 e684e8c141 files · +1367 −497Browse files

Unified · Split

141 files
  1. .editorconfig +3 −0
  2. .github/CONTRIBUTING.md +5 −14
  3. .github/ISSUE_TEMPLATE.md +1 −1
  4. .gitignore +3 −4
  5. .ruby-version +1 −1
  6. .travis.yml +8 −0
  7. CODE_OF_CONDUCT.md +2 −0
  8. COPYRIGHT +4 −4
  9. Dockerfile +2 −1
  10. Dockerfile-alpine +2 −1
  11. Gemfile +3 −2
  12. Gemfile.lock +47 −44
  13. PULL_REQUEST_TEMPLATE.md +11 −0
  14. README.md +45 −20
  15. assets/javascripts/app/app.coffee +10 −8
  16. assets/javascripts/app/appcache.coffee +0 −46
  17. assets/javascripts/app/config.coffee.erb +2 −0
  18. assets/javascripts/app/serviceworker.coffee +49 −0
  19. assets/javascripts/app/settings.coffee +30 −0
  20. assets/javascripts/app/update_checker.coffee +3 −3
  21. assets/javascripts/lib/license.coffee +1 −1
  22. assets/javascripts/news.json +4 −0
  23. assets/javascripts/templates/error_tmpl.coffee +8 −8
  24. assets/javascripts/templates/pages/about_tmpl.coffee +9 −14
  25. assets/javascripts/templates/pages/offline_tmpl.coffee +9 −4
  26. assets/javascripts/templates/pages/root_tmpl.coffee.erb +2 −2
  27. assets/javascripts/views/content/entry_page.coffee +1 −1
  28. assets/javascripts/views/content/settings_page.coffee +1 −11
  29. assets/javascripts/views/layout/resizer.coffee +2 −9
  30. assets/javascripts/views/layout/settings.coffee +1 −9
  31. assets/javascripts/views/search/search.coffee +8 −0
  32. assets/javascripts/views/search/search_scope.coffee +9 −2
  33. assets/javascripts/views/sidebar/sidebar.coffee +1 −1
  34. assets/stylesheets/application.css.scss +1 −1
  35. assets/stylesheets/components/_fail.scss +0 −2
  36. assets/stylesheets/components/_header.scss +1 −0
  37. assets/stylesheets/pages/_mdn.scss +25 −0
  38. assets/stylesheets/pages/_rdoc.scss +0 −11
  39. docs/adding-docs.md +23 −0
  40. docs/filter-reference.md +224 −0
  41. docs/maintainers.md +102 −0
  42. docs/scraper-reference.md +186 −0
  43. lib/app.rb +36 −21
  44. lib/docs.rb +17 −0
  45. lib/docs/core/doc.rb +6 −0
  46. lib/docs/core/requester.rb +8 −4
  47. lib/docs/core/scrapers/file_scraper.rb +18 −7
  48. lib/docs/core/url.rb +1 −1
  49. lib/docs/filters/cpp/entries.rb +2 −0
  50. lib/docs/filters/css/entries.rb +17 −10
  51. lib/docs/filters/dom/clean_html.rb +1 −1
  52. lib/docs/filters/dom/entries.rb +12 −2
  53. lib/docs/filters/html/entries.rb +1 −1
  54. lib/docs/filters/javascript/clean_html.rb +2 −2
  55. lib/docs/filters/javascript/entries.rb +1 −1
  56. lib/docs/filters/mdn/clean_html.rb +90 −0
  57. lib/docs/filters/rdoc/clean_html.rb +4 −2
  58. lib/docs/filters/rust/clean_html.rb +4 −2
  59. lib/docs/filters/rust/entries.rb +5 −6
  60. lib/docs/scrapers/c.rb +0 −1
  61. lib/docs/scrapers/cpp.rb +0 −1
  62. lib/docs/scrapers/dart.rb +0 −2
  63. lib/docs/scrapers/django.rb +0 −6
  64. lib/docs/scrapers/erlang.rb +0 −4
  65. lib/docs/scrapers/gnu/gcc.rb +0 −8
  66. lib/docs/scrapers/gnu/gnu_fortran.rb +0 −4
  67. lib/docs/scrapers/nokogiri2.rb +8 −4
  68. lib/docs/scrapers/numpy.rb +0 −1
  69. lib/docs/scrapers/openjdk.rb +2 −3
  70. lib/docs/scrapers/perl.rb +0 −1
  71. lib/docs/scrapers/php.rb +2 −3
  72. lib/docs/scrapers/python.rb +4 −8
  73. lib/docs/scrapers/rdoc/minitest.rb +7 −2
  74. lib/docs/scrapers/rdoc/rails.rb +10 −3
  75. lib/docs/scrapers/rdoc/ruby.rb +8 −19
  76. lib/docs/scrapers/rust.rb +4 −1
  77. lib/docs/scrapers/sqlite.rb +0 −1
  78. lib/docs/subscribers/requester_subscriber.rb +13 −0
  79. lib/tasks/docs.thor +74 −13
  80. public/docs/docs.json +0 −1
  81. public/favicon.pxm +0 −0
  82. public/favicon@2x.pxm +0 −0
  83. public/icons/docs/css/CSS3.sketch/Data +0 −0
  84. public/icons/docs/css/CSS3.sketch/QuickLook/Preview.png +0 −0
  85. public/icons/docs/css/CSS3.sketch/QuickLook/Thumbnail.png +0 −0
  86. public/icons/docs/css/CSS3.sketch/fonts +0 −0
  87. public/icons/docs/css/CSS3.sketch/version +1 −0
  88. public/icons/docs/dom/DOM.sketch/Data +0 −0
  89. public/icons/docs/dom/DOM.sketch/QuickLook/Preview.png +0 −0
  90. public/icons/docs/dom/DOM.sketch/QuickLook/Thumbnail.png +0 −0
  91. public/icons/docs/dom/DOM.sketch/fonts +0 −0
  92. public/icons/docs/dom/DOM.sketch/version +1 −0
  93. public/icons/docs/dom_events/DOM_events.sketch/Data +0 −0
  94. public/icons/docs/dom_events/DOM_events.sketch/QuickLook/Preview.png +0 −0
  95. public/icons/docs/dom_events/DOM_events.sketch/QuickLook/Thumbnail.png +0 −0
  96. public/icons/docs/dom_events/DOM_events.sketch/fonts +0 −0
  97. public/icons/docs/dom_events/DOM_events.sketch/version +1 −0
  98. public/icons/docs/express/16.pxm +0 −0
  99. public/icons/docs/express/16@2x.pxm +0 −0
  100. public/icons/docs/html/HTML5.sketch/Data +0 −0
  101. public/icons/docs/html/HTML5.sketch/QuickLook/Preview.png +0 −0
  102. public/icons/docs/html/HTML5.sketch/QuickLook/Thumbnail.png +0 −0
  103. public/icons/docs/html/HTML5.sketch/fonts +0 −0
  104. public/icons/docs/html/HTML5.sketch/version +1 −0
  105. public/icons/docs/http/16.pxm +0 −0
  106. public/icons/docs/http/16@2x.pxm +0 −0
  107. public/icons/docs/javascript/16.pxm +0 −0
  108. public/icons/docs/javascript/16@2x.pxm +0 −0
  109. public/icons/docs/knockout/16@2x.pxm +0 −0
  110. public/icons/docs/less/less.pxm +0 −0
  111. public/icons/docs/moment/moment.sketch/Data +0 −0
  112. public/icons/docs/moment/moment.sketch/QuickLook/Preview.png +0 −0
  113. public/icons/docs/moment/moment.sketch/QuickLook/Thumbnail.png +0 −0
  114. public/icons/docs/moment/moment.sketch/fonts +0 −0
  115. public/icons/docs/moment/moment.sketch/metadata +14 −0
  116. public/icons/docs/moment/moment.sketch/version +1 −0
  117. public/icons/docs/nokogiri/icon.pxm +0 −0
  118. public/icons/docs/php/16@2x.pxm +0 −0
  119. public/icons/docs/rails/rails.pxm +0 −0
  120. public/icons/docs/sass/sass.pxm +0 −0
  121. public/icons/docs/support_tables/16.pxm +0 −0
  122. public/icons/docs/support_tables/16@2x.pxm +0 −0
  123. public/icons/docs/xpath/XPath.sketch/Data +0 −0
  124. public/icons/docs/xpath/XPath.sketch/QuickLook/Preview.png +0 −0
  125. public/icons/docs/xpath/XPath.sketch/QuickLook/Thumbnail.png +0 −0
  126. public/icons/docs/xpath/XPath.sketch/metadata +18 −0
  127. public/icons/docs/xpath/XPath.sketch/version +1 −0
  128. public/images/apple-icon.pxm +0 −0
  129. public/images/fluid-icon.pxm +0 −0
  130. public/images/icon.pxm +0 −0
  131. public/images/webapp-icon-192.png +0 −0
  132. public/images/webapp-icon-small.pxm +0 −0
  133. public/manifest.json +6 −0
  134. test/app_test.rb +5 −81
  135. test/lib/docs/core/scrapers/file_scraper_test.rb +89 −47
  136. views/app.erb +1 −13
  137. views/index.erb +3 −2
  138. views/manifest.erb +0 −14
  139. views/other.erb +3 −2
  140. views/service-worker.js.erb +49 −0
  141. views/unsupported.erb +6 −6
.editorconfig+3−0
@@ -6,3 +6,6 @@ indent_style = space · 5 unmodified lines
6indent_size = 26indent_size = 2
7insert_final_newline = true7insert_final_newline = true
8trim_trailing_whitespace = true8trim_trailing_whitespace = true
9
10[*.md]
11trim_trailing_whitespace = false
.github/CONTRIBUTING.md+5−14
@@ -24,20 +24,18 @@ Want to contribute? Great. Please review the following guidelines carefully and · 23 unmodified lines
24## Requesting new features24## Requesting new features
25 25
261. Search for similar feature requests; someone may have already requested it.261. Search for similar feature requests; someone may have already requested it.
272. Make sure your feature fits DevDocs's [vision](https://github.com/freeCodeCamp/devdocs/blob/master/README.md#vision).272. Make sure your feature fits DevDocs's [vision](../README.md#vision).
283. Provide a clear and detailed explanation of the feature and why it's important to add it.283. Provide a clear and detailed explanation of the feature and why it's important to add it.
29 29
30For general feedback and ideas, please use the [mailing list](https://groups.google.com/d/forum/devdocs).
31
32## Requesting new documentations30## Requesting new documentations
33 31
34Please don't open issues to request new documentations. 32Please don't open issues to request new documentations.
35Use the [Trello board](https://trello.com/b/6BmTulfx/devdocs-documentation) where everyone can vote.33Use the [Trello board](https://trello.com/b/6BmTulfx/devdocs-documentation) where everyone can vote.
36 34
37## Contributing code and features35## Contributing code and features
38 36
391. Search for existing issues; someone may already be working on a similar feature.371. Search for existing issues; someone may already be working on a similar feature.
402. Before embarking on any significant pull request, please open an issue describing the changes you intend to make. Otherwise you risk spending a lot of time working on something I may not want to merge. This also tells other contributors that you're working on the feature.382. Before embarking on any significant pull request, please open an issue describing the changes you intend to make. Otherwise you risk spending a lot of time working on something we may not want to merge. This also tells other contributors that you're working on the feature.
413. Follow the [coding conventions](#coding-conventions).393. Follow the [coding conventions](#coding-conventions).
424. If you're modifying the Ruby code, include tests and ensure they pass.404. If you're modifying the Ruby code, include tests and ensure they pass.
435. Try to keep your pull request small and simple.415. Try to keep your pull request small and simple.
@@ -46,7 +44,7 @@ Use the [Trello board](https://trello.com/b/6BmTulfx/devdocs-documentation) wher · 2 unmodified lines
46 44
47## Contributing new documentations45## Contributing new documentations
48 46
49See the [wiki](https://github.com/freeCodeCamp/devdocs/wiki) to learn how to add new documentations.47See the [`docs` folder](https://github.com/freeCodeCamp/devdocs/tree/master/docs) to learn how to add new documentations.
50 48
51**Important:** the documentation's license must permit alteration, redistribution and commercial use, and the documented software must be released under an open source license. Feel free to get in touch if you're not sure if a documentation meets those requirements.49**Important:** the documentation's license must permit alteration, redistribution and commercial use, and the documented software must be released under an open source license. Feel free to get in touch if you're not sure if a documentation meets those requirements.
52 50
@@ -65,13 +63,6 @@ Please don't submit a pull request updating the version number of a documentatio · 12 unmodified lines
65 63
66To ask that an existing documentation be updated, please use the [Trello board](https://trello.com/c/2B0hmW7M/52-request-updates-here).64To ask that an existing documentation be updated, please use the [Trello board](https://trello.com/c/2B0hmW7M/52-request-updates-here).
67 65
68## Other contributions
69
70Besides new docs and features, here are other ways you can contribute:
71
72* **Improve our copy.** English isn't my first language so if you notice grammatical or usage errors, feel free to submit a pull request — it'll be much appreciated.
73* **Participate in the issue tracker.** Your opinion matters — feel free to add comments to existing issues. You're also welcome to participate to the [mailing list](https://groups.google.com/d/forum/devdocs).
74
75## Coding conventions66## Coding conventions
76 67
77* two spaces; no tabs68* two spaces; no tabs
@@ -80,4 +71,4 @@ Besides new docs and features, here are other ways you can contribute: · 2 unmodified lines
80 71
81## Questions?72## Questions?
82 73
83If you have any questions, please feel free to ask on the [mailing list](https://groups.google.com/d/forum/devdocs).74If you have any questions, please feel free to ask them on the contributor chat room on [Gitter](https://gitter.im/FreeCodeCamp/DevDocs).
.github/ISSUE_TEMPLATE.md+1−1
@@ -1,6 +1,6 @@
1<!--1<!--
2 Please read the contributing guidelines before opening an issue:2 Please read the contributing guidelines before opening an issue:
3 https://github.com/freeCodeCamp/devdocs/blob/master/CONTRIBUTING.md3 https://github.com/freeCodeCamp/devdocs/blob/master/.github/CONTRIBUTING.md
4 4
5 To request a new documentation, or an update of an existing documentation, go here:5 To request a new documentation, or an update of an existing documentation, go here:
6 https://trello.com/b/6BmTulfx/devdocs-documentation6 https://trello.com/b/6BmTulfx/devdocs-documentation
.gitignore+3−4
@@ -1,10 +1,9 @@
1.DS_Store1.DS_Store
2.bundle2.bundle
3*.pxm3log
4*.sketch
5tmp4tmp
6public/assets5public/assets
7public/fonts6public/fonts
8public/docs/**/*7public/docs/**/*
9!public/docs/docs.json8docs/**/*
10!public/docs/**/index.json9!docs/*.md
.ruby-version+1−1
@@ -1 +1 @@
12.5.112.6.0
.travis.yml+8−0
@@ -5,3 +5,11 @@ cache: bundler · 4 unmodified lines
5before_script:5before_script:
6 - gem update --system6 - gem update --system
7 - gem install bundler7 - gem install bundler
8
9deploy:
10 provider: heroku
11 app: devdocs
12 on:
13 branch: master
14 api_key:
15 secure: "4p1klvWJZSOImzFcKOduILjP93hlOlAhceWlYMKS4tU+TCFE8qTBzdKdFPSCsCgjB+YR9pBss+L0lJpVVMjSwFHXqpKe6EeUSltO2k7DFHfW7kXLUM/L0AfqXz+YXk76XUyZMhvOEbldPfaMaj10e8vgDOQCSHABDyK/4CU+hnI="
CODE_OF_CONDUCT.md+2−0
@@ -0,0 +1,2 @@
1
2> Our Code of Conduct is available here: <https://code-of-conduct.freecodecamp.org/>
COPYRIGHT+4−4
@@ -1,13 +1,13 @@
1Copyright 2013-2018 Thibaut Courouble and other contributors1Copyright 2013-2019 Thibaut Courouble and other contributors
2 2
3 This Source Code Form is subject to the terms of the Mozilla Public3 This Source Code Form is subject to the terms of the Mozilla Public
4 License, v. 2.0. If a copy of the MPL was not distributed with this4 License, v. 2.0. If a copy of the MPL was not distributed with this
5 file, You can obtain one at http://mozilla.org/MPL/2.0/.5 file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 6
7Please do not use the name DevDocs to endorse or promote products7Please do not use the name DevDocs to endorse or promote products
8derived from this software without my permission, except as may be8derived from this software without the maintainers' permission, except
9necessary to comply with the notice/attribution requirements.9as may be necessary to comply with the notice/attribution requirements.
10 10
11I also wish that any documentation file generated using this software11We also wish that any documentation file generated using this software
12be attributed to DevDocs. Let's be fair to all contributors by giving12be attributed to DevDocs. Let's be fair to all contributors by giving
13credit where credit's due. Thanks.13credit where credit's due. Thanks.
Dockerfile+2−1
@@ -1,6 +1,7 @@
1FROM ruby:2.5.11FROM ruby:2.6.0
2 2
3ENV LANG=C.UTF-83ENV LANG=C.UTF-8
4ENV ENABLE_SERVICE_WORKER=true
4 5
5WORKDIR /devdocs6WORKDIR /devdocs
6 7
Dockerfile-alpine+2−1
@@ -1,6 +1,7 @@
1FROM ruby:2.5.1-alpine1FROM ruby:2.6.0-alpine
2 2
3ENV LANG=C.UTF-83ENV LANG=C.UTF-8
4ENV ENABLE_SERVICE_WORKER=true
4 5
5WORKDIR /devdocs6WORKDIR /devdocs
6 7
Gemfile+3−2
@@ -1,9 +1,9 @@
1source 'https://rubygems.org'1source 'https://rubygems.org'
2ruby '2.5.1'2ruby '2.6.0'
3 3
4gem 'rake'4gem 'rake'
5gem 'thor'5gem 'thor'
6gem 'pry', '~> 0.11.0'6gem 'pry', '~> 0.12.0'
7gem 'activesupport', '~> 5.2', require: false7gem 'activesupport', '~> 5.2', require: false
8gem 'yajl-ruby', require: false8gem 'yajl-ruby', require: false
9gem 'html-pipeline'9gem 'html-pipeline'
@@ -39,6 +39,7 @@ group :docs do · 29 unmodified lines
39 gem 'progress_bar', require: false39 gem 'progress_bar', require: false
40 gem 'unix_utils', require: false40 gem 'unix_utils', require: false
41 gem 'tty-pager', require: false41 gem 'tty-pager', require: false
42 gem 'net-sftp', '>= 2.1.3.rc2', require: false
42end43end
43 44
44group :test do45group :test do
Gemfile.lock+47−44
@@ -1,7 +1,7 @@
1GEM1GEM
2 remote: https://rubygems.org/2 remote: https://rubygems.org/
3 specs:3 specs:
4 activesupport (5.2.1)4 activesupport (5.2.2)
5 concurrent-ruby (~> 1.0, >= 1.0.2)5 concurrent-ruby (~> 1.0, >= 1.0.2)
6 i18n (>= 0.7, < 2)6 i18n (>= 0.7, < 2)
7 minitest (~> 5.1)7 minitest (~> 5.1)
@@ -17,77 +17,79 @@ GEM · 9 unmodified lines
17 coffee-script-source17 coffee-script-source
18 execjs18 execjs
19 coffee-script-source (1.12.2)19 coffee-script-source (1.12.2)
20 concurrent-ruby (1.0.5)20 concurrent-ruby (1.1.4)
21 daemons (1.2.6)21 daemons (1.3.1)
22 erubi (1.7.1)22 erubi (1.8.0)
23 ethon (0.11.0)23 ethon (0.12.0)
24 ffi (>= 1.3.0)24 ffi (>= 1.3.0)
25 eventmachine (1.2.7)25 eventmachine (1.2.7)
26 execjs (2.7.0)26 execjs (2.7.0)
27 exifr (1.3.4)27 exifr (1.3.5)
28 ffi (1.9.25)28 ffi (1.10.0)
29 fspath (3.1.0)29 fspath (3.1.0)
30 highline (2.0.0)30 highline (2.0.0)
31 html-pipeline (2.8.4)31 html-pipeline (2.10.0)
32 activesupport (>= 2)32 activesupport (>= 2)
33 nokogiri (>= 1.4)33 nokogiri (>= 1.4)
34 i18n (1.1.0)34 i18n (1.5.2)
35 concurrent-ruby (~> 1.0)35 concurrent-ruby (~> 1.0)
36 image_optim (0.26.2)36 image_optim (0.26.3)
37 exifr (~> 1.2, >= 1.2.2)37 exifr (~> 1.2, >= 1.2.2)
38 fspath (~> 3.0)38 fspath (~> 3.0)
39 image_size (>= 1.5, < 3)39 image_size (>= 1.5, < 3)
40 in_threads (~> 1.3)40 in_threads (~> 1.3)
41 progress (~> 3.0, >= 3.0.1)41 progress (~> 3.0, >= 3.0.1)
42 image_optim_pack (0.5.1)42 image_optim_pack (0.5.1.20190105)
43 fspath (>= 2.1, < 4)43 fspath (>= 2.1, < 4)
44 image_optim (~> 0.19)44 image_optim (~> 0.19)
45 image_size (2.0.0)45 image_size (2.0.0)
46 in_threads (1.5.0)46 in_threads (1.5.1)
47 method_source (0.9.0)47 method_source (0.9.2)
48 mini_portile2 (2.3.0)48 mini_portile2 (2.4.0)
49 minitest (5.11.3)49 minitest (5.11.3)
50 multi_json (1.13.1)50 multi_json (1.13.1)
51 mustermann (1.0.3)51 mustermann (1.0.3)
52 newrelic_rpm (5.4.0.347)52 net-sftp (3.0.0.beta1)
53 nokogiri (1.8.4)53 net-ssh (>= 5.0.0, < 6.0.0)
54 mini_portile2 (~> 2.3.0)54 net-ssh (5.1.0)
55 newrelic_rpm (5.7.0.350)
56 nokogiri (1.10.1)
57 mini_portile2 (~> 2.4.0)
55 options (2.3.2)58 options (2.3.2)
56 progress (3.4.0)59 progress (3.5.0)
57 progress_bar (1.3.0)60 progress_bar (1.3.0)
58 highline (>= 1.6, < 3)61 highline (>= 1.6, < 3)
59 options (~> 2.3.0)62 options (~> 2.3.0)
60 pry (0.11.3)63 pry (0.12.2)
61 coderay (~> 1.1.0)64 coderay (~> 1.1.0)
62 method_source (~> 0.9.0)65 method_source (~> 0.9.0)
63 rack (2.0.5)66 rack (2.0.6)
64 rack-protection (2.0.4)67 rack-protection (2.0.5)
65 rack68 rack
66 rack-ssl-enforcer (0.2.9)69 rack-ssl-enforcer (0.2.9)
67 rack-test (1.1.0)70 rack-test (1.1.0)
68 rack (>= 1.0, < 3)71 rack (>= 1.0, < 3)
69 rake (12.3.1)72 rake (12.3.2)
70 rb-fsevent (0.10.3)73 rb-fsevent (0.10.3)
71 rb-inotify (0.9.10)74 rb-inotify (0.10.0)
72 ffi (>= 0.5.0, < 2)75 ffi (~> 1.0)
73 rr (1.2.1)76 rr (1.2.1)
74 sass (3.6.0)77 sass (3.7.3)
75 sass-listen (~> 4.0.0)78 sass-listen (~> 4.0.0)
76 sass-listen (4.0.0)79 sass-listen (4.0.0)
77 rb-fsevent (~> 0.9, >= 0.9.4)80 rb-fsevent (~> 0.9, >= 0.9.4)
78 rb-inotify (~> 0.9, >= 0.9.7)81 rb-inotify (~> 0.9, >= 0.9.7)
79 sinatra (2.0.4)82 sinatra (2.0.5)
80 mustermann (~> 1.0)83 mustermann (~> 1.0)
81 rack (~> 2.0)84 rack (~> 2.0)
82 rack-protection (= 2.0.4)85 rack-protection (= 2.0.5)
83 tilt (~> 2.0)86 tilt (~> 2.0)
84 sinatra-contrib (2.0.4)87 sinatra-contrib (2.0.5)
85 activesupport (>= 4.0.0)
86 backports (>= 2.8.2)88 backports (>= 2.8.2)
87 multi_json89 multi_json
88 mustermann (~> 1.0)90 mustermann (~> 1.0)
89 rack-protection (= 2.0.4)91 rack-protection (= 2.0.5)
90 sinatra (= 2.0.4)92 sinatra (= 2.0.5)
91 tilt (>= 1.3, < 3)93 tilt (>= 1.3, < 3)
92 sprockets (3.7.2)94 sprockets (3.7.2)
93 concurrent-ruby (~> 1.0)95 concurrent-ruby (~> 1.0)
@@ -103,22 +105,22 @@ GEM · 9 unmodified lines
103 daemons (~> 1.0, >= 1.0.9)105 daemons (~> 1.0, >= 1.0.9)
104 eventmachine (~> 1.0, >= 1.0.4)106 eventmachine (~> 1.0, >= 1.0.4)
105 rack (>= 1, < 3)107 rack (>= 1, < 3)
106 thor (0.20.0)108 thor (0.20.3)
107 thread_safe (0.3.6)109 thread_safe (0.3.6)
108 tilt (2.0.8)110 tilt (2.0.9)
109 tty-pager (0.11.0)111 tty-pager (0.12.0)
110 strings (~> 0.1.0)112 strings (~> 0.1.4)
111 tty-screen (~> 0.6.4)113 tty-screen (~> 0.6)
112 tty-which (~> 0.3.0)114 tty-which (~> 0.4)
113 tty-screen (0.6.5)115 tty-screen (0.6.5)
114 tty-which (0.3.0)116 tty-which (0.4.0)
115 typhoeus (1.3.0)117 typhoeus (1.3.1)
116 ethon (>= 0.9.0)118 ethon (>= 0.9.0)
117 tzinfo (1.2.5)119 tzinfo (1.2.5)
118 thread_safe (~> 0.1)120 thread_safe (~> 0.1)
119 uglifier (4.1.19)121 uglifier (4.1.20)
120 execjs (>= 0.3.0, < 3)122 execjs (>= 0.3.0, < 3)
121 unicode-display_width (1.4.0)123 unicode-display_width (1.4.1)
122 unicode_utils (1.4.0)124 unicode_utils (1.4.0)
123 unix_utils (0.0.15)125 unix_utils (0.0.15)
124 yajl-ruby (1.4.1)126 yajl-ruby (1.4.1)
@@ -136,10 +138,11 @@ DEPENDENCIES · 11 unmodified lines
136 image_optim138 image_optim
137 image_optim_pack139 image_optim_pack
138 minitest140 minitest
141 net-sftp (>= 2.1.3.rc2)
139 newrelic_rpm142 newrelic_rpm
140 nokogiri143 nokogiri
141 progress_bar144 progress_bar
142 pry (~> 0.11.0)145 pry (~> 0.12.0)
143 rack146 rack
144 rack-ssl-enforcer147 rack-ssl-enforcer
145 rack-test148 rack-test
@@ -159,7 +162,7 @@ DEPENDENCIES · 13 unmodified lines
159 yajl-ruby162 yajl-ruby
160 163
161RUBY VERSION164RUBY VERSION
162 ruby 2.5.1p57165 ruby 2.6.0p0
163 166
164BUNDLED WITH167BUNDLED WITH
165 1.16.1168 1.17.2
PULL_REQUEST_TEMPLATE.md+11−0
@@ -0,0 +1,11 @@
1If you’re adding a new scraper, please ensure that you have:
2
3- [ ] Tested the scraper on a local copy of DevDocs
4- [ ] Ensured that the docs are styled similarly to other docs on DevDocs
5<!-- If the docs don’t have an icon, delete the next four items: -->
6- [ ] Added these files to the <code>public/icons/*your_scraper_name*/</code> directory:
7 - [ ] `16.png`: a 16×16 pixel icon for the doc
8 - [ ] `16@2x.png`: a 32×32 pixel icon for the doc
9 - [ ] `SOURCE`: A text file containing the URL to the page the image can be found on or the URL of the original image itself
10
11<!-- Replace the `[ ]` with a `[x]` once you’ve completed each step. -->
README.md+45−20
@@ -1,25 +1,24 @@
1# [DevDocs](https://devdocs.io) [![Build Status](https://travis-ci.org/freeCodeCamp/devdocs.svg?branch=master)](https://travis-ci.org/freeCodeCamp/devdocs)1# [DevDocs](https://devdocs.io) — API Documentation Browser [![Build Status](https://travis-ci.org/freeCodeCamp/devdocs.svg?branch=master)](https://travis-ci.org/freeCodeCamp/devdocs)
2 2
3DevDocs combines multiple API documentations in a fast, organized, and searchable interface.3DevDocs combines multiple developer documentations in a clean and organized web UI with instant search, offline support, mobile version, dark theme, keyboard shortcuts, and more.
4 4
5* Created by [Thibaut Courouble](https://thibaut.me)5DevDocs was created by [Thibaut Courouble](https://thibaut.me) and is operated by [freeCodeCamp](https://www.freecodecamp.org).
6 6
7Keep track of development news:7Keep track of development news:
8 8
9* Join the contributor chat room on [Gitter](https://gitter.im/FreeCodeCamp/DevDocs)9* Join the contributor chat room on [Gitter](https://gitter.im/FreeCodeCamp/DevDocs)
10* Watch the repository on [GitHub](https://github.com/freeCodeCamp/devdocs/subscription)10* Watch the repository on [GitHub](https://github.com/freeCodeCamp/devdocs/subscription)
11* Follow [@DevDocs](https://twitter.com/DevDocs) on Twitter11* Follow [@DevDocs](https://twitter.com/DevDocs) on Twitter
12* Join the [mailing list](https://groups.google.com/d/forum/devdocs)
13 12
14**Table of Contents:** [Quick Start](#quick-start) · [Vision](#vision) · [App](#app) · [Scraper](#scraper) · [Commands](#available-commands) · [Contributing](#contributing) · [License](#copyright--license) · [Questions?](#questions)13**Table of Contents:** [Quick Start](#quick-start) · [Vision](#vision) · [App](#app) · [Scraper](#scraper) · [Commands](#available-commands) · [Contributing](#contributing) · [Documentation](#documentation) · [Plugins and Extensions](#plugins-and-extensions) · [License](#copyright--license) · [Questions?](#questions)
15 14
16## Quick Start15## Quick Start
17 16
18Unless you wish to contribute to the project, I recommend using the hosted version at [devdocs.io](https://devdocs.io). It's up-to-date and works offline out-of-the-box.17Unless you wish to contribute to the project, we recommend using the hosted version at [devdocs.io](https://devdocs.io). It's up-to-date and works offline out-of-the-box.
19 18
20DevDocs is made of two pieces: a Ruby scraper that generates the documentation and metadata, and a JavaScript app powered by a small Sinatra app.19DevDocs is made of two pieces: a Ruby scraper that generates the documentation and metadata, and a JavaScript app powered by a small Sinatra app.
21 20
22DevDocs requires Ruby 2.5.1, libcurl, and a JavaScript runtime supported by [ExecJS](https://github.com/rails/execjs#readme) (included in OS X and Windows; [Node.js](https://nodejs.org/en/) on Linux). Once you have these installed, run the following commands:21DevDocs requires Ruby 2.6.0, libcurl, and a JavaScript runtime supported by [ExecJS](https://github.com/rails/execjs#readme) (included in OS X and Windows; [Node.js](https://nodejs.org/en/) on Linux). Once you have these installed, run the following commands:
23 22
24```23```
25git clone https://github.com/freeCodeCamp/devdocs.git && cd devdocs24git clone https://github.com/freeCodeCamp/devdocs.git && cd devdocs
@@ -60,14 +59,14 @@ The web app is all client-side JavaScript, written in [CoffeeScript](http://coff · 34 unmodified lines
60 59
61Many of the code's design decisions were driven by the fact that the app uses XHR to load content directly into the main frame. This includes stripping the original documents of most of their HTML markup (e.g. scripts and stylesheets) to avoid polluting the main frame, and prefixing all CSS class names with an underscore to prevent conflicts.60Many of the code's design decisions were driven by the fact that the app uses XHR to load content directly into the main frame. This includes stripping the original documents of most of their HTML markup (e.g. scripts and stylesheets) to avoid polluting the main frame, and prefixing all CSS class names with an underscore to prevent conflicts.
62 61
63Another driving factor is performance and the fact that everything happens in the browser. `applicationCache` (which comes with its own set of constraints) and `localStorage` are used to speed up the boot time, while memory consumption is kept in check by allowing the user to pick his/her own set of documentations. The search algorithm is kept simple because it needs to be fast even searching through 100,000 strings.62Another driving factor is performance and the fact that everything happens in the browser. A service worker (which comes with its own set of constraints) and `localStorage` are used to speed up the boot time, while memory consumption is kept in check by allowing the user to pick his/her own set of documentations. The search algorithm is kept simple because it needs to be fast even searching through 100,000 strings.
64 63
65DevDocs being a developer tool, the browser requirements are high:64DevDocs being a developer tool, the browser requirements are high:
66 65
67* Recent versions of Firefox, Chrome, or Opera66* Recent versions of Firefox, Chrome, or Opera
68* Safari 9.1+67* Safari 11.1+
69* Edge 16+68* Edge 17+
70* iOS 10+69* iOS 11.3+
71 70
72This allows the code to take advantage of the latest DOM and HTML5 APIs and make developing DevDocs a lot more fun!71This allows the code to take advantage of the latest DOM and HTML5 APIs and make developing DevDocs a lot more fun!
73 72
@@ -84,12 +83,13 @@ Modifications made to each document include: · 10 unmodified lines
84* replacing all external (not scraped) URLs with their fully qualified counterpart83* replacing all external (not scraped) URLs with their fully qualified counterpart
85* replacing all internal (scraped) URLs with their unqualified and relative counterpart84* replacing all internal (scraped) URLs with their unqualified and relative counterpart
86* adding content, such as a title and link to the original document85* adding content, such as a title and link to the original document
86* ensuring correct syntax highlighting using [Prism](http://prismjs.com/)
87 87
88These modifications are applied via a set of filters using the [HTML::Pipeline](https://github.com/jch/html-pipeline) library. Each scraper includes filters specific to itself, one of which is tasked with figuring out the pages' metadata.88These modifications are applied via a set of filters using the [HTML::Pipeline](https://github.com/jch/html-pipeline) library. Each scraper includes filters specific to itself, one of which is tasked with figuring out the pages' metadata.
89 89
90The end result is a set of normalized HTML partials and two JSON files (index + offline data). Because the index files are loaded separately by the [app](#app) following the user's preferences, the scraper also creates a JSON manifest file containing information about the documentations currently available on the system (such as their name, version, update date, etc.).90The end result is a set of normalized HTML partials and two JSON files (index + offline data). Because the index files are loaded separately by the [app](#app) following the user's preferences, the scraper also creates a JSON manifest file containing information about the documentations currently available on the system (such as their name, version, update date, etc.).
91 91
92More information about scrapers and filters is available on the [wiki](https://github.com/freeCodeCamp/devdocs/wiki).92More information about [scrapers](./docs/scraper-reference.md) and [filters](./docs/filter-reference.md) is available in the `docs` folder.
93 93
94## Available Commands94## Available Commands
95 95
@@ -129,20 +129,45 @@ If multiple versions of Ruby are installed on your system, commands must be run · 33 unmodified lines
129 129
130## Contributing130## Contributing
131 131
132Contributions are welcome. Please read the [contributing guidelines](https://github.com/freeCodeCamp/devdocs/blob/master/.github/CONTRIBUTING.md).132Contributions are welcome. Please read the [contributing guidelines](./.github/CONTRIBUTING.md).
133 133
134DevDocs's own documentation is available on the [wiki](https://github.com/freeCodeCamp/devdocs/wiki).134## Documentation
135
136* [Adding documentations to DevDocs](./docs/adding-docs.md)
137* [Scraper Reference](./docs/scraper-reference.md)
138* [Filter Reference](./docs/filter-reference.md)
139* [Maintainers’ Guide](./docs/maintainers.md)
140
141## Plugins and Extensions
142
143* [Chrome web app](https://chrome.google.com/webstore/detail/devdocs/mnfehgbmkapmjnhcnbodoamcioleeooe)
144* [Ubuntu Touch app](https://uappexplorer.com/app/devdocsunofficial.berkes)
145* [Sublime Text plugin](https://sublime.wbond.net/packages/DevDocs)
146* [Atom plugin](https://atom.io/packages/devdocs)
147* [Brackets extension](https://github.com/gruehle/dev-docs-viewer)
148* [Fluid](http://fluidapp.com) for turning DevDocs into a real OS X app
149* [GTK shell / Vim integration](https://github.com/naquad/devdocs-shell)
150* [Emacs lookup](https://github.com/skeeto/devdocs-lookup)
151* [Alfred Workflow](https://github.com/yannickglt/alfred-devdocs)
152* [Vim search plugin with Devdocs in its defaults](https://github.com/waiting-for-dev/vim-www) Just set `let g:www_shortcut_engines = { 'devdocs': ['Devdocs', '<leader>dd'] }` to have a `:Devdocs` command and a `<leader>dd` mapping.
153* [Visual Studio Code plugin](https://marketplace.visualstudio.com/items?itemName=akfish.vscode-devdocs ) (1)
154* [Visual Studio Code plugin](https://marketplace.visualstudio.com/items?itemName=deibit.devdocs) (2)
155* [Desktop application](https://github.com/egoist/devdocs-desktop)
156* [Doc Browser](https://github.com/qwfy/doc-browser) is a native Linux app that supports DevDocs docsets
157* [GNOME Application](https://github.com/hardpixel/devdocs-desktop) GTK3 application with search integrated in headerbar
158* [macOS Application](https://github.com/dteoh/devdocs-macos)
159* [Android Application](https://github.com/Merith-TK/devdocs_webapp_kotlin) is a fully working, advanced WebView
135 160
136## Copyright / License161## Copyright / License
137 162
138Copyright 2013-2018 Thibaut Courouble and [other contributors](https://github.com/freeCodeCamp/devdocs/graphs/contributors)163Copyright 2013-2019 Thibaut Courouble and [other contributors](https://github.com/freeCodeCamp/devdocs/graphs/contributors)
139 164
140This software is licensed under the terms of the Mozilla Public License v2.0. See the [COPYRIGHT](https://github.com/freeCodeCamp/devdocs/blob/master/COPYRIGHT) and [LICENSE](https://github.com/freeCodeCamp/devdocs/blob/master/LICENSE) files.165This software is licensed under the terms of the Mozilla Public License v2.0. See the [COPYRIGHT](./COPYRIGHT) and [LICENSE](./LICENSE) files.
141 166
142Please do not use the name DevDocs to endorse or promote products derived from this software without my permission, except as may be necessary to comply with the notice/attribution requirements.167Please do not use the name DevDocs to endorse or promote products derived from this software without the maintainers' permission, except as may be necessary to comply with the notice/attribution requirements.
143 168
144I also wish that any documentation file generated using this software be attributed to DevDocs. Let's be fair to all contributors by giving credit where credit's due. Thanks!169We also wish that any documentation file generated using this software be attributed to DevDocs. Let's be fair to all contributors by giving credit where credit's due. Thanks!
145 170
146## Questions?171## Questions?
147 172
148If you have any questions, please feel free to ask them on the [mailing list](https://groups.google.com/d/forum/devdocs).173If you have any questions, please feel free to ask them on the contributor chat room on [Gitter](https://gitter.im/FreeCodeCamp/DevDocs).
assets/javascripts/app/app.coffee+10−8
@@ -13,10 +13,12 @@ · 12 unmodified lines
13 13
14 @el = $('._app')14 @el = $('._app')
15 @localStorage = new LocalStorageStore15 @localStorage = new LocalStorageStore
16 @appCache = new app.AppCache if app.AppCache.isEnabled()16 @serviceWorker = new app.ServiceWorker if app.ServiceWorker.isEnabled()
17 @settings = new app.Settings17 @settings = new app.Settings
18 @db = new app.DB()18 @db = new app.DB()
19 19
20 @settings.initLayout()
21
20 @docs = new app.collections.Docs22 @docs = new app.collections.Docs
21 @disabledDocs = new app.collections.Docs23 @disabledDocs = new app.collections.Docs
22 @entries = new app.collections.Entries24 @entries = new app.collections.Entries
@@ -147,7 +149,7 @@ · 124 unmodified lines
147 saveDocs: ->149 saveDocs: ->
148 @settings.setDocs(doc.slug for doc in @docs.all())150 @settings.setDocs(doc.slug for doc in @docs.all())
149 @db.migrate()151 @db.migrate()
150 @appCache?.updateInBackground()152 @serviceWorker?.updateInBackground()
151 153
152 welcomeBack: ->154 welcomeBack: ->
153 visitCount = @settings.get('count')155 visitCount = @settings.get('count')
@@ -158,7 +160,7 @@ · 4 unmodified lines
158 @updateChecker = new app.UpdateChecker()160 @updateChecker = new app.UpdateChecker()
159 161
160 reboot: ->162 reboot: ->
161 if location.pathname isnt '/'163 if location.pathname isnt '/' and location.pathname isnt '/settings'
162 window.location = "/##{location.pathname}"164 window.location = "/##{location.pathname}"
163 else165 else
164 window.location = '/'166 window.location = '/'
@@ -167,14 +169,14 @@ · 2 unmodified lines
167 reload: ->169 reload: ->
168 @docs.clearCache()170 @docs.clearCache()
169 @disabledDocs.clearCache()171 @disabledDocs.clearCache()
170 if @appCache then @appCache.reload() else @reboot()172 if @serviceWorker then @serviceWorker.reload() else @reboot()
171 return173 return
172 174
173 reset: ->175 reset: ->
174 @localStorage.reset()176 @localStorage.reset()
175 @settings.reset()177 @settings.reset()
176 @db?.reset()178 @db?.reset()
177 @appCache?.update()179 @serviceWorker?.update()
178 window.location = '/'180 window.location = '/'
179 return181 return
180 182
@@ -193,9 +195,9 @@ · 12 unmodified lines
193 return195 return
194 196
195 indexHost: ->197 indexHost: ->
196 # Can't load the index files from the host/CDN when applicationCache is198 # Can't load the index files from the host/CDN when service worker is
197 # enabled because it doesn't support caching URLs that use CORS.199 # enabled because it doesn't support caching URLs that use CORS.
198 @config[if @appCache and @settings.hasDocs() then 'index_path' else 'docs_origin']200 @config[if @serviceWorker and @settings.hasDocs() then 'index_path' else 'docs_origin']
199 201
200 onBootError: (args...) ->202 onBootError: (args...) ->
201 @trigger 'bootError'203 @trigger 'bootError'
@@ -252,7 +254,7 @@ · 50 unmodified lines
252 matchMedia: !!window.matchMedia254 matchMedia: !!window.matchMedia
253 insertAdjacentHTML: !!document.body.insertAdjacentHTML255 insertAdjacentHTML: !!document.body.insertAdjacentHTML
254 defaultPrevented: document.createEvent('CustomEvent').defaultPrevented is false256 defaultPrevented: document.createEvent('CustomEvent').defaultPrevented is false
255 cssVariables: CSS.supports and CSS.supports('(--t: 0)')257 cssVariables: !!CSS?.supports?('(--t: 0)')
256 258
257 for key, value of features when not value259 for key, value of features when not value
258 Raven.captureMessage "unsupported/#{key}", level: 'info'260 Raven.captureMessage "unsupported/#{key}", level: 'info'
assets/javascripts/app/appcache.coffee+0−46
@@ -1,46 +0,0 @@
1class app.AppCache
2 $.extend @prototype, Events
3
4 @isEnabled: ->
5 try
6 applicationCache and applicationCache.status isnt applicationCache.UNCACHED
7 catch
8
9 constructor: ->
10 @cache = applicationCache
11 @notifyUpdate = true
12 @onUpdateReady() if @cache.status is @cache.UPDATEREADY
13
14 $.on @cache, 'progress', @onProgress
15 $.on @cache, 'updateready', @onUpdateReady
16
17 update: ->
18 @notifyUpdate = true
19 @notifyProgress = true
20 try @cache.update() catch
21 return
22
23 updateInBackground: ->
24 @notifyUpdate = false
25 @notifyProgress = false
26 try @cache.update() catch
27 return
28
29 reload: ->
30 $.on @cache, 'updateready noupdate error', ->
31 if location.pathname isnt '/'
32 window.location = "/##{location.pathname}"
33 else
34 window.location = '/'
35 @notifyUpdate = false
36 @notifyProgress = true
37 try @cache.update() catch
38 return
39
40 onProgress: (event) =>
41 @trigger 'progress', event if @notifyProgress
42 return
43
44 onUpdateReady: =>
45 @trigger 'updateready' if @notifyUpdate
46 return
assets/javascripts/app/config.coffee.erb+2−0
@@ -13,3 +13,5 @@ app.config = · 12 unmodified lines
13 version: <%= Time.now.to_i %>13 version: <%= Time.now.to_i %>
14 release: <%= Time.now.utc.httpdate.to_json %>14 release: <%= Time.now.utc.httpdate.to_json %>
15 mathml_stylesheet: '<%= App.cdn_origin %>/mathml.css'15 mathml_stylesheet: '<%= App.cdn_origin %>/mathml.css'
16 service_worker_path: '/service-worker.js'
17 service_worker_enabled: <%= App.environment == :production || ENV['ENABLE_SERVICE_WORKER'] == 'true' %>
assets/javascripts/app/serviceworker.coffee+49−0
@@ -0,0 +1,49 @@
1class app.ServiceWorker
2 $.extend @prototype, Events
3
4 @isEnabled: ->
5 !!navigator.serviceWorker and app.config.service_worker_enabled
6
7 constructor: ->
8 @registration = null
9 @notifyUpdate = true
10
11 navigator.serviceWorker.register(app.config.service_worker_path, {scope: '/'})
12 .then(
13 (registration) => @updateRegistration(registration),
14 (error) -> console.error('Could not register service worker:', error)
15 )
16
17 update: ->
18 return unless @registration
19 @notifyUpdate = true
20 return @registration.update().catch(->)
21
22 updateInBackground: ->
23 return unless @registration
24 @notifyUpdate = false
25 return @registration.update().catch(->)
26
27 reload: ->
28 return @updateInBackground().then(() -> app.reboot())
29
30 updateRegistration: (registration) ->
31 @registration = registration
32 $.on @registration, 'updatefound', @onUpdateFound
33 return
34
35 onUpdateFound: =>
36 $.off @installingRegistration, 'statechange', @onStateChange() if @installingRegistration
37 @installingRegistration = @registration.installing
38 $.on @installingRegistration, 'statechange', @onStateChange
39 return
40
41 onStateChange: =>
42 if @installingRegistration and @installingRegistration.state == 'installed' and navigator.serviceWorker.controller
43 @installingRegistration = null
44 @onUpdateReady()
45 return
46
47 onUpdateReady: ->
48 @trigger 'updateready' if @notifyUpdate
49 return
assets/javascripts/app/settings.coffee+30−0
@@ -19,6 +19,8 @@ class app.Settings · 18 unmodified lines
19 'news'19 'news'
20 ]20 ]
21 21
22 LAYOUTS: ['_max-width', '_sidebar-hidden', '_native-scrollbars']
23
22 @defaults:24 @defaults:
23 count: 025 count: 0
24 hideDisabled: false26 hideDisabled: false
@@ -38,6 +40,7 @@ class app.Settings · 13 unmodified lines
38 set: (key, value) ->40 set: (key, value) ->
39 @store.set(key, value)41 @store.set(key, value)
40 delete @cache[key]42 delete @cache[key]
43 @toggleDark(value) if key == 'dark'
41 return44 return
42 45
43 del: (key) ->46 del: (key) ->
@@ -63,6 +66,8 @@ class app.Settings · 19 unmodified lines
63 return66 return
64 67
65 setLayout: (name, enable) ->68 setLayout: (name, enable) ->
69 @toggleLayout(name, enable)
70
66 layout = (@store.get('layout') || '').split(' ')71 layout = (@store.get('layout') || '').split(' ')
67 $.arrayDelete(layout, '')72 $.arrayDelete(layout, '')
68 73
@@ -104,3 +109,28 @@ class app.Settings · 35 unmodified lines
104 @store.reset()109 @store.reset()
105 @cache = {}110 @cache = {}
106 return111 return
112
113 initLayout: ->
114 @toggleDark(@get('dark') is 1)
115 @toggleLayout(layout, @hasLayout(layout)) for layout in @LAYOUTS
116 @initSidebarWidth()
117 return
118
119 toggleDark: (enable) ->
120 classList = document.documentElement.classList
121 classList.toggle('_theme-default', !enable)
122 classList.toggle('_theme-dark', enable)
123 color = getComputedStyle(document.documentElement).getPropertyValue('--headerBackground').trim()
124 $('meta[name=theme-color]').setAttribute('content', color)
125 return
126
127 toggleLayout: (layout, enable) ->
128 classList = document.body.classList
129 classList.toggle(layout, enable) unless layout is '_sidebar-hidden'
130 classList.toggle('_overlay-scrollbars', $.overlayScrollbarsEnabled())
131 return
132
133 initSidebarWidth: ->
134 size = @get('size')
135 document.documentElement.style.setProperty('--sidebarWidth', size + 'px') if size
136 return
assets/javascripts/app/update_checker.coffee+3−3
@@ -3,13 +3,13 @@ class app.UpdateChecker · 2 unmodified lines
3 @lastCheck = Date.now()3 @lastCheck = Date.now()
4 4
5 $.on window, 'focus', @onFocus5 $.on window, 'focus', @onFocus
6 app.appCache.on 'updateready', @onUpdateReady if app.appCache6 app.serviceWorker?.on 'updateready', @onUpdateReady
7 7
8 setTimeout @checkDocs, 08 setTimeout @checkDocs, 0
9 9
10 check: ->10 check: ->
11 if app.appCache11 if app.serviceWorker
12 app.appCache.update()12 app.serviceWorker.update()
13 else13 else
14 ajax14 ajax
15 url: $('script[src*="application"]').getAttribute('src')15 url: $('script[src*="application"]').getAttribute('src')
assets/javascripts/lib/license.coffee+1−1
@@ -1,5 +1,5 @@
1###1###
2 * Copyright 2013-2018 Thibaut Courouble and other contributors2 * Copyright 2013-2019 Thibaut Courouble and other contributors
3 *3 *
4 * This source code is licensed under the terms of the Mozilla4 * This source code is licensed under the terms of the Mozilla
5 * Public License, v. 2.0, a copy of which may be obtained at:5 * Public License, v. 2.0, a copy of which may be obtained at:
assets/javascripts/news.json+4−0
@@ -1,4 +1,8 @@
1[1[
2 [
3 "2019-07-19",
4 "Replaced the AppCache with a Service Worker (which makes DevDocs an installable PWA) and fixed layout preferences on Firefox."
5 ],
2 [6 [
3 "2018-09-23",7 "2018-09-23",
4 "New documentations: <a href=\"/puppeteer/\">Puppeteer</a> and <a href=\"/handlebars/\">Handlebars.js</a>"8 "New documentations: <a href=\"/puppeteer/\">Puppeteer</a> and <a href=\"/handlebars/\">Handlebars.js</a>"
assets/javascripts/templates/error_tmpl.coffee+8−8
@@ -12,8 +12,8 @@ app.templates.notFoundPage = -> · 11 unmodified lines
12 12
13app.templates.pageLoadError = ->13app.templates.pageLoadError = ->
14 error """ The page failed to load. """,14 error """ The page failed to load. """,
15 """ It may be missing from the server (try reloading the app) or you could be offline.<br>15 """ It may be missing from the server (try reloading the app) or you could be offline (try <a href="/offline">installing the documentation for offline usage</a> when online again).<br>
16 If you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. """,16 If you're online and you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. """,
17 """ #{back} &middot; <a href="/##{location.pathname}" target="_top" class="_error-link">Reload</a>17 """ #{back} &middot; <a href="/##{location.pathname}" target="_top" class="_error-link">Reload</a>
18 &middot; <a href="#" class="_error-link" data-retry>Retry</a> """18 &middot; <a href="#" class="_error-link" data-retry>Retry</a> """
19 19
@@ -57,17 +57,17 @@ app.templates.unsupportedBrowser = """ · 37 unmodified lines
57 <p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:57 <p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:
58 <ul class="_fail-list">58 <ul class="_fail-list">
59 <li>Recent versions of Firefox, Chrome, or Opera59 <li>Recent versions of Firefox, Chrome, or Opera
60 <li>Safari 9.1+60 <li>Safari 11.1+
61 <li>Edge 16+61 <li>Edge 17+
62 <li>iOS 10+62 <li>iOS 11.3+
63 </ul>63 </ul>
64 <p class="_fail-text">64 <p class="_fail-text">
65 If you're unable to upgrade, I apologize.65 If you're unable to upgrade, we apologize.
66 I decided to prioritize speed and new features over support for older browsers.66 We decided to prioritize speed and new features over support for older browsers.
67 <p class="_fail-text">67 <p class="_fail-text">
68 Note: if you're already using one of the browsers above, check your settings and add-ons.68 Note: if you're already using one of the browsers above, check your settings and add-ons.
69 The app uses feature detection, not user agent sniffing.69 The app uses feature detection, not user agent sniffing.
70 <p class="_fail-text">70 <p class="_fail-text">
71 &mdash; Thibaut <a href="https://twitter.com/DevDocs" class="_fail-link">@DevDocs</a>71 &mdash; <a href="https://twitter.com/DevDocs">@DevDocs</a>
72 </div>72 </div>
73"""73"""
assets/javascripts/templates/pages/about_tmpl.coffee+9−14
@@ -11,22 +11,18 @@ app.templates.aboutPage = -> """ · 10 unmodified lines
11 </nav>11 </nav>
12 12
13 <h1 class="_lined-heading">DevDocs: API Documentation Browser</h1>13 <h1 class="_lined-heading">DevDocs: API Documentation Browser</h1>
14 <p>DevDocs combines multiple API documentations in a clean and organized web UI with instant search, offline support, mobile version, dark theme, keyboard shortcuts, and more.14 <p>DevDocs combines multiple developer documentations in a clean and organized web UI with instant search, offline support, mobile version, dark theme, keyboard shortcuts, and more.
15 <ul>15 <p>DevDocs is free and <a href="https://github.com/freeCodeCamp/devdocs">open source</a>. It was created by <a href="https://thibaut.me">Thibaut Courouble</a> and is operated by <a href="https://www.freecodecamp.org/">freeCodeCamp</a>.
16 <li>Created and maintained by <a href="https://thibaut.me">Thibaut Courouble</a>
17 <li>Free and <a href="https://github.com/freeCodeCamp/devdocs">open source</a>
18 <iframe class="_github-btn" src="https://ghbtns.com/github-btn.html?user=freeCodeCamp&repo=devdocs&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20" tabindex="-1"></iframe>
19 </ul>
20 <p>To keep up-to-date with the latest news:16 <p>To keep up-to-date with the latest news:
21 <ul>17 <ul>
22 <li>Follow <a href="https://twitter.com/DevDocs">@DevDocs</a> on Twitter18 <li>Follow <a href="https://twitter.com/DevDocs">@DevDocs</a> on Twitter
23 <li>Watch the repository on <a href="https://github.com/freeCodeCamp/devdocs/subscription">GitHub</a>19 <li>Watch the repository on <a href="https://github.com/freeCodeCamp/devdocs/subscription">GitHub</a> <iframe class="_github-btn" src="https://ghbtns.com/github-btn.html?user=freeCodeCamp&repo=devdocs&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20" tabindex="-1"></iframe>
24 <li>Join the <a href="https://groups.google.com/d/forum/devdocs">mailing list</a>20 <li>Join the <a href="https://gitter.im/FreeCodeCamp/DevDocs">Gitter</a> chat room
25 </ul>21 </ul>
26 22
27 <h2 class="_block-heading" id="copyright">Copyright and License</h2>23 <h2 class="_block-heading" id="copyright">Copyright and License</h2>
28 <p class="_note">24 <p class="_note">
29 <strong>Copyright 2013&ndash;2018 Thibaut Courouble and <a href="https://github.com/freeCodeCamp/devdocs/graphs/contributors">other contributors</a></strong><br>25 <strong>Copyright 2013&ndash;2019 Thibaut Courouble and <a href="https://github.com/freeCodeCamp/devdocs/graphs/contributors">other contributors</a></strong><br>
30 This software is licensed under the terms of the Mozilla Public License v2.0.<br>26 This software is licensed under the terms of the Mozilla Public License v2.0.<br>
31 You may obtain a copy of the source code at <a href="https://github.com/freeCodeCamp/devdocs">github.com/freeCodeCamp/devdocs</a>.<br>27 You may obtain a copy of the source code at <a href="https://github.com/freeCodeCamp/devdocs">github.com/freeCodeCamp/devdocs</a>.<br>
32 For more information, see the <a href="https://github.com/freeCodeCamp/devdocs/blob/master/COPYRIGHT">COPYRIGHT</a>28 For more information, see the <a href="https://github.com/freeCodeCamp/devdocs/blob/master/COPYRIGHT">COPYRIGHT</a>
@@ -48,11 +44,10 @@ app.templates.aboutPage = -> """ · 15 unmodified lines
48 <dt>Where can I suggest new docs and features?44 <dt>Where can I suggest new docs and features?
49 <dd>You can suggest and vote for new docs on the <a href="https://trello.com/b/6BmTulfx/devdocs-documentation">Trello board</a>.<br>45 <dd>You can suggest and vote for new docs on the <a href="https://trello.com/b/6BmTulfx/devdocs-documentation">Trello board</a>.<br>
50 If you have a specific feature request, add it to the <a href="https://github.com/freeCodeCamp/devdocs/issues">issue tracker</a>.<br>46 If you have a specific feature request, add it to the <a href="https://github.com/freeCodeCamp/devdocs/issues">issue tracker</a>.<br>
51 Otherwise use the <a href="https://groups.google.com/d/forum/devdocs">mailing list</a>.47 Otherwise, come talk to us in the <a href="https://gitter.im/FreeCodeCamp/DevDocs">Gitter</a> chat room.
52 <dt>Where can I report bugs?48 <dt>Where can I report bugs?
53 <dd>In the <a href="https://github.com/freeCodeCamp/devdocs/issues">issue tracker</a>. Thanks!49 <dd>In the <a href="https://github.com/freeCodeCamp/devdocs/issues">issue tracker</a>. Thanks!
54 </dl>50 </dl>
55 <p>For anything else, feel free to email me at <a href="mailto:thibaut@devdocs.io">thibaut@devdocs.io</a>.
56 51
57 <h2 class="_block-heading" id="credits">Credits</h2>52 <h2 class="_block-heading" id="credits">Credits</h2>
58 53
@@ -76,12 +71,12 @@ app.templates.aboutPage = -> """ · 17 unmodified lines
76 71
77 <h2 class="_block-heading" id="privacy">Privacy Policy</h2>72 <h2 class="_block-heading" id="privacy">Privacy Policy</h2>
78 <ul>73 <ul>
79 <li><a href="https://devdocs.io">devdocs.io</a> ("App") is operated by Thibaut Courouble ("We").74 <li><a href="https://devdocs.io">devdocs.io</a> ("App") is operated by <a href="https://www.freecodecamp.org/">freeCodeCamp</a> ("We").
80 <li>We do not collect personal information.75 <li>We do not collect personal information through the app.
81 <li>We use Google Analytics, Gauges and Sentry to collect anonymous traffic information and improve the app.76 <li>We use Google Analytics, Gauges and Sentry to collect anonymous traffic information and improve the app.
82 <li>The app uses cookies to store user preferences.77 <li>The app uses cookies to store user preferences.
83 <li>By using the app, you signify your acceptance of this policy. If you do not agree to this policy, please do not use the app.78 <li>By using the app, you signify your acceptance of this policy. If you do not agree to this policy, please do not use the app.
84 <li>If you have any questions regarding privacy, please email <a href="mailto:thibaut@devdocs.io">thibaut@devdocs.io</a>.79 <li>If you have any questions regarding privacy, please email <a href="mailto:privacy@freecodecamp.org">privacy@freecodecamp.org</a>.
85 </ul>80 </ul>
86"""81"""
87 82
assets/javascripts/templates/pages/offline_tmpl.coffee+9−4
@@ -25,8 +25,8 @@ app.templates.offlinePage = (docs) -> """ · 24 unmodified lines
25 <h2 class="_block-heading">Questions & Answers</h2>25 <h2 class="_block-heading">Questions & Answers</h2>
26 <dl>26 <dl>
27 <dt>How does this work?27 <dt>How does this work?
28 <dd>Each page is cached as a key-value pair in <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a> (downloaded from a single file).<br>28 <dd>Each page is cached as a key-value pair in <a href="https://devdocs.io/dom/indexeddb_api">IndexedDB</a> (downloaded from a single file).<br>
29 The app also uses <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache">AppCache</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API">localStorage</a> to cache the assets and index files.29 The app also uses <a href="https://devdocs.io/dom/service_worker_api/using_service_workers">Service Workers</a> and <a href="https://devdocs.io/dom/web_storage_api">localStorage</a> to cache the assets and index files.
30 <dt>Can I close the tab/browser?30 <dt>Can I close the tab/browser?
31 <dd>#{canICloseTheTab()}31 <dd>#{canICloseTheTab()}
32 <dt>What if I don't update a documentation?32 <dt>What if I don't update a documentation?
@@ -41,10 +41,15 @@ app.templates.offlinePage = (docs) -> """ · 8 unmodified lines
41"""41"""
42 42
43canICloseTheTab = ->43canICloseTheTab = ->
44 if app.AppCache.isEnabled()44 if app.ServiceWorker.isEnabled()
45 """ Yes! Even offline, you can open a new tab, go to <a href="//devdocs.io">devdocs.io</a>, and everything will work as if you were online (provided you installed all the documentations you want to use beforehand). """45 """ Yes! Even offline, you can open a new tab, go to <a href="//devdocs.io">devdocs.io</a>, and everything will work as if you were online (provided you installed all the documentations you want to use beforehand). """
46 else46 else
47 """ No. AppCache isn't available in your browser (or is disabled), so loading <a href="//devdocs.io">devdocs.io</a> offline won't work.<br>47 reason = "aren't available in your browser (or are disabled)"
48
49 if app.config.env != 'production'
50 reason = "are disabled in your development instance of DevDocs (enable them by setting the <code>ENABLE_SERVICE_WORKER</code> environment variable to <code>true</code>)"
51
52 """ No. Service Workers #{reason}, so loading <a href="//devdocs.io">devdocs.io</a> offline won't work.<br>
48 The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). """53 The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). """
49 54
50app.templates.offlineDoc = (doc, status) ->55app.templates.offlineDoc = (doc, status) ->
assets/javascripts/templates/pages/root_tmpl.coffee.erb+2−2
@@ -8,13 +8,13 @@ app.templates.intro = """ · 7 unmodified lines
8 <p>Thanks for downloading DevDocs. Here are a few things you should know:8 <p>Thanks for downloading DevDocs. Here are a few things you should know:
9 <ol class="_intro-list">9 <ol class="_intro-list">
10 <li>Your local version of DevDocs won't self-update. Unless you're modifying the code,10 <li>Your local version of DevDocs won't self-update. Unless you're modifying the code,
11 I&nbsp;recommend using the hosted version at <a href="https://devdocs.io">devdocs.io</a>.11 we&nbsp;recommend using the hosted version at <a href="https://devdocs.io">devdocs.io</a>.
12 <li>Run <code>thor docs:list</code> to see all available documentations.12 <li>Run <code>thor docs:list</code> to see all available documentations.
13 <li>Run <code>thor docs:download &lt;name&gt;</code> to download documentations.13 <li>Run <code>thor docs:download &lt;name&gt;</code> to download documentations.
14 <li>Run <code>thor docs:download --installed</code> to update all downloaded documentations.14 <li>Run <code>thor docs:download --installed</code> to update all downloaded documentations.
15 <li>To be notified about new versions, don't forget to <a href="https://github.com/freeCodeCamp/devdocs/subscription">watch the repository</a> on GitHub.15 <li>To be notified about new versions, don't forget to <a href="https://github.com/freeCodeCamp/devdocs/subscription">watch the repository</a> on GitHub.
16 <li>The <a href="https://github.com/freeCodeCamp/devdocs/issues">issue tracker</a> is the preferred channel for bug reports and16 <li>The <a href="https://github.com/freeCodeCamp/devdocs/issues">issue tracker</a> is the preferred channel for bug reports and
17 feature requests. For everything else, use the <a href="https://groups.google.com/d/forum/devdocs">mailing list</a>.17 feature requests. For everything else, use <a href="https://gitter.im/FreeCodeCamp/DevDocs">Gitter</a>.
18 <li>Contributions are welcome. See the <a href="https://github.com/freeCodeCamp/devdocs/blob/master/CONTRIBUTING.md">guidelines</a>.18 <li>Contributions are welcome. See the <a href="https://github.com/freeCodeCamp/devdocs/blob/master/CONTRIBUTING.md">guidelines</a>.
19 <li>DevDocs is licensed under the terms of the Mozilla Public License v2.0. For more information,19 <li>DevDocs is licensed under the terms of the Mozilla Public License v2.0. For more information,
20 see the <a href="https://github.com/freeCodeCamp/devdocs/blob/master/COPYRIGHT">COPYRIGHT</a> and20 see the <a href="https://github.com/freeCodeCamp/devdocs/blob/master/COPYRIGHT">COPYRIGHT</a> and
assets/javascripts/views/content/entry_page.coffee+1−1
@@ -123,7 +123,7 @@ class app.views.EntryPage extends app.View · 122 unmodified lines
123 @render @tmpl('pageLoadError')123 @render @tmpl('pageLoadError')
124 @resetClass()124 @resetClass()
125 @addClass @constructor.errorClass125 @addClass @constructor.errorClass
126 app.appCache?.update()126 app.serviceWorker?.update()
127 return127 return
128 128
129 cache: ->129 cache: ->
assets/javascripts/views/content/settings_page.coffee+1−11
@@ -1,7 +1,4 @@
1class app.views.SettingsPage extends app.View1class app.views.SettingsPage extends app.View
2 LAYOUTS = ['_max-width', '_sidebar-hidden', '_native-scrollbars']
3 SIDEBAR_HIDDEN_LAYOUT = '_sidebar-hidden'
4
5 @className: '_static'2 @className: '_static'
6 3
7 @events:4 @events:
@@ -17,25 +14,18 @@ class app.views.SettingsPage extends app.View · 9 unmodified lines
17 settings.dark = app.settings.get('dark')14 settings.dark = app.settings.get('dark')
18 settings.smoothScroll = !app.settings.get('fastScroll')15 settings.smoothScroll = !app.settings.get('fastScroll')
19 settings.arrowScroll = app.settings.get('arrowScroll')16 settings.arrowScroll = app.settings.get('arrowScroll')
20 settings[layout] = app.settings.hasLayout(layout) for layout in LAYOUTS17 settings[layout] = app.settings.hasLayout(layout) for layout in app.settings.LAYOUTS
21 settings18 settings
22 19
23 getTitle: ->20 getTitle: ->
24 'Preferences'21 'Preferences'
25 22
26 toggleDark: (enable) ->23 toggleDark: (enable) ->
27 html = document.documentElement
28 html.classList.toggle('_theme-default')
29 html.classList.toggle('_theme-dark')
30 app.settings.set('dark', !!enable)24 app.settings.set('dark', !!enable)
31 app.appCache?.updateInBackground()
32 return25 return
33 26
34 toggleLayout: (layout, enable) ->27 toggleLayout: (layout, enable) ->
35 document.body.classList[if enable then 'add' else 'remove'](layout) unless layout is SIDEBAR_HIDDEN_LAYOUT
36 document.body.classList[if $.overlayScrollbarsEnabled() then 'add' else 'remove']('_overlay-scrollbars')
37 app.settings.setLayout(layout, enable)28 app.settings.setLayout(layout, enable)
38 app.appCache?.updateInBackground()
39 return29 return
40 30
41 toggleSmoothScroll: (enable) ->31 toggleSmoothScroll: (enable) ->
assets/javascripts/views/layout/resizer.coffee+2−9
@@ -11,9 +11,6 @@ class app.views.Resizer extends app.View · 10 unmodified lines
11 init: ->11 init: ->
12 @el.setAttribute('draggable', 'true')12 @el.setAttribute('draggable', 'true')
13 @appendTo $('._app')13 @appendTo $('._app')
14
15 @style = $('style[data-resizer]')
16 @size = @style.getAttribute('data-size')
17 return14 return
18 15
19 MIN = 26016 MIN = 260
@@ -24,15 +21,11 @@ class app.views.Resizer extends app.View · 4 unmodified lines
24 return unless value > 021 return unless value > 0
25 value = Math.min(Math.max(Math.round(value), MIN), MAX)22 value = Math.min(Math.max(Math.round(value), MIN), MAX)
26 newSize = "#{value}px"23 newSize = "#{value}px"
27 @style.innerHTML = @style.innerHTML.replace(new RegExp(@size, 'g'), newSize)24 document.documentElement.style.setProperty('--sidebarWidth', newSize)
28 @size = newSize25 app.settings.setSize(value) if save
29 if save
30 app.settings.setSize(value)
31 app.appCache?.updateInBackground()
32 return26 return
33 27
34 onDragStart: (event) =>28 onDragStart: (event) =>
35 @style.removeAttribute('disabled')
36 event.dataTransfer.effectAllowed = 'link'29 event.dataTransfer.effectAllowed = 'link'
37 event.dataTransfer.setData('Text', '')30 event.dataTransfer.setData('Text', '')
38 $.on(window, 'dragover', @onDrag)31 $.on(window, 'dragover', @onDrag)
assets/javascripts/views/layout/settings.coffee+1−9
@@ -25,7 +25,6 @@ class app.views.Settings extends app.View · 24 unmodified lines
25 if super25 if super
26 @render()26 @render()
27 document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT)27 document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT)
28 app.appCache?.on 'progress', @onAppCacheProgress
29 return28 return
30 29
31 deactivate: ->30 deactivate: ->
@@ -33,7 +32,6 @@ class app.views.Settings extends app.View · 1 unmodified line
33 @resetClass()32 @resetClass()
34 @docPicker.detach()33 @docPicker.detach()
35 document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT) if app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT)34 document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT) if app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT)
36 app.appCache?.off 'progress', @onAppCacheProgress
37 return35 return
38 36
39 render: ->37 render: ->
@@ -52,7 +50,7 @@ class app.views.Settings extends app.View · 12 unmodified lines
52 docs = @docPicker.getSelectedDocs()50 docs = @docPicker.getSelectedDocs()
53 app.settings.setDocs(docs)51 app.settings.setDocs(docs)
54 52
55 @saveBtn.textContent = if app.appCache then 'Downloading\u2026' else 'Saving\u2026'53 @saveBtn.textContent = 'Saving\u2026'
56 disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1)54 disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1)
57 disabledDocs.uninstall ->55 disabledDocs.uninstall ->
58 app.db.migrate()56 app.db.migrate()
@@ -83,9 +81,3 @@ class app.views.Settings extends app.View · 24 unmodified lines
83 $.stopEvent(event)81 $.stopEvent(event)
84 app.router.show '/'82 app.router.show '/'
85 return83 return
86
87 onAppCacheProgress: (event) =>
88 if event.lengthComputable
89 percentage = Math.round event.loaded * 100 / event.total
90 @saveBtn.textContent = "Downloading\u2026 (#{percentage}%)"
91 return
assets/javascripts/views/search/search.coffee+8−0
@@ -30,6 +30,9 @@ class app.views.Search extends app.View · 29 unmodified lines
30 .on 'results', @onResults30 .on 'results', @onResults
31 .on 'end', @onEnd31 .on 'end', @onEnd
32 32
33 @scope
34 .on 'change', @onScopeChange
35
33 app.on 'ready', @onReady36 app.on 'ready', @onReady
34 $.on window, 'hashchange', @searchUrl37 $.on window, 'hashchange', @searchUrl
35 $.on window, 'focus', @onWindowFocus38 $.on window, 'focus', @onWindowFocus
@@ -138,6 +141,11 @@ class app.views.Search extends app.View · 102 unmodified lines
138 $.stopEvent(event)141 $.stopEvent(event)
139 return142 return
140 143
144 onScopeChange: =>
145 @value = ''
146 @onInput()
147 return
148
141 afterRoute: (name, context) =>149 afterRoute: (name, context) =>
142 return if app.shortcuts.eventInProgress?.name is 'escape'150 return if app.shortcuts.eventInProgress?.name is 'escape'
143 @reset(true) if not context.init and app.router.isIndex()151 @reset(true) if not context.init and app.router.isIndex()
assets/javascripts/views/search/search_scope.coffee+9−2
@@ -6,6 +6,7 @@ class app.views.SearchScope extends app.View · 5 unmodified lines
6 tag: '._search-tag'6 tag: '._search-tag'
7 7
8 @events:8 @events:
9 click: 'onClick'
9 keydown: 'onKeydown'10 keydown: 'onKeydown'
10 textInput: 'onTextInput'11 textInput: 'onTextInput'
11 12
@@ -93,11 +94,17 @@ class app.views.SearchScope extends app.View · 81 unmodified lines
93 $.stopEvent(event) if @doc94 $.stopEvent(event) if @doc
94 return95 return
95 96
97 onClick: (event) =>
98 if event.target is @tag
99 @reset()
100 $.stopEvent(event)
101 return
102
96 onKeydown: (event) =>103 onKeydown: (event) =>
97 if event.which is 8 # backspace104 if event.which is 8 # backspace
98 if @doc and not @input.value105 if @doc and @input.selectionEnd is 0
99 $.stopEvent(event)
100 @reset()106 @reset()
107 $.stopEvent(event)
101 else if not @doc and @input.value and not $.isChromeForAndroid()108 else if not @doc and @input.value and not $.isChromeForAndroid()
102 return if event.ctrlKey or event.metaKey or event.altKey or event.shiftKey109 return if event.ctrlKey or event.metaKey or event.altKey or event.shiftKey
103 if event.which is 9 or # tab110 if event.which is 9 or # tab
assets/javascripts/views/sidebar/sidebar.coffee+1−1
@@ -28,7 +28,7 @@ class app.views.Sidebar extends app.View · 27 unmodified lines
28 28
29 app.on 'ready', @onReady29 app.on 'ready', @onReady
30 30
31 $.on document.documentElement, 'mouseleave', (event) => @display() if event.clientX < 1031 $.on document.documentElement, 'mouseleave', (event) => @display() unless event.clientX <= 0
32 $.on document.documentElement, 'mouseenter', => @resetDisplay(forceNoHover: false)32 $.on document.documentElement, 'mouseenter', => @resetDisplay(forceNoHover: false)
33 return33 return
34 34
assets/stylesheets/application.css.scss+1−1
@@ -4,7 +4,7 @@ · 3 unmodified lines
4//= depend_on docs-2@2x.png4//= depend_on docs-2@2x.png
5 5
6/*!6/*!
7 * Copyright 2013-2018 Thibaut Courouble and other contributors7 * Copyright 2013-2019 Thibaut Courouble and other contributors
8 *8 *
9 * This source code is licensed under the terms of the Mozilla9 * This source code is licensed under the terms of the Mozilla
10 * Public License, v. 2.0, a copy of which may be obtained at:10 * Public License, v. 2.0, a copy of which may be obtained at:
assets/stylesheets/components/_fail.scss+0−2
@@ -32,5 +32,3 @@ · 31 unmodified lines
32}32}
33 33
34._fail-text:last-child { margin: 0; }34._fail-text:last-child { margin: 0; }
35
36._fail-link { float: right; }
assets/stylesheets/components/_header.scss+1−0
@@ -215,5 +215,6 @@ · 214 unmodified lines
215 color: var(--textColorLight);215 color: var(--textColorLight);
216 background: var(--searchTagBackground);216 background: var(--searchTagBackground);
217 border-radius: 2px;217 border-radius: 2px;
218 cursor: pointer;
218 @extend %truncate-text;219 @extend %truncate-text;
219}220}
assets/stylesheets/pages/_mdn.scss+25−0
@@ -30,6 +30,7 @@ · 29 unmodified lines
30 .notice,30 .notice,
31 .warning,31 .warning,
32 .overheadIndicator,32 .overheadIndicator,
33 .blockIndicator,
33 .syntaxbox, // CSS, JavaScript34 .syntaxbox, // CSS, JavaScript
34 .twopartsyntaxbox, // CSS35 .twopartsyntaxbox, // CSS
35 .inheritsbox, // JavaScript36 .inheritsbox, // JavaScript
@@ -104,4 +105,28 @@ · 68 unmodified lines
104 .cleared { clear: both; } // CSS/box-shadow105 .cleared { clear: both; } // CSS/box-shadow
105 106
106 code > strong { font-weight: normal; }107 code > strong { font-weight: normal; }
108
109 // Compatibility tablees
110
111 .bc-github-link {
112 float: right;
113 font-size: .75rem;
114 }
115
116 .bc-supports-yes, .bc-supports-yes + dd, .bc-supports-yes + dd + dd { background: var(--noteGreenBackground); }
117 .bc-supports-partial, .bc-supports-partial + dd, .bc-supports-partial + dd + dd { background: var(--noteOrangeBackground); }
118 .bc-supports-no, .bc-supports-no + dd, .bc-supports-no + dd + dd { background: var(--noteRedBackground); }
119
120 .bc-table {
121 min-width: 100%;
122
123 dl {
124 margin: .25rem 0 0;
125 padding: .25rem 0 0;
126 font-size: .75rem;
127 border-top: 1px solid var(--boxBorder);
128 }
129
130 dd { margin: 0; }
131 }
107}132}
assets/stylesheets/pages/_rdoc.scss+0−11
@@ -33,19 +33,8 @@ · 32 unmodified lines
33 }33 }
34 }34 }
35 35
36 .method-description { position: relative; }
37
38 .method-source-code {36 .method-source-code {
39 display: none;37 display: none;
40 position: absolute;
41 z-index: 1;
42 top: 0;
43 left: -1em;
44 right: 0;
45 background: var(--contentBackground);
46 box-shadow: 0 1em 1em 1em var(--contentBackground);
47
48 > pre { margin: 0; }
49 }38 }
50 39
51 // Rails guides40 // Rails guides
docs/adding-docs.md+23−0
@@ -0,0 +1,23 @@
1Adding a documentation may look like a daunting task but once you get the hang of it, it's actually quite simple. Don't hesitate to ask for help [in Gitter](https://gitter.im/FreeCodeCamp/DevDocs) if you ever get stuck.
2
3**Note:** please read the [contributing guidelines](../.github/CONTRIBUTING.md) before submitting a new documentation.
4
51. Create a subclass of `Docs::UrlScraper` or `Docs::FileScraper` in the `lib/docs/scrapers/` directory. Its name should be the [PascalCase](http://api.rubyonrails.org/classes/String.html#method-i-camelize) equivalent of the filename (e.g. `my_doc` → `MyDoc`)
62. Add the appropriate class attributes and filter options (see the [Scraper Reference](./scraper-reference.md) page).
73. Check that the scraper is listed in `thor docs:list`.
84. Create filters specific to the scraper in the `lib/docs/filters/[my_doc]/` directory and add them to the class's [filter stacks](./scraper-reference.md#filter-stacks). You may create any number of filters but will need at least the following two:
9 * A [`CleanHtml`](./filter-reference.md#cleanhtmlfilter) filter whose task is to clean the HTML markup (e.g. adding `id` attributes to headings) and remove everything superfluous and/or nonessential.
10 * An [`Entries`](./filter-reference.md#entriesfilter) filter whose task is to determine the pages' metadata (the list of entries, each with a name, type and path).
11 The [Filter Reference](./filter-reference.md) page has all the details about filters.
125. Using the `thor docs:page [my_doc] [path]` command, check that the scraper works properly. Files will appear in the `public/docs/[my_doc]/` directory (but not inside the app as the command doesn't touch the index). `path` in this case refers to either the remote path (if using `UrlScraper`) or the local path (if using `FileScraper`).
136. Generate the full documentation using the `thor docs:generate [my_doc] --force` command. Additionally, you can use the `--verbose` option to see which files are being created/updated/deleted (useful to see what changed since the last run), and the `--debug` option to see which URLs are being requested and added to the queue (useful to pin down which page adds unwanted URLs to the queue).
147. Start the server, open the app, enable the documentation, and see how everything plays out.
158. Tweak the scraper/filters and repeat 5) and 6) until the pages and metadata are ok.
169. To customize the pages' styling, create an SCSS file in the `assets/stylesheets/pages/` directory and import it in both `application.css.scss` AND `application-dark.css.scss`. Both the file and CSS class should be named `_[type]` where [type] is equal to the scraper's `type` attribute (documentations with the same type share the same custom CSS and JS). Setting the type to `simple` will apply the general styling rules in `assets/stylesheets/pages/_simple.scss`, which can be used for documentations where little to no CSS changes are needed.
1710. To add syntax highlighting or execute custom JavaScript on the pages, create a file in the `assets/javascripts/views/pages/` directory (take a look at the other files to see how it works).
1811. Add the documentation's icon in the `public/icons/docs/[my_doc]/` directory, in both 16x16 and 32x32-pixels formats. It'll be added to the icon spritesheet after your pull request is merged.
1912. Add the documentation's copyright details to the list in `assets/javascripts/templates/pages/about_tmpl.coffee`. This is the data shown in the table on the [about](https://devdocs.io/about) page, and is ordered alphabetically. Simply copying an existing item, placing it in the right slot and updating the values to match the new scraper will do the job.
20
21If the documentation includes more than a few hundreds pages and is available for download, try to scrape it locally (e.g. using `FileScraper`). It'll make the development process much faster and avoids putting too much load on the source site. (It's not a problem if your scraper is coupled to your local setup, just explain how it works in your pull request.)
22
23Finally, try to document your scraper and filters' behavior as much as possible using comments (e.g. why some URLs are ignored, HTML markup removed, metadata that way, etc.). It'll make updating the documentation much easier.
docs/filter-reference.md+224−0
@@ -0,0 +1,224 @@
1**Table of contents:**
2
3* [Overview](#overview)
4* [Instance methods](#instance-methods)
5* [Core filters](#core-filters)
6* [Custom filters](#custom-filters)
7 - [CleanHtmlFilter](#cleanhtmlfilter)
8 - [EntriesFilter](#entriesfilter)
9
10## Overview
11
12Filters use the [HTML::Pipeline](https://github.com/jch/html-pipeline) library. They take an HTML string or [Nokogiri](http://nokogiri.org/) node as input, optionally perform modifications and/or extract information from it, and then outputs the result. Together they form a pipeline where each filter hands its output to the next filter's input. Every documentation page passes through this pipeline before being copied on the local filesystem.
13
14Filters are subclasses of the [`Docs::Filter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/core/filter.rb) class and require a `call` method. A basic implementation looks like this:
15
16```ruby
17module Docs
18 class CustomFilter < Filter
19 def call
20 doc
21 end
22 end
23end
24```
25
26Filters which manipulate the Nokogiri node object (`doc` and related methods) are _HTML filters_ and must not manipulate the HTML string (`html`). Vice-versa, filters which manipulate the string representation of the document are _text filters_ and must not manipulate the Nokogiri node object. The two types are divided into two stacks within the scrapers. These stacks are then combined into a pipeline that calls the HTML filters before the text filters (more details [here](./scraper-reference.md#filter-stacks)). This is to avoid parsing the document multiple times.
27
28The `call` method must return either `doc` or `html`, depending on the type of filter.
29
30## Instance methods
31
32* `doc` [Nokogiri::XML::Node]
33 The Nokogiri representation of the container element.
34 See [Nokogiri's API docs](http://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node) for the list of available methods.
35
36* `html` [String]
37 The string representation of the container element.
38
39* `context` [Hash] **(frozen)**
40 The scraper's `options` along with a few additional keys: `:base_url`, `:root_url`, `:root_page` and `:url`.
41
42* `result` [Hash]
43 Used to store the page's metadata and pass back information to the scraper.
44 Possible keys:
45
46 - `:path` — the page's normalized path
47 - `:store_path` — the path where the page will be stored (equal to `:path` with `.html` at the end)
48 - `:internal_urls` — the list of distinct internal URLs found within the page
49 - `:entries` — the [`Entry`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/core/models/entry.rb) objects to add to the index
50
51* `css`, `at_css`, `xpath`, `at_xpath`
52 Shortcuts for `doc.css`, `doc.xpath`, etc.
53
54* `base_url`, `current_url`, `root_url` [Docs::URL]
55 Shortcuts for `context[:base_url]`, `context[:url]`, and `context[:root_url]` respectively.
56
57* `root_path` [String]
58 Shortcut for `context[:root_path]`.
59
60* `subpath` [String]
61 The sub-path from the base URL of the current URL.
62 _Example: if `base_url` equals `example.com/docs` and `current_url` equals `example.com/docs/file?raw`, the returned value is `/file`._
63
64* `slug` [String]
65 The `subpath` removed of any leading slash or `.html` extension.
66 _Example: if `subpath` equals `/dir/file.html`, the returned value is `dir/file`._
67
68* `root_page?` [Boolean]
69 Returns `true` if the current page is the root page.
70
71* `initial_page?` [Boolean]
72 Returns `true` if the current page is the root page or its subpath is one of the scraper's `initial_paths`.
73
74## Core filters
75
76* [`ContainerFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/container.rb) — changes the root node of the document (remove everything outside)
77* [`CleanHtmlFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/clean_html.rb) — removes HTML comments, `<script>`, `<style>`, etc.
78* [`NormalizeUrlsFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/normalize_urls.rb) — replaces all URLs with their fully qualified counterpart
79* [`InternalUrlsFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/internal_urls.rb) — detects internal URLs (the ones to scrape) and replaces them with their unqualified, relative counterpart
80* [`NormalizePathsFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/normalize_paths.rb) — makes the internal paths consistent (e.g. always end with `.html`)
81* [`CleanLocalUrlsFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/clean_local_urls.rb) — removes links, iframes and images pointing to localhost (`FileScraper` only)
82* [`InnerHtmlFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/inner_html.rb) — converts the document to a string
83* [`CleanTextFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/clean_text.rb) — removes empty nodes
84* [`AttributionFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/attribution.rb) — appends the license info and link to the original document
85* [`TitleFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/title.rb) — prepends the document with a title (disabled by default)
86* [`EntriesFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/entries.rb) — abstract filter for extracting the page's metadata
87
88## Custom filters
89
90Scrapers can have any number of custom filters but require at least the two described below.
91
92**Note:** filters are located in the [`lib/docs/filters`](https://github.com/Thibaut/devdocs/tree/master/lib/docs/filters/) directory. The class's name must be the [CamelCase](http://api.rubyonrails.org/classes/String.html#method-i-camelize) equivalent of the filename.
93
94### `CleanHtmlFilter`
95
96The `CleanHtml` filter is tasked with cleaning the HTML markup where necessary and removing anything superfluous or nonessential. Only the core documentation should remain at the end.
97
98Nokogiri's many jQuery-like methods make it easy to search and modify elements — see the [API docs](http://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node).
99
100Here's an example implementation that covers the most common use-cases:
101
102```ruby
103module Docs
104 class MyScraper
105 class CleanHtmlFilter < Filter
106 def call
107 css('hr').remove
108 css('#changelog').remove if root_page?
109
110 # Set id attributes on <h3> instead of an empty <a>
111 css('h3').each do |node|
112 node['id'] = node.at_css('a')['id']
113 end
114
115 # Make proper table headers
116 css('td.header').each do |node|
117 node.name = 'th'
118 end
119
120 # Remove code highlighting
121 css('pre').each do |node|
122 node.content = node.content
123 end
124
125 doc
126 end
127 end
128 end
129end
130```
131
132**Notes:**
133
134* Empty elements will be automatically removed by the core `CleanTextFilter` later in the pipeline's execution.
135* Although the goal is to end up with a clean version of the page, try to keep the number of modifications to a minimum, so as to make the code easier to maintain. Custom CSS is the preferred way of normalizing the pages (except for hiding stuff which should always be done by removing the markup).
136* Try to document your filter's behavior as much as possible, particularly modifications that apply only to a subset of pages. It'll make updating the documentation easier.
137
138### `EntriesFilter`
139
140The `Entries` filter is responsible for extracting the page's metadata, represented by a set of _entries_, each with a name, type and path.
141
142The following two models are used under the hood to represent the metadata:
143
144* [`Entry(name, type, path)`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/core/models/entry.rb)
145* [`Type(name, slug, count)`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/core/models/type.rb)
146
147Each scraper must implement its own `EntriesFilter` by subclassing the [`Docs::EntriesFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/entries.rb) class. The base class already implements the `call` method and includes four methods which the subclasses can override:
148
149* `get_name` [String]
150 The name of the default entry (aka. the page's name).
151 It is usually guessed from the `slug` (documented above) or by searching the HTML markup.
152 **Default:** modified version of `slug` (underscores are replaced with spaces and forward slashes with dots)
153
154* `get_type` [String]
155 The type of the default entry (aka. the page's type).
156 Entries without a type can be searched for but won't be listed in the app's sidebar (unless no other entries have a type).
157 **Default:** `nil`
158
159* `include_default_entry?` [Boolean]
160 Whether to include the default entry.
161 Used when a page consists of multiple entries (returned by `additional_entries`) but doesn't have a name/type of its own, or to remove a page from the index (if it has no additional entries), in which case it won't be copied on the local filesystem and any link to it in the other pages will be broken (as explained on the [Scraper Reference](./scraper-reference.md) page, this is used to keep the `:skip` / `:skip_patterns` options to a maintainable size, or if the page includes links that can't reached from anywhere else).
162 **Default:** `true`
163
164* `additional_entries` [Array]
165 The list of additional entries.
166 Each entry is represented by an Array of three attributes: its name, fragment identifier, and type. The fragment identifier refers to the `id` attribute of the HTML element (usually a heading) that the entry relates to. It is combined with the page's path to become the entry's path. If absent or `nil`, the page's path is used. If the type is absent or `nil`, the default `type` is used.
167 Example: `[ ['One'], ['Two', 'id'], ['Three', nil, 'type'] ]` adds three additional entries, the first one named "One" with the default path and type, the second one named "Two" with the URL fragment "#id" and the default type, and the third one named "Three" with the default path and the type "type".
168 The list is usually constructed by running through the markup. Exceptions can also be hard-coded for specific pages.
169 **Default:** `[]`
170
171The following accessors are also available, but must not be overridden:
172
173* `name` [String]
174 Memoized version of `get_name` (`nil` for the root page).
175
176* `type` [String]
177 Memoized version of `get_type` (`nil` for the root page).
178
179**Notes:**
180
181* Leading and trailing whitespace is automatically removed from names and types.
182* Names must be unique across the documentation and as short as possible (ideally less than 30 characters). Whenever possible, methods should be differentiated from properties by appending `()`, and instance methods should be differentiated from class methods using the `Class#method` or `object.method` conventions.
183* You can call `name` from `get_type` or `type` from `get_name` but doing both will cause a stack overflow (i.e. you can infer the name from the type or the type from the name, but you can't do both at the same time). Don't call `get_name` or `get_type` directly as their value isn't memoized.
184* The root page has no name and no type (both are `nil`). `get_name` and `get_type` won't get called with the page (but `additional_entries` will).
185* `Docs::EntriesFilter` is an _HTML filter_. It must be added to the scraper's `html_filters` stack.
186* Try to document the code as much as possible, particularly the special cases. It'll make updating the documentation easier.
187
188**Example:**
189
190```ruby
191module Docs
192 class MyScraper
193 class EntriesFilter < Docs::EntriesFilter
194 def get_name
195 node = at_css('h1')
196 result = node.content.strip
197 result << ' event' if type == 'Events'
198 result << '()' if node['class'].try(:include?, 'function')
199 result
200 end
201
202 def get_type
203 object, method = *slug.split('/')
204 method ? object : 'Miscellaneous'
205 end
206
207 def additional_entries
208 return [] if root_page?
209
210 css('h2').map do |node|
211 [node.content, node['id']]
212 end
213 end
214
215 def include_default_entry?
216 !at_css('.obsolete')
217 end
218 end
219 end
220end
221```
222
223
224return [[Home]]
docs/maintainers.md+102−0
@@ -0,0 +1,102 @@
1# Maintainer's Guide
2
3This document is intended for [DevDocs maintainers](#list-of-maintainers).
4
5## Merging pull requests
6
7- PRs should be approved by at least one maintainer before being merged.
8
9- PRs that add or update documentations should always be merged locally, and the app deployed, before the merge is pushed to GitHub.
10
11 This workflow is required because there is a dependency between the local and production environments. The `thor docs:download` command downloads documentations from production files uploaded by the `thor docs:upload` command. If a PR adding a new documentation is merged and pushed to GitHub before the files have been uploaded to production, the `thor docs:download` will fail for the new documentation and the docker container will not build properly until the new documentation is deployed to production.
12
13## Updating docs
14
15The process for updating docs is as follow:
16
17- Make version/release changes in the scraper file.
18- If needed, update the copyright notice of the documentation in the scraper file (`options[:attribution]`) and the about page (`about_tmpl.coffee`). The copyright notice must be the same as the one on the original documentation.
19- Run `thor docs:generate`.
20- Make sure the documentation still works well. The `thor docs:generate` command outputs a summary of the changes, which helps identifying issues (e.g. deleted files) and new pages to check out in the app. Verify locally that everything works, especially the files that were created (if any), and that the categorization of entries is still good. Often, updates will require code changes to tweak some new markup in the source website or categorize new entries.
21- Commit the changes (protip: use the `thor docs:commit` command documented below).
22- Optional: do more updates.
23- Run `thor docs:upload` (documented below).
24- Push to GitHub to [deploy the app](#deploying-devdocs) and verify that everything works in production.
25- Run `thor docs:clean` (documented below).
26
27Note: changes to `public/docs/docs.json` should never be committed. This file reflects which documentations have been downloaded or generated locally, which is always none on a fresh `git clone`.
28
29## Setup requirements
30
31In order to deploy DevDocs, you must:
32
33- be given access to Heroku, [configure the Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) on your computer, and familiarize yourself with Heroku's UI and CLI, as well as that of New Relic (accessible through [the Heroku dashboard](https://dashboard.heroku.com/apps/devdocs)).
34
35- be given access to DevDocs's [Sentry instance](https://sentry.io/devdocs/devdocs-js/) (for JS error tracking) and familiarize yourself with its UI.
36
37- be provided with DevDocs's S3 credentials, and install (`brew install awscli` on macOS) and [configure](https://docs.aws.amazon.com/cli/latest/reference/configure/) the AWS CLI on your computer. The configuration must add a named profile called "devdocs":
38 ```
39 aws configure --profile devdocs
40 ```
41
42- be provided with DevDocs's MaxCDN push zone credentials, and add them to your `.bash_profile` as such:
43 ```
44 export DEVDOCS_DL_USERNAME="username"
45 export DEVDOCS_DL_PASSWORD="password"
46 ```
47
48## Thor commands
49
50In addition to the [publicly-documented commands](https://github.com/freeCodeCamp/devdocs#available-commands), the following commands are aimed at DevDocs maintainers:
51
52- `thor docs:package`
53
54 Generates packages for one or more documentations. Those packages are intended to be uploaded to DevDocs's MaxCDN push zone by maintainers via the `thor docs:upload` command, and downloaded by users via the `thor docs:download` command.
55
56 Versions can be specified as such: `thor docs:package rails@5.2 node@10\ LTS`.
57
58 Packages can also be automatically generated during the scraping process by passing the `--package` option to `thor docs:generate`.
59
60- `thor docs:upload`
61
62 This command does two operations:
63
64 1. sync the files for the specified documentations with S3 (used by the Heroku app);
65 2. upload the documentations' packages to DevDocs's MaxCDN push zone (used by the `thor docs:download` command).
66
67 For the command to work, you must have the AWS CLI and MaxCDN credentials configured as indicated above.
68
69 **Important:** the app should always be deployed immediately after this command has finished running. Do not run this command unless you are able and ready to deploy DevDocs.
70
71 To upload all documentations that are packaged on your computer, run `thor docs:upload --packaged`.
72 To test your configuration and the effect of this command without uploading anything, pass the `--dryrun` option.
73
74- `thor docs:commit`
75
76 Shortcut command to create a Git commit for a given documentation once it has been updated. Scraper and `assets/` file changes will be committed. The commit message will include the most recent version that the documentation was updated to. If some files were missed by the commit, use `git commit --amend` to add them to the commit. The command may be run before `thor docs:upload` is run, but the commit should not be pushed to GitHub before the files have been uploaded and the app deployed.
77
78- `thor docs:clean`
79
80 Shortcut command to delete all package files (once uploaded via `thor docs:upload`, they are not needed anymore).
81
82## Deploying DevDocs
83
84Once docs have been uploaded via `thor docs:upload` (if applicable), you can push to the DevDocs master branch (or merge the PR containing the updates). If the Travis build succeeds, the Heroku application will be deployed automatically.
85
86- If you're deploying documentation updates, verify that the documentations work properly once the deploy is done. Keep in mind that you'll need to wait a few seconds for the service worker to finish caching the new assets. You should see a "DevDocs has been updated" notification appear when the caching is done, after which you need to refresh the page to see the changes.
87- If you're deploying frontend changes, monitor [Sentry](https://sentry.io/devdocs/devdocs-js/) for new JS errors once the deploy is done.
88- If you're deploying server changes, monitor New Relic (accessible through [the Heroku dashboard](https://dashboard.heroku.com/apps/devdocs)) for Ruby exceptions and throughput or response time changes once the deploy is done.
89
90If any issue arises, run `heroku rollback` to rollback to the previous version of the app (this can also be done via Heroku's UI). Note that this will not revert changes made to documentation files that were uploaded via `thor docs:upload`. Try and fix the issue as quickly as possible, then re-deploy the app. Reach out to other maintainers if you need help.
91
92If this is your first deploy, make sure another maintainer is around to assist.
93
94## List of maintainers
95
96- [Jed Fox](https://github.com/j-f1)
97- [Jasper van Merle](https://github.com/jmerle)
98- [Ahmad Abdolsaheb](https://github.com/ahmadabdolsaheb)
99- [Mrugesh Mohapatra](https://github.com/raisedadead)
100- [Thibaut Courouble](https://github.com/thibaut)
101
102Interested in helping maintain DevDocs? Come talk to us on [Gitter](https://gitter.im/FreeCodeCamp/DevDocs) :)
docs/scraper-reference.md+186−0
@@ -0,0 +1,186 @@
1**Table of contents:**
2
3* [Overview](#overview)
4* [Configuration](#configuration)
5 - [Attributes](#attributes)
6 - [Filter stacks](#filter-stacks)
7 - [Filter options](#filter-options)
8
9## Overview
10
11Starting from a root URL, scrapers recursively follow links that match a set of rules, passing each valid response through a chain of filters before writing the file on the local filesystem. They also create an index of the pages' metadata (determined by one filter), which is dumped into a JSON file at the end.
12
13Scrapers rely on the following libraries:
14
15* [Typhoeus](https://github.com/typhoeus/typhoeus) for making HTTP requests
16* [HTML::Pipeline](https://github.com/jch/html-pipeline) for applying filters
17* [Nokogiri](http://nokogiri.org/) for parsing HTML
18
19There are currently two kinds of scrapers: [`UrlScraper`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/core/scrapers/url_scraper.rb) which downloads files via HTTP and [`FileScraper`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/core/scrapers/file_scraper.rb) which reads them from the local filesystem. They function almost identically (both use URLs), except that `FileScraper` substitutes the base URL with a local path before reading a file. `FileScraper` uses the placeholder `localhost` base URL by default and includes a filter to remove any URL pointing to it at the end.
20
21To be processed, a response must meet the following requirements:
22
23* 200 status code
24* HTML content type
25* effective URL (after redirection) contained in the base URL (explained below)
26
27(`FileScraper` only checks if the file exists and is not empty.)
28
29Each URL is requested only once (case-insensitive).
30
31## Configuration
32
33Configuration is done via class attributes and divided into three main categories:
34
35* [Attributes](#attributes) — essential information such as name, version, URL, etc.
36* [Filter stacks](#filter-stacks) — the list of filters that will be applied to each page.
37* [Filter options](#filter-options) — the options passed to said filters.
38
39**Note:** scrapers are located in the [`lib/docs/scrapers`](https://github.com/Thibaut/devdocs/tree/master/lib/docs/scrapers/) directory. The class's name must be the [CamelCase](http://api.rubyonrails.org/classes/String.html#method-i-camelize) equivalent of the filename.
40
41### Attributes
42
43* `name` [String]
44 Must be unique.
45 Defaults to the class's name.
46
47* `slug` [String]
48 Must be unique, lowercase, and not include dashes (underscores are ok).
49 Defaults to `name` lowercased.
50
51* `type` [String] **(required, inherited)**
52 Defines the CSS class name (`_[type]`) and custom JavaScript class (`app.views.[Type]Page`) that will be added/loaded on each page. Documentations sharing a similar structure (e.g. generated with the same tool or originating from the same website) should use the same `type` to avoid duplicating the CSS and JS.
53 Must include lowercase letters only.
54
55* `release` [String] **(required)**
56 The version of the software at the time the scraper was last run. This is only informational and doesn't affect the scraper's behavior.
57
58* `base_url` [String] **(required in `UrlScraper`)**
59 The documents' location. Only URLs _inside_ the `base_url` will be scraped. "inside" more or less means "starting with" except that `/docs` is outside `/doc` (but `/doc/` is inside).
60 Defaults to `localhost` in `FileScraper`. _(Note: any iframe, image, or skipped link pointing to localhost will be removed by the `CleanLocalUrls` filter; the value should be overridden if the documents are available online.)_
61 Unless `root_path` is set, the root/initial URL is equal to `base_url`.
62
63* `root_path` [String] **(inherited)**
64 The path from the `base_url` of the root URL.
65
66* `initial_paths` [Array] **(inherited)**
67 A list of paths (from the `base_url`) to add to the initial queue. Useful for scraping isolated documents.
68 Defaults to `[]`. _(Note: the `root_path` is added to the array at runtime.)_
69
70* `dir` [String] **(required, `FileScraper` only)**
71 The absolute path where the files are located on the local filesystem.
72 _Note: `FileScraper` works exactly like `UrlScraper` (manipulating the same kind of URLs) except that it substitutes `base_url` with `dir` in order to read files instead of making HTTP requests._
73
74* `params` [Hash] **(inherited, `UrlScraper` only)**
75 Query string parameters to append to every URL. (e.g. `{ format: 'raw' }` → `?format=raw`)
76 Defaults to `{}`.
77
78* `abstract` [Boolean]
79 Make the scraper abstract / not runnable. Used for sharing behavior with other scraper classes (e.g. all MDN scrapers inherit from the abstract [`Mdn`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/scrapers/mdn/mdn.rb) class).
80 Defaults to `false`.
81
82### Filter stacks
83
84Each scraper has two [filter](https://github.com/Thibaut/devdocs/blob/master/lib/docs/core/filter.rb) [stacks](https://github.com/Thibaut/devdocs/blob/master/lib/docs/core/filter_stack.rb): `html_filters` and `text_filters`. They are combined into a pipeline (using the [HTML::Pipeline](https://github.com/jch/html-pipeline) library) which causes each filter to hand its output to the next filter's input.
85
86HTML filters are executed first and manipulate a parsed version of the document (a [Nokogiri](http://nokogiri.org/Nokogiri/XML/Node.html) node object), whereas text filters manipulate the document as a string. This separation avoids parsing the document multiple times.
87
88Filter stacks are like sorted sets. They can modified using the following methods:
89
90```ruby
91push(*names) # append one or more filters at the end
92insert_before(index, *names) # insert one filter before another (index can be a name)
93insert_after(index, *names) # insert one filter after another (index can be a name)
94replace(index, name) # replace one filter with another (index can be a name)
95```
96
97"names" are `require` paths relative to `Docs` (e.g. `jquery/clean_html` → `Docs::Jquery::CleanHtml`).
98
99Default `html_filters`:
100
101* [`ContainerFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/container.rb) — changes the root node of the document (remove everything outside)
102* [`CleanHtmlFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/clean_html.rb) — removes HTML comments, `<script>`, `<style>`, etc.
103* [`NormalizeUrlsFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/normalize_urls.rb) — replaces all URLs with their fully qualified counterpart
104* [`InternalUrlsFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/internal_urls.rb) — detects internal URLs (the ones to scrape) and replaces them with their unqualified, relative counterpart
105* [`NormalizePathsFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/normalize_paths.rb) — makes the internal paths consistent (e.g. always end with `.html`)
106* [`CleanLocalUrlsFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/clean_local_urls.rb) — removes links, iframes and images pointing to localhost (`FileScraper` only)
107
108Default `text_filters`:
109
110* [`InnerHtmlFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/inner_html.rb) — converts the document to a string
111* [`CleanTextFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/clean_text.rb) — removes empty nodes
112* [`AttributionFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/attribution.rb) — appends the license info and link to the original document
113
114Additionally:
115
116* [`TitleFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/title.rb) is a core HTML filter, disabled by default, which prepends the document with a title (`<h1>`).
117* [`EntriesFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/entries.rb) is an abstract HTML filter that each scraper must implement and responsible for extracting the page's metadata.
118
119### Filter options
120
121The filter options are stored in the `options` Hash. The Hash is inheritable (a recursive copy) and empty by default.
122
123More information about how filters work is available on the [Filter Reference](./filter-reference.md) page.
124
125* [`ContainerFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/container.rb)
126
127 - `:container` [String or Proc]
128 A CSS selector of the container element. Everything outside of it will be removed and become unavailable to the other filters. If more than one element match the selector, the first one inside the DOM is used. If no elements match the selector, an error is raised.
129 If the value is a Proc, it is called for each page with the filter instance as argument, and should return a selector or `nil`.
130 The default container is the `<body>` element.
131 _Note: links outside of the container element will not be followed by the scraper. To remove links that should be followed, use a [`CleanHtml`](./filter-reference.md#cleanhtmlfilter) filter later in the stack._
132
133* [`NormalizeUrlsFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/normalize_urls.rb)
134 The following options are used to modify URLs in the pages. They are useful to remove duplicates (when the same page is accessible from multiple URLs) and fix websites that have a bunch of redirections in place (when URLs that should be scraped, aren't, because they are behind a redirection which is outside of the `base_url` — see the MDN scrapers for examples of this).
135
136 - `:replace_urls` [Hash]
137 Replaces all instances of a URL with another.
138 Format: `{ 'original_url' => 'new_url' }`
139 - `:replace_paths` [Hash]
140 Replaces all instances of a sub-path (path from the `base_url`) with another.
141 Format: `{ 'original_path' => 'new_path' }`
142 - `:fix_urls` [Proc]
143 Called with each URL. If the returned value is `nil`, the URL isn't modified. Otherwise the returned value is used as replacement.
144
145 _Note: before these rules are applied, all URLs are converted to their fully qualified counterpart (http://...)._
146
147* [`InternalUrlsFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/internal_urls.rb)
148
149 Internal URLs are the ones _inside_ the scraper's `base_url` ("inside" more or less means "starting with", except that `/docs` is outside `/doc`). They will be scraped unless excluded by one of the following rules. All internal URLs are converted to relative URLs inside the pages.
150
151 - `:skip_links` [Boolean or Proc]
152 If `false`, does not convert or follow any internal URL (creating a single-page documentation).
153 If the value is a Proc, it is called for each page with the filter instance as argument.
154 - `:follow_links` [Proc]
155 Called for page with the filter instance as argument. If the returned value is `false`, does not add internal URLs to the queue.
156 - `:trailing_slash` [Boolean]
157 If `true`, adds a trailing slash to all internal URLs. If `false`, removes it.
158 This is another option used to remove duplicate pages.
159 - `:skip` [Array]
160 Ignores internal URLs whose sub-paths (path from the `base_url`) are in the Array (case-insensitive).
161 - `:skip_patterns` [Array]
162 Ignores internal URLs whose sub-paths match any Regexp in the Array.
163 - `:only` [Array]
164 Ignores internal URLs whose sub-paths aren't in the Array (case-insensitive) and don't match any Regexp in `:only_patterns`.
165 - `:only_patterns` [Array]
166 Ignores internal URLs whose sub-paths don't match any Regexp in the Array and aren't in `:only`.
167
168 If the scraper has a `root_path`, the empty and `/` paths are automatically skipped.
169 If `:only` or `:only_patterns` is set, the root path is automatically added to `:only`.
170
171 _Note: pages can be excluded from the index based on their content using the [`Entries`](./filter-reference.md#entriesfilter) filter. However, their URLs will still be converted to relative in the other pages and trying to open them will return a 404 error. Although not ideal, this is often better than having to maintain a long list of `:skip` URLs._
172
173* [`AttributionFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/attribution.rb)
174
175 - `:attribution` [String] **(required)**
176 An HTML string with the copyright and license information. See the other scrapers for examples.
177
178* [`TitleFilter`](https://github.com/Thibaut/devdocs/blob/master/lib/docs/filters/core/title.rb)
179
180 - `:title` [String or Boolean or Proc]
181 Unless the value is `false`, adds a title to every page.
182 If the value is `nil`, the title is the name of the page as determined by the [`Entries`](./filter-reference.md#entriesfilter) filter. Otherwise the title is the String or the value returned by the Proc (called for each page, with the filter instance as argument). If the Proc returns `nil` or `false`, no title is added.
183 - `:root_title` [String or Boolean]
184 Overrides the `:title` option for the root page only.
185
186 _Note: this filter is disabled by default._
lib/app.rb+36−21
@@ -12,7 +12,7 @@ class App < Sinatra::Application · 11 unmodified lines
12 Rack::Mime::MIME_TYPES['.webapp'] = 'application/x-web-app-manifest+json'12 Rack::Mime::MIME_TYPES['.webapp'] = 'application/x-web-app-manifest+json'
13 13
14 configure do14 configure do
15 use Rack::SslEnforcer, only_environments: ['production', 'test'], hsts: false, force_secure_cookies: false15 use Rack::SslEnforcer, only_environments: ['production', 'test'], hsts: true, force_secure_cookies: false
16 16
17 set :sentry_dsn, ENV['SENTRY_DSN']17 set :sentry_dsn, ENV['SENTRY_DSN']
18 set :protection, except: [:frame_options, :xss_header]18 set :protection, except: [:frame_options, :xss_header]
@@ -37,6 +37,9 @@ class App < Sinatra::Application · 18 unmodified lines
37 37
38 set :csp, false38 set :csp, false
39 39
40 require 'docs'
41 Docs.generate_manifest
42
40 Dir[docs_path, root.join(assets_prefix, '*/')].each do |path|43 Dir[docs_path, root.join(assets_prefix, '*/')].each do |path|
41 sprockets.append_path(path)44 sprockets.append_path(path)
42 end45 end
@@ -192,35 +195,47 @@ class App < Sinatra::Application · 149 unmodified lines
192 request.query_string.empty? ? nil : "?#{request.query_string}"195 request.query_string.empty? ? nil : "?#{request.query_string}"
193 end196 end
194 197
195 def manifest_asset_urls198 def service_worker_asset_urls
196 @@manifest_asset_urls ||= [199 @@service_worker_asset_urls ||= [
197 javascript_path('application', asset_host: false),200 javascript_path('application', asset_host: false),
198 stylesheet_path('application'),201 stylesheet_path('application'),
199 image_path('docs-1.png'),202 image_path('docs-1.png'),
200 image_path('docs-1@2x.png'),203 image_path('docs-1@2x.png'),
201 image_path('docs-2.png'),204 image_path('docs-2.png'),
202 image_path('docs-2@2x.png'),205 image_path('docs-2@2x.png'),
203 asset_path('docs.js')206 asset_path('docs.js'),
204 ]207 App.production? ? nil : javascript_path('debug'),
208 ].compact
205 end209 end
206 210
207 def app_size211 # Returns a cache name for the service worker to use which changes if any of the assets changes
208 @app_size ||= memoized_cookies['size'].nil? ? '20rem' : "#{memoized_cookies['size']}px"212 # When a manifest exist, this name is only created once based on the asset manifest because it never changes without a server restart
209 end213 # If a manifest does not exist, it is created every time this method is called because the assets can change while the server is running
214 def service_worker_cache_name
215 if File.exist?(App.assets_manifest_path)
216 if defined?(@@service_worker_cache_name)
217 return @@service_worker_cache_name
218 end
210 219
211 def app_layout220 digest = Sprockets::Manifest
212 memoized_cookies['layout']221 .new(nil, App.assets_manifest_path)
213 end222 .files
223 .values
224 .map {|file| file["digest"]}
225 .join
214 226
215 def app_theme227 return @@service_worker_cache_name ||= Digest::MD5.hexdigest(digest)
216 @app_theme ||= memoized_cookies['dark'].nil? ? 'default' : 'dark'228 else
217 end229 paths = App.sprockets
230 .each_file
231 .to_a
232 .reject {|file| file.start_with?(App.docs_path)}
218 233
219 def dark_theme?234 return App.sprockets.pack_hexdigest(App.sprockets.files_digest(paths))
220 app_theme == 'dark'235 end
221 end236 end
222 237
223 def redirect_via_js(path) # courtesy of HTML5 App Cache238 def redirect_via_js(path)
224 response.set_cookie :initial_path, value: path, expires: Time.now + 15, path: '/'239 response.set_cookie :initial_path, value: path, expires: Time.now + 15, path: '/'
225 redirect '/', 302240 redirect '/', 302
226 end241 end
@@ -243,15 +258,15 @@ class App < Sinatra::Application · 16 unmodified lines
243 end258 end
244 end259 end
245 260
246 get '/manifest.appcache' do261 get '/service-worker.js' do
247 content_type 'text/cache-manifest'262 content_type 'application/javascript'
248 expires 0, :'no-cache'263 expires 0, :'no-cache'
249 erb :manifest264 erb :'service-worker.js'
250 end265 end
251 266
252 get '/' do267 get '/' do
253 return redirect "/#q=#{params[:q]}" if params[:q]268 return redirect "/#q=#{params[:q]}" if params[:q]
254 return redirect '/' unless request.query_string.empty? # courtesy of HTML5 App Cache269 return redirect '/' unless request.query_string.empty?
255 response.headers['Content-Security-Policy'] = settings.csp if settings.csp270 response.headers['Content-Security-Policy'] = settings.csp if settings.csp
256 erb :index271 erb :index
257 end272 end
lib/docs.rb+17−0
@@ -29,6 +29,7 @@ module Docs · 28 unmodified lines
29 self.rescue_errors = false29 self.rescue_errors = false
30 30
31 class DocNotFound < NameError; end31 class DocNotFound < NameError; end
32 class SetupError < StandardError; end
32 33
33 def self.all34 def self.all
34 Dir["#{root_path}/docs/scrapers/**/*.rb"].35 Dir["#{root_path}/docs/scrapers/**/*.rb"].
@@ -74,6 +75,22 @@ module Docs · 39 unmodified lines
74 end75 end
75 end76 end
76 77
78 def self.find_by_slug(slug, version = nil)
79 doc = all.find { |klass| klass.slug == slug }
80
81 unless doc
82 raise DocNotFound.new(%(could not find doc with "#{slug}"), slug)
83 end
84
85 if version.present?
86 version = doc.versions.find { |klass| klass.version == version || klass.version_slug == version }
87 raise DocNotFound.new(%(could not find version "#{version}" for doc "#{doc.name}"), doc.name) unless version
88 doc = version
89 end
90
91 doc
92 end
93
77 def self.generate_page(name, version, page_id)94 def self.generate_page(name, version, page_id)
78 find(name, version).store_page(store, page_id)95 find(name, version).store_page(store, page_id)
79 end96 end
lib/docs/core/doc.rb+6−0
@@ -95,6 +95,9 @@ module Docs · 94 unmodified lines
95 false95 false
96 end96 end
97 end97 end
98 rescue Docs::SetupError => error
99 puts "ERROR: #{error.message}"
100 false
98 end101 end
99 102
100 def store_pages(store)103 def store_pages(store)
@@ -118,6 +121,9 @@ module Docs · 17 unmodified lines
118 false121 false
119 end122 end
120 end123 end
124 rescue Docs::SetupError => error
125 puts "ERROR: #{error.message}"
126 false
121 end127 end
122 128
123 private129 private
lib/docs/core/requester.rb+8−4
@@ -1,5 +1,7 @@
1module Docs1module Docs
2 class Requester < Typhoeus::Hydra2 class Requester < Typhoeus::Hydra
3 include Instrumentable
4
3 attr_reader :request_options5 attr_reader :request_options
4 6
5 def self.run(urls, options = {}, &block)7 def self.run(urls, options = {}, &block)
@@ -20,7 +22,7 @@ module Docs · 14 unmodified lines
20 def initialize(options = {})22 def initialize(options = {})
21 @request_options = options.extract!(:request_options)[:request_options].try(:dup) || {}23 @request_options = options.extract!(:request_options)[:request_options].try(:dup) || {}
22 options[:max_concurrency] ||= 2024 options[:max_concurrency] ||= 20
23 options[:pipelining] = false25 options[:pipelining] = 0
24 super26 super
25 end27 end
26 28
@@ -52,9 +54,11 @@ module Docs · 25 unmodified lines
52 end54 end
53 55
54 def handle_response(response)56 def handle_response(response)
55 on_response.each do |callback|57 instrument 'handle_response.requester', url: response.url do
56 result = callback.call(response)58 on_response.each do |callback|
57 result.each { |url| request(url) } if result.is_a? Array59 result = callback.call(response)
60 result.each { |url| request(url) } if result.is_a?(Array)
61 end
58 end62 end
59 end63 end
60 end64 end
lib/docs/core/scrapers/file_scraper.rb+18−7
@@ -1,14 +1,13 @@
1module Docs1module Docs
2 class FileScraper < Scraper2 class FileScraper < Scraper
3 SOURCE_DIRECTORY = File.expand_path '../../../../../docs', __FILE__
4
3 Response = Struct.new :body, :url5 Response = Struct.new :body, :url
4 6
5 class << self7 class << self
6 attr_accessor :dir
7
8 def inherited(subclass)8 def inherited(subclass)
9 super9 super
10 subclass.base_url = base_url10 subclass.base_url = base_url
11 subclass.dir = dir
12 end11 end
13 end12 end
14 13
@@ -16,13 +15,25 @@ module Docs · 1 unmodified line
16 15
17 html_filters.push 'clean_local_urls'16 html_filters.push 'clean_local_urls'
18 17
18 def source_directory
19 @source_directory ||= File.join(SOURCE_DIRECTORY, self.class.path)
20 end
21
19 private22 private
20 23
24 def assert_source_directory_exists
25 unless Dir.exists?(source_directory)
26 raise SetupError, "The #{self.class.name} scraper requires the original documentation files to be stored in the \"#{source_directory}\" directory."
27 end
28 end
29
21 def request_one(url)30 def request_one(url)
22 Response.new read_file(file_path_for(url)), URL.parse(url)31 assert_source_directory_exists
32 Response.new read_file(url_to_path(url)), URL.parse(url)
23 end33 end
24 34
25 def request_all(urls)35 def request_all(urls)
36 assert_source_directory_exists
26 queue = [urls].flatten37 queue = [urls].flatten
27 until queue.empty?38 until queue.empty?
28 result = yield request_one(queue.shift)39 result = yield request_one(queue.shift)
@@ -34,12 +45,12 @@ module Docs · 5 unmodified lines
34 response.body.present?45 response.body.present?
35 end46 end
36 47
37 def file_path_for(url)48 def url_to_path(url)
38 File.join self.class.dir, url.remove(base_url.to_s)49 url.remove(base_url.to_s)
39 end50 end
40 51
41 def read_file(path)52 def read_file(path)
42 File.read(path)53 File.read(File.join(source_directory, path))
43 rescue54 rescue
44 instrument 'warn.doc', msg: "Failed to open file: #{path}"55 instrument 'warn.doc', msg: "Failed to open file: #{path}"
45 nil56 nil
lib/docs/core/url.rb+1−1
@@ -72,7 +72,7 @@ module Docs · 71 unmodified lines
72 72
73 if base == dest73 if base == dest
74 ''74 ''
75 elsif dest.start_with? File.join(base, '')75 elsif dest.start_with?(::File.join(base, ''))
76 url.path[(path.length)..-1]76 url.path[(path.length)..-1]
77 end77 end
78 end78 end
lib/docs/filters/cpp/entries.rb+2−0
@@ -31,6 +31,8 @@ module Docs · 30 unmodified lines
31 'Keywords'31 'Keywords'
32 elsif subpath.start_with?('experimental')32 elsif subpath.start_with?('experimental')
33 'Experimental libraries'33 'Experimental libraries'
34 elsif subpath.start_with?('language/')
35 'Language'
34 elsif type = at_css('.t-navbar > div:nth-child(4) > :first-child').try(:content)36 elsif type = at_css('.t-navbar > div:nth-child(4) > :first-child').try(:content)
35 type.strip!37 type.strip!
36 type.remove! ' library'38 type.remove! ' library'
lib/docs/filters/css/entries.rb+17−10
@@ -6,13 +6,16 @@ module Docs · 5 unmodified lines
6 'CSS_Background_and_Borders' => 'Backgrounds & Borders',6 'CSS_Background_and_Borders' => 'Backgrounds & Borders',
7 'CSS_Columns' => 'Multi-column Layout',7 'CSS_Columns' => 'Multi-column Layout',
8 'CSS_Flexible_Box_Layout' => 'Flexible Box Layout',8 'CSS_Flexible_Box_Layout' => 'Flexible Box Layout',
9 'CSS_Fonts' => 'Fonts',
9 'CSS_Grid_Layout' => 'Grid Layout',10 'CSS_Grid_Layout' => 'Grid Layout',
10 'CSS_Images' => 'Images',11 'CSS_Images' => 'Images',
11 'CSS_Lists_and_Counters' => 'Lists',12 'CSS_Lists_and_Counters' => 'Lists',
12 'CSS_Transforms' => 'Transforms',13 'CSS_Transforms' => 'Transforms',
13 'Media_Queries' => 'Media Queries',14 'Media_Queries' => 'Media Queries',
15 'filter-function' => 'Filter Effects',
14 'transform-function' => 'Transforms',16 'transform-function' => 'Transforms',
15 '@media' => 'Media Queries',17 '@media' => 'Media Queries',
18 'overscroll' => 'Overscroll',
16 'text-size-adjust' => 'Miscellaneous',19 'text-size-adjust' => 'Miscellaneous',
17 'resolved_value' => 'Miscellaneous',20 'resolved_value' => 'Miscellaneous',
18 'touch-action' => 'Miscellaneous',21 'touch-action' => 'Miscellaneous',
@@ -42,7 +45,7 @@ module Docs · 23 unmodified lines
42 end45 end
43 46
44 def get_type47 def get_type
45 if slug.include?('-webkit') || slug.include?('-moz')48 if slug.include?('-webkit') || slug.include?('-moz') || slug.include?('-ms')
46 'Extensions'49 'Extensions'
47 elsif type = TYPE_BY_PATH[slug.split('/').first]50 elsif type = TYPE_BY_PATH[slug.split('/').first]
48 type51 type
@@ -66,19 +69,22 @@ module Docs · 17 unmodified lines
66 'Pseudo-Elements'69 'Pseudo-Elements'
67 elsif name.start_with?(':')70 elsif name.start_with?(':')
68 'Selectors'71 'Selectors'
72 elsif name.start_with?('display-')
73 'Display'
69 else74 else
70 'Miscellaneous'75 'Miscellaneous'
71 end76 end
72 end77 end
73 78
74 STATUSES = {79 STATUSES = {
75 'spec-Living' => 0,80 'spec-Living' => 0,
76 'spec-REC' => 1,81 'spec-REC' => 1,
77 'spec-CR' => 2,82 'spec-CR' => 2,
78 'spec-PR' => 3,83 'spec-PR' => 3,
79 'spec-LC' => 4,84 'spec-LC' => 4,
80 'spec-WD' => 5,85 'spec-WD' => 5,
81 'spec-ED' => 686 'spec-ED' => 6,
87 'spec-Obsolete' => 7
82 }88 }
83 89
84 PRIORITY_STATUSES = %w(spec-REC spec-CR)90 PRIORITY_STATUSES = %w(spec-REC spec-CR)
@@ -86,6 +92,7 @@ module Docs · 1 unmodified line
86 92
87 def get_spec93 def get_spec
88 return unless table = at_css('#Specifications + table') || css('.standard-table').last94 return unless table = at_css('#Specifications + table') || css('.standard-table').last
95
89 specs = table.css('tbody tr').to_a96 specs = table.css('tbody tr').to_a
90 # [link, span]97 # [link, span]
91 specs.map! { |node| [node.at_css('> td:nth-child(1) > a'), node.at_css('> td:nth-child(2) > span')] }98 specs.map! { |node| [node.at_css('> td:nth-child(1) > a'), node.at_css('> td:nth-child(2) > span')] }
@@ -110,8 +117,8 @@ module Docs · 18 unmodified lines
110 'shape' => [117 'shape' => [
111 %w(rect() Syntax) ],118 %w(rect() Syntax) ],
112 'timing-function' => [119 'timing-function' => [
113 %w(cubic-bezier() The_cubic-bezier()_class_of_timing-functions),120 %w(cubic-bezier()),
114 %w(steps() The_steps()_class_of_timing-functions),121 %w(steps()),
115 %w(linear linear),122 %w(linear linear),
116 %w(ease ease),123 %w(ease ease),
117 %w(ease-in ease-in),124 %w(ease-in ease-in),
lib/docs/filters/dom/clean_html.rb+1−1
@@ -30,7 +30,7 @@ module Docs · 29 unmodified lines
30 end30 end
31 31
32 # Remove <div> wrapping .overheadIndicator32 # Remove <div> wrapping .overheadIndicator
33 css('div > .overheadIndicator:first-child:last-child').each do |node|33 css('div > .overheadIndicator:first-child:last-child', 'div > .blockIndicator:first-child:last-child').each do |node|
34 node.parent.replace(node)34 node.parent.replace(node)
35 end35 end
36 36
lib/docs/filters/dom/entries.rb+12−2
@@ -6,6 +6,7 @@ module Docs · 5 unmodified lines
6 'EXT_' => 'WebGL',6 'EXT_' => 'WebGL',
7 'OES_' => 'WebGL',7 'OES_' => 'WebGL',
8 'WEBGL_' => 'WebGL',8 'WEBGL_' => 'WebGL',
9 'Sensor API' => 'Sensors',
9 'Ambient Light' => 'Ambient Light',10 'Ambient Light' => 'Ambient Light',
10 'Audio' => 'Audio',11 'Audio' => 'Audio',
11 'Battery Status' => 'Battery Status',12 'Battery Status' => 'Battery Status',
@@ -22,10 +23,12 @@ module Docs · 10 unmodified lines
22 'Encrypted Media Extensions' => 'Encrypted Media',23 'Encrypted Media Extensions' => 'Encrypted Media',
23 'Fetch' => 'Fetch',24 'Fetch' => 'Fetch',
24 'File API' => 'File',25 'File API' => 'File',
26 'Fullscreen' => 'Fullscreen',
25 'Geolocation' => 'Geolocation',27 'Geolocation' => 'Geolocation',
26 'Geometry' => 'Geometry',28 'Geometry' => 'Geometry',
27 'High Resolution Time' => 'Performance',29 'High Resolution Time' => 'Performance',
28 'Intersection' => 'Intersection Observer',30 'Intersection' => 'Intersection Observer',
31 'Keyboard' => 'Keyboard',
29 'Media Capabilities' => 'Media',32 'Media Capabilities' => 'Media',
30 'Media Capture' => 'Media',33 'Media Capture' => 'Media',
31 'Media Session' => 'Media',34 'Media Session' => 'Media',
@@ -35,6 +38,7 @@ module Docs · 3 unmodified lines
35 'MIDI' => 'Audio',38 'MIDI' => 'Audio',
36 'Navigation Timing' => 'Performance',39 'Navigation Timing' => 'Performance',
37 'Network Information' => 'Network Information',40 'Network Information' => 'Network Information',
41 'Orientation Sensor' => 'Sensors',
38 'Payment' => 'Payments',42 'Payment' => 'Payments',
39 'Performance Timeline' => 'Performance',43 'Performance Timeline' => 'Performance',
40 'Pointer Events' => 'Pointer Events',44 'Pointer Events' => 'Pointer Events',
@@ -53,11 +57,13 @@ module Docs · 12 unmodified lines
53 'Web App Manifest' => 'Web App Manifest',57 'Web App Manifest' => 'Web App Manifest',
54 'Budget' => 'Budget',58 'Budget' => 'Budget',
55 'Web Authentication' => 'Authentication',59 'Web Authentication' => 'Authentication',
60 'Web Locks' => 'Locks',
56 'Web Workers' => 'Web Workers',61 'Web Workers' => 'Web Workers',
57 'WebGL' => 'WebGL',62 'WebGL' => 'WebGL',
58 'WebRTC' => 'WebRTC',63 'WebRTC' => 'WebRTC',
59 'WebUSB' => 'WebUSB',64 'WebUSB' => 'WebUSB',
60 'WebVR' => 'WebVR' }65 'WebVR' => 'WebVR',
66 'WebVTT' => 'WebVTT' }
61 67
62 TYPE_BY_NAME_STARTS_WITH = {68 TYPE_BY_NAME_STARTS_WITH = {
63 'AbortController' => 'Fetch',69 'AbortController' => 'Fetch',
@@ -89,10 +95,12 @@ module Docs · 25 unmodified lines
89 'Fetch' => 'Fetch',95 'Fetch' => 'Fetch',
90 'File' => 'File',96 'File' => 'File',
91 'GlobalEventHandlers' => 'GlobalEventHandlers',97 'GlobalEventHandlers' => 'GlobalEventHandlers',
98 'HMDVR' => 'WebVR',
92 'history' => 'History',99 'history' => 'History',
93 'HTML Drag' => 'Drag & Drop',100 'HTML Drag' => 'Drag & Drop',
94 'HTML' => 'Elements',101 'HTML' => 'Elements',
95 'IDB' => 'IndexedDB',102 'IDB' => 'IndexedDB',
103 'Keyboard' => 'Keyboard',
96 'location' => 'Location',104 'location' => 'Location',
97 'navigator' => 'Navigator',105 'navigator' => 'Navigator',
98 'MediaKeySession' => 'Encrypted Media',106 'MediaKeySession' => 'Encrypted Media',
@@ -122,6 +130,7 @@ module Docs · 23 unmodified lines
122 'StyleSheet' => 'CSS',130 'StyleSheet' => 'CSS',
123 'Stylesheet' => 'CSS',131 'Stylesheet' => 'CSS',
124 'SVG' => 'SVG',132 'SVG' => 'SVG',
133 'TextTrack' => 'WebVTT',
125 'TimeRanges' => 'Media',134 'TimeRanges' => 'Media',
126 'timing' => 'Performance',135 'timing' => 'Performance',
127 'Timing' => 'Performance',136 'Timing' => 'Performance',
@@ -160,6 +169,7 @@ module Docs · 32 unmodified lines
160 'timing' => 'Performance',169 'timing' => 'Performance',
161 'Timing' => 'Performance',170 'Timing' => 'Performance',
162 'udio' => 'Audio',171 'udio' => 'Audio',
172 'VRDevice' => 'WebVR',
163 'WebGL' => 'WebGL',173 'WebGL' => 'WebGL',
164 'WEBGL' => 'WebGL',174 'WEBGL' => 'WebGL',
165 'WebRTC' => 'WebRTC',175 'WebRTC' => 'WebRTC',
@@ -239,7 +249,7 @@ module Docs · 73 unmodified lines
239 249
240 def include_default_entry?250 def include_default_entry?
241 return true if type == 'Console'251 return true if type == 'Console'
242 return true unless node = doc.at_css('.overheadIndicator')252 return true unless node = doc.at_css('.overheadIndicator, .blockIndicator')
243 node = node.parent while node.parent != doc253 node = node.parent while node.parent != doc
244 return true if node.previous_element.try(:name).in?(%w(h2 h3))254 return true if node.previous_element.try(:name).in?(%w(h2 h3))
245 content = node.content255 content = node.content
lib/docs/filters/html/entries.rb+1−1
@@ -28,7 +28,7 @@ module Docs · 27 unmodified lines
28 28
29 def include_default_entry?29 def include_default_entry?
30 return false if %w(Element/Heading_Elements).include?(slug)30 return false if %w(Element/Heading_Elements).include?(slug)
31 (node = doc.at_css '.overheadIndicator').nil? || node.content.exclude?('not on a standards track')31 (node = doc.at_css '.overheadIndicator, .blockIndicator').nil? || node.content.exclude?('not on a standards track')
32 end32 end
33 33
34 def additional_entries34 def additional_entries
lib/docs/filters/javascript/clean_html.rb+2−2
@@ -11,12 +11,12 @@ module Docs · 10 unmodified lines
11 11
12 def other12 def other
13 # Remove "style" attribute13 # Remove "style" attribute
14 css('.inheritsbox', '.overheadIndicator').each do |node|14 css('.inheritsbox', '.overheadIndicator', '.blockIndicator').each do |node|
15 node.remove_attribute 'style'15 node.remove_attribute 'style'
16 end16 end
17 17
18 # Remove <div> wrapping .overheadIndicator18 # Remove <div> wrapping .overheadIndicator
19 css('div > .overheadIndicator:first-child:last-child').each do |node|19 css('div > .overheadIndicator:first-child:last-child', 'div > .blockIndicator:first-child:last-child').each do |node|
20 node.parent.replace(node)20 node.parent.replace(node)
21 end21 end
22 end22 end
lib/docs/filters/javascript/entries.rb+1−1
@@ -88,7 +88,7 @@ module Docs · 87 unmodified lines
88 end88 end
89 89
90 def include_default_entry?90 def include_default_entry?
91 node = doc.at_css '.overheadIndicator, .warning'91 node = doc.at_css '.blockIndicator, .warning'
92 92
93 # Can't use :first-child because #doc is a DocumentFragment93 # Can't use :first-child because #doc is a DocumentFragment
94 return true unless node && node.parent == doc && !node.previous_element94 return true unless node && node.parent == doc && !node.previous_element
lib/docs/filters/mdn/clean_html.rb+90−0
@@ -14,6 +14,8 @@ module Docs · 13 unmodified lines
14 '#Quick_Links',14 '#Quick_Links',
15 'hr']15 'hr']
16 16
17 BROWSER_UNNECESSARY_CLASS_REGEX = /\s*bc-browser[\w_-]+/
18
17 def call19 def call
18 css(*REMOVE_NODES).remove20 css(*REMOVE_NODES).remove
19 21
@@ -61,6 +63,94 @@ module Docs · 41 unmodified lines
61 node.previous_element << node63 node.previous_element << node
62 end64 end
63 65
66 # New compatibility tables
67
68 css('.bc-data #Legend + dl', '.bc-data #Legend', '.bc-data #Legend_2 + dl', '.bc-data #Legend_2', '.bc-browser-name').remove
69
70 css('abbr.only-icon[title="Full support"]',
71 'abbr.only-icon[title="Partial support"]',
72 'abbr.only-icon[title="No support"]',
73 'abbr.only-icon[title="See implementation notes"]').remove
74
75 css('.bc-data .ic-altname', '.bc-data .ic-deprecated', '.bc-data .ic-non-standard', '.bc-data .ic-experimental').each do |node|
76 node.parent.remove
77 end
78
79 css('abbr.only-icon').each do |node|
80 node.replace(node.content)
81 end
82
83 css('.bc-table .bc-platforms td', '.bc-table .bc-browsers td').each do |node|
84 node.name = 'th'
85 end
86
87 css('.bc-data').each do |node|
88 link = node.at_css('.bc-github-link')
89 prev = node.previous_element
90 prev = prev.previous_element until prev.name == 'h2'
91 prev.add_child(link)
92
93 node.before(node.children).remove
94 end
95
96 css('.bc-table').each do |node|
97 desktop_table = node
98
99 mobile_table = node.dup
100 desktop_table.after(mobile_table)
101
102 if desktop_table.at_css('.bc-platform-server')
103 server_table = node.dup
104 mobile_table.after(server_table)
105 end
106
107 desktop_columns = desktop_table.at_css('th.bc-platform-desktop')['colspan'].to_i
108 mobile_columns = desktop_table.at_css('th.bc-platform-mobile')['colspan'].to_i
109
110 desktop_table.css('.bc-platform-mobile').remove
111 desktop_table.css('.bc-platform-server').remove
112 desktop_table.css('.bc-browsers th').to_a[(desktop_columns + 1)..-1].each(&:remove)
113 desktop_table.css('tr:not(.bc-platforms):not(.bc-browsers)').each do |line|
114 line.css('td').to_a[(desktop_columns)..-1].each(&:remove)
115 end
116
117 mobile_table.css('.bc-platform-desktop').remove
118 mobile_table.css('.bc-platform-server').remove
119 mobile_table.css('.bc-browsers th').to_a[1..(desktop_columns)].each(&:remove)
120 mobile_table.css('.bc-browsers th').to_a[(mobile_columns + 1)..-1].each(&:remove)
121 mobile_table.css('tr:not(.bc-platforms):not(.bc-browsers)').each do |line|
122 line.css('td').to_a[0..(desktop_columns - 1)].each(&:remove)
123 line.css('td').to_a[(mobile_columns)..-1].each(&:remove)
124 end
125
126 if server_table
127 server_table.css('.bc-platform-desktop').remove
128 server_table.css('.bc-platform-mobile').remove
129 server_table.css('.bc-browsers th').to_a[1..(desktop_columns + mobile_columns)].each(&:remove)
130 server_table.css('tr:not(.bc-platforms):not(.bc-browsers)').each do |line|
131 line.css('td').to_a[0..(desktop_columns + mobile_columns - 1)].each(&:remove)
132 end
133 end
134 end
135
136 # Reduce page size to make the offline bundle smaller.
137 css('.bc-supports-unknown').remove_attr('class')
138 css('td[class*="bc-platform"], th[class*="bc-platform"]').remove_attr('class')
139 css('td[class*="bc-browser"], th[class*="bc-browser"]').each do |node|
140 class_name = node['class']
141 class_name.remove!(BROWSER_UNNECESSARY_CLASS_REGEX)
142
143 if class_name.present?
144 node['class'] = class_name
145 else
146 node.remove_attribute('class')
147 end
148 end
149
150 css('abbr[title*="Compatibility unknown"]').each do |node|
151 node.before(node.children).remove
152 end
153
64 doc154 doc
65 end155 end
66 end156 end
lib/docs/filters/rdoc/clean_html.rb+4−2
@@ -37,8 +37,10 @@ module Docs · 36 unmodified lines
37 end37 end
38 38
39 # Add class to differentiate Ruby code from C code39 # Add class to differentiate Ruby code from C code
40 css('.method-source-code > pre').each do |node|40 css('.method-source-code').each do |node|
41 node['class'] = node.at_css('.ruby-keyword') ? 'ruby' : 'c'41 node.parent.prepend_child(node)
42 pre = node.at_css('pre')
43 pre['class'] = pre.at_css('.ruby-keyword') ? 'ruby' : 'c'
42 end44 end
43 45
44 # Remove code highlighting46 # Remove code highlighting
lib/docs/filters/rust/clean_html.rb+4−2
@@ -1,3 +1,5 @@
1# frozen_string_literal: true
2
1module Docs3module Docs
2 class Rust4 class Rust
3 class CleanHtmlFilter < Filter5 class CleanHtmlFilter < Filter
@@ -74,10 +76,10 @@ module Docs · 70 unmodified lines
74 node.remove76 node.remove
75 end77 end
76 78
77 css('h2 .important-traits', 'h3 .important-traits', 'h4 .important-traits').each do |node|79 css('.important-traits').to_a.each_with_index do |node, index|
78 content = node.at_css('.content.hidden .content')80 content = node.at_css('.content.hidden .content')
79 node.at_css('.content.hidden').replace(content) if content81 node.at_css('.content.hidden').replace(content) if content
80 node.parent.after(node)82 node.parent.after(node) if node.parent.name.in?(%(h2 h3 h4))
81 end83 end
82 84
83 css('code.content').each do |node|85 css('code.content').each do |node|
lib/docs/filters/rust/entries.rb+5−6
@@ -43,13 +43,12 @@ module Docs · 42 unmodified lines
43 end43 end
44 else44 else
45 css('.method')45 css('.method')
46 .select {|node| !node.at_css('.fnname').nil?}46 .each_with_object({}) { |node, entries|
47 .map {|node|47 name = node.at_css('.fnname').try(:content)
48 name = node.at_css('.fnname').content48 next unless name
49 name.prepend "#{self.name}::"49 name.prepend "#{self.name}::"
50 [name, node['id']]50 entries[name] ||= [name, node['id']]
51 }51 }.values
52 .uniq {|item| item[0]}
53 end52 end
54 end53 end
55 end54 end
lib/docs/scrapers/c.rb+0−1
@@ -1,7 +1,6 @@
1module Docs1module Docs
2 class C < FileScraper2 class C < FileScraper
3 self.type = 'c'3 self.type = 'c'
4 self.dir = '/Users/Thibaut/DevDocs/Docs/c'
5 self.base_url = 'http://en.cppreference.com/w/c/'4 self.base_url = 'http://en.cppreference.com/w/c/'
6 self.root_path = 'header.html'5 self.root_path = 'header.html'
7 6
lib/docs/scrapers/cpp.rb+0−1
@@ -3,7 +3,6 @@ module Docs · 2 unmodified lines
3 self.name = 'C++'3 self.name = 'C++'
4 self.slug = 'cpp'4 self.slug = 'cpp'
5 self.type = 'c'5 self.type = 'c'
6 self.dir = '/Users/Thibaut/DevDocs/Docs/cpp'
7 self.base_url = 'http://en.cppreference.com/w/cpp/'6 self.base_url = 'http://en.cppreference.com/w/cpp/'
8 self.root_path = 'header.html'7 self.root_path = 'header.html'
9 8
lib/docs/scrapers/dart.rb+0−2
@@ -24,13 +24,11 @@ module Docs · 23 unmodified lines
24 24
25 version '2' do25 version '2' do
26 self.release = '2.0.0'26 self.release = '2.0.0'
27 self.dir = '/Users/Thibaut/DevDocs/Docs/Dart2'
28 self.base_url = "https://api.dartlang.org/stable/#{release}/"27 self.base_url = "https://api.dartlang.org/stable/#{release}/"
29 end28 end
30 29
31 version '1' do30 version '1' do
32 self.release = '1.24.3'31 self.release = '1.24.3'
33 self.dir = '/Users/Thibaut/DevDocs/Docs/Dart1'
34 self.base_url = "https://api.dartlang.org/stable/#{release}/"32 self.base_url = "https://api.dartlang.org/stable/#{release}/"
35 end33 end
36 end34 end
lib/docs/scrapers/django.rb+0−6
@@ -36,37 +36,31 @@ module Docs · 35 unmodified lines
36 36
37 version '2.1' do37 version '2.1' do
38 self.release = '2.1.0'38 self.release = '2.1.0'
39 self.dir = '/Users/Thibaut/DevDocs/Docs/Django21'
40 self.base_url = 'https://docs.djangoproject.com/en/2.1/'39 self.base_url = 'https://docs.djangoproject.com/en/2.1/'
41 end40 end
42 41
43 version '2.0' do42 version '2.0' do
44 self.release = '2.0.7'43 self.release = '2.0.7'
45 self.dir = '/Users/Thibaut/DevDocs/Docs/Django20'
46 self.base_url = 'https://docs.djangoproject.com/en/2.0/'44 self.base_url = 'https://docs.djangoproject.com/en/2.0/'
47 end45 end
48 46
49 version '1.11' do47 version '1.11' do
50 self.release = '1.11.9'48 self.release = '1.11.9'
51 self.dir = '/Users/Thibaut/DevDocs/Docs/Django111'
52 self.base_url = 'https://docs.djangoproject.com/en/1.11/'49 self.base_url = 'https://docs.djangoproject.com/en/1.11/'
53 end50 end
54 51
55 version '1.10' do52 version '1.10' do
56 self.release = '1.10.8'53 self.release = '1.10.8'
57 self.dir = '/Users/Thibaut/DevDocs/Docs/Django110'
58 self.base_url = 'https://docs.djangoproject.com/en/1.10/'54 self.base_url = 'https://docs.djangoproject.com/en/1.10/'
59 end55 end
60 56
61 version '1.9' do57 version '1.9' do
62 self.release = '1.9.13'58 self.release = '1.9.13'
63 self.dir = '/Users/Thibaut/DevDocs/Docs/Django19'
64 self.base_url = 'https://docs.djangoproject.com/en/1.9/'59 self.base_url = 'https://docs.djangoproject.com/en/1.9/'
65 end60 end
66 61
67 version '1.8' do62 version '1.8' do
68 self.release = '1.8.18'63 self.release = '1.8.18'
69 self.dir = '/Users/Thibaut/DevDocs/Docs/Django18'
70 self.base_url = 'https://docs.djangoproject.com/en/1.8/'64 self.base_url = 'https://docs.djangoproject.com/en/1.8/'
71 end65 end
72 end66 end
lib/docs/scrapers/erlang.rb+0−4
@@ -42,22 +42,18 @@ module Docs · 41 unmodified lines
42 42
43 version '21' do43 version '21' do
44 self.release = '21.0'44 self.release = '21.0'
45 self.dir = '/Users/Thibaut/DevDocs/Docs/Erlang21'
46 end45 end
47 46
48 version '20' do47 version '20' do
49 self.release = '20.3'48 self.release = '20.3'
50 self.dir = '/Users/Thibaut/DevDocs/Docs/Erlang20'
51 end49 end
52 50
53 version '19' do51 version '19' do
54 self.release = '19.3'52 self.release = '19.3'
55 self.dir = '/Users/Thibaut/DevDocs/Docs/Erlang19'
56 end53 end
57 54
58 version '18' do55 version '18' do
59 self.release = '18.3'56 self.release = '18.3'
60 self.dir = '/Users/Thibaut/DevDocs/Docs/Erlang18'
61 end57 end
62 end58 end
63end59end
lib/docs/scrapers/gnu/gcc.rb+0−8
@@ -48,13 +48,11 @@ module Docs · 47 unmodified lines
48 48
49 version '7' do49 version '7' do
50 self.release = '7.3.0'50 self.release = '7.3.0'
51 self.dir = '/Users/Thibaut/DevDocs/Docs/gcc7'
52 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gcc/"51 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gcc/"
53 end52 end
54 53
55 version '7 CPP' do54 version '7 CPP' do
56 self.release = '7.3.0'55 self.release = '7.3.0'
57 self.dir = '/Users/Thibaut/DevDocs/Docs/gcpp7'
58 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/cpp/"56 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/cpp/"
59 57
60 options[:replace_paths] = CPP_PATHS58 options[:replace_paths] = CPP_PATHS
@@ -62,7 +60,6 @@ module Docs · 1 unmodified line
62 60
63 version '6' do61 version '6' do
64 self.release = '6.4.0'62 self.release = '6.4.0'
65 self.dir = '/Users/Thibaut/DevDocs/Docs/gcc6'
66 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gcc/"63 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gcc/"
67 64
68 options[:root_title] = 'Using the GNU Compiler Collection (GCC)'65 options[:root_title] = 'Using the GNU Compiler Collection (GCC)'
@@ -70,7 +67,6 @@ module Docs · 1 unmodified line
70 67
71 version '6 CPP' do68 version '6 CPP' do
72 self.release = '6.4.0'69 self.release = '6.4.0'
73 self.dir = '/Users/Thibaut/DevDocs/Docs/gcpp6'
74 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/cpp/"70 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/cpp/"
75 71
76 options[:replace_paths] = CPP_PATHS72 options[:replace_paths] = CPP_PATHS
@@ -78,7 +74,6 @@ module Docs · 1 unmodified line
78 74
79 version '5' do75 version '5' do
80 self.release = '5.4.0'76 self.release = '5.4.0'
81 self.dir = '/Users/Thibaut/DevDocs/Docs/gcc5'
82 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gcc/"77 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gcc/"
83 78
84 options[:root_title] = 'Using the GNU Compiler Collection (GCC)'79 options[:root_title] = 'Using the GNU Compiler Collection (GCC)'
@@ -86,7 +81,6 @@ module Docs · 1 unmodified line
86 81
87 version '5 CPP' do82 version '5 CPP' do
88 self.release = '5.4.0'83 self.release = '5.4.0'
89 self.dir = '/Users/Thibaut/DevDocs/Docs/gcpp5'
90 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/cpp/"84 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/cpp/"
91 85
92 options[:replace_paths] = CPP_PATHS86 options[:replace_paths] = CPP_PATHS
@@ -94,7 +88,6 @@ module Docs · 1 unmodified line
94 88
95 version '4' do89 version '4' do
96 self.release = '4.9.3'90 self.release = '4.9.3'
97 self.dir = '/Users/Thibaut/DevDocs/Docs/gcc4'
98 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gcc/"91 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gcc/"
99 92
100 options[:root_title] = 'Using the GNU Compiler Collection (GCC)'93 options[:root_title] = 'Using the GNU Compiler Collection (GCC)'
@@ -102,7 +95,6 @@ module Docs · 1 unmodified line
102 95
103 version '4 CPP' do96 version '4 CPP' do
104 self.release = '4.9.3'97 self.release = '4.9.3'
105 self.dir = '/Users/Thibaut/DevDocs/Docs/gcpp4'
106 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/cpp/"98 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/cpp/"
107 99
108 options[:replace_paths] = CPP_PATHS100 options[:replace_paths] = CPP_PATHS
lib/docs/scrapers/gnu/gnu_fortran.rb+0−4
@@ -8,25 +8,21 @@ module Docs · 7 unmodified lines
8 8
9 version '7' do9 version '7' do
10 self.release = '7.3.0'10 self.release = '7.3.0'
11 self.dir = '/Users/Thibaut/DevDocs/Docs/gfortran7'
12 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gfortran/"11 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gfortran/"
13 end12 end
14 13
15 version '6' do14 version '6' do
16 self.release = '6.4.0'15 self.release = '6.4.0'
17 self.dir = '/Users/Thibaut/DevDocs/Docs/gfortran6'
18 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gfortran/"16 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gfortran/"
19 end17 end
20 18
21 version '5' do19 version '5' do
22 self.release = '5.4.0'20 self.release = '5.4.0'
23 self.dir = '/Users/Thibaut/DevDocs/Docs/gfortran5'
24 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gfortran/"21 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gfortran/"
25 end22 end
26 23
27 version '4' do24 version '4' do
28 self.release = '4.9.3'25 self.release = '4.9.3'
29 self.dir = '/Users/Thibaut/DevDocs/Docs/gfortran4'
30 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gfortran/"26 self.base_url = "https://gcc.gnu.org/onlinedocs/gcc-#{release}/gfortran/"
31 end27 end
32 end28 end
lib/docs/scrapers/nokogiri2.rb+8−4
@@ -1,9 +1,13 @@
1module Docs1module Docs
2 class Nokogiri2 < Rdoc2 class Nokogiri2 < Rdoc
3 # Instructions:
4 # 1. Download the latest release at https://github.com/sparklemotion/nokogiri/releases
5 # 2. Run "bundle install && bundle exec rake docs" (in the Nokogiri directory)
6 # 4. Copy the "doc" directory to "docs/nokgiri"
7
3 self.name = 'Nokogiri'8 self.name = 'Nokogiri'
4 self.slug = 'nokogiri'9 self.slug = 'nokogiri'
5 self.release = '1.8.1'10 self.release = '1.9.0'
6 self.dir = '/Users/Thibaut/DevDocs/Docs/RDoc/Nokogiri'
7 11
8 html_filters.replace 'rdoc/entries', 'nokogiri2/entries'12 html_filters.replace 'rdoc/entries', 'nokogiri2/entries'
9 13
@@ -11,8 +15,8 @@ module Docs · 1 unmodified line
11 options[:only_patterns] = [/\ANokogiri/, /\AXSD/]15 options[:only_patterns] = [/\ANokogiri/, /\AXSD/]
12 16
13 options[:attribution] = <<-HTML17 options[:attribution] = <<-HTML
14 &copy; 2008&ndash;2017 Aaron Patterson, Mike Dalessio, Charles Nutter, Sergio Arbeo<br>18 &copy; 2008&ndash;2018 Aaron Patterson, Mike Dalessio, Charles Nutter, Sergio Arbeo,<br>
15 Patrick Mahoney, Yoko Harada, Akinori Musha, John Shahid<br>19 Patrick Mahoney, Yoko Harada, Akinori Musha, John Shahid, Lars Kanis<br>
16 Licensed under the MIT License.20 Licensed under the MIT License.
17 HTML21 HTML
18 end22 end
lib/docs/scrapers/numpy.rb+0−1
@@ -2,7 +2,6 @@ module Docs · 1 unmodified line
2 class Numpy < FileScraper2 class Numpy < FileScraper
3 self.name = 'NumPy'3 self.name = 'NumPy'
4 self.type = 'sphinx'4 self.type = 'sphinx'
5 self.dir = '/Users/Thibaut/DevDocs/Docs/numpy/reference/'
6 self.root_path = 'index.html'5 self.root_path = 'index.html'
7 self.links = {6 self.links = {
8 home: 'http://www.numpy.org/',7 home: 'http://www.numpy.org/',
lib/docs/scrapers/openjdk.rb+2−3
@@ -1,11 +1,10 @@
1module Docs1module Docs
2 class Openjdk < FileScraper2 class Openjdk < FileScraper
3 # Downloaded from packages.debian.org/sid/openjdk-8-doc
4 # Extracting subdirectory /usr/share/doc/openjdk-8-jre-headless/api
3 self.name = 'OpenJDK'5 self.name = 'OpenJDK'
4 self.type = 'openjdk'6 self.type = 'openjdk'
5 self.root_path = 'overview-summary.html'7 self.root_path = 'overview-summary.html'
6 # Downloaded from packages.debian.org/sid/openjdk-8-doc
7 # Extracting subdirectory /usr/share/doc/openjdk-8-jre-headless/api
8 self.dir = '/Users/Thibaut/DevDocs/Docs/OpenJDK'
9 8
10 html_filters.insert_after 'internal_urls', 'openjdk/clean_urls'9 html_filters.insert_after 'internal_urls', 'openjdk/clean_urls'
11 html_filters.push 'openjdk/entries', 'openjdk/clean_html'10 html_filters.push 'openjdk/entries', 'openjdk/clean_html'
lib/docs/scrapers/perl.rb+0−1
@@ -2,7 +2,6 @@ module Docs · 1 unmodified line
2 class Perl < FileScraper2 class Perl < FileScraper
3 self.name = 'Perl'3 self.name = 'Perl'
4 self.type = 'perl'4 self.type = 'perl'
5 self.dir = '/Users/Thibaut/DevDocs/Docs/Perl'
6 self.root_path = 'index.html'5 self.root_path = 'index.html'
7 self.links = {6 self.links = {
8 home: 'https://www.perl.org/'7 home: 'https://www.perl.org/'
lib/docs/scrapers/php.rb+2−3
@@ -1,5 +1,7 @@
1module Docs1module Docs
2 class Php < FileScraper2 class Php < FileScraper
3 # Downloaded from php.net/download-docs.php
4
3 include FixInternalUrlsBehavior5 include FixInternalUrlsBehavior
4 6
5 self.name = 'PHP'7 self.name = 'PHP'
@@ -23,9 +25,6 @@ module Docs · 17 unmodified lines
23 code: 'https://git.php.net/?p=php-src.git;a=summary'25 code: 'https://git.php.net/?p=php-src.git;a=summary'
24 }26 }
25 27
26 # Downloaded from php.net/download-docs.php
27 self.dir = '/Users/Thibaut/DevDocs/Docs/PHP'
28
29 html_filters.push 'php/internal_urls', 'php/entries', 'php/clean_html', 'title'28 html_filters.push 'php/internal_urls', 'php/entries', 'php/clean_html', 'title'
30 text_filters.push 'php/fix_urls'29 text_filters.push 'php/fix_urls'
31 30
lib/docs/scrapers/python.rb+4−8
@@ -23,33 +23,29 @@ module Docs · 22 unmodified lines
23 Licensed under the PSF License.23 Licensed under the PSF License.
24 HTML24 HTML
25 25
26 version '3.7' do26 version '3.7' do # docs.python.org/3.7/download.html
27 self.release = '3.7.0'27 self.release = '3.7.0'
28 self.dir = '/Users/Thibaut/DevDocs/Docs/Python37' # docs.python.org/3.7/download.html
29 self.base_url = 'https://docs.python.org/3.7/'28 self.base_url = 'https://docs.python.org/3.7/'
30 29
31 html_filters.push 'python/entries_v3', 'sphinx/clean_html', 'python/clean_html'30 html_filters.push 'python/entries_v3', 'sphinx/clean_html', 'python/clean_html'
32 end31 end
33 32
34 version '3.6' do33 version '3.6' do # docs.python.org/3.6/download.html
35 self.release = '3.6.6'34 self.release = '3.6.6'
36 self.dir = '/Users/Thibaut/DevDocs/Docs/Python36' # docs.python.org/3.6/download.html
37 self.base_url = 'https://docs.python.org/3.6/'35 self.base_url = 'https://docs.python.org/3.6/'
38 36
39 html_filters.push 'python/entries_v3', 'sphinx/clean_html', 'python/clean_html'37 html_filters.push 'python/entries_v3', 'sphinx/clean_html', 'python/clean_html'
40 end38 end
41 39
42 version '3.5' do40 version '3.5' do # docs.python.org/3.5/download.html
43 self.release = '3.5.3'41 self.release = '3.5.3'
44 self.dir = '/Users/Thibaut/DevDocs/Docs/Python35' # docs.python.org/3.5/download.html
45 self.base_url = 'https://docs.python.org/3.5/'42 self.base_url = 'https://docs.python.org/3.5/'
46 43
47 html_filters.push 'python/entries_v3', 'sphinx/clean_html', 'python/clean_html'44 html_filters.push 'python/entries_v3', 'sphinx/clean_html', 'python/clean_html'
48 end45 end
49 46
50 version '2.7' do47 version '2.7' do # docs.python.org/2.7/download.html
51 self.release = '2.7.13'48 self.release = '2.7.13'
52 self.dir = '/Users/Thibaut/DevDocs/Docs/Python27' # docs.python.org/2.7/download.html
53 self.base_url = 'https://docs.python.org/2.7/'49 self.base_url = 'https://docs.python.org/2.7/'
54 50
55 html_filters.push 'python/entries_v2', 'sphinx/clean_html', 'python/clean_html'51 html_filters.push 'python/entries_v2', 'sphinx/clean_html', 'python/clean_html'
lib/docs/scrapers/rdoc/minitest.rb+7−2
@@ -1,9 +1,14 @@
1module Docs1module Docs
2 class Minitest < Rdoc2 class Minitest < Rdoc
3 # Instructions:
4 # 1. Run "gem update rdoc hoe"
5 # 2. Download the source code at https://github.com/seattlerb/minitest
6 # 3. Run "rake docs" (in the Minitest directory)
7 # 4. Copy the "docs" directory to "docs/minitest"
8
3 self.name = 'Ruby / Minitest'9 self.name = 'Ruby / Minitest'
4 self.slug = 'minitest'10 self.slug = 'minitest'
5 self.release = '5.10.3'11 self.release = '5.11.3'
6 self.dir = '/Users/Thibaut/DevDocs/Docs/RDoc/Minitest' # rake docs
7 self.links = {12 self.links = {
8 code: 'https://github.com/seattlerb/minitest'13 code: 'https://github.com/seattlerb/minitest'
9 }14 }
lib/docs/scrapers/rdoc/rails.rb+10−3
@@ -1,10 +1,17 @@
1module Docs1module Docs
2 class Rails < Rdoc2 class Rails < Rdoc
3 # Instructions:
4 # 1. Download a release at https://github.com/rails/rails/releases
5 # 2. Open "railties/lib/rails/api/task.rb" and comment out any code related to sdoc ("configure_sdoc")
6 # 3. Run "bundle install --without db && bundle exec rake rdoc" (in the Rails directory)
7 # 4. Run "cd guides && bundle exec rake guides:generate:html"
8 # 5. Copy the "guides/output" directory to "html/guides"
9 # 6. Copy the "html" directory to "docs/rails~[version]"
10
3 include FixInternalUrlsBehavior11 include FixInternalUrlsBehavior
4 12
5 self.name = 'Ruby on Rails'13 self.name = 'Ruby on Rails'
6 self.slug = 'rails'14 self.slug = 'rails'
7 self.dir = '/Users/Thibaut/DevDocs/Docs/RDoc/Rails'
8 self.initial_paths = %w(guides/index.html)15 self.initial_paths = %w(guides/index.html)
9 self.links = {16 self.links = {
10 home: 'http://rubyonrails.org/',17 home: 'http://rubyonrails.org/',
@@ -68,7 +75,7 @@ module Docs · 57 unmodified lines
68 end75 end
69 76
70 version '5.2' do77 version '5.2' do
71 self.release = '5.2.0'78 self.release = '5.2.2'
72 end79 end
73 80
74 version '5.1' do81 version '5.1' do
@@ -80,7 +87,7 @@ module Docs · 5 unmodified lines
80 end87 end
81 88
82 version '4.2' do89 version '4.2' do
83 self.release = '4.2.10'90 self.release = '4.2.11'
84 end91 end
85 92
86 version '4.1' do93 version '4.1' do
lib/docs/scrapers/rdoc/ruby.rb+8−19
@@ -1,16 +1,9 @@
1module Docs1module Docs
2 class Ruby < Rdoc2 class Ruby < Rdoc
3 # Generated with:3 # Instructions:
4 # ./configure && make html4 # 1. Download Ruby's source code
5 # or:5 # 2. Run "./configure && make html" (in the Ruby directory)
6 # rdoc \6 # 3. Copy the ".ext/html" directory to "docs/ruby~[version]"
7 # --root . \
8 # --page-dir doc \
9 # --encoding=UTF-8 \
10 # --visibility=public \
11 # --format=darkfish \
12 # --no-line-numbers \
13 # --op html .
14 7
15 include FixInternalUrlsBehavior8 include FixInternalUrlsBehavior
16 9
@@ -77,23 +70,19 @@ module Docs · 60 unmodified lines
77 HTML70 HTML
78 71
79 version '2.5' do72 version '2.5' do
80 self.release = '2.5.0'73 self.release = '2.5.3'
81 self.dir = '/Users/Thibaut/DevDocs/Docs/RDoc/Ruby25'
82 end74 end
83 75
84 version '2.4' do76 version '2.4' do
85 self.release = '2.4.3'77 self.release = '2.4.5'
86 self.dir = '/Users/Thibaut/DevDocs/Docs/RDoc/Ruby24'
87 end78 end
88 79
89 version '2.3' do80 version '2.3' do
90 self.release = '2.3.6'81 self.release = '2.3.8'
91 self.dir = '/Users/Thibaut/DevDocs/Docs/RDoc/Ruby23'
92 end82 end
93 83
94 version '2.2' do84 version '2.2' do
95 self.release = '2.2.9'85 self.release = '2.2.10'
96 self.dir = '/Users/Thibaut/DevDocs/Docs/RDoc/Ruby22'
97 end86 end
98 end87 end
99end88end
lib/docs/scrapers/rust.rb+4−1
@@ -1,7 +1,9 @@
1# frozen_string_literal: true
2
1module Docs3module Docs
2 class Rust < UrlScraper4 class Rust < UrlScraper
3 self.type = 'rust'5 self.type = 'rust'
4 self.release = '1.29.1'6 self.release = '1.30.1'
5 self.base_url = 'https://doc.rust-lang.org/'7 self.base_url = 'https://doc.rust-lang.org/'
6 self.root_path = 'book/second-edition/index.html'8 self.root_path = 'book/second-edition/index.html'
7 self.initial_paths = %w(9 self.initial_paths = %w(
@@ -28,6 +30,7 @@ module Docs · 20 unmodified lines
28 options[:fix_urls] = ->(url) do30 options[:fix_urls] = ->(url) do
29 url.sub! %r{(#{Rust.base_url}.+/)\z}, '\1index.html'31 url.sub! %r{(#{Rust.base_url}.+/)\z}, '\1index.html'
30 url.sub! '/unicode/u_str', '/unicode/str/'32 url.sub! '/unicode/u_str', '/unicode/str/'
33 url.sub! '/std/std/', '/std/'
31 url34 url
32 end35 end
33 36
lib/docs/scrapers/sqlite.rb+0−1
@@ -3,7 +3,6 @@ module Docs · 2 unmodified lines
3 self.name = 'SQLite'3 self.name = 'SQLite'
4 self.type = 'sqlite'4 self.type = 'sqlite'
5 self.release = '3.25.2'5 self.release = '3.25.2'
6 self.dir = '/Users/Thibaut/DevDocs/Docs/sqlite/'
7 self.base_url = 'https://sqlite.org/'6 self.base_url = 'https://sqlite.org/'
8 self.root_path = 'docs.html'7 self.root_path = 'docs.html'
9 self.initial_paths = %w(keyword_index.html)8 self.initial_paths = %w(keyword_index.html)
lib/docs/subscribers/requester_subscriber.rb+13−0
@@ -0,0 +1,13 @@
1# frozen_string_literal: true
2
3module Docs
4 class RequesterSubscriber < Subscriber
5 self.namespace = 'requester'
6
7 def handle_response(event)
8 if event.duration > 10_000
9 log "WARN: #{format_url event.payload[:url]} was slow to process (#{(event.duration / 1000).round}s)"
10 end
11 end
12 end
13end
lib/tasks/docs.thor+74−13
@@ -12,15 +12,29 @@ class DocsCLI < Thor · 11 unmodified lines
12 end12 end
13 13
14 desc 'list', 'List available documentations'14 desc 'list', 'List available documentations'
15 option :packaged, type: :boolean
15 def list16 def list
16 output = Docs.all.flat_map do |doc|17 if options[:packaged]
17 name = doc.to_s.demodulize.underscore18 slugs = Dir[File.join(Docs.store_path, '*.tar.gz')].map { |f| File.basename(f, '.tar.gz') }
18 if doc.versioned?19 names = find_docs_by_slugs(slugs).map do |doc|
19 doc.versions.map { |_doc| "#{name}@#{_doc.version}" }20 name = if doc.version?
20 else21 "#{doc.superclass.to_s.demodulize.underscore}@#{doc.version}"
21 name22 else
23 doc.to_s.demodulize.underscore
24 end
22 end25 end
23 end.join("\n")26 else
27 names = Docs.all.flat_map do |doc|
28 name = doc.to_s.demodulize.underscore
29 if doc.versioned?
30 doc.versions.map { |_doc| "#{name}@#{_doc.version}" }
31 else
32 name
33 end
34 end
35 end
36
37 output = names.join("\n")
24 38
25 require 'tty-pager'39 require 'tty-pager'
26 TTY::Pager.new.page(output)40 TTY::Pager.new.page(output)
@@ -39,7 +53,7 @@ class DocsCLI < Thor · 12 unmodified lines
39 Docs.install_report :store if options[:verbose]53 Docs.install_report :store if options[:verbose]
40 if options[:debug]54 if options[:debug]
41 GC.disable55 GC.disable
42 Docs.install_report :filter, :request56 Docs.install_report :filter, :request, :doc
43 end57 end
44 58
45 if Docs.generate_page(name, options[:version], path)59 if Docs.generate_page(name, options[:version], path)
@@ -62,7 +76,7 @@ class DocsCLI < Thor · 16 unmodified lines
62 Docs.rescue_errors = true76 Docs.rescue_errors = true
63 Docs.install_report :store if options[:verbose]77 Docs.install_report :store if options[:verbose]
64 Docs.install_report :scraper if options[:debug]78 Docs.install_report :scraper if options[:debug]
65 Docs.install_report :progress_bar, :doc, :image if $stdout.tty?79 Docs.install_report :progress_bar, :doc, :image, :requester if $stdout.tty?
66 80
67 require 'unix_utils' if options[:package]81 require 'unix_utils' if options[:package]
68 82
@@ -149,15 +163,55 @@ class DocsCLI < Thor · 80 unmodified lines
149 option :dryrun, type: :boolean163 option :dryrun, type: :boolean
150 option :packaged, type: :boolean164 option :packaged, type: :boolean
151 def upload(*names)165 def upload(*names)
152 names = Dir[File.join(Docs.store_path, '*.tar.gz')].map { |f| File.basename(f, '.tar.gz') } if options[:packaged]166 require 'net/sftp'
153 docs = find_docs(names)167
168 if options[:packaged]
169 slugs = Dir[File.join(Docs.store_path, '*.tar.gz')].map { |f| File.basename(f, '.tar.gz') }
170 docs = find_docs_by_slugs(slugs)
171 else
172 docs = find_docs(names)
173 end
174
154 assert_docs(docs)175 assert_docs(docs)
176
177 # Verify files are present
178 docs.each do |doc|
179 unless Dir.exists?(File.join(Docs.store_path, doc.path))
180 puts "ERROR: directory #{File.join(Docs.store_path, doc.path)} not found."
181 return
182 end
183
184 unless File.exists?(File.join(Docs.store_path, "#{doc.path}.tar.gz"))
185 puts "ERROR: package for '#{doc.slug}' documentation not found. Run 'thor docs:package #{doc.slug}' to create it."
186 return
187 end
188 end
189
190 # Sync files with S3 (used by the web app)
191 puts '[S3] Begin syncing.'
155 docs.each do |doc|192 docs.each do |doc|
156 puts "Syncing #{doc.path}..."193 puts "[S3] Syncing #{doc.path}..."
157 cmd = "aws s3 sync #{File.join(Docs.store_path, doc.path)} s3://docs.devdocs.io/#{doc.path} --delete"194 cmd = "aws s3 sync #{File.join(Docs.store_path, doc.path)} s3://devdocs-assets/#{doc.path} --delete --profile devdocs"
158 cmd << ' --dryrun' if options[:dryrun]195 cmd << ' --dryrun' if options[:dryrun]
159 system(cmd)196 system(cmd)
160 end197 end
198 puts '[S3] Done syncing.'
199
200 # Upload packages to dl.devdocs.io (used by the "thor docs:download" command)
201 puts '[MaxCDN] Begin uploading.'
202 Net::SFTP.start('ftp.devdocs-dl.devdocs.netdna-cdn.com', ENV['DEVDOCS_DL_USERNAME'], password: ENV['DEVDOCS_DL_PASSWORD']) do |sftp|
203 docs.each do |doc|
204 filename = "#{doc.path}.tar.gz"
205 print "[MaxCDN] Uploading #{filename}..."
206 if options[:dryrun]
207 print "\n"
208 else
209 sftp.upload! File.join(Docs.store_path, filename), File.join('', 'public_html', filename)
210 print " OK\n"
211 end
212 end
213 end
214 puts '[MaxCDN] Done uploading.'
161 end215 end
162 216
163 desc 'commit', '[private]'217 desc 'commit', '[private]'
@@ -222,6 +276,13 @@ class DocsCLI < Thor · 58 unmodified lines
222 end276 end
223 end277 end
224 278
279 def find_docs_by_slugs(slugs)
280 slugs.flat_map do |slug|
281 slug, version = slug.split(/~/)
282 Docs.find_by_slug(slug, version)
283 end
284 end
285
225 def assert_docs(docs)286 def assert_docs(docs)
226 if docs.empty?287 if docs.empty?
227 puts 'ERROR: called with no arguments.'288 puts 'ERROR: called with no arguments.'
public/docs/docs.json+0−1
@@ -1 +0,0 @@
1[]
public/favicon.pxm+0−0

Binary file not shown.

public/favicon@2x.pxm+0−0

Binary file not shown.

public/icons/docs/css/CSS3.sketch/Data+0−0

Binary file not shown.

public/icons/docs/css/CSS3.sketch/QuickLook/Preview.png+0−0

Binary file not shown.

public/icons/docs/css/CSS3.sketch/QuickLook/Thumbnail.png+0−0

Binary file not shown.

public/icons/docs/css/CSS3.sketch/fonts+0−0

Empty file added.

public/icons/docs/css/CSS3.sketch/version+1−0
@@ -0,0 +1 @@
110
public/icons/docs/dom/DOM.sketch/Data+0−0

Binary file not shown.

public/icons/docs/dom/DOM.sketch/QuickLook/Preview.png+0−0

Binary file not shown.

public/icons/docs/dom/DOM.sketch/QuickLook/Thumbnail.png+0−0

Binary file not shown.

public/icons/docs/dom/DOM.sketch/fonts+0−0

Empty file added.

public/icons/docs/dom/DOM.sketch/version+1−0
@@ -0,0 +1 @@
110
public/icons/docs/dom_events/DOM_events.sketch/Data+0−0

Binary file not shown.

public/icons/docs/dom_events/DOM_events.sketch/QuickLook/Preview.png+0−0

Binary file not shown.

public/icons/docs/dom_events/DOM_events.sketch/QuickLook/Thumbnail.png+0−0

Binary file not shown.

public/icons/docs/dom_events/DOM_events.sketch/fonts+0−0

Empty file added.

public/icons/docs/dom_events/DOM_events.sketch/version+1−0
@@ -0,0 +1 @@
110
public/icons/docs/express/16.pxm+0−0

Binary file not shown.

public/icons/docs/express/16@2x.pxm+0−0

Binary file not shown.

public/icons/docs/html/HTML5.sketch/Data+0−0

Binary file not shown.

public/icons/docs/html/HTML5.sketch/QuickLook/Preview.png+0−0

Binary file not shown.

public/icons/docs/html/HTML5.sketch/QuickLook/Thumbnail.png+0−0

Binary file not shown.

public/icons/docs/html/HTML5.sketch/fonts+0−0

Empty file added.

public/icons/docs/html/HTML5.sketch/version+1−0
@@ -0,0 +1 @@
110
public/icons/docs/http/16.pxm+0−0

Binary file not shown.

public/icons/docs/http/16@2x.pxm+0−0

Binary file not shown.

public/icons/docs/javascript/16.pxm+0−0

Binary file not shown.

public/icons/docs/javascript/16@2x.pxm+0−0

Binary file not shown.

public/icons/docs/knockout/16@2x.pxm+0−0

Binary file not shown.

public/icons/docs/less/less.pxm+0−0

Binary file not shown.

public/icons/docs/moment/moment.sketch/Data+0−0

Binary file not shown.

public/icons/docs/moment/moment.sketch/QuickLook/Preview.png+0−0

Binary file not shown.

public/icons/docs/moment/moment.sketch/QuickLook/Thumbnail.png+0−0

Binary file not shown.

public/icons/docs/moment/moment.sketch/fonts+0−0

Empty file added.

public/icons/docs/moment/moment.sketch/metadata+14−0
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3<plist version="1.0">
4<dict>
5 <key>app</key>
6 <string>com.bohemiancoding.sketch</string>
7 <key>build</key>
8 <integer>5302</integer>
9 <key>commit</key>
10 <string>9460a4bc62af5e9ba50dd4143578fd9401710ce5</string>
11 <key>version</key>
12 <integer>18</integer>
13</dict>
14</plist>
public/icons/docs/moment/moment.sketch/version+1−0
@@ -0,0 +1 @@
118
public/icons/docs/nokogiri/icon.pxm+0−0

Binary file not shown.

public/icons/docs/php/16@2x.pxm+0−0

Binary file not shown.

public/icons/docs/rails/rails.pxm+0−0

Binary file not shown.

public/icons/docs/sass/sass.pxm+0−0

Binary file not shown.

public/icons/docs/support_tables/16.pxm+0−0

Binary file not shown.

public/icons/docs/support_tables/16@2x.pxm+0−0

Binary file not shown.

public/icons/docs/xpath/XPath.sketch/Data+0−0

Binary file not shown.

public/icons/docs/xpath/XPath.sketch/QuickLook/Preview.png+0−0

Binary file not shown.

public/icons/docs/xpath/XPath.sketch/QuickLook/Thumbnail.png+0−0

Binary file not shown.

public/icons/docs/xpath/XPath.sketch/metadata+18−0
@@ -0,0 +1,18 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3<plist version="1.0">
4<dict>
5 <key>app</key>
6 <string>com.bohemiancoding.sketch</string>
7 <key>build</key>
8 <integer>5355</integer>
9 <key>commit</key>
10 <string>b7d299b0a34651d1a0e066786b75aa36168d5809</string>
11 <key>fonts</key>
12 <array/>
13 <key>length</key>
14 <integer>22373</integer>
15 <key>version</key>
16 <integer>18</integer>
17</dict>
18</plist>
public/icons/docs/xpath/XPath.sketch/version+1−0
@@ -0,0 +1 @@
118
public/images/apple-icon.pxm+0−0

Binary file not shown.

public/images/fluid-icon.pxm+0−0

Binary file not shown.

public/images/icon.pxm+0−0

Binary file not shown.

public/images/webapp-icon-192.png+0−0

Binary file not shown.

public/images/webapp-icon-small.pxm+0−0

Binary file not shown.

public/manifest.json+6−0
@@ -4,6 +4,7 @@ · 3 unmodified lines
4 "description": "API Documentation Browser",4 "description": "API Documentation Browser",
5 "start_url": "/",5 "start_url": "/",
6 "display": "standalone",6 "display": "standalone",
7 "background_color": "#EEEEEE",
7 "icons": [8 "icons": [
8 {9 {
9 "src": "/images/webapp-icon-32.png",10 "src": "/images/webapp-icon-32.png",
@@ -25,6 +26,11 @@ · 15 unmodified lines
25 "sizes": "128x128",26 "sizes": "128x128",
26 "type": "image/png"27 "type": "image/png"
27 },28 },
29 {
30 "src": "/images/webapp-icon-192.png",
31 "sizes": "192x192",
32 "type": "image/png"
33 },
28 {34 {
29 "src": "/images/webapp-icon-256.png",35 "src": "/images/webapp-icon-256.png",
30 "sizes": "256x256",36 "sizes": "256x256",
test/app_test.rb+5−81
@@ -21,6 +21,11 @@ class AppTest < MiniTest::Spec · 20 unmodified lines
21 assert_equal 'https://example.com/test?q=1', last_response['Location']21 assert_equal 'https://example.com/test?q=1', last_response['Location']
22 end22 end
23 23
24 it 'returns HSTS header' do
25 get 'https://example.com/test'
26 assert_equal 'max-age=31536000; includeSubDomains', last_response['Strict-Transport-Security']
27 end
28
24 describe "/" do29 describe "/" do
25 it "works" do30 it "works" do
26 get '/'31 get '/'
@@ -38,35 +43,6 @@ class AppTest < MiniTest::Spec · 11 unmodified lines
38 assert last_response.redirect?43 assert last_response.redirect?
39 assert_equal 'https://example.org/', last_response['Location']44 assert_equal 'https://example.org/', last_response['Location']
40 end45 end
41
42 it "sets default size" do
43 get '/'
44 assert_includes last_response.body, 'data-size="20rem"'
45 end
46
47 it "sets size from cookie" do
48 set_cookie('size=42')
49 get '/'
50 assert_includes last_response.body, 'data-size="42px"'
51 end
52
53 it "sets layout from cookie" do
54 set_cookie('layout=foo')
55 get '/'
56 assert_includes last_response.body, '<body class="foo">'
57 end
58
59 it "sets the <html> theme from cookie" do
60 get '/'
61 assert_match %r{<html [^>]*class="[^\"]*_theme-default}, last_response.body
62 refute_includes last_response.body, '_theme-dark'
63
64 set_cookie('dark=1')
65
66 get '/'
67 assert_match %r{<html [^>]*class="[^\"]*_theme-dark}, last_response.body
68 refute_includes last_response.body, '_theme-default'
69 end
70 end46 end
71 47
72 describe "/[static-page]" do48 describe "/[static-page]" do
@@ -101,58 +77,6 @@ class AppTest < MiniTest::Spec · 28 unmodified lines
101 end77 end
102 end78 end
103 79
104 describe "/manifest.appcache" do
105 it "works" do
106 get '/manifest.appcache'
107 assert last_response.ok?
108 end
109
110 it "works with cookie" do
111 set_cookie('docs=css/html~5')
112 get '/manifest.appcache'
113 assert last_response.ok?
114 assert_includes last_response.body, '/css/index.json?1420139788'
115 assert_includes last_response.body, '/html~5/index.json?1420139791'
116 end
117
118 it "ignores invalid docs in the cookie" do
119 set_cookie('docs=foo')
120 get '/manifest.appcache'
121 assert last_response.ok?
122 refute_includes last_response.body, 'foo'
123 end
124
125 it "has the word 'default' when no 'dark' cookie is set" do
126 get '/manifest.appcache'
127 assert_includes last_response.body, '# default'
128 refute_includes last_response.body, '# dark'
129 end
130
131 it "has the word 'dark' when the cookie is set" do
132 set_cookie('dark=1')
133 get '/manifest.appcache'
134 assert_includes last_response.body, '# dark'
135 refute_includes last_response.body, '# default'
136 end
137
138 it "sets default size" do
139 get '/manifest.appcache'
140 assert_includes last_response.body, '20rem'
141 end
142
143 it "sets size from cookie" do
144 set_cookie('size=42')
145 get '/manifest.appcache'
146 assert_includes last_response.body, '42px'
147 end
148
149 it "sets layout from cookie" do
150 set_cookie('layout=foo_layout')
151 get '/manifest.appcache'
152 assert_includes last_response.body, 'foo_layout'
153 end
154 end
155
156 describe "/[doc]" do80 describe "/[doc]" do
157 it "renders when the doc exists and isn't enabled" do81 it "renders when the doc exists and isn't enabled" do
158 set_cookie('docs=html~5')82 set_cookie('docs=html~5')
test/lib/docs/core/scrapers/file_scraper_test.rb+89−47
@@ -2,16 +2,23 @@ require 'test_helper' · 1 unmodified line
2require 'docs'2require 'docs'
3 3
4class FileScraperTest < MiniTest::Spec4class FileScraperTest < MiniTest::Spec
5 ROOT_PATH = File.expand_path('../../../../../../', __FILE__)
6
5 class Scraper < Docs::FileScraper7 class Scraper < Docs::FileScraper
6 self.dir = '/'
7 self.html_filters = Docs::FilterStack.new8 self.html_filters = Docs::FilterStack.new
8 self.text_filters = Docs::FilterStack.new9 self.text_filters = Docs::FilterStack.new
10
11 version 'version' do; end
9 end12 end
10 13
11 let :scraper do14 let :scraper do
12 Scraper.new15 Scraper.new
13 end16 end
14 17
18 let :versioned_scraper do
19 Scraper.versions.first.new
20 end
21
15 let :response do22 let :response do
16 OpenStruct.new body: 'body', url: Docs::URL.parse(Scraper.base_url)23 OpenStruct.new body: 'body', url: Docs::URL.parse(Scraper.base_url)
17 end24 end
@@ -22,9 +29,16 @@ class FileScraperTest < MiniTest::Spec · 4 unmodified lines
22 end29 end
23 end30 end
24 31
32 describe "#source_directory" do
33 it "returns the directory at docs/[slug]" do
34 assert_equal File.join(ROOT_PATH, 'docs', 'scraper'), scraper.source_directory
35 assert_equal File.join(ROOT_PATH, 'docs', 'scraper~version'), versioned_scraper.source_directory
36 end
37 end
38
25 describe "#request_one" do39 describe "#request_one" do
26 let :path do40 let :path do
27 File.join(Scraper.dir, 'path')41 'path'
28 end42 end
29 43
30 let :result do44 let :result do
@@ -35,20 +49,34 @@ class FileScraperTest < MiniTest::Spec · 4 unmodified lines
35 stub(scraper).read_file49 stub(scraper).read_file
36 end50 end
37 51
38 it "reads a file" do52 context "when the source directory doesn't exist" do
39 mock(scraper).read_file(path)53 it "raises an error" do
40 result54 assert_raises Docs::SetupError do
55 result
56 end
57 end
41 end58 end
42 59
43 describe "the returned response object" do60 context "when the source directory exists" do
44 it "has a #body" do61 before do
45 stub(scraper).read_file { 'body' }62 stub(scraper).assert_source_directory_exists
46 assert_equal 'body', result.body63 end
64
65 it "reads a file" do
66 mock(scraper).read_file(path)
67 result
47 end68 end
48 69
49 it "has a #url" do70 describe "the returned response object" do
50 assert_equal path, result.url.to_s71 it "has a #body" do
51 assert_instance_of Docs::URL, result.url72 stub(scraper).read_file { 'body' }
73 assert_equal 'body', result.body
74 end
75
76 it "has a #url" do
77 assert_equal path, result.url.to_s
78 assert_instance_of Docs::URL, result.url
79 end
52 end80 end
53 end81 end
54 end82 end
@@ -58,49 +86,63 @@ class FileScraperTest < MiniTest::Spec · 3 unmodified lines
58 %w(one two)86 %w(one two)
59 end87 end
60 88
61 it "requests the given url" do89 context "when the source directory doesn't exist" do
62 mock(scraper).request_one('url')90 it "raises an error" do
63 scraper.send(:request_all, 'url') {}91 assert_raises Docs::SetupError do
64 end92 scraper.send(:request_all, urls) {}
65 93 end
66 it "requests the given urls" do94 end
67 requests = []
68 stub(scraper).request_one { |url| requests << url; nil }
69 scraper.send(:request_all, urls) {}
70 assert_equal urls, requests
71 end
72
73 it "yields the responses" do
74 responses = []
75 stub(scraper).request_one { |url| urls.index(url) }
76 scraper.send(:request_all, urls) { |response| responses << response; nil }
77 assert_equal (0...urls.length).to_a, responses
78 end95 end
79 96
80 context "when the block returns an array" do97 context "when the source directory exists" do
81 let :next_urls do98 before do
82 %w(three four)99 stub(scraper).assert_source_directory_exists
83 end100 end
84 101
85 let :all_urls do102 it "requests the given url" do
86 urls + %w(three four)103 mock(scraper).request_one('url')
104 scraper.send(:request_all, 'url') {}
87 end105 end
88 106
89 it "requests the returned urls" do107 it "requests the given urls" do
90 requests = []108 requests = []
91 stub(scraper).request_one { |url| requests << url; url }109 stub(scraper).request_one { |url| requests << url; nil }
92 scraper.send(:request_all, urls) { [next_urls.shift].compact }110 scraper.send(:request_all, urls) {}
93 assert_equal all_urls, requests111 assert_equal urls, requests
94 end112 end
95 113
96 it "yields their responses" do114 it "yields the responses" do
97 responses = []115 responses = []
98 stub(scraper).request_one { |url| all_urls.index(url) }116 stub(scraper).request_one { |url| urls.index(url) }
99 scraper.send :request_all, urls do |response|117 scraper.send(:request_all, urls) { |response| responses << response; nil }
100 responses << response118 assert_equal (0...urls.length).to_a, responses
101 [next_urls.shift].compact119 end
120
121 context "when the block returns an array" do
122 let :next_urls do
123 %w(three four)
124 end
125
126 let :all_urls do
127 urls + %w(three four)
128 end
129
130 it "requests the returned urls" do
131 requests = []
132 stub(scraper).request_one { |url| requests << url; url }
133 scraper.send(:request_all, urls) { [next_urls.shift].compact }
134 assert_equal all_urls, requests
135 end
136
137 it "yields their responses" do
138 responses = []
139 stub(scraper).request_one { |url| all_urls.index(url) }
140 scraper.send :request_all, urls do |response|
141 responses << response
142 [next_urls.shift].compact
143 end
144 assert_equal (0...all_urls.length).to_a, responses
102 end145 end
103 assert_equal (0...all_urls.length).to_a, responses
104 end146 end
105 end147 end
106 end148 end
@@ -126,13 +168,13 @@ class FileScraperTest < MiniTest::Spec · 19 unmodified lines
126 scraper.send :read_file, 'file'168 scraper.send :read_file, 'file'
127 end169 end
128 170
129 it "returns the file's content when the file exists" do171 it "returns the file's content when the file exists in the source directory" do
130 stub(File).read('file') { 'content' }172 stub(File).read(File.join(ROOT_PATH, 'docs', 'scraper', 'file')) { 'content' }
131 assert_equal 'content', result173 assert_equal 'content', result
132 end174 end
133 175
134 it "returns nil when the file doesn't exist" do176 it "returns nil when the file doesn't exist" do
135 stub(File).read('file') { raise }177 stub(File).read(File.join(ROOT_PATH, 'docs', 'scraper', 'file')) { raise }
136 assert_nil result178 assert_nil result
137 end179 end
138 end180 end
views/app.erb+1−13
@@ -28,13 +28,7 @@ · 27 unmodified lines
28 </nav>28 </nav>
29 </header>29 </header>
30 <section class="_sidebar" tabindex="-1">30 <section class="_sidebar" tabindex="-1">
31 <div class="_list" role="navigation">31 <div class="_list" role="navigation"></div>
32 <% unless @doc %>
33 <% App.docs.each do |slug, doc| %>
34 <a href="/<%= slug %>/" class="_list-item"><%= doc['full_name'] %></a>
35 <% end %>
36 <% end %>
37 </div>
38 </section>32 </section>
39 <div class="_container" role="document">33 <div class="_container" role="document">
40 <main class="_content _content-loading" role="main"></main>34 <main class="_content _content-loading" role="main"></main>
@@ -63,9 +57,3 @@ · 22 unmodified lines
63 <symbol id="icon-external-link" viewBox="0 0 24 24"><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></symbol>57 <symbol id="icon-external-link" viewBox="0 0 24 24"><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></symbol>
64 </defs>58 </defs>
65</svg>59</svg>
66<style data-size="<%= app_size %>" data-resizer>
67 ._container { margin-left: <%= app_size %>; }
68 ._header, ._list { width: <%= app_size %>; }
69 ._list-hover.clone { min-width: <%= app_size %>; }
70 ._notice, ._path, ._resizer { left: <%= app_size %>; }
71</style>
views/index.erb+3−2
@@ -1,5 +1,5 @@
1<!DOCTYPE html>1<!DOCTYPE html>
2<html<%= ' manifest="/manifest.appcache"' if App.production? %> prefix="og: http://ogp.me/ns#" lang="en" class="_booting _theme-<%= app_theme %>">2<html prefix="og: http://ogp.me/ns#" lang="en" class="_booting">
3<head>3<head>
4 <meta charset="utf-8">4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no">5 <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no">
@@ -14,6 +14,7 @@ · 8 unmodified lines
14 <meta name="apple-mobile-web-app-capable" content="yes">14 <meta name="apple-mobile-web-app-capable" content="yes">
15 <meta name="apple-mobile-web-app-status-bar-style" content="black">15 <meta name="apple-mobile-web-app-status-bar-style" content="black">
16 <meta name="format-detection" content="telephone=no">16 <meta name="format-detection" content="telephone=no">
17 <meta name="theme-color" content="#eee">
17 <meta name="robots" content="noodp">18 <meta name="robots" content="noodp">
18 <title>DevDocs API Documentation</title>19 <title>DevDocs API Documentation</title>
19 <link rel="canonical" href="<%= canonical_origin %>">20 <link rel="canonical" href="<%= canonical_origin %>">
@@ -32,7 +33,7 @@ · 12 unmodified lines
32 <link rel="mask-icon" href="<%= App.cdn_origin %>/images/webkit-mask-icon.svg" color="#398df0">33 <link rel="mask-icon" href="<%= App.cdn_origin %>/images/webkit-mask-icon.svg" color="#398df0">
33 <%= stylesheet_tag 'application' %>34 <%= stylesheet_tag 'application' %>
34</head>35</head>
35<body<%= %( class="#{app_layout}") if app_layout %>>36<body>
36<noscript class="_fail">DevDocs requires JavaScript to run.</noscript>37<noscript class="_fail">DevDocs requires JavaScript to run.</noscript>
37<%= erb :app -%>38<%= erb :app -%>
38<%= javascript_tag 'application', asset_host: false %>39<%= javascript_tag 'application', asset_host: false %>
views/manifest.erb+0−14
@@ -1,14 +0,0 @@
1CACHE MANIFEST
2# <%= app_theme %> <%= app_size %> <%= app_layout %>
3
4CACHE:
5/
6<%= manifest_asset_urls.join "\n" %>
7<%= doc_index_urls.join "\n" %>
8
9NETWORK:
10/s/
11*
12
13FALLBACK:
14/ /
views/other.erb+3−2
@@ -1,10 +1,11 @@
1<!DOCTYPE html>1<!DOCTYPE html>
2<html lang="en" class="_booting _theme-<%= app_theme %>">2<html lang="en" class="_booting">
3<head>3<head>
4 <meta charset="utf-8">4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">5 <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
6 <% if doc_index_page? %><meta name="description" content="<%= @doc['name'] %> <%= @doc['release'] %> API documentation with instant search, offline support, keyboard shortcuts, mobile version, and more."><% else %><meta name="robots" content="noindex"><% end %>6 <% if doc_index_page? %><meta name="description" content="<%= @doc['name'] %> <%= @doc['release'] %> API documentation with instant search, offline support, keyboard shortcuts, mobile version, and more."><% else %><meta name="robots" content="noindex"><% end %>
7 <meta name="format-detection" content="telephone=no">7 <meta name="format-detection" content="telephone=no">
8 <meta name="theme-color" content="#eee">
8 <meta property="og:image" content="<%= App.cdn_origin %>/images/icon-320.png">9 <meta property="og:image" content="<%= App.cdn_origin %>/images/icon-320.png">
9 <title>DevDocs<%= " &mdash; #{@doc['full_name']} documentation" if doc_index_page? %></title>10 <title>DevDocs<%= " &mdash; #{@doc['full_name']} documentation" if doc_index_page? %></title>
10 <link rel="canonical" href="<%= canonical_origin %><%= request.path %>">11 <link rel="canonical" href="<%= canonical_origin %><%= request.path %>">
@@ -13,7 +14,7 @@ · 2 unmodified lines
13 <link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Search DevDocs">14 <link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Search DevDocs">
14 <%= stylesheet_tag 'application' %>15 <%= stylesheet_tag 'application' %>
15</head>16</head>
16<body<%= %( class="#{app_layout}") if app_layout %> data-doc="<%= CGI::escape_html @doc.to_json %>">17<body data-doc="<%= CGI::escape_html @doc.to_json %>">
17<noscript class="_fail">DevDocs requires JavaScript to run.</noscript>18<noscript class="_fail">DevDocs requires JavaScript to run.</noscript>
18<%= erb :app -%>19<%= erb :app -%>
19<%= javascript_tag 'application', asset_host: false %><% unless App.production? %>20<%= javascript_tag 'application', asset_host: false %><% unless App.production? %>
views/service-worker.js.erb+49−0
@@ -0,0 +1,49 @@
1<%# The name of the cache to store responses in %>
2<%# If the cache name changes DevDocs is assumed to be updated %>
3const cacheName = '<%= service_worker_cache_name %>';
4
5<%# Url's to cache when the service worker is installed %>
6const urlsToCache = [
7 '/',
8 '/favicon.ico',
9 '/manifest.json',
10 '<%= service_worker_asset_urls.join "',\n '" %>',
11 '<%= doc_index_urls.join "',\n '" %>',
12];
13
14<%# Set-up the cache %>
15self.addEventListener('install', event => {
16 self.skipWaiting();
17
18 event.waitUntil(
19 caches.open(cacheName).then(cache => cache.addAll(urlsToCache)),
20 );
21});
22
23<%# Remove old caches %>
24self.addEventListener('activate', event => {
25 event.waitUntil((async () => {
26 const keys = await caches.keys();
27 const jobs = keys.map(key => key !== cacheName ? caches.delete(key) : Promise.resolve());
28 return Promise.all(jobs);
29 })());
30});
31
32<%# Handle HTTP requests %>
33self.addEventListener('fetch', event => {
34 event.respondWith((async () => {
35 const cachedResponse = await caches.match(event.request);
36 if (cachedResponse) return cachedResponse;
37
38 const url = new URL(event.request.url);
39
40 <%# Attempt to return the index page from the cache if the user is visiting a url like devdocs.io/offline or devdocs.io/javascript/global_objects/array/find %>
41 <%# The index page will handle the routing %>
42 if (url.origin === location.origin && !url.pathname.includes('.')) {
43 const cachedIndex = await caches.match('/');
44 if (cachedIndex) return cachedIndex;
45 }
46
47 return fetch(event.request);
48 })());
49});
views/unsupported.erb+6−6
@@ -11,16 +11,16 @@ · 10 unmodified lines
11 <p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:</p>11 <p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:</p>
12 <ul class="_fail-list">12 <ul class="_fail-list">
13 <li>Recent versions of Firefox, Chrome, or Opera</li>13 <li>Recent versions of Firefox, Chrome, or Opera</li>
14 <li>Safari 9.1+</li>14 <li>Safari 11.1+</li>
15 <li>Edge 16+</li>15 <li>Edge 17+</li>
16 <li>iOS 10+</li>16 <li>iOS 11.3+</li>
17 </ul>17 </ul>
18 <p class="_fail-text">18 <p class="_fail-text">
19 If you're unable to upgrade, I apologize.19 If you're unable to upgrade, we apologize.
20 I decided to prioritize speed and new features over support for older browsers.20 We decided to prioritize speed and new features over support for older browsers.
21 </p>21 </p>
22 <p class="_fail-text">22 <p class="_fail-text">
23 &mdash; Thibaut <a href="https://twitter.com/DevDocs" class="_fail-link">@DevDocs</a>23 &mdash; <a href="https://twitter.com/DevDocs">@DevDocs</a>
24 </p>24 </p>
25 </div>25 </div>
26</body>26</body>