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 @@
1.DS_Store1.DS_Store
2.bundle2.bundle
3log
3tmp4tmp
4public/assets5public/assets
5public/fonts6public/fonts
.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="
Dockerfile+1−0
@@ -1,6 +1,7 @@
1FROM ruby:2.6.01FROM 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+1−0
@@ -1,6 +1,7 @@
1FROM ruby:2.6.0-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
README.md+5−5
@@ -59,14 +59,14 @@ The web app is all client-side JavaScript, written in [CoffeeScript](http://coff · 58 unmodified lines
59 59
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.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.
61 61
62Another 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.
63 63
64DevDocs being a developer tool, the browser requirements are high:64DevDocs being a developer tool, the browser requirements are high:
65 65
66* Recent versions of Firefox, Chrome, or Opera66* Recent versions of Firefox, Chrome, or Opera
67* Safari 9.1+67* Safari 11.1+
68* Edge 16+68* Edge 17+
69* iOS 10+69* iOS 11.3+
70 70
71This 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!
72 72
@@ -156,7 +156,7 @@ Contributions are welcome. Please read the [contributing guidelines](./.github/C · 83 unmodified lines
156* [Doc Browser](https://github.com/qwfy/doc-browser) is a native Linux app that supports DevDocs docsets156* [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 headerbar157* [GNOME Application](https://github.com/hardpixel/devdocs-desktop) GTK3 application with search integrated in headerbar
158* [macOS Application](https://github.com/dteoh/devdocs-macos)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 with AppCache enabled159* [Android Application](https://github.com/Merith-TK/devdocs_webapp_kotlin) is a fully working, advanced WebView
160 160
161## Copyright / License161## Copyright / License
162 162
assets/javascripts/app/app.coffee+13−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
@@ -138,7 +140,10 @@ · 115 unmodified lines
138 @docs.sort()140 @docs.sort()
139 @initDoc(doc)141 @initDoc(doc)
140 @saveDocs()142 @saveDocs()
141 _onSuccess()143 if app.settings.get('autoInstall')
144 doc.install(_onSuccess, onError)
145 else
146 _onSuccess()
142 return147 return
143 148
144 doc.load onSuccess, onError, writeCache: true149 doc.load onSuccess, onError, writeCache: true
@@ -147,7 +152,7 @@ · 2 unmodified lines
147 saveDocs: ->152 saveDocs: ->
148 @settings.setDocs(doc.slug for doc in @docs.all())153 @settings.setDocs(doc.slug for doc in @docs.all())
149 @db.migrate()154 @db.migrate()
150 @appCache?.updateInBackground()155 @serviceWorker?.updateInBackground()
151 156
152 welcomeBack: ->157 welcomeBack: ->
153 visitCount = @settings.get('count')158 visitCount = @settings.get('count')
@@ -167,14 +172,14 @@ · 13 unmodified lines
167 reload: ->172 reload: ->
168 @docs.clearCache()173 @docs.clearCache()
169 @disabledDocs.clearCache()174 @disabledDocs.clearCache()
170 if @appCache then @appCache.reload() else @reboot()175 if @serviceWorker then @serviceWorker.reload() else @reboot()
171 return176 return
172 177
173 reset: ->178 reset: ->
174 @localStorage.reset()179 @localStorage.reset()
175 @settings.reset()180 @settings.reset()
176 @db?.reset()181 @db?.reset()
177 @appCache?.update()182 @serviceWorker?.update()
178 window.location = '/'183 window.location = '/'
179 return184 return
180 185
@@ -193,9 +198,9 @@ · 12 unmodified lines
193 return198 return
194 199
195 indexHost: ->200 indexHost: ->
196 # Can't load the index files from the host/CDN when applicationCache is201 # 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.202 # 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']
199 204
200 onBootError: (args...) ->205 onBootError: (args...) ->
201 @trigger 'bootError'206 @trigger 'bootError'
@@ -252,7 +257,7 @@ · 50 unmodified lines
252 matchMedia: !!window.matchMedia257 matchMedia: !!window.matchMedia
253 insertAdjacentHTML: !!document.body.insertAdjacentHTML258 insertAdjacentHTML: !!document.body.insertAdjacentHTML
254 defaultPrevented: document.createEvent('CustomEvent').defaultPrevented is false259 defaultPrevented: document.createEvent('CustomEvent').defaultPrevented is false
255 cssVariables: CSS.supports and CSS.supports('(--t: 0)')260 cssVariables: !!CSS?.supports?('(--t: 0)')
256 261
257 for key, value of features when not value262 for key, value of features when not value
258 Raven.captureMessage "unsupported/#{key}", level: 'info'263 Raven.captureMessage "unsupported/#{key}", level: 'info'
assets/javascripts/app/appcache.coffee+0−42
@@ -1,42 +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', -> 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
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+33−0
@@ -5,11 +5,13 @@ class app.Settings · 4 unmodified lines
5 'manualUpdate'5 'manualUpdate'
6 'fastScroll'6 'fastScroll'
7 'arrowScroll'7 'arrowScroll'
8 'analyticsConsent'
8 'docs'9 'docs'
9 'dark'10 'dark'
10 'layout'11 'layout'
11 'size'12 'size'
12 'tips'13 'tips'
14 'autoInstall'
13 ]15 ]
14 16
15 INTERNAL_KEYS = [17 INTERNAL_KEYS = [
@@ -19,6 +21,8 @@ class app.Settings · 3 unmodified lines
19 'news'21 'news'
20 ]22 ]
21 23
24 LAYOUTS: ['_max-width', '_sidebar-hidden', '_native-scrollbars']
25
22 @defaults:26 @defaults:
23 count: 027 count: 0
24 hideDisabled: false28 hideDisabled: false
@@ -26,6 +30,7 @@ class app.Settings · 1 unmodified line
26 news: 030 news: 0
27 manualUpdate: false31 manualUpdate: false
28 schema: 132 schema: 1
33 analyticsConsent: false
29 34
30 constructor: ->35 constructor: ->
31 @store = new CookieStore36 @store = new CookieStore
@@ -38,6 +43,7 @@ class app.Settings · 6 unmodified lines
38 set: (key, value) ->43 set: (key, value) ->
39 @store.set(key, value)44 @store.set(key, value)
40 delete @cache[key]45 delete @cache[key]
46 @toggleDark(value) if key == 'dark'
41 return47 return
42 48
43 del: (key) ->49 del: (key) ->
@@ -63,6 +69,8 @@ class app.Settings · 19 unmodified lines
63 return69 return
64 70
65 setLayout: (name, enable) ->71 setLayout: (name, enable) ->
72 @toggleLayout(name, enable)
73
66 layout = (@store.get('layout') || '').split(' ')74 layout = (@store.get('layout') || '').split(' ')
67 $.arrayDelete(layout, '')75 $.arrayDelete(layout, '')
68 76
@@ -104,3 +112,28 @@ class app.Settings · 35 unmodified lines
104 @store.reset()112 @store.reset()
105 @cache = {}113 @cache = {}
106 return114 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
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/page.coffee+17−1
@@ -199,5 +199,21 @@ page.track = (fn) -> · 198 unmodified lines
199 return199 return
200 200
201track = ->201track = ->
202 tracker.call() for tracker in trackers202 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)
203 return219 return
assets/javascripts/lib/util.coffee+4−0
@@ -352,6 +352,10 @@ isIE = null · 351 unmodified lines
352$.isIE = ->352$.isIE = ->
353 isIE ?= navigator.userAgent?.indexOf('MSIE') >= 0 || navigator.userAgent?.indexOf('rv:11.0') >= 0353 isIE ?= navigator.userAgent?.indexOf('MSIE') >= 0 || navigator.userAgent?.indexOf('rv:11.0') >= 0
354 354
355isChromeForAndroid = null
356$.isChromeForAndroid = ->
357 isChromeForAndroid ?= navigator.userAgent?.indexOf('Android') >= 0 && /Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent)
358
355isAndroid = null359isAndroid = null
356$.isAndroid = ->360$.isAndroid = ->
357 isAndroid ?= navigator.userAgent?.indexOf('Android') >= 0361 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
142 return142 return
143 143
144 isOutdated: (status) ->144 isOutdated: (status) ->
145 status and status.installed and @mtime isnt status.mtime145 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 @@
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+5−5
@@ -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,9 +57,9 @@ 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, we apologize.65 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
68app.templates.notifUpdateDocs = ->68app.templates.notifUpdateDocs = ->
69 textNotif """ Documentation updates available. """,69 textNotif """ Documentation updates available. """,
70 """ <a href="/offline">Install them</a> as soon as possible to avoid broken pages. """70 """ <a href="/offline">Install them</a> as soon as possible to avoid broken pages. """
71
72app.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
73 <ul>73 <ul>
74 <li><a href="https://devdocs.io">devdocs.io</a> ("App") is operated by <a href="https://www.freecodecamp.org/">freeCodeCamp</a> ("We").74 <li><a href="https://devdocs.io">devdocs.io</a> ("App") is operated by <a href="https://www.freecodecamp.org/">freeCodeCamp</a> ("We").
75 <li>We do not collect personal information through the app.75 <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.
77 <li>The app uses cookies to store user preferences.78 <li>The app uses cookies to store user preferences.
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.79 <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.
79 <li>If you have any questions regarding privacy, please email <a href="mailto:privacy@freecodecamp.org">privacy@freecodecamp.org</a>.80 <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
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,7 +8,7 @@ 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.
@@ -39,7 +39,7 @@ app.templates.intro = """ · 24 unmodified lines
39 <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>.39 <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>.
40 <li>For the latest news, follow <a href="https://twitter.com/DevDocs">@DevDocs</a>.40 <li>For the latest news, follow <a href="https://twitter.com/DevDocs">@DevDocs</a>.
41 <li>DevDocs is free and <a href="https://github.com/freeCodeCamp/devdocs">open source</a>.41 <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>
43 <li>And if you're new to coding, check out <a href="https://www.freecodecamp.org/">freeCodeCamp's open source curriculum</a>.43 <li>And if you're new to coding, check out <a href="https://www.freecodecamp.org/">freeCodeCamp's open source curriculum</a>.
44 </ol>44 </ol>
45 <p>Happy coding!45 <p>Happy coding!
assets/javascripts/templates/pages/settings_tmpl.coffee+8−0
@@ -15,6 +15,14 @@ app.templates.settingsPage = (settings) -> """ · 14 unmodified lines
15 <input type="checkbox" form="settings" name="layout" value="_sidebar-hidden"#{if settings['_sidebar-hidden'] then ' checked' else ''}>Automatically hide and show the sidebar15 <input type="checkbox" form="settings" name="layout" value="_sidebar-hidden"#{if settings['_sidebar-hidden'] then ' checked' else ''}>Automatically hide and show the sidebar
16 <small>Tip: drag the edge of the sidebar to resize it.</small>16 <small>Tip: drag the edge of the sidebar to resize it.</small>
17 </label>17 </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>
18 </div>26 </div>
19 </div>27 </div>
20 28
assets/javascripts/tracking.js+27−23
@@ -1,28 +1,32 @@
1try {1try {
2 if (app.config.env == 'production') {2 if (app.config.env === 'production') {
3 (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){3 if (Cookies.get('analyticsConsent') === '1') {
4 (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),4 (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
5 m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)5 (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
6 })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');6 m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
7 ga('create', 'UA-5544833-12', 'devdocs.io');7 })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
8 page.track(function() {8 ga('create', 'UA-5544833-12', 'devdocs.io');
9 ga('send', 'pageview', {9 page.track(function() {
10 page: location.pathname + location.search + location.hash,10 ga('send', 'pageview', {
11 dimension1: app.router.context && app.router.context.doc && app.router.context.doc.slug_without_version11 page: location.pathname + location.search + location.hash,
12 dimension1: app.router.context && app.router.context.doc && app.router.context.doc.slug_without_version
13 });
12 });14 });
13 });
14 15
15 page.track(function() {16 page.track(function() {
16 if (window._gauges)17 if (window._gauges)
17 _gauges.push(['track']);18 _gauges.push(['track']);
18 else19 else
19 (function() {20 (function() {
20 var _gauges=_gauges||[];!function(){var a=document.createElement("script");21 var _gauges=_gauges||[];!function(){var a=document.createElement("script");
21 a.type="text/javascript",a.async=!0,a.id="gauges-tracker",22 a.type="text/javascript",a.async=!0,a.id="gauges-tracker",
22 a.setAttribute("data-site-id","51c15f82613f5d7819000067"),23 a.setAttribute("data-site-id","51c15f82613f5d7819000067"),
23 a.src="https://secure.gaug.es/track.js";var b=document.getElementsByTagName("script")[0];24 a.src="https://secure.gaug.es/track.js";var b=document.getElementsByTagName("script")[0];
24 b.parentNode.insertBefore(a,b)}();25 b.parentNode.insertBefore(a,b)}();
25 })();26 })();
26 });27 });
28 } else {
29 resetAnalytics();
30 }
27 }31 }
28} catch(e) { }32} 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
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+10−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,31 +14,31 @@ 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.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
21 settings20 settings
22 21
23 getTitle: ->22 getTitle: ->
24 'Preferences'23 'Preferences'
25 24
26 toggleDark: (enable) ->25 toggleDark: (enable) ->
27 html = document.documentElement
28 html.classList.toggle('_theme-default')
29 html.classList.toggle('_theme-dark')
30 app.settings.set('dark', !!enable)26 app.settings.set('dark', !!enable)
31 app.appCache?.updateInBackground()
32 return27 return
33 28
34 toggleLayout: (layout, enable) ->29 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)30 app.settings.setLayout(layout, enable)
38 app.appCache?.updateInBackground()
39 return31 return
40 32
41 toggleSmoothScroll: (enable) ->33 toggleSmoothScroll: (enable) ->
42 app.settings.set('fastScroll', !enable)34 app.settings.set('fastScroll', !enable)
43 return35 return
44 36
37 toggleAnalyticsConsent: (enable) ->
38 app.settings.set('analyticsConsent', if enable then '1' else '0')
39 resetAnalytics() unless enable
40 return
41
45 toggle: (name, enable) ->42 toggle: (name, enable) ->
46 app.settings.set(name, enable)43 app.settings.set(name, enable)
47 return44 return
@@ -85,6 +82,8 @@ class app.views.SettingsPage extends app.View · 37 unmodified lines
85 @toggleSmoothScroll input.checked82 @toggleSmoothScroll input.checked
86 when 'import'83 when 'import'
87 @import input.files[0], input84 @import input.files[0], input
85 when 'analyticsConsent'
86 @toggleAnalyticsConsent input.checked
88 else87 else
89 @toggle input.name, input.checked88 @toggle input.name, input.checked
90 return89 return
assets/javascripts/views/layout/document.coffee+7−5
@@ -75,9 +75,11 @@ class app.views.Document extends app.View · 74 unmodified lines
75 return unless target.hasAttribute('data-behavior')75 return unless target.hasAttribute('data-behavior')
76 $.stopEvent(event)76 $.stopEvent(event)
77 switch target.getAttribute('data-behavior')77 switch target.getAttribute('data-behavior')
78 when 'back' then history.back()78 when 'back' then history.back()
79 when 'reload' then window.location.reload()79 when 'reload' then window.location.reload()
80 when 'reboot' then app.reboot()80 when 'reboot' then app.reboot()
81 when 'hard-reload' then app.reload()81 when 'hard-reload' then app.reload()
82 when 'reset' then app.reset() if confirm('Are you sure you want to reset DevDocs?')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()
83 return85 return
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+23−5
@@ -6,7 +6,9 @@ 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'
11 textInput: 'onTextInput'
10 12
11 @routes:13 @routes:
12 after: 'afterRoute'14 after: 'afterRoute'
@@ -87,17 +89,33 @@ class app.views.SearchScope extends app.View · 74 unmodified lines
87 @trigger 'change', null, previousDoc89 @trigger 'change', null, previousDoc
88 return90 return
89 91
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
90 onKeydown: (event) =>103 onKeydown: (event) =>
91 if event.which is 8 # backspace104 if event.which is 8 # backspace
92 if @doc and not @input.value105 if @doc and @input.selectionEnd is 0
93 $.stopEvent(event)
94 @reset()106 @reset()
95 else if not @doc and @input.value107 $.stopEvent(event)
108 else if not @doc and @input.value and not $.isChromeForAndroid()
96 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
97 if event.which is 9 or # tab110 if event.which is 9 or # tab
98 (event.which is 32 and app.isMobile()) # space111 (event.which is 32 and app.isMobile()) # space
99 @search @input.value[0...@input.selectionStart]112 @doScopeSearch(event)
100 $.stopEvent(event) if @doc113 return
114
115 onTextInput: (event) =>
116 return unless $.isChromeForAndroid()
117 if not @doc and @input.value and event.data == ' '
118 @doScopeSearch(event)
101 return119 return
102 120
103 extractHashValue: ->121 extractHashValue: ->
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/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/components/_notif.scss+4−0
@@ -134,3 +134,7 @@ · 133 unmodified lines
134 134
135 ._notif-info { color: var(--textColorLight); }135 ._notif-info { color: var(--textColorLight); }
136}136}
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
21- Commit the changes (protip: use the `thor docs:commit` command documented below).21- Commit the changes (protip: use the `thor docs:commit` command documented below).
22- Optional: do more updates.22- Optional: do more updates.
23- Run `thor docs:upload` (documented below).23- Run `thor docs:upload` (documented below).
24- [Deploy the app](#deploying-devdocs) and verify that everything works in production.24- Push to GitHub to [deploy the app](#deploying-devdocs) and verify that everything works in production.
25- Push to GitHub.
26- Run `thor docs:clean` (documented below).25- Run `thor docs:clean` (documented below).
27 26
28Note: 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`.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`.
@@ -82,13 +81,13 @@ In addition to the [publicly-documented commands](https://github.com/freeCodeCam · 53 unmodified lines
82 81
83## Deploying DevDocs82## Deploying DevDocs
84 83
85Once 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.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.
86 85
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.
88- If you're deploying frontend changes, monitor [Sentry](https://sentry.io/devdocs/devdocs-js/) for new JS errors once the deploy is done.87- If you're deploying frontend changes, monitor [Sentry](https://sentry.io/devdocs/devdocs-js/) for new JS errors once the deploy is done.
89- 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.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.
90 89
91If 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.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.
92 91
93If this is your first deploy, make sure another maintainer is around to assist. 92If this is your first deploy, make sure another maintainer is around to assist.
94 93
lib/app.rb+35−20
@@ -37,6 +37,9 @@ class App < Sinatra::Application · 36 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
@@ -197,33 +200,45 @@ class App < Sinatra::Application · 154 unmodified lines
197 request.query_string.empty? ? nil : "?#{request.query_string}"200 request.query_string.empty? ? nil : "?#{request.query_string}"
198 end201 end
199 202
200 def manifest_asset_urls203 def service_worker_asset_urls
201 @@manifest_asset_urls ||= [204 @@service_worker_asset_urls ||= [
202 javascript_path('application', asset_host: false),205 javascript_path('application', asset_host: false),
203 stylesheet_path('application'),206 stylesheet_path('application'),
204 image_path('sprites/docs.png'),207 image_path('sprites/docs.png'),
205 image_path('sprites/docs@2x.png'),208 image_path('sprites/docs@2x.png'),
206 asset_path('docs.js')209 asset_path('docs.js'),
207 ]210 App.production? ? nil : javascript_path('debug'),
211 ].compact
208 end212 end
209 213
210 def app_size214 # Returns a cache name for the service worker to use which changes if any of the assets changes
211 @app_size ||= memoized_cookies['size'].nil? ? '20rem' : "#{memoized_cookies['size']}px"215 # When a manifest exist, this name is only created once based on the asset manifest because it never changes without a server restart
212 end216 # 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
213 222
214 def app_layout223 digest = Sprockets::Manifest
215 memoized_cookies['layout']224 .new(nil, App.assets_manifest_path)
216 end225 .files
226 .values
227 .map {|file| file["digest"]}
228 .join
217 229
218 def app_theme230 return @@service_worker_cache_name ||= Digest::MD5.hexdigest(digest)
219 @app_theme ||= memoized_cookies['dark'].nil? ? 'default' : 'dark'231 else
220 end232 paths = App.sprockets
233 .each_file
234 .to_a
235 .reject {|file| file.start_with?(App.docs_path)}
221 236
222 def dark_theme?237 return App.sprockets.pack_hexdigest(App.sprockets.files_digest(paths))
223 app_theme == 'dark'238 end
224 end239 end
225 240
226 def redirect_via_js(path) # courtesy of HTML5 App Cache241 def redirect_via_js(path)
227 response.set_cookie :initial_path, value: path, expires: Time.now + 15, path: '/'242 response.set_cookie :initial_path, value: path, expires: Time.now + 15, path: '/'
228 redirect '/', 302243 redirect '/', 302
229 end244 end
@@ -246,15 +261,15 @@ class App < Sinatra::Application · 16 unmodified lines
246 end261 end
247 end262 end
248 263
249 get '/manifest.appcache' do264 get '/service-worker.js' do
250 content_type 'text/cache-manifest'265 content_type 'application/javascript'
251 expires 0, :'no-cache'266 expires 0, :'no-cache'
252 erb :manifest267 erb :'service-worker.js'
253 end268 end
254 269
255 get '/' do270 get '/' do
256 return redirect "/#q=#{params[:q]}" if params[:q]271 return redirect "/#q=#{params[:q]}" if params[:q]
257 return redirect '/' unless request.query_string.empty? # courtesy of HTML5 App Cache272 return redirect '/' unless request.query_string.empty?
258 response.headers['Content-Security-Policy'] = settings.csp if settings.csp273 response.headers['Content-Security-Policy'] = settings.csp if settings.csp
259 erb :index274 erb :index
260 end275 end
lib/docs/core/scrapers/file_scraper.rb+2−2
@@ -29,7 +29,7 @@ module Docs · 28 unmodified lines
29 29
30 def request_one(url)30 def request_one(url)
31 assert_source_directory_exists31 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)
33 end33 end
34 34
35 def request_all(urls)35 def request_all(urls)
@@ -50,7 +50,7 @@ module Docs · 14 unmodified lines
50 end50 end
51 51
52 def read_file(path)52 def read_file(path)
53 File.read(File.join(source_directory, path))53 File.read(path)
54 rescue54 rescue
55 instrument 'warn.doc', msg: "Failed to open file: #{path}"55 instrument 'warn.doc', msg: "Failed to open file: #{path}"
56 nil56 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
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+0−81
@@ -43,35 +43,6 @@ class AppTest < MiniTest::Spec · 42 unmodified lines
43 assert last_response.redirect?43 assert last_response.redirect?
44 assert_equal 'https://example.org/', last_response['Location']44 assert_equal 'https://example.org/', last_response['Location']
45 end45 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
75 end46 end
76 47
77 describe "/[static-page]" do48 describe "/[static-page]" do
@@ -106,58 +77,6 @@ class AppTest < MiniTest::Spec · 28 unmodified lines
106 end77 end
107 end78 end
108 79
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
161 describe "/[doc]" do80 describe "/[doc]" do
162 it "renders when the doc exists and isn't enabled" do81 it "renders when the doc exists and isn't enabled" do
163 set_cookie('docs=html~5')82 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
63 end63 end
64 64
65 it "reads a file" do65 it "reads a file" do
66 mock(scraper).read_file(path)66 mock(scraper).read_file(File.join(ROOT_PATH, 'docs/scraper', path))
67 result67 result
68 end68 end
69 69
@@ -165,7 +165,7 @@ class FileScraperTest < MiniTest::Spec · 95 unmodified lines
165 165
166 describe "#read_file" do166 describe "#read_file" do
167 let :result do167 let :result do
168 scraper.send :read_file, 'file'168 scraper.send :read_file, File.join(ROOT_PATH, 'docs', 'scraper', 'file')
169 end169 end
170 170
171 it "returns the file's content when the file exists in the source directory" do171 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
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+3−3
@@ -11,9 +11,9 @@ · 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, we apologize.19 If you're unable to upgrade, we apologize.