History

Fix merge conflicts

Jasper van Merle committed 7 years ago

Commit 8a71af92 parents e2063f3 0f9ff4046 files · +368 −280Browse files

Unified · Split

46 files
  1. .gitignore +1 −0
  2. .travis.yml +8 −0
  3. Dockerfile +1 −0
  4. Dockerfile-alpine +1 −0
  5. README.md +5 −5
  6. assets/javascripts/app/app.coffee +13 −8
  7. assets/javascripts/app/appcache.coffee +0 −42
  8. assets/javascripts/app/config.coffee.erb +2 −0
  9. assets/javascripts/app/serviceworker.coffee +49 −0
  10. assets/javascripts/app/settings.coffee +33 −0
  11. assets/javascripts/app/update_checker.coffee +3 −3
  12. assets/javascripts/lib/page.coffee +17 −1
  13. assets/javascripts/lib/util.coffee +4 −0
  14. assets/javascripts/models/doc.coffee +3 −1
  15. assets/javascripts/news.json +4 −0
  16. assets/javascripts/templates/error_tmpl.coffee +5 −5
  17. assets/javascripts/templates/notif_tmpl.coffee +6 −0
  18. assets/javascripts/templates/pages/about_tmpl.coffee +2 −1
  19. assets/javascripts/templates/pages/offline_tmpl.coffee +9 −4
  20. assets/javascripts/templates/pages/root_tmpl.coffee.erb +2 −2
  21. assets/javascripts/templates/pages/settings_tmpl.coffee +8 −0
  22. assets/javascripts/tracking.js +27 −23
  23. assets/javascripts/views/content/entry_page.coffee +1 −1
  24. assets/javascripts/views/content/settings_page.coffee +10 −11
  25. assets/javascripts/views/layout/document.coffee +7 −5
  26. assets/javascripts/views/layout/resizer.coffee +2 −9
  27. assets/javascripts/views/layout/settings.coffee +1 −9
  28. assets/javascripts/views/search/search.coffee +8 −0
  29. assets/javascripts/views/search/search_scope.coffee +23 −5
  30. assets/javascripts/views/sidebar/sidebar.coffee +1 −1
  31. assets/stylesheets/components/_header.scss +1 −0
  32. assets/stylesheets/components/_notif.scss +4 −0
  33. docs/maintainers.md +4 −5
  34. lib/app.rb +35 −20
  35. lib/docs/core/scrapers/file_scraper.rb +2 −2
  36. public/docs/docs.json +0 −1
  37. public/images/webapp-icon-192.png +0 −0
  38. public/manifest.json +6 −0
  39. test/app_test.rb +0 −81
  40. test/lib/docs/core/scrapers/file_scraper_test.rb +2 −2
  41. views/app.erb +1 −13
  42. views/index.erb +3 −2
  43. views/manifest.erb +0 −14
  44. views/other.erb +3 −2
  45. views/service-worker.js.erb +49 −0
  46. views/unsupported.erb +3 −3
.gitignore+1−0
@@ -1,5 +1,6 @@
11 .DS_Store
22 .bundle
3+log
34 tmp
45 public/assets
56 public/fonts
.travis.yml+8−0
@@ -5,3 +5,11 @@ cache: bundler · 4 unmodified lines
55 before_script:
66 - gem update --system
77 - gem install bundler
8+
9+deploy:
10+ provider: heroku
11+ app: devdocs
12+ on:
13+ branch: master
14+ api_key:
15+ secure: "4p1klvWJZSOImzFcKOduILjP93hlOlAhceWlYMKS4tU+TCFE8qTBzdKdFPSCsCgjB+YR9pBss+L0lJpVVMjSwFHXqpKe6EeUSltO2k7DFHfW7kXLUM/L0AfqXz+YXk76XUyZMhvOEbldPfaMaj10e8vgDOQCSHABDyK/4CU+hnI="
Dockerfile+1−0
@@ -1,6 +1,7 @@
11 FROM ruby:2.6.0
22
33 ENV LANG=C.UTF-8
4+ENV ENABLE_SERVICE_WORKER=true
45
56 WORKDIR /devdocs
67
Dockerfile-alpine+1−0
@@ -1,6 +1,7 @@
11 FROM ruby:2.6.0-alpine
22
33 ENV LANG=C.UTF-8
4+ENV ENABLE_SERVICE_WORKER=true
45
56 WORKDIR /devdocs
67
README.md+5−5
@@ -59,14 +59,14 @@ The web app is all client-side JavaScript, written in [CoffeeScript](http://coff · 58 unmodified lines
5959
6060 Many 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.
6161
62−Another 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.
62+Another 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.
6363
6464 DevDocs being a developer tool, the browser requirements are high:
6565
6666 * Recent versions of Firefox, Chrome, or Opera
67−* Safari 9.1+
68−* Edge 16+
69−* iOS 10+
67+* Safari 11.1+
68+* Edge 17+
69+* iOS 11.3+
7070
7171 This allows the code to take advantage of the latest DOM and HTML5 APIs and make developing DevDocs a lot more fun!
7272
@@ -156,7 +156,7 @@ Contributions are welcome. Please read the [contributing guidelines](./.github/C · 83 unmodified lines
156156 * [Doc Browser](https://github.com/qwfy/doc-browser) is a native Linux app that supports DevDocs docsets
157157 * [GNOME Application](https://github.com/hardpixel/devdocs-desktop) GTK3 application with search integrated in headerbar
158158 * [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 with AppCache enabled
159+* [Android Application](https://github.com/Merith-TK/devdocs_webapp_kotlin) is a fully working, advanced WebView
160160
161161 ## Copyright / License
162162
assets/javascripts/app/app.coffee+13−8
@@ -13,10 +13,12 @@ · 12 unmodified lines
1313
1414 @el = $('._app')
1515 @localStorage = new LocalStorageStore
16− @appCache = new app.AppCache if app.AppCache.isEnabled()
16+ @serviceWorker = new app.ServiceWorker if app.ServiceWorker.isEnabled()
1717 @settings = new app.Settings
1818 @db = new app.DB()
1919
20+ @settings.initLayout()
21+
2022 @docs = new app.collections.Docs
2123 @disabledDocs = new app.collections.Docs
2224 @entries = new app.collections.Entries
@@ -138,7 +140,10 @@ · 115 unmodified lines
138140 @docs.sort()
139141 @initDoc(doc)
140142 @saveDocs()
141− _onSuccess()
143+ if app.settings.get('autoInstall')
144+ doc.install(_onSuccess, onError)
145+ else
146+ _onSuccess()
142147 return
143148
144149 doc.load onSuccess, onError, writeCache: true
@@ -147,7 +152,7 @@ · 2 unmodified lines
147152 saveDocs: ->
148153 @settings.setDocs(doc.slug for doc in @docs.all())
149154 @db.migrate()
150− @appCache?.updateInBackground()
155+ @serviceWorker?.updateInBackground()
151156
152157 welcomeBack: ->
153158 visitCount = @settings.get('count')
@@ -167,14 +172,14 @@ · 13 unmodified lines
167172 reload: ->
168173 @docs.clearCache()
169174 @disabledDocs.clearCache()
170− if @appCache then @appCache.reload() else @reboot()
175+ if @serviceWorker then @serviceWorker.reload() else @reboot()
171176 return
172177
173178 reset: ->
174179 @localStorage.reset()
175180 @settings.reset()
176181 @db?.reset()
177− @appCache?.update()
182+ @serviceWorker?.update()
178183 window.location = '/'
179184 return
180185
@@ -193,9 +198,9 @@ · 12 unmodified lines
193198 return
194199
195200 indexHost: ->
196− # Can't load the index files from the host/CDN when applicationCache is
201+ # Can't load the index files from the host/CDN when service worker is
197202 # enabled because it doesn't support caching URLs that use CORS.
198− @config[if @appCache and @settings.hasDocs() then 'index_path' else 'docs_origin']
203+ @config[if @serviceWorker and @settings.hasDocs() then 'index_path' else 'docs_origin']
199204
200205 onBootError: (args...) ->
201206 @trigger 'bootError'
@@ -252,7 +257,7 @@ · 50 unmodified lines
252257 matchMedia: !!window.matchMedia
253258 insertAdjacentHTML: !!document.body.insertAdjacentHTML
254259 defaultPrevented: document.createEvent('CustomEvent').defaultPrevented is false
255− cssVariables: CSS.supports and CSS.supports('(--t: 0)')
260+ cssVariables: !!CSS?.supports?('(--t: 0)')
256261
257262 for key, value of features when not value
258263 Raven.captureMessage "unsupported/#{key}", level: 'info'
assets/javascripts/app/appcache.coffee+0−42
@@ -1,42 +0,0 @@
1−class 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', -> app.reboot()
31− @notifyUpdate = false
32− @notifyProgress = true
33− try @cache.update() catch
34− return
35−
36− onProgress: (event) =>
37− @trigger 'progress', event if @notifyProgress
38− return
39−
40− onUpdateReady: =>
41− @trigger 'updateready' if @notifyUpdate
42− return
assets/javascripts/app/config.coffee.erb+2−0
@@ -13,3 +13,5 @@ app.config = · 12 unmodified lines
1313 version: <%= Time.now.to_i %>
1414 release: <%= Time.now.utc.httpdate.to_json %>
1515 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 @@
1+class 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+33−0
@@ -5,11 +5,13 @@ class app.Settings · 4 unmodified lines
55 'manualUpdate'
66 'fastScroll'
77 'arrowScroll'
8+ 'analyticsConsent'
89 'docs'
910 'dark'
1011 'layout'
1112 'size'
1213 'tips'
14+ 'autoInstall'
1315 ]
1416
1517 INTERNAL_KEYS = [
@@ -19,6 +21,8 @@ class app.Settings · 3 unmodified lines
1921 'news'
2022 ]
2123
24+ LAYOUTS: ['_max-width', '_sidebar-hidden', '_native-scrollbars']
25+
2226 @defaults:
2327 count: 0
2428 hideDisabled: false
@@ -26,6 +30,7 @@ class app.Settings · 1 unmodified line
2630 news: 0
2731 manualUpdate: false
2832 schema: 1
33+ analyticsConsent: false
2934
3035 constructor: ->
3136 @store = new CookieStore
@@ -38,6 +43,7 @@ class app.Settings · 6 unmodified lines
3843 set: (key, value) ->
3944 @store.set(key, value)
4045 delete @cache[key]
46+ @toggleDark(value) if key == 'dark'
4147 return
4248
4349 del: (key) ->
@@ -63,6 +69,8 @@ class app.Settings · 19 unmodified lines
6369 return
6470
6571 setLayout: (name, enable) ->
72+ @toggleLayout(name, enable)
73+
6674 layout = (@store.get('layout') || '').split(' ')
6775 $.arrayDelete(layout, '')
6876
@@ -104,3 +112,28 @@ class app.Settings · 35 unmodified lines
104112 @store.reset()
105113 @cache = {}
106114 return
115+
116+ initLayout: ->
117+ @toggleDark(@get('dark') is 1)
118+ @toggleLayout(layout, @hasLayout(layout)) for layout in @LAYOUTS
119+ @initSidebarWidth()
120+ return
121+
122+ toggleDark: (enable) ->
123+ classList = document.documentElement.classList
124+ classList.toggle('_theme-default', !enable)
125+ classList.toggle('_theme-dark', enable)
126+ color = getComputedStyle(document.documentElement).getPropertyValue('--headerBackground').trim()
127+ $('meta[name=theme-color]').setAttribute('content', color)
128+ return
129+
130+ toggleLayout: (layout, enable) ->
131+ classList = document.body.classList
132+ classList.toggle(layout, enable) unless layout is '_sidebar-hidden'
133+ classList.toggle('_overlay-scrollbars', $.overlayScrollbarsEnabled())
134+ return
135+
136+ initSidebarWidth: ->
137+ size = @get('size')
138+ document.documentElement.style.setProperty('--sidebarWidth', size + 'px') if size
139+ return
assets/javascripts/app/update_checker.coffee+3−3
@@ -3,13 +3,13 @@ class app.UpdateChecker · 2 unmodified lines
33 @lastCheck = Date.now()
44
55 $.on window, 'focus', @onFocus
6− app.appCache.on 'updateready', @onUpdateReady if app.appCache
6+ app.serviceWorker?.on 'updateready', @onUpdateReady
77
88 setTimeout @checkDocs, 0
99
1010 check: ->
11− if app.appCache
12− app.appCache.update()
11+ if app.serviceWorker
12+ app.serviceWorker.update()
1313 else
1414 ajax
1515 url: $('script[src*="application"]').getAttribute('src')
assets/javascripts/lib/page.coffee+17−1
@@ -199,5 +199,21 @@ page.track = (fn) -> · 198 unmodified lines
199199 return
200200
201201 track = ->
202− tracker.call() for tracker in trackers
202+ consentGiven = Cookies.get('analyticsConsent')
203+ consentAsked = Cookies.get('analyticsConsentAsked')
204+
205+ if consentGiven == '1'
206+ tracker.call() for tracker in trackers
207+ else if consentGiven == undefined and consentAsked == undefined
208+ # Only ask for consent once per browser session
209+ Cookies.set('analyticsConsentAsked', '1')
210+
211+ new app.views.Notif 'AnalyticsConsent', autoHide: null
212+ return
213+
214+@resetAnalytics = ->
215+ for cookie in document.cookie.split(/;\s?/)
216+ name = cookie.split('=')[0]
217+ if name[0] == '_'
218+ Cookies.expire(name)
203219 return
assets/javascripts/lib/util.coffee+4−0
@@ -352,6 +352,10 @@ isIE = null · 351 unmodified lines
352352 $.isIE = ->
353353 isIE ?= navigator.userAgent?.indexOf('MSIE') >= 0 || navigator.userAgent?.indexOf('rv:11.0') >= 0
354354
355+isChromeForAndroid = null
356+$.isChromeForAndroid = ->
357+ isChromeForAndroid ?= navigator.userAgent?.indexOf('Android') >= 0 && /Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent)
358+
355359 isAndroid = null
356360 $.isAndroid = ->
357361 isAndroid ?= navigator.userAgent?.indexOf('Android') >= 0
assets/javascripts/models/doc.coffee+3−1
@@ -142,4 +142,6 @@ class app.models.Doc extends app.Model · 141 unmodified lines
142142 return
143143
144144 isOutdated: (status) ->
145− status and status.installed and @mtime isnt status.mtime
145+ return false if not status
146+ isInstalled = status.installed or app.settings.get('autoInstall')
147+ isInstalled and @mtime isnt status.mtime
assets/javascripts/news.json+4−0
@@ -1,4 +1,8 @@
11 [
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+ ],
26 [
37 "2018-09-23",
48 "New documentations: <a href=\"/puppeteer/\">Puppeteer</a> and <a href=\"/handlebars/\">Handlebars.js</a>"
assets/javascripts/templates/error_tmpl.coffee+5−5
@@ -12,8 +12,8 @@ app.templates.notFoundPage = -> · 11 unmodified lines
1212
1313 app.templates.pageLoadError = ->
1414 error """ The page failed to load. """,
15− """ It may be missing from the server (try reloading the app) or you could be offline.<br>
16− If you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. """,
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're online and you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. """,
1717 """ #{back} &middot; <a href="/##{location.pathname}" target="_top" class="_error-link">Reload</a>
1818 &middot; <a href="#" class="_error-link" data-retry>Retry</a> """
1919
@@ -57,9 +57,9 @@ app.templates.unsupportedBrowser = """ · 37 unmodified lines
5757 <p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:
5858 <ul class="_fail-list">
5959 <li>Recent versions of Firefox, Chrome, or Opera
60− <li>Safari 9.1+
61− <li>Edge 16+
62− <li>iOS 10+
60+ <li>Safari 11.1+
61+ <li>Edge 17+
62+ <li>iOS 11.3+
6363 </ul>
6464 <p class="_fail-text">
6565 If you're unable to upgrade, we apologize.
assets/javascripts/templates/notif_tmpl.coffee+6−0
@@ -68,3 +68,9 @@ app.templates.notifShare = -> · 67 unmodified lines
6868 app.templates.notifUpdateDocs = ->
6969 textNotif """ Documentation updates available. """,
7070 """ <a href="/offline">Install them</a> as soon as possible to avoid broken pages. """
71+
72+app.templates.notifAnalyticsConsent = ->
73+ textNotif """ Tracking cookies """,
74+ """ We would like to gather usage data about how DevDocs is used through Google Analytics and Gauges. We only collect anonymous traffic information.
75+ Please confirm if you accept our tracking cookies. You can always change your decision in the settings.
76+ <br><span class="_notif-right"><a href="#" data-behavior="accept-analytics">Accept</a> or <a href="#" data-behavior="decline-analytics">Decline</a></span> """
assets/javascripts/templates/pages/about_tmpl.coffee+2−1
@@ -73,7 +73,8 @@ app.templates.aboutPage = -> """ · 72 unmodified lines
7373 <ul>
7474 <li><a href="https://devdocs.io">devdocs.io</a> ("App") is operated by <a href="https://www.freecodecamp.org/">freeCodeCamp</a> ("We").
7575 <li>We do not collect personal information through the app.
76− <li>We use Google Analytics, Gauges and Sentry to collect anonymous traffic information and improve the app.
76+ <li>We use Google Analytics and Gauges to collect anonymous traffic information if you have given consent to this. You can change your decision in the <a href="/settings">settings</a>.
77+ <li>We use Sentry to collect crash data and improve the app.
7778 <li>The app uses cookies to store user preferences.
7879 <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.
7980 <li>If you have any questions regarding privacy, please email <a href="mailto:privacy@freecodecamp.org">privacy@freecodecamp.org</a>.
assets/javascripts/templates/pages/offline_tmpl.coffee+9−4
@@ -25,8 +25,8 @@ app.templates.offlinePage = (docs) -> """ · 24 unmodified lines
2525 <h2 class="_block-heading">Questions & Answers</h2>
2626 <dl>
2727 <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>
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.
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://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.
3030 <dt>Can I close the tab/browser?
3131 <dd>#{canICloseTheTab()}
3232 <dt>What if I don't update a documentation?
@@ -41,10 +41,15 @@ app.templates.offlinePage = (docs) -> """ · 8 unmodified lines
4141 """
4242
4343 canICloseTheTab = ->
44− if app.AppCache.isEnabled()
44+ if app.ServiceWorker.isEnabled()
4545 """ 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). """
4646 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>
4853 The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). """
4954
5055 app.templates.offlineDoc = (doc, status) ->
assets/javascripts/templates/pages/root_tmpl.coffee.erb+2−2
@@ -8,7 +8,7 @@ app.templates.intro = """ · 7 unmodified lines
88 <p>Thanks for downloading DevDocs. Here are a few things you should know:
99 <ol class="_intro-list">
1010 <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>.
1212 <li>Run <code>thor docs:list</code> to see all available documentations.
1313 <li>Run <code>thor docs:download &lt;name&gt;</code> to download documentations.
1414 <li>Run <code>thor docs:download --installed</code> to update all downloaded documentations.
@@ -39,7 +39,7 @@ app.templates.intro = """ · 24 unmodified lines
3939 <li>DevDocs works <a href="/offline">offline</a>, on mobile, and can be installed on <a href="https://chrome.google.com/webstore/detail/devdocs/mnfehgbmkapmjnhcnbodoamcioleeooe">Chrome</a>.
4040 <li>For the latest news, follow <a href="https://twitter.com/DevDocs">@DevDocs</a>.
4141 <li>DevDocs is free and <a href="https://github.com/freeCodeCamp/devdocs">open source</a>.
42− <iframe class="_github-btn" src="//ghbtns.com/github-btn.html?user=freeCodeCamp&repo=devdocs&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20"></iframe>
42+ <object data="https://img.shields.io/github/stars/freeCodeCamp/devdocs.svg?style=social" type="image/svg+xml" aria-hidden="true"></object>
4343 <li>And if you're new to coding, check out <a href="https://www.freecodecamp.org/">freeCodeCamp's open source curriculum</a>.
4444 </ol>
4545 <p>Happy coding!
assets/javascripts/templates/pages/settings_tmpl.coffee+8−0
@@ -15,6 +15,14 @@ app.templates.settingsPage = (settings) -> """ · 14 unmodified lines
1515 <input type="checkbox" form="settings" name="layout" value="_sidebar-hidden"#{if settings['_sidebar-hidden'] then ' checked' else ''}>Automatically hide and show the sidebar
1616 <small>Tip: drag the edge of the sidebar to resize it.</small>
1717 </label>
18+ <label class="_settings-label">
19+ <input type="checkbox" form="settings" name="autoInstall" value="_auto-install"#{if settings.autoInstall then ' checked' else ''}>Automatically download documentation for offline use
20+ <small>Only enable this when bandwidth isn't a concern to you.</small>
21+ </label>
22+ <label class="_settings-label">
23+ <input type="checkbox" form="settings" name="analyticsConsent"#{if settings.analyticsConsent then ' checked' else ''}>Enable tracking cookies
24+ <small>With this checked, we enable Google Analytics and Gauges to collect anonymous traffic information.</small>
25+ </label>
1826 </div>
1927 </div>
2028
assets/javascripts/tracking.js+27−23
@@ -1,28 +1,32 @@
11 try {
2− if (app.config.env == 'production') {
3− (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
4− (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
5− m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
6− })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
7− ga('create', 'UA-5544833-12', 'devdocs.io');
8− page.track(function() {
9− ga('send', 'pageview', {
10− page: location.pathname + location.search + location.hash,
11− dimension1: app.router.context && app.router.context.doc && app.router.context.doc.slug_without_version
2+ if (app.config.env === 'production') {
3+ if (Cookies.get('analyticsConsent') === '1') {
4+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
5+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
6+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
7+ })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
8+ ga('create', 'UA-5544833-12', 'devdocs.io');
9+ page.track(function() {
10+ ga('send', 'pageview', {
11+ page: location.pathname + location.search + location.hash,
12+ dimension1: app.router.context && app.router.context.doc && app.router.context.doc.slug_without_version
13+ });
1214 });
13− });
1415
15− page.track(function() {
16− if (window._gauges)
17− _gauges.push(['track']);
18− else
19− (function() {
20− var _gauges=_gauges||[];!function(){var a=document.createElement("script");
21− a.type="text/javascript",a.async=!0,a.id="gauges-tracker",
22− a.setAttribute("data-site-id","51c15f82613f5d7819000067"),
23− a.src="https://secure.gaug.es/track.js";var b=document.getElementsByTagName("script")[0];
24− b.parentNode.insertBefore(a,b)}();
25− })();
26− });
16+ page.track(function() {
17+ if (window._gauges)
18+ _gauges.push(['track']);
19+ else
20+ (function() {
21+ var _gauges=_gauges||[];!function(){var a=document.createElement("script");
22+ a.type="text/javascript",a.async=!0,a.id="gauges-tracker",
23+ a.setAttribute("data-site-id","51c15f82613f5d7819000067"),
24+ a.src="https://secure.gaug.es/track.js";var b=document.getElementsByTagName("script")[0];
25+ b.parentNode.insertBefore(a,b)}();
26+ })();
27+ });
28+ } else {
29+ resetAnalytics();
30+ }
2731 }
2832 } catch(e) { }
assets/javascripts/views/content/entry_page.coffee+1−1
@@ -123,7 +123,7 @@ class app.views.EntryPage extends app.View · 122 unmodified lines
123123 @render @tmpl('pageLoadError')
124124 @resetClass()
125125 @addClass @constructor.errorClass
126− app.appCache?.update()
126+ app.serviceWorker?.update()
127127 return
128128
129129 cache: ->
assets/javascripts/views/content/settings_page.coffee+10−11
@@ -1,7 +1,4 @@
11 class app.views.SettingsPage extends app.View
2− LAYOUTS = ['_max-width', '_sidebar-hidden', '_native-scrollbars']
3− SIDEBAR_HIDDEN_LAYOUT = '_sidebar-hidden'
4−
52 @className: '_static'
63
74 @events:
@@ -17,31 +14,31 @@ class app.views.SettingsPage extends app.View · 9 unmodified lines
1714 settings.dark = app.settings.get('dark')
1815 settings.smoothScroll = !app.settings.get('fastScroll')
1916 settings.arrowScroll = app.settings.get('arrowScroll')
20− settings[layout] = app.settings.hasLayout(layout) for layout in LAYOUTS
17+ settings.autoInstall = app.settings.get('autoInstall')
18+ settings.analyticsConsent = app.settings.get('analyticsConsent')
19+ settings[layout] = app.settings.hasLayout(layout) for layout in app.settings.LAYOUTS
2120 settings
2221
2322 getTitle: ->
2423 'Preferences'
2524
2625 toggleDark: (enable) ->
27− html = document.documentElement
28− html.classList.toggle('_theme-default')
29− html.classList.toggle('_theme-dark')
3026 app.settings.set('dark', !!enable)
31− app.appCache?.updateInBackground()
3227 return
3328
3429 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')
3730 app.settings.setLayout(layout, enable)
38− app.appCache?.updateInBackground()
3931 return
4032
4133 toggleSmoothScroll: (enable) ->
4234 app.settings.set('fastScroll', !enable)
4335 return
4436
37+ toggleAnalyticsConsent: (enable) ->
38+ app.settings.set('analyticsConsent', if enable then '1' else '0')
39+ resetAnalytics() unless enable
40+ return
41+
4542 toggle: (name, enable) ->
4643 app.settings.set(name, enable)
4744 return
@@ -85,6 +82,8 @@ class app.views.SettingsPage extends app.View · 37 unmodified lines
8582 @toggleSmoothScroll input.checked
8683 when 'import'
8784 @import input.files[0], input
85+ when 'analyticsConsent'
86+ @toggleAnalyticsConsent input.checked
8887 else
8988 @toggle input.name, input.checked
9089 return
assets/javascripts/views/layout/document.coffee+7−5
@@ -75,9 +75,11 @@ class app.views.Document extends app.View · 74 unmodified lines
7575 return unless target.hasAttribute('data-behavior')
7676 $.stopEvent(event)
7777 switch target.getAttribute('data-behavior')
78− when 'back' then history.back()
79− when 'reload' then window.location.reload()
80− when 'reboot' then app.reboot()
81− when 'hard-reload' then app.reload()
82− when 'reset' then app.reset() if confirm('Are you sure you want to reset DevDocs?')
78+ when 'back' then history.back()
79+ when 'reload' then window.location.reload()
80+ when 'reboot' then app.reboot()
81+ when 'hard-reload' then app.reload()
82+ when 'reset' then app.reset() if confirm('Are you sure you want to reset DevDocs?')
83+ when 'accept-analytics' then Cookies.set('analyticsConsent', '1') && app.reboot()
84+ when 'decline-analytics' then Cookies.set('analyticsConsent', '0') && app.reboot()
8385 return
assets/javascripts/views/layout/resizer.coffee+2−9
@@ -11,9 +11,6 @@ class app.views.Resizer extends app.View · 10 unmodified lines
1111 init: ->
1212 @el.setAttribute('draggable', 'true')
1313 @appendTo $('._app')
14−
15− @style = $('style[data-resizer]')
16− @size = @style.getAttribute('data-size')
1714 return
1815
1916 MIN = 260
@@ -24,15 +21,11 @@ class app.views.Resizer extends app.View · 4 unmodified lines
2421 return unless value > 0
2522 value = Math.min(Math.max(Math.round(value), MIN), MAX)
2623 newSize = "#{value}px"
27− @style.innerHTML = @style.innerHTML.replace(new RegExp(@size, 'g'), newSize)
28− @size = newSize
29− if save
30− app.settings.setSize(value)
31− app.appCache?.updateInBackground()
24+ document.documentElement.style.setProperty('--sidebarWidth', newSize)
25+ app.settings.setSize(value) if save
3226 return
3327
3428 onDragStart: (event) =>
35− @style.removeAttribute('disabled')
3629 event.dataTransfer.effectAllowed = 'link'
3730 event.dataTransfer.setData('Text', '')
3831 $.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
2525 if super
2626 @render()
2727 document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT)
28− app.appCache?.on 'progress', @onAppCacheProgress
2928 return
3029
3130 deactivate: ->
@@ -33,7 +32,6 @@ class app.views.Settings extends app.View · 1 unmodified line
3332 @resetClass()
3433 @docPicker.detach()
3534 document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT) if app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT)
36− app.appCache?.off 'progress', @onAppCacheProgress
3735 return
3836
3937 render: ->
@@ -52,7 +50,7 @@ class app.views.Settings extends app.View · 12 unmodified lines
5250 docs = @docPicker.getSelectedDocs()
5351 app.settings.setDocs(docs)
5452
55− @saveBtn.textContent = if app.appCache then 'Downloading\u2026' else 'Saving\u2026'
53+ @saveBtn.textContent = 'Saving\u2026'
5654 disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1)
5755 disabledDocs.uninstall ->
5856 app.db.migrate()
@@ -83,9 +81,3 @@ class app.views.Settings extends app.View · 24 unmodified lines
8381 $.stopEvent(event)
8482 app.router.show '/'
8583 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
3030 .on 'results', @onResults
3131 .on 'end', @onEnd
3232
33+ @scope
34+ .on 'change', @onScopeChange
35+
3336 app.on 'ready', @onReady
3437 $.on window, 'hashchange', @searchUrl
3538 $.on window, 'focus', @onWindowFocus
@@ -138,6 +141,11 @@ class app.views.Search extends app.View · 102 unmodified lines
138141 $.stopEvent(event)
139142 return
140143
144+ onScopeChange: =>
145+ @value = ''
146+ @onInput()
147+ return
148+
141149 afterRoute: (name, context) =>
142150 return if app.shortcuts.eventInProgress?.name is 'escape'
143151 @reset(true) if not context.init and app.router.isIndex()
assets/javascripts/views/search/search_scope.coffee+23−5
@@ -6,7 +6,9 @@ class app.views.SearchScope extends app.View · 5 unmodified lines
66 tag: '._search-tag'
77
88 @events:
9+ click: 'onClick'
910 keydown: 'onKeydown'
11+ textInput: 'onTextInput'
1012
1113 @routes:
1214 after: 'afterRoute'
@@ -87,17 +89,33 @@ class app.views.SearchScope extends app.View · 74 unmodified lines
8789 @trigger 'change', null, previousDoc
8890 return
8991
92+ doScopeSearch: (event) =>
93+ @search @input.value[0...@input.selectionStart]
94+ $.stopEvent(event) if @doc
95+ return
96+
97+ onClick: (event) =>
98+ if event.target is @tag
99+ @reset()
100+ $.stopEvent(event)
101+ return
102+
90103 onKeydown: (event) =>
91104 if event.which is 8 # backspace
92− if @doc and not @input.value
93− $.stopEvent(event)
105+ if @doc and @input.selectionEnd is 0
94106 @reset()
95− else if not @doc and @input.value
107+ $.stopEvent(event)
108+ else if not @doc and @input.value and not $.isChromeForAndroid()
96109 return if event.ctrlKey or event.metaKey or event.altKey or event.shiftKey
97110 if event.which is 9 or # tab
98111 (event.which is 32 and app.isMobile()) # space
99− @search @input.value[0...@input.selectionStart]
100− $.stopEvent(event) if @doc
112+ @doScopeSearch(event)
113+ return
114+
115+ onTextInput: (event) =>
116+ return unless $.isChromeForAndroid()
117+ if not @doc and @input.value and event.data == ' '
118+ @doScopeSearch(event)
101119 return
102120
103121 extractHashValue: ->
assets/javascripts/views/sidebar/sidebar.coffee+1−1
@@ -28,7 +28,7 @@ class app.views.Sidebar extends app.View · 27 unmodified lines
2828
2929 app.on 'ready', @onReady
3030
31− $.on document.documentElement, 'mouseleave', (event) => @display() if event.clientX < 10
31+ $.on document.documentElement, 'mouseleave', (event) => @display() unless event.clientX <= 0
3232 $.on document.documentElement, 'mouseenter', => @resetDisplay(forceNoHover: false)
3333 return
3434
assets/stylesheets/components/_header.scss+1−0
@@ -215,5 +215,6 @@ · 214 unmodified lines
215215 color: var(--textColorLight);
216216 background: var(--searchTagBackground);
217217 border-radius: 2px;
218+ cursor: pointer;
218219 @extend %truncate-text;
219220 }
assets/stylesheets/components/_notif.scss+4−0
@@ -134,3 +134,7 @@ · 133 unmodified lines
134134
135135 ._notif-info { color: var(--textColorLight); }
136136 }
137+
138+._notif-right {
139+ float: right;
140+}
docs/maintainers.md+4−5
@@ -21,8 +21,7 @@ The process for updating docs is as follow: · 20 unmodified lines
2121 - Commit the changes (protip: use the `thor docs:commit` command documented below).
2222 - Optional: do more updates.
2323 - Run `thor docs:upload` (documented below).
24−- [Deploy the app](#deploying-devdocs) and verify that everything works in production.
25−- Push to GitHub.
24+- Push to GitHub to [deploy the app](#deploying-devdocs) and verify that everything works in production.
2625 - Run `thor docs:clean` (documented below).
2726
2827 Note: 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`.
@@ -82,13 +81,13 @@ In addition to the [publicly-documented commands](https://github.com/freeCodeCam · 53 unmodified lines
8281
8382 ## Deploying DevDocs
8483
85−Once docs have been uploaded via `thor docs:upload` (if applicable), deploying DevDocs is as simple as running `git push heroku master`. See [Heroku's documentation](https://devcenter.heroku.com/articles/git) for more information.
84+Once 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.
8685
87−- If you're deploying documentation updates, verify that the documentations work properly once the deploy is done (you will need to reload [devdocs.io](https://devdocs.io/) a couple times for the application cache to update and the new version to load).
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.
8887 - If you're deploying frontend changes, monitor [Sentry](https://sentry.io/devdocs/devdocs-js/) for new JS errors once the deploy is done.
8988 - 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.
9089
91−If any issue arises, run `heroku rollback` to rollback to the previous 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.
90+If 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.
9291
9392 If this is your first deploy, make sure another maintainer is around to assist.
9493
lib/app.rb+35−20
@@ -37,6 +37,9 @@ class App < Sinatra::Application · 36 unmodified lines
3737
3838 set :csp, false
3939
40+ require 'docs'
41+ Docs.generate_manifest
42+
4043 Dir[docs_path, root.join(assets_prefix, '*/')].each do |path|
4144 sprockets.append_path(path)
4245 end
@@ -197,33 +200,45 @@ class App < Sinatra::Application · 154 unmodified lines
197200 request.query_string.empty? ? nil : "?#{request.query_string}"
198201 end
199202
200− def manifest_asset_urls
201− @@manifest_asset_urls ||= [
203+ def service_worker_asset_urls
204+ @@service_worker_asset_urls ||= [
202205 javascript_path('application', asset_host: false),
203206 stylesheet_path('application'),
204207 image_path('sprites/docs.png'),
205208 image_path('sprites/docs@2x.png'),
206− asset_path('docs.js')
207− ]
209+ asset_path('docs.js'),
210+ App.production? ? nil : javascript_path('debug'),
211+ ].compact
208212 end
209213
210− def app_size
211− @app_size ||= memoized_cookies['size'].nil? ? '20rem' : "#{memoized_cookies['size']}px"
212− end
214+ # Returns a cache name for the service worker to use which changes if any of the assets changes
215+ # When a manifest exist, this name is only created once based on the asset manifest because it never changes without a server restart
216+ # 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
217+ def service_worker_cache_name
218+ if File.exist?(App.assets_manifest_path)
219+ if defined?(@@service_worker_cache_name)
220+ return @@service_worker_cache_name
221+ end
213222
214− def app_layout
215− memoized_cookies['layout']
216− end
223+ digest = Sprockets::Manifest
224+ .new(nil, App.assets_manifest_path)
225+ .files
226+ .values
227+ .map {|file| file["digest"]}
228+ .join
217229
218− def app_theme
219− @app_theme ||= memoized_cookies['dark'].nil? ? 'default' : 'dark'
220− end
230+ return @@service_worker_cache_name ||= Digest::MD5.hexdigest(digest)
231+ else
232+ paths = App.sprockets
233+ .each_file
234+ .to_a
235+ .reject {|file| file.start_with?(App.docs_path)}
221236
222− def dark_theme?
223− app_theme == 'dark'
237+ return App.sprockets.pack_hexdigest(App.sprockets.files_digest(paths))
238+ end
224239 end
225240
226− def redirect_via_js(path) # courtesy of HTML5 App Cache
241+ def redirect_via_js(path)
227242 response.set_cookie :initial_path, value: path, expires: Time.now + 15, path: '/'
228243 redirect '/', 302
229244 end
@@ -246,15 +261,15 @@ class App < Sinatra::Application · 16 unmodified lines
246261 end
247262 end
248263
249− get '/manifest.appcache' do
250− content_type 'text/cache-manifest'
264+ get '/service-worker.js' do
265+ content_type 'application/javascript'
251266 expires 0, :'no-cache'
252− erb :manifest
267+ erb :'service-worker.js'
253268 end
254269
255270 get '/' do
256271 return redirect "/#q=#{params[:q]}" if params[:q]
257− return redirect '/' unless request.query_string.empty? # courtesy of HTML5 App Cache
272+ return redirect '/' unless request.query_string.empty?
258273 response.headers['Content-Security-Policy'] = settings.csp if settings.csp
259274 erb :index
260275 end
lib/docs/core/scrapers/file_scraper.rb+2−2
@@ -29,7 +29,7 @@ module Docs · 28 unmodified lines
2929
3030 def request_one(url)
3131 assert_source_directory_exists
32− Response.new read_file(url_to_path(url)), URL.parse(url)
32+ Response.new read_file(File.join(source_directory, url_to_path(url))), URL.parse(url)
3333 end
3434
3535 def request_all(urls)
@@ -50,7 +50,7 @@ module Docs · 14 unmodified lines
5050 end
5151
5252 def read_file(path)
53− File.read(File.join(source_directory, path))
53+ File.read(path)
5454 rescue
5555 instrument 'warn.doc', msg: "Failed to open file: #{path}"
5656 nil
public/docs/docs.json+0−1
@@ -1 +0,0 @@
1−[]
public/images/webapp-icon-192.png+0−0

Binary file not shown.

public/manifest.json+6−0
@@ -4,6 +4,7 @@ · 3 unmodified lines
44 "description": "API Documentation Browser",
55 "start_url": "/",
66 "display": "standalone",
7+ "background_color": "#EEEEEE",
78 "icons": [
89 {
910 "src": "/images/webapp-icon-32.png",
@@ -25,6 +26,11 @@ · 15 unmodified lines
2526 "sizes": "128x128",
2627 "type": "image/png"
2728 },
29+ {
30+ "src": "/images/webapp-icon-192.png",
31+ "sizes": "192x192",
32+ "type": "image/png"
33+ },
2834 {
2935 "src": "/images/webapp-icon-256.png",
3036 "sizes": "256x256",
test/app_test.rb+0−81
@@ -43,35 +43,6 @@ class AppTest < MiniTest::Spec · 42 unmodified lines
4343 assert last_response.redirect?
4444 assert_equal 'https://example.org/', last_response['Location']
4545 end
46−
47− it "sets default size" do
48− get '/'
49− assert_includes last_response.body, 'data-size="20rem"'
50− end
51−
52− it "sets size from cookie" do
53− set_cookie('size=42')
54− get '/'
55− assert_includes last_response.body, 'data-size="42px"'
56− end
57−
58− it "sets layout from cookie" do
59− set_cookie('layout=foo')
60− get '/'
61− assert_includes last_response.body, '<body class="foo">'
62− end
63−
64− it "sets the <html> theme from cookie" do
65− get '/'
66− assert_match %r{<html [^>]*class="[^\"]*_theme-default}, last_response.body
67− refute_includes last_response.body, '_theme-dark'
68−
69− set_cookie('dark=1')
70−
71− get '/'
72− assert_match %r{<html [^>]*class="[^\"]*_theme-dark}, last_response.body
73− refute_includes last_response.body, '_theme-default'
74− end
7546 end
7647
7748 describe "/[static-page]" do
@@ -106,58 +77,6 @@ class AppTest < MiniTest::Spec · 28 unmodified lines
10677 end
10778 end
10879
109− describe "/manifest.appcache" do
110− it "works" do
111− get '/manifest.appcache'
112− assert last_response.ok?
113− end
114−
115− it "works with cookie" do
116− set_cookie('docs=css/html~5')
117− get '/manifest.appcache'
118− assert last_response.ok?
119− assert_includes last_response.body, '/css/index.json?1420139788'
120− assert_includes last_response.body, '/html~5/index.json?1420139791'
121− end
122−
123− it "ignores invalid docs in the cookie" do
124− set_cookie('docs=foo')
125− get '/manifest.appcache'
126− assert last_response.ok?
127− refute_includes last_response.body, 'foo'
128− end
129−
130− it "has the word 'default' when no 'dark' cookie is set" do
131− get '/manifest.appcache'
132− assert_includes last_response.body, '# default'
133− refute_includes last_response.body, '# dark'
134− end
135−
136− it "has the word 'dark' when the cookie is set" do
137− set_cookie('dark=1')
138− get '/manifest.appcache'
139− assert_includes last_response.body, '# dark'
140− refute_includes last_response.body, '# default'
141− end
142−
143− it "sets default size" do
144− get '/manifest.appcache'
145− assert_includes last_response.body, '20rem'
146− end
147−
148− it "sets size from cookie" do
149− set_cookie('size=42')
150− get '/manifest.appcache'
151− assert_includes last_response.body, '42px'
152− end
153−
154− it "sets layout from cookie" do
155− set_cookie('layout=foo_layout')
156− get '/manifest.appcache'
157− assert_includes last_response.body, 'foo_layout'
158− end
159− end
160−
16180 describe "/[doc]" do
16281 it "renders when the doc exists and isn't enabled" do
16382 set_cookie('docs=html~5')
test/lib/docs/core/scrapers/file_scraper_test.rb+2−2
@@ -63,7 +63,7 @@ class FileScraperTest < MiniTest::Spec · 62 unmodified lines
6363 end
6464
6565 it "reads a file" do
66− mock(scraper).read_file(path)
66+ mock(scraper).read_file(File.join(ROOT_PATH, 'docs/scraper', path))
6767 result
6868 end
6969
@@ -165,7 +165,7 @@ class FileScraperTest < MiniTest::Spec · 95 unmodified lines
165165
166166 describe "#read_file" do
167167 let :result do
168− scraper.send :read_file, 'file'
168+ scraper.send :read_file, File.join(ROOT_PATH, 'docs', 'scraper', 'file')
169169 end
170170
171171 it "returns the file's content when the file exists in the source directory" do
views/app.erb+1−13
@@ -28,13 +28,7 @@ · 27 unmodified lines
2828 </nav>
2929 </header>
3030 <section class="_sidebar" tabindex="-1">
31− <div class="_list" role="navigation">
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>
31+ <div class="_list" role="navigation"></div>
3832 </section>
3933 <div class="_container" role="document">
4034 <main class="_content _content-loading" role="main"></main>
@@ -63,9 +57,3 @@ · 22 unmodified lines
6357 <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>
6458 </defs>
6559 </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 @@
11 <!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">
33 <head>
44 <meta charset="utf-8">
55 <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
1414 <meta name="apple-mobile-web-app-capable" content="yes">
1515 <meta name="apple-mobile-web-app-status-bar-style" content="black">
1616 <meta name="format-detection" content="telephone=no">
17+ <meta name="theme-color" content="#eee">
1718 <meta name="robots" content="noodp">
1819 <title>DevDocs API Documentation</title>
1920 <link rel="canonical" href="<%= canonical_origin %>">
@@ -32,7 +33,7 @@ · 12 unmodified lines
3233 <link rel="mask-icon" href="<%= App.cdn_origin %>/images/webkit-mask-icon.svg" color="#398df0">
3334 <%= stylesheet_tag 'application' %>
3435 </head>
35−<body<%= %( class="#{app_layout}") if app_layout %>>
36+<body>
3637 <noscript class="_fail">DevDocs requires JavaScript to run.</noscript>
3738 <%= erb :app -%>
3839 <%= javascript_tag 'application', asset_host: false %>
views/manifest.erb+0−14
@@ -1,14 +0,0 @@
1−CACHE MANIFEST
2−# <%= app_theme %> <%= app_size %> <%= app_layout %>
3−
4−CACHE:
5−/
6−<%= manifest_asset_urls.join "\n" %>
7−<%= doc_index_urls.join "\n" %>
8−
9−NETWORK:
10−/s/
11−*
12−
13−FALLBACK:
14−/ /
views/other.erb+3−2
@@ -1,10 +1,11 @@
11 <!DOCTYPE html>
2−<html lang="en" class="_booting _theme-<%= app_theme %>">
2+<html lang="en" class="_booting">
33 <head>
44 <meta charset="utf-8">
55 <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
66 <% 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 %>
77 <meta name="format-detection" content="telephone=no">
8+ <meta name="theme-color" content="#eee">
89 <meta property="og:image" content="<%= App.cdn_origin %>/images/icon-320.png">
910 <title>DevDocs<%= " &mdash; #{@doc['full_name']} documentation" if doc_index_page? %></title>
1011 <link rel="canonical" href="<%= canonical_origin %><%= request.path %>">
@@ -13,7 +14,7 @@ · 2 unmodified lines
1314 <link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Search DevDocs">
1415 <%= stylesheet_tag 'application' %>
1516 </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 %>">
1718 <noscript class="_fail">DevDocs requires JavaScript to run.</noscript>
1819 <%= erb :app -%>
1920 <%= 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 %>
3+const cacheName = '<%= service_worker_cache_name %>';
4+
5+<%# Url's to cache when the service worker is installed %>
6+const 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 %>
15+self.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 %>
24+self.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 %>
33+self.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+3−3
@@ -11,9 +11,9 @@ · 10 unmodified lines
1111 <p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:</p>
1212 <ul class="_fail-list">
1313 <li>Recent versions of Firefox, Chrome, or Opera</li>
14− <li>Safari 9.1+</li>
15− <li>Edge 16+</li>
16− <li>iOS 10+</li>
14+ <li>Safari 11.1+</li>
15+ <li>Edge 17+</li>
16+ <li>iOS 11.3+</li>
1717 </ul>
1818 <p class="_fail-text">
1919 If you're unable to upgrade, we apologize.