Merge remote-tracking branch 'bla/master'

pull/667/head
Matthias Guenther 8 years ago
commit a0897f4958
No known key found for this signature in database
GPG Key ID: 5287E11BD64C14E5

@ -0,0 +1,7 @@
.git
test
Dockerfile*
.gitignore
.dockerignore
.travis.yml
*.md

@ -0,0 +1,22 @@
verbose: false
skip_missing_workers: true
allow_lossy: true
threads: 1
advpng: false
gifsicle:
interlace: false
level: 3
careful: true
jhead: false
jpegoptim:
strip: all
max_quality: 100
jpegrecompress: false
jpegtran: false
optipng: false
pngcrush: false
pngout: false
pngquant:
quality: !ruby/range 80..99
speed: 3
svgo: false

@ -1 +1 @@
2.3.0 2.4.1

@ -48,7 +48,7 @@ Use the [Trello board](https://trello.com/b/6BmTulfx/devdocs-documentation) wher
See the [wiki](https://github.com/Thibaut/devdocs/wiki) to learn how to add new documentations. See the [wiki](https://github.com/Thibaut/devdocs/wiki) to learn how to add new documentations.
**Important:** the documentation's license must permit alteration, redistribution, and commercial use. **Important:** the documentation's license must permit alteration, redistribution and commercial use, and the documented software must be released under an open source license. Feel free to get in touch if you're not sure if a documentation meets those requirements.
In addition to the [guidelines for contributing code](#contributing-code-and-features), the following guidelines apply to pull requests that add a new documentation: In addition to the [guidelines for contributing code](#contributing-code-and-features), the following guidelines apply to pull requests that add a new documentation:

@ -1,4 +1,4 @@
Copyright 2013-2016 Thibaut Courouble and other contributors Copyright 2013-2017 Thibaut Courouble and other contributors
This Source Code Form is subject to the terms of the Mozilla Public This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this License, v. 2.0. If a copy of the MPL was not distributed with this

@ -1,17 +1,22 @@
FROM ruby:2.4.1
FROM ruby:2.3.0 WORKDIR /devdocs
MAINTAINER Conor Heine <conor.heine@gmail.com>
RUN apt-get update RUN apt-get update && \
RUN apt-get -y install git nodejs apt-get -y install git nodejs && \
RUN git clone https://github.com/Thibaut/devdocs.git /devdocs gem install bundler && \
RUN gem install bundler rm -rf /var/lib/apt/lists/*
WORKDIR /devdocs COPY Gemfile Gemfile.lock Rakefile /devdocs/
RUN bundle install --system && \
rm -rf ~/.gem /root/.bundle/cache /usr/local/bundle/cache
RUN bundle install --system COPY . /devdocs
RUN thor docs:download --all
RUN thor docs:download --all && \
thor assets:compile && \
rm -rf /tmp
EXPOSE 9292 EXPOSE 9292
CMD rackup -o 0.0.0.0 CMD rackup -o 0.0.0.0

@ -0,0 +1,17 @@
FROM ruby:2.4.1-alpine
WORKDIR /devdocs
COPY . /devdocs
RUN apk --update add nodejs build-base libstdc++ gzip git zlib-dev && \
gem install bundler && \
bundle install --system --without test && \
thor docs:download --all && \
thor assets:compile && \
apk del gzip build-base git zlib-dev && \
rm -rf /var/cache/apk/* /tmp ~/.gem /root/.bundle/cache \
/usr/local/bundle/cache /usr/lib/node_modules
EXPOSE 9292
CMD rackup -o 0.0.0.0

@ -1,10 +1,10 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '2.3.0' ruby '2.4.1'
gem 'rake' gem 'rake'
gem 'thor' gem 'thor'
gem 'pry', '~> 0.10.0' gem 'pry', '~> 0.10.0'
gem 'activesupport', '~> 4.2', require: false gem 'activesupport', '~> 5.0', require: false
gem 'yajl-ruby', require: false gem 'yajl-ruby', require: false
group :app do group :app do
@ -14,7 +14,7 @@ group :app do
gem 'thin' gem 'thin'
gem 'sprockets' gem 'sprockets'
gem 'sprockets-helpers' gem 'sprockets-helpers'
gem 'erubis' gem 'erubi'
gem 'browser' gem 'browser'
gem 'sass' gem 'sass'
gem 'coffee-script' gem 'coffee-script'
@ -32,6 +32,8 @@ group :docs do
gem 'typhoeus' gem 'typhoeus'
gem 'nokogiri' gem 'nokogiri'
gem 'html-pipeline' gem 'html-pipeline'
gem 'image_optim'
gem 'image_optim_pack', platforms: :ruby
gem 'progress_bar', require: false gem 'progress_bar', require: false
gem 'unix_utils', require: false gem 'unix_utils', require: false
gem 'tty-pager', require: false gem 'tty-pager', require: false

@ -1,111 +1,136 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
activesupport (4.2.6) activesupport (5.1.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7) i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1) tzinfo (~> 1.1)
backports (3.6.8) backports (3.8.0)
better_errors (2.1.1) better_errors (2.3.0)
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubis (>= 2.6.6) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
browser (2.1.0) browser (2.4.0)
coderay (1.1.1) coderay (1.1.1)
coffee-script (2.4.1) coffee-script (2.4.1)
coffee-script-source coffee-script-source
execjs execjs
coffee-script-source (1.10.0) coffee-script-source (1.12.2)
concurrent-ruby (1.0.2) concurrent-ruby (1.0.5)
daemons (1.2.3) daemons (1.2.4)
erubis (2.7.0) erubi (1.6.1)
ethon (0.9.0) ethon (0.10.1)
ffi (>= 1.3.0) ffi (>= 1.3.0)
eventmachine (1.2.0.1) eventmachine (1.2.5)
execjs (2.6.0) execjs (2.7.0)
ffi (1.9.10) exifr (1.3.1)
ffi (1.9.18)
fspath (3.1.0)
highline (1.7.8) highline (1.7.8)
html-pipeline (2.4.1) html-pipeline (2.6.0)
activesupport (>= 2, < 5) activesupport (>= 2)
nokogiri (>= 1.4) nokogiri (>= 1.4)
i18n (0.7.0) i18n (0.8.6)
json (1.8.3) image_optim (0.25.0)
exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0)
image_size (~> 1.5)
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
image_optim_pack (0.5.0.20170803)
fspath (>= 2.1, < 4)
image_optim (~> 0.19)
image_size (1.5.0)
in_threads (1.4.0)
method_source (0.8.2) method_source (0.8.2)
mini_portile2 (2.0.0) mini_portile2 (2.2.0)
minitest (5.8.4) minitest (5.10.3)
multi_json (1.12.0) multi_json (1.12.1)
nokogiri (1.6.7.2) mustermann (1.0.0)
mini_portile2 (~> 2.0.0.rc2) nokogiri (1.8.0)
mini_portile2 (~> 2.2.0)
options (2.3.2) options (2.3.2)
progress_bar (1.0.5) progress (3.3.1)
progress_bar (1.1.0)
highline (~> 1.6) highline (~> 1.6)
options (~> 2.3.0) options (~> 2.3.0)
pry (0.10.3) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
slop (~> 3.4) slop (~> 3.4)
rack (1.6.4) rack (2.0.3)
rack-protection (1.5.3) rack-protection (2.0.0)
rack rack
rack-test (0.6.3) rack-test (0.7.0)
rack (>= 1.0) rack (>= 1.0, < 3)
rake (11.1.2) rake (12.0.0)
rr (1.1.2) rb-fsevent (0.10.2)
sass (3.4.22) rb-inotify (0.9.10)
sinatra (1.4.7) ffi (>= 0.5.0, < 2)
rack (~> 1.5) rr (1.2.1)
rack-protection (~> 1.4) sass (3.5.1)
tilt (>= 1.3, < 3) sass-listen (~> 4.0.0)
sinatra-contrib (1.4.7) sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sinatra (2.0.0)
mustermann (~> 1.0)
rack (~> 2.0)
rack-protection (= 2.0.0)
tilt (~> 2.0)
sinatra-contrib (2.0.0)
backports (>= 2.0) backports (>= 2.0)
multi_json multi_json
rack-protection mustermann (~> 1.0)
rack-test rack-protection (= 2.0.0)
sinatra (~> 1.4.0) sinatra (= 2.0.0)
tilt (>= 1.3, < 3) tilt (>= 1.3, < 3)
slop (3.6.0) slop (3.6.0)
sprockets (3.6.0) sprockets (3.7.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-helpers (1.2.1) sprockets-helpers (1.2.1)
sprockets (>= 2.2) sprockets (>= 2.2)
thin (1.6.4) thin (1.7.2)
daemons (~> 1.0, >= 1.0.9) daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4) eventmachine (~> 1.0, >= 1.0.4)
rack (~> 1.0) rack (>= 1, < 3)
thor (0.19.1) thor (0.19.4)
thread_safe (0.3.5) thread_safe (0.3.6)
tilt (2.0.3) tilt (2.0.8)
tty-pager (0.4.0) tty-pager (0.8.0)
tty-screen (~> 0.5.0) tty-screen (~> 0.5.0)
tty-which (~> 0.1.0) tty-which (~> 0.3.0)
verse (~> 0.4.0) verse (~> 0.5.0)
tty-screen (0.5.0) tty-screen (0.5.0)
tty-which (0.1.0) tty-which (0.3.0)
typhoeus (1.0.2) typhoeus (1.1.2)
ethon (>= 0.9.0) ethon (>= 0.9.0)
tzinfo (1.2.2) tzinfo (1.2.3)
thread_safe (~> 0.1) thread_safe (~> 0.1)
uglifier (3.0.0) uglifier (3.2.0)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unicode-display_width (1.1.3)
unicode_utils (1.4.0) unicode_utils (1.4.0)
unix_utils (0.0.15) unix_utils (0.0.15)
verse (0.4.0) verse (0.5.0)
unicode-display_width (~> 1.1.0)
unicode_utils (~> 1.4.0) unicode_utils (~> 1.4.0)
yajl-ruby (1.2.1) yajl-ruby (1.3.0)
PLATFORMS PLATFORMS
ruby ruby
DEPENDENCIES DEPENDENCIES
activesupport (~> 4.2) activesupport (~> 5.0)
better_errors better_errors
browser browser
coffee-script coffee-script
erubis erubi
html-pipeline html-pipeline
image_optim
image_optim_pack
minitest minitest
nokogiri nokogiri
progress_bar progress_bar
@ -127,5 +152,8 @@ DEPENDENCIES
unix_utils unix_utils
yajl-ruby yajl-ruby
RUBY VERSION
ruby 2.4.1p111
BUNDLED WITH BUNDLED WITH
1.11.2 1.14.1

@ -0,0 +1,7 @@
<!--
Please read the contributing guidelines before opening an issue:
https://github.com/Thibaut/devdocs/blob/master/CONTRIBUTING.md
To request a new documentation, or an update of an existing documentation, go here:
https://trello.com/b/6BmTulfx/devdocs-documentation
-->

@ -20,14 +20,14 @@ Unless you wish to contribute to the project, I recommend using the hosted versi
DevDocs is made of two pieces: a Ruby scraper that generates the documentation and metadata, and a JavaScript app powered by a small Sinatra app. DevDocs is made of two pieces: a Ruby scraper that generates the documentation and metadata, and a JavaScript app powered by a small Sinatra app.
DevDocs requires Ruby 2.3.0, libcurl, and a JavaScript runtime supported by [ExecJS](https://github.com/rails/execjs#readme) (included in OS X and Windows; [Node.js](https://nodejs.org/en/) on Linux). Once you have these installed, run the following commands: DevDocs requires Ruby 2.4.1, libcurl, and a JavaScript runtime supported by [ExecJS](https://github.com/rails/execjs#readme) (included in OS X and Windows; [Node.js](https://nodejs.org/en/) on Linux). Once you have these installed, run the following commands:
``` ```
git clone https://github.com/Thibaut/devdocs.git && cd devdocs git clone https://github.com/Thibaut/devdocs.git && cd devdocs
gem install bundler gem install bundler
bundle install bundle install
thor docs:download --default bundle exec thor docs:download --default
rackup bundle exec rackup
``` ```
Finally, point your browser at [localhost:9292](http://localhost:9292) (the first request will take a few seconds to compile the assets). You're all set. Finally, point your browser at [localhost:9292](http://localhost:9292) (the first request will take a few seconds to compile the assets). You're all set.
@ -130,6 +130,8 @@ thor assets:compile # Compile assets (not required in development mode)
thor assets:clean # Clean old assets thor assets:clean # Clean old assets
``` ```
If multiple versions of Ruby are installed on your system, commands must be run through `bundle exec`.
## Contributing ## Contributing
Contributions are welcome. Please read the [contributing guidelines](https://github.com/Thibaut/devdocs/blob/master/CONTRIBUTING.md). Contributions are welcome. Please read the [contributing guidelines](https://github.com/Thibaut/devdocs/blob/master/CONTRIBUTING.md).
@ -138,7 +140,7 @@ DevDocs's own documentation is available on the [wiki](https://github.com/Thibau
## Copyright / License ## Copyright / License
Copyright 2013-2016 Thibaut Courouble and [other contributors](https://github.com/Thibaut/devdocs/graphs/contributors) Copyright 2013-2017 Thibaut Courouble and [other contributors](https://github.com/Thibaut/devdocs/graphs/contributors)
This software is licensed under the terms of the Mozilla Public License v2.0. See the [COPYRIGHT](https://github.com/Thibaut/devdocs/blob/master/COPYRIGHT) and [LICENSE](https://github.com/Thibaut/devdocs/blob/master/LICENSE) files. This software is licensed under the terms of the Mozilla Public License v2.0. See the [COPYRIGHT](https://github.com/Thibaut/devdocs/blob/master/COPYRIGHT) and [LICENSE](https://github.com/Thibaut/devdocs/blob/master/LICENSE) files.

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -13,9 +13,9 @@
@showLoading() @showLoading()
@el = $('._app') @el = $('._app')
@store = new Store @localStorage = new LocalStorageStore
@appCache = new app.AppCache if app.AppCache.isEnabled() @appCache = new app.AppCache if app.AppCache.isEnabled()
@settings = new app.Settings @store @settings = new app.Settings
@db = new app.DB() @db = new app.DB()
@docs = new app.collections.Docs @docs = new app.collections.Docs
@ -27,7 +27,8 @@
@document = new app.views.Document @document = new app.views.Document
@mobile = new app.views.Mobile if @isMobile() @mobile = new app.views.Mobile if @isMobile()
if @DOC if document.body.hasAttribute('data-doc')
@DOC = JSON.parse(document.body.getAttribute('data-doc'))
@bootOne() @bootOne()
else if @DOCS else if @DOCS
@bootAll() @bootAll()
@ -50,26 +51,33 @@
else else
if @config.sentry_dsn if @config.sentry_dsn
Raven.config @config.sentry_dsn, Raven.config @config.sentry_dsn,
release: @config.release
whitelistUrls: [/devdocs/] whitelistUrls: [/devdocs/]
includePaths: [/devdocs/] includePaths: [/devdocs/]
ignoreErrors: [/dpQuery/, /NPObject/, /NS_ERROR/, /^null$/] ignoreErrors: [/NPObject/, /NS_ERROR/, /^null$/, /EvalError/]
tags: tags:
mode: if @DOC then 'single' else 'full' mode: if @isSingleDoc() then 'single' else 'full'
iframe: (window.top isnt window).toString() iframe: (window.top isnt window).toString()
electron: (!!window.process?.versions?.electron).toString()
shouldSendCallback: => shouldSendCallback: =>
try try
if @isInjectionError() if @isInjectionError()
@onInjectionError() @onInjectionError()
return false return false
if @isAndroidWebview()
return false
true true
dataCallback: (data) -> dataCallback: (data) ->
try try
$.extend(data.user ||= {}, app.settings.settings) $.extend(data.user ||= {}, app.settings.dump())
data.user.docs = data.user.docs.split('/') if data.user.docs
data.user.lastIDBTransaction = app.lastIDBTransaction if app.lastIDBTransaction data.user.lastIDBTransaction = app.lastIDBTransaction if app.lastIDBTransaction
data.tags.scriptCount = document.scripts.length
data data
.install() .install()
@previousErrorHandler = onerror @previousErrorHandler = onerror
window.onerror = @onWindowError.bind(@) window.onerror = @onWindowError.bind(@)
CookieStore.onBlocked = @onCookieBlocked
return return
bootOne: -> bootOne: ->
@ -85,8 +93,6 @@
for doc in @DOCS for doc in @DOCS
(if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc) (if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc)
@migrateDocs() @migrateDocs()
@docs.sort()
@disabledDocs.sort()
@docs.load @start.bind(@), @onBootError.bind(@), readCache: true, writeCache: true @docs.load @start.bind(@), @onBootError.bind(@), readCache: true, writeCache: true
delete @DOCS delete @DOCS
return return
@ -98,21 +104,23 @@
@trigger 'ready' @trigger 'ready'
@router.start() @router.start()
@hideLoading() @hideLoading()
@welcomeBack() unless @doc setTimeout =>
@removeEvent 'ready bootError' @welcomeBack() unless @doc
try navigator.mozApps?.getSelf().onsuccess = -> app.mozApp = true catch @removeEvent 'ready bootError'
, 50
return return
initDoc: (doc) -> initDoc: (doc) ->
@entries.add type.toEntry() for type in doc.types.all() doc.entries.add type.toEntry() for type in doc.types.all()
@entries.add doc.entries.all() @entries.add doc.entries.all()
return return
migrateDocs: -> migrateDocs: ->
for slug in @settings.getDocs() when not @docs.findBy('slug', slug) for slug in @settings.getDocs() when not @docs.findBy('slug', slug)
needsSaving = true needsSaving = true
doc = @disabledDocs.findBy('slug', 'node~4_lts') if slug == 'node~4.2_lts' doc = @disabledDocs.findBy('slug', 'webpack') if slug == 'webpack~2'
doc = @disabledDocs.findBy('slug', 'xslt_xpath') if slug == 'xpath' doc = @disabledDocs.findBy('slug', 'angular') if slug == 'angular~4_typescript'
doc = @disabledDocs.findBy('slug', 'angular~2') if slug == 'angular~2_typescript'
doc ||= @disabledDocs.findBy('slug_without_version', slug) doc ||= @disabledDocs.findBy('slug_without_version', slug)
if doc if doc
@disabledDocs.remove(doc) @disabledDocs.remove(doc)
@ -157,7 +165,7 @@
return return
reset: -> reset: ->
@store.clear() @localStorage.reset()
@settings.reset() @settings.reset()
@db?.reset() @db?.reset()
@appCache?.update() @appCache?.update()
@ -166,10 +174,10 @@
showTip: (tip) -> showTip: (tip) ->
return if @isSingleDoc() return if @isSingleDoc()
tips = @settings.get('tips') || [] tips = @settings.getTips()
if tips.indexOf(tip) is -1 if tips.indexOf(tip) is -1
tips.push(tip) tips.push(tip)
@settings.set('tips', tips) @settings.setTips(tips)
new app.views.Tip(tip) new app.views.Tip(tip)
return return
@ -179,6 +187,7 @@
return return
hideLoading: -> hideLoading: ->
document.body.classList.add '_overlay-scrollbars' if $.overlayScrollbarsEnabled()
document.body.classList.remove '_booting' document.body.classList.remove '_booting'
document.body.classList.remove '_loading' document.body.classList.remove '_loading'
return return
@ -186,7 +195,7 @@
indexHost: -> indexHost: ->
# Can't load the index files from the host/CDN when applicationCache is # Can't load the index files from the host/CDN when applicationCache is
# enabled because it doesn't support caching URLs that use CORS. # enabled because it doesn't support caching URLs that use CORS.
@config[if @appCache and @settings.hasDocs() then 'index_path' else 'docs_host'] @config[if @appCache and @settings.hasDocs() then 'index_path' else 'docs_origin']
onBootError: (args...) -> onBootError: (args...) ->
@trigger 'bootError' @trigger 'bootError'
@ -197,9 +206,17 @@
return if @quotaExceeded return if @quotaExceeded
@quotaExceeded = true @quotaExceeded = true
new app.views.Notif 'QuotaExceeded', autoHide: null new app.views.Notif 'QuotaExceeded', autoHide: null
Raven.captureMessage 'QuotaExceededError' return
onCookieBlocked: (key, value, actual) ->
return if @cookieBlocked
@cookieBlocked = true
new app.views.Notif 'CookieBlocked', autoHide: null
Raven.captureMessage "CookieBlocked/#{key}", level: 'warning', extra: {value, actual}
return
onWindowError: (args...) -> onWindowError: (args...) ->
return if @cookieBlocked
if @isInjectionError args... if @isInjectionError args...
@onInjectionError() @onInjectionError()
else if @isAppError args... else if @isAppError args...
@ -215,7 +232,7 @@
alert """ alert """
JavaScript code has been injected in the page which prevents DevDocs from running correctly. JavaScript code has been injected in the page which prevents DevDocs from running correctly.
Please check your browser extensions/addons. """ Please check your browser extensions/addons. """
Raven.captureMessage 'injection error' Raven.captureMessage 'injection error', level: 'info'
return return
isInjectionError: -> isInjectionError: ->
@ -239,16 +256,16 @@
cssGradients: supportsCssGradients() cssGradients: supportsCssGradients()
for key, value of features when not value for key, value of features when not value
Raven.captureMessage "unsupported/#{key}" Raven.captureMessage "unsupported/#{key}", level: 'info'
return false return false
true true
catch error catch error
Raven.captureMessage 'unsupported/exception', extra: { error: error } Raven.captureMessage 'unsupported/exception', level: 'info', extra: { error: error }
false false
isSingleDoc: -> isSingleDoc: ->
!!(@DOC or @doc) document.body.hasAttribute('data-doc')
isMobile: -> isMobile: ->
@_isMobile ?= app.views.Mobile.detect() @_isMobile ?= app.views.Mobile.detect()

@ -16,21 +16,25 @@ class app.AppCache
update: -> update: ->
@notifyUpdate = true @notifyUpdate = true
@notifyProgress = true
try @cache.update() catch try @cache.update() catch
return return
updateInBackground: -> updateInBackground: ->
@notifyUpdate = false @notifyUpdate = false
@notifyProgress = false
try @cache.update() catch try @cache.update() catch
return return
reload: -> reload: ->
$.on @cache, 'updateready noupdate error', -> window.location = '/' $.on @cache, 'updateready noupdate error', -> window.location = '/'
@updateInBackground() @notifyUpdate = false
@notifyProgress = true
try @cache.update() catch
return return
onProgress: (event) => onProgress: (event) =>
@trigger 'progress', event @trigger 'progress', event if @notifyProgress
return return
onUpdateReady: => onUpdateReady: =>

@ -1,7 +1,7 @@
app.config = app.config =
db_filename: 'db.json' db_filename: 'db.json'
default_docs: <%= App.default_docs.to_json %> default_docs: <%= App.default_docs.to_json %>
docs_host: '<%= App.docs_host %>' docs_origin: '<%= App.docs_origin %>'
env: '<%= App.environment %>' env: '<%= App.environment %>'
history_cache_size: 10 history_cache_size: 10
index_filename: 'index.json' index_filename: 'index.json'
@ -11,3 +11,5 @@ app.config =
search_param: 'q' search_param: 'q'
sentry_dsn: '<%= App.sentry_dsn %>' sentry_dsn: '<%= App.sentry_dsn %>'
version: <%= Time.now.to_i %> version: <%= Time.now.to_i %>
release: <%= Time.now.utc.httpdate.to_json %>
mathml_stylesheet: '<%= App.cdn_origin %>/mathml.css'

@ -1,9 +1,10 @@
class app.DB class app.DB
NAME = 'docs' NAME = 'docs'
VERSION = 15
constructor: -> constructor: ->
@versionMultipler = if $.isIE() then 1e5 else 1e9
@useIndexedDB = @useIndexedDB() @useIndexedDB = @useIndexedDB()
@appVersion = @appVersion()
@callbacks = [] @callbacks = []
db: (fn) -> db: (fn) ->
@ -13,45 +14,91 @@ class app.DB
try try
@open = true @open = true
req = indexedDB.open(NAME, @schemaVersion()) req = indexedDB.open(NAME, VERSION * @versionMultipler + @userVersion())
req.onsuccess = @onOpenSuccess req.onsuccess = @onOpenSuccess
req.onerror = @onOpenError req.onerror = @onOpenError
req.onupgradeneeded = @onUpgradeNeeded req.onupgradeneeded = @onUpgradeNeeded
catch catch error
@onOpenError() @fail 'exception', error
return return
onOpenSuccess: (event) => onOpenSuccess: (event) =>
try db = event.target.result
db = event.target.result
unless @checkedBuggyIDB
@idbTransaction(db, stores: ['docs', app.docs.all()[0].slug], mode: 'readwrite').abort() # https://bugs.webkit.org/show_bug.cgi?id=136937
@checkedBuggyIDB = true
catch
try db.close()
@reason = 'apple'
@onOpenError()
return
@runCallbacks(db) if db.objectStoreNames.length is 0
@open = false try db.close()
db.close() @open = false
@fail 'empty'
else if error = @buggyIDB(db)
try db.close()
@open = false
@fail 'buggy', error
else
@runCallbacks(db)
@open = false
db.close()
return return
onOpenError: (event) => onOpenError: (event) =>
event?.preventDefault() event.preventDefault()
@open = false @open = false
error = event.target.error
switch error.name
when 'QuotaExceededError'
@onQuotaExceededError()
when 'VersionError'
@onVersionError()
when 'InvalidStateError'
@fail 'private_mode'
else
@fail 'cant_open', error
return
if event?.target?.error?.name is 'QuotaExceededError' fail: (reason, error) ->
@reset() @cachedDocs = null
@db() @useIndexedDB = false
app.onQuotaExceeded() @reason or= reason
@error or= error
console.error? 'IDB error', error if error
@runCallbacks()
if error and reason is 'cant_open'
Raven.captureMessage "#{error.name}: #{error.message}", level: 'warning', fingerprint: [error.name]
return
onQuotaExceededError: ->
@reset()
@db()
app.onQuotaExceeded()
Raven.captureMessage 'QuotaExceededError', level: 'warning'
return
onVersionError: ->
req = indexedDB.open(NAME)
req.onsuccess = (event) =>
@handleVersionMismatch event.target.result.version
req.onerror = (event) ->
event.preventDefault()
@fail 'cant_open', error
return
handleVersionMismatch: (actualVersion) ->
if Math.floor(actualVersion / @versionMultipler) isnt VERSION
@fail 'version'
else else
@useIndexedDB = false @setUserVersion actualVersion - VERSION * @versionMultipler
@reason or= 'cant_open' @db()
@runCallbacks()
return return
buggyIDB: (db) ->
return if @checkedBuggyIDB
@checkedBuggyIDB = true
try
@idbTransaction(db, stores: $.makeArray(db.objectStoreNames)[0..1], mode: 'readwrite').abort() # https://bugs.webkit.org/show_bug.cgi?id=136937
return
catch error
return error
runCallbacks: (db) -> runCallbacks: (db) ->
fn(db) while fn = @callbacks.shift() fn(db) while fn = @callbacks.shift()
return return
@ -62,7 +109,7 @@ class app.DB
objectStoreNames = $.makeArray(db.objectStoreNames) objectStoreNames = $.makeArray(db.objectStoreNames)
unless $.arrayDelete(objectStoreNames, 'docs') unless $.arrayDelete(objectStoreNames, 'docs')
db.createObjectStore('docs') try db.createObjectStore('docs')
for doc in app.docs.all() when not $.arrayDelete(objectStoreNames, doc.slug) for doc in app.docs.all() when not $.arrayDelete(objectStoreNames, doc.slug)
try db.createObjectStore(doc.slug) try db.createObjectStore(doc.slug)
@ -71,7 +118,7 @@ class app.DB
try db.deleteObjectStore(name) try db.deleteObjectStore(name)
return return
store: (doc, data, onSuccess, onError) -> store: (doc, data, onSuccess, onError, _retry = true) ->
@db (db) => @db (db) =>
unless db unless db
onError() onError()
@ -82,9 +129,15 @@ class app.DB
@cachedDocs?[doc.slug] = doc.mtime @cachedDocs?[doc.slug] = doc.mtime
onSuccess() onSuccess()
return return
txn.onerror = (event) -> txn.onerror = (event) =>
event.preventDefault() event.preventDefault()
onError(event) if txn.error?.name is 'NotFoundError' and _retry
@migrate()
setTimeout =>
@store(doc, data, onSuccess, onError, false)
, 0
else
onError(event)
return return
store = txn.objectStore(doc.slug) store = txn.objectStore(doc.slug)
@ -96,7 +149,7 @@ class app.DB
return return
return return
unstore: (doc, onSuccess, onError) -> unstore: (doc, onSuccess, onError, _retry = true) ->
@db (db) => @db (db) =>
unless db unless db
onError() onError()
@ -109,14 +162,20 @@ class app.DB
return return
txn.onerror = (event) -> txn.onerror = (event) ->
event.preventDefault() event.preventDefault()
onError(event) if txn.error?.name is 'NotFoundError' and _retry
@migrate()
setTimeout =>
@unstore(doc, onSuccess, onError, false)
, 0
else
onError(event)
return return
store = txn.objectStore(doc.slug)
store.clear()
store = txn.objectStore('docs') store = txn.objectStore('docs')
store.delete(doc.slug) store.delete(doc.slug)
store = txn.objectStore(doc.slug)
store.clear()
return return
return return
@ -227,9 +286,11 @@ class app.DB
@cachedDocs = {} @cachedDocs = {}
txn = @idbTransaction db, stores: ['docs'], mode: 'readonly' txn = @idbTransaction db, stores: ['docs'], mode: 'readonly'
store = txn.objectStore('docs') txn.oncomplete = =>
setTimeout(@checkForCorruptedDocs, 50)
return
req = store.openCursor() req = txn.objectStore('docs').openCursor()
req.onsuccess = (event) => req.onsuccess = (event) =>
return unless cursor = event.target.result return unless cursor = event.target.result
@cachedDocs[cursor.key] = cursor.value @cachedDocs[cursor.key] = cursor.value
@ -240,6 +301,45 @@ class app.DB
return return
return return
checkForCorruptedDocs: =>
@db (db) =>
@corruptedDocs = []
docs = (key for key, value of @cachedDocs when value)
return if docs.length is 0
for slug in docs when not app.docs.findBy('slug', slug)
@corruptedDocs.push(slug)
for slug in @corruptedDocs
$.arrayDelete(docs, slug)
if docs.length is 0
setTimeout(@deleteCorruptedDocs, 0)
return
txn = @idbTransaction(db, stores: docs, mode: 'readonly', ignoreError: false)
txn.oncomplete = =>
setTimeout(@deleteCorruptedDocs, 0) if @corruptedDocs.length > 0
return
for doc in docs
txn.objectStore(doc).get('index').onsuccess = (event) =>
@corruptedDocs.push(event.target.source.name) unless event.target.result
return
return
return
deleteCorruptedDocs: =>
@db (db) =>
txn = @idbTransaction(db, stores: ['docs'], mode: 'readwrite', ignoreError: false)
store = txn.objectStore('docs')
while doc = @corruptedDocs.pop()
@cachedDocs[doc] = false
store.delete(doc)
return
Raven.captureMessage 'corruptedDocs', level: 'info', extra: { docs: @corruptedDocs.join(',') }
return
shouldLoadWithIDB: (entry) -> shouldLoadWithIDB: (entry) ->
@useIndexedDB and (not @cachedDocs or @cachedDocs[entry.doc.slug]) @useIndexedDB and (not @cachedDocs or @cachedDocs[entry.doc.slug])
@ -274,11 +374,9 @@ class app.DB
app.settings.set('schema', @userVersion() + 1) app.settings.set('schema', @userVersion() + 1)
return return
schemaVersion: -> setUserVersion: (version) ->
@appVersion * 10 + @userVersion() app.settings.set('schema', version)
return
userVersion: -> userVersion: ->
app.settings.get('schema') app.settings.get('schema')
appVersion: ->
if app.config.env is 'production' then app.config.version else Math.floor(Date.now() / 1000)

@ -2,16 +2,17 @@ class app.Router
$.extend @prototype, Events $.extend @prototype, Events
@routes: [ @routes: [
['*', 'before' ] ['*', 'before' ]
['/', 'root' ] ['/', 'root' ]
['/offline', 'offline' ] ['/settings', 'settings' ]
['/about', 'about' ] ['/offline', 'offline' ]
['/news', 'news' ] ['/about', 'about' ]
['/help', 'help' ] ['/news', 'news' ]
['/:doc-:type/', 'type' ] ['/help', 'help' ]
['/:doc/', 'doc' ] ['/:doc-:type/', 'type' ]
['/:doc/:path(*)', 'entry' ] ['/:doc/', 'doc' ]
['*', 'notFound'] ['/:doc/:path(*)', 'entry' ]
['*', 'notFound' ]
] ]
constructor: -> constructor: ->
@ -33,19 +34,24 @@ class app.Router
return return
before: (context, next) -> before: (context, next) ->
previousContext = @context
@context = context @context = context
@trigger 'before', context @trigger 'before', context
next()
return if res = next()
@context = previousContext
return res
else
return
doc: (context, next) -> doc: (context, next) ->
if doc = app.docs.findBySlug(context.params.doc) or app.disabledDocs.findBySlug(context.params.doc) if doc = app.docs.findBySlug(context.params.doc) or app.disabledDocs.findBySlug(context.params.doc)
context.doc = doc context.doc = doc
context.entry = doc.toEntry() context.entry = doc.toEntry()
@triggerRoute 'entry' @triggerRoute 'entry'
return
else else
next() return next()
return
type: (context, next) -> type: (context, next) ->
doc = app.docs.findBySlug(context.params.doc) doc = app.docs.findBySlug(context.params.doc)
@ -54,9 +60,9 @@ class app.Router
context.doc = doc context.doc = doc
context.type = type context.type = type
@triggerRoute 'type' @triggerRoute 'type'
return
else else
next() return next()
return
entry: (context, next) -> entry: (context, next) ->
doc = app.docs.findBySlug(context.params.doc) doc = app.docs.findBySlug(context.params.doc)
@ -65,32 +71,39 @@ class app.Router
context.doc = doc context.doc = doc
context.entry = entry context.entry = entry
@triggerRoute 'entry' @triggerRoute 'entry'
return
else else
next() return next()
return
root: -> root: ->
if app.isSingleDoc() return '/' if app.isSingleDoc()
setTimeout (-> window.location = '/'), 0 @triggerRoute 'root'
else return
@triggerRoute 'root'
settings: (context) ->
return "/#/#{context.path}" if app.isSingleDoc()
@triggerRoute 'settings'
return return
offline: -> offline: (context)->
return "/#/#{context.path}" if app.isSingleDoc()
@triggerRoute 'offline' @triggerRoute 'offline'
return return
about: (context) -> about: (context) ->
return "/#/#{context.path}" if app.isSingleDoc()
context.page = 'about' context.page = 'about'
@triggerRoute 'page' @triggerRoute 'page'
return return
news: (context) -> news: (context) ->
return "/#/#{context.path}" if app.isSingleDoc()
context.page = 'news' context.page = 'news'
@triggerRoute 'page' @triggerRoute 'page'
return return
help: (context) -> help: (context) ->
return "/#/#{context.path}" if app.isSingleDoc()
context.page = 'help' context.page = 'help'
@triggerRoute 'page' @triggerRoute 'page'
return return
@ -99,15 +112,15 @@ class app.Router
@triggerRoute 'notFound' @triggerRoute 'notFound'
return return
isRoot: -> isIndex: ->
location.pathname is '/' @context?.path is '/' or (app.isSingleDoc() and @context?.entry?.isIndex())
setInitialPath: -> setInitialPath: ->
# Remove superfluous forward slashes at the beginning of the path # Remove superfluous forward slashes at the beginning of the path
if (path = location.pathname.replace /^\/{2,}/g, '/') isnt location.pathname if (path = location.pathname.replace /^\/{2,}/g, '/') isnt location.pathname
page.replace path + location.search + location.hash, null, true page.replace path + location.search + location.hash, null, true
if @isRoot() if location.pathname is '/'
if path = @getInitialPathFromHash() if path = @getInitialPathFromHash()
page.replace path + location.search, null, true page.replace path + location.search, null, true
else if path = @getInitialPathFromCookie() else if path = @getInitialPathFromCookie()

@ -26,7 +26,7 @@ return unless index >= 0
lastIndex = value.lastIndexOf(query) lastIndex = value.lastIndexOf(query)
if index isnt lastIndex if index isnt lastIndex
return Math.max(scoreExactMatch(), (index = lastIndex) and scoreExactMatch()) return Math.max(scoreExactMatch(), ((index = lastIndex) and scoreExactMatch()) or 0)
else else
return scoreExactMatch() return scoreExactMatch()
`}` `}`
@ -112,7 +112,8 @@ class app.Searcher
max_results: app.config.max_results max_results: app.config.max_results
fuzzy_min_length: 3 fuzzy_min_length: 3
SEPARATORS_REGEXP = /\:?\ |#|::|->|\$(?=\w)|\-(?=\w)/g SEPARATORS_REGEXP = /#|::|:-|->|\$(?=\w)|\-(?=\w)|\:(?=\w)|\ [\/\-&]\ |:\ |\ /g
EOS_SEPARATORS_REGEXP = /(\w)[\-:]$/
INFO_PARANTHESES_REGEXP = /\ \(\w+?\)$/ INFO_PARANTHESES_REGEXP = /\ \(\w+?\)$/
EMPTY_PARANTHESES_REGEXP = /\(\)/ EMPTY_PARANTHESES_REGEXP = /\(\)/
EVENT_REGEXP = /\ event$/ EVENT_REGEXP = /\ event$/
@ -134,6 +135,10 @@ class app.Searcher
.replace EMPTY_PARANTHESES_REGEXP, EMPTY_STRING .replace EMPTY_PARANTHESES_REGEXP, EMPTY_STRING
.replace WHITESPACE_REGEXP, EMPTY_STRING .replace WHITESPACE_REGEXP, EMPTY_STRING
@normalizeQuery: (string) ->
string = @normalizeString(string)
string.replace EOS_SEPARATORS_REGEXP, '$1.'
constructor: (options = {}) -> constructor: (options = {}) ->
@options = $.extend {}, DEFAULTS, options @options = $.extend {}, DEFAULTS, options
@ -149,7 +154,7 @@ class app.Searcher
return return
setup: -> setup: ->
query = @query = @constructor.normalizeString(@query) query = @query = @constructor.normalizeQuery(@query)
queryLength = query.length queryLength = query.length
@dataLength = @data.length @dataLength = @data.length
@matchers = [exactMatch] @matchers = [exactMatch]
@ -166,7 +171,7 @@ class app.Searcher
return return
isValid: -> isValid: ->
queryLength > 0 queryLength > 0 and query isnt SEPARATOR
end: -> end: ->
@triggerResults [] unless @totalResults @triggerResults [] unless @totalResults

@ -1,101 +1,80 @@
class app.Settings class app.Settings
SETTINGS_KEY = 'settings'
DOCS_KEY = 'docs' DOCS_KEY = 'docs'
DARK_KEY = 'dark' DARK_KEY = 'dark'
LAYOUT_KEY = 'layout' LAYOUT_KEY = 'layout'
SIZE_KEY = 'size' SIZE_KEY = 'size'
TIPS_KEY = 'tips'
@defaults: @defaults:
count: 0 count: 0
hideDisabled: false hideDisabled: false
hideIntro: false hideIntro: false
news: 0 news: 0
autoUpdate: true manualUpdate: false
schema: 1 schema: 1
constructor: (@store) -> constructor: ->
@create() unless @settings = @store.get(SETTINGS_KEY) @store = new CookieStore
@cache = {}
create: -> get: (key) ->
@settings = $.extend({}, @constructor.defaults) return @cache[key] if @cache.hasOwnProperty(key)
@applyLegacyValues @settings @cache[key] = @store.get(key) ? @constructor.defaults[key]
@save()
return
applyLegacyValues: (settings) ->
for key, v of settings when (value = @store.get(key))?
settings[key] = value
@store.del(key)
return
save: ->
@store.set SETTINGS_KEY, @settings
set: (key, value) -> set: (key, value) ->
@settings[key] = value @store.set(key, value)
@save() delete @cache[key]
return
get: (key) -> del: (key) ->
@settings[key] ? @constructor.defaults[key] @store.del(key)
delete @cache[key]
return
hasDocs: -> hasDocs: ->
try !!Cookies.get DOCS_KEY try !!@store.get(DOCS_KEY)
getDocs: -> getDocs: ->
try @store.get(DOCS_KEY)?.split('/') or app.config.default_docs
Cookies.get(DOCS_KEY)?.split('/') or app.config.default_docs
catch
app.config.default_docs
setDocs: (docs) -> setDocs: (docs) ->
try @set DOCS_KEY, docs.join('/')
Cookies.set DOCS_KEY, docs.join('/'), path: '/', expires: 1e8
catch
return return
setDark: (value) -> getTips: ->
try @store.get(TIPS_KEY)?.split('/') or []
if value
Cookies.set DARK_KEY, '1', path: '/', expires: 1e8 setTips: (tips) ->
else @set TIPS_KEY, tips.join('/')
Cookies.expire DARK_KEY
catch
return return
setLayout: (name, enable) -> setLayout: (name, enable) ->
try layout = (@store.get(LAYOUT_KEY) || '').split(' ')
layout = (Cookies.get(LAYOUT_KEY) || '').split(' ') $.arrayDelete(layout, '')
$.arrayDelete(layout, '')
if enable
if enable layout.push(name) if layout.indexOf(name) is -1
layout.push(name) if layout.indexOf(name) is -1 else
else $.arrayDelete(layout, name)
$.arrayDelete(layout, name)
if layout.length > 0
if layout.length > 0 @set LAYOUT_KEY, layout.join(' ')
Cookies.set LAYOUT_KEY, layout.join(' '), path: '/', expires: 1e8 else
else @del LAYOUT_KEY
Cookies.expire LAYOUT_KEY
catch
return return
hasLayout: (name) -> hasLayout: (name) ->
try layout = (@store.get(LAYOUT_KEY) || '').split(' ')
layout = (Cookies.get(LAYOUT_KEY) || '').split(' ') layout.indexOf(name) isnt -1
layout.indexOf(name) isnt -1
catch
false
setSize: (value) -> setSize: (value) ->
try @set SIZE_KEY, value
Cookies.set SIZE_KEY, value, path: '/', expires: 1e8
catch
return return
dump: ->
@store.dump()
reset: -> reset: ->
try Cookies.expire DOCS_KEY @store.reset()
try Cookies.expire DARK_KEY @cache = {}
try Cookies.expire LAYOUT_KEY
try Cookies.expire SIZE_KEY
try @store.del(SETTINGS_KEY)
return return

@ -2,7 +2,7 @@ class app.Shortcuts
$.extend @prototype, Events $.extend @prototype, Events
constructor: -> constructor: ->
@isWindows = $.isWindows() @isMac = $.isMac()
@start() @start()
start: -> start: ->
@ -15,11 +15,15 @@ class app.Shortcuts
$.off document, 'keypress', @onKeypress $.off document, 'keypress', @onKeypress
return return
swapArrowKeysBehavior: ->
app.settings.get('arrowScroll')
showTip: -> showTip: ->
app.showTip('KeyNav') app.showTip('KeyNav')
@showTip = null @showTip = null
onKeydown: (event) => onKeydown: (event) =>
return if @buggyEvent(event)
result = if event.ctrlKey or event.metaKey result = if event.ctrlKey or event.metaKey
@handleKeydownSuperEvent event unless event.altKey or event.shiftKey @handleKeydownSuperEvent event unless event.altKey or event.shiftKey
else if event.shiftKey else if event.shiftKey
@ -33,12 +37,15 @@ class app.Shortcuts
return return
onKeypress: (event) => onKeypress: (event) =>
return if @buggyEvent(event)
unless event.ctrlKey or event.metaKey unless event.ctrlKey or event.metaKey
result = @handleKeypressEvent event result = @handleKeypressEvent event
event.preventDefault() if result is false event.preventDefault() if result is false
return return
handleKeydownEvent: (event) -> handleKeydownEvent: (event, _force) ->
return @handleKeydownAltEvent(event, true) if not _force and event.which in [37, 38, 39, 40] and @swapArrowKeysBehavior()
if not event.target.form and (48 <= event.which <= 57 or 65 <= event.which <= 90) if not event.target.form and (48 <= event.which <= 57 or 65 <= event.which <= 90)
@trigger 'typing' @trigger 'typing'
return return
@ -50,8 +57,9 @@ class app.Shortcuts
@trigger 'enter' @trigger 'enter'
when 27 when 27
@trigger 'escape' @trigger 'escape'
false
when 32 when 32
if not @lastKeypress or @lastKeypress < Date.now() - 500 if event.target.type is 'search' and (not @lastKeypress or @lastKeypress < Date.now() - 500)
@trigger 'pageDown' @trigger 'pageDown'
false false
when 33 when 33
@ -59,9 +67,9 @@ class app.Shortcuts
when 34 when 34
@trigger 'pageDown' @trigger 'pageDown'
when 35 when 35
@trigger 'end' @trigger 'pageBottom' unless event.target.form
when 36 when 36
@trigger 'home' @trigger 'pageTop' unless event.target.form
when 37 when 37
@trigger 'left' unless event.target.value @trigger 'left' unless event.target.value
when 38 when 38
@ -74,27 +82,36 @@ class app.Shortcuts
@trigger 'down' @trigger 'down'
@showTip?() @showTip?()
false false
when 191
unless event.target.form
@trigger 'typing'
false
handleKeydownSuperEvent: (event) -> handleKeydownSuperEvent: (event) ->
switch event.which switch event.which
when 13 when 13
@trigger 'superEnter' @trigger 'superEnter'
when 37 when 37
unless @isWindows if @isMac
@trigger 'superLeft' @trigger 'superLeft'
false false
when 38 when 38
@trigger 'home' @trigger 'pageTop'
false false
when 39 when 39
unless @isWindows if @isMac
@trigger 'superRight' @trigger 'superRight'
false false
when 40 when 40
@trigger 'end' @trigger 'pageBottom'
false
when 188
@trigger 'preferences'
false false
handleKeydownShiftEvent: (event) -> handleKeydownShiftEvent: (event, _force) ->
return @handleKeydownEvent(event, true) if not _force and event.which in [37, 38, 39, 40] and @swapArrowKeysBehavior()
if not event.target.form and 65 <= event.which <= 90 if not event.target.form and 65 <= event.which <= 90
@trigger 'typing' @trigger 'typing'
return return
@ -112,19 +129,21 @@ class app.Shortcuts
@trigger 'altDown' @trigger 'altDown'
false false
handleKeydownAltEvent: (event) -> handleKeydownAltEvent: (event, _force) ->
return @handleKeydownEvent(event, true) if not _force and event.which in [37, 38, 39, 40] and @swapArrowKeysBehavior()
switch event.which switch event.which
when 9 when 9
@trigger 'altRight', event @trigger 'altRight', event
when 37 when 37
if @isWindows unless @isMac
@trigger 'superLeft' @trigger 'superLeft'
false false
when 38 when 38
@trigger 'altUp' @trigger 'altUp'
false false
when 39 when 39
if @isWindows unless @isMac
@trigger 'superRight' @trigger 'superRight'
false false
when 40 when 40
@ -135,6 +154,9 @@ class app.Shortcuts
when 71 when 71
@trigger 'altG' @trigger 'altG'
false false
when 79
@trigger 'altO'
false
when 82 when 82
@trigger 'altR' @trigger 'altR'
false false
@ -148,3 +170,12 @@ class app.Shortcuts
false false
else else
@lastKeypress = Date.now() @lastKeypress = Date.now()
buggyEvent: (event) ->
try
event.target
event.ctrlKey
event.which
return false
catch
return true

@ -2,10 +2,10 @@ class app.UpdateChecker
constructor: -> constructor: ->
@lastCheck = Date.now() @lastCheck = Date.now()
$.on window, 'focus', @checkForUpdate $.on window, 'focus', @onFocus
app.appCache.on 'updateready', @onUpdateReady if app.appCache app.appCache.on 'updateready', @onUpdateReady if app.appCache
@checkDocs() setTimeout @checkDocs, 0
check: -> check: ->
if app.appCache if app.appCache
@ -21,8 +21,8 @@ class app.UpdateChecker
new app.views.Notif 'UpdateReady', autoHide: null new app.views.Notif 'UpdateReady', autoHide: null
return return
checkDocs: -> checkDocs: =>
if app.settings.get('autoUpdate') unless app.settings.get('manualUpdate')
app.docs.updateInBackground() app.docs.updateInBackground()
else else
app.docs.checkForUpdates (i) => @onDocsUpdateReady() if i > 0 app.docs.checkForUpdates (i) => @onDocsUpdateReady() if i > 0

@ -18,6 +18,8 @@
#= require_tree ./templates #= require_tree ./templates
#= require tracking
init = -> init = ->
document.removeEventListener 'DOMContentLoaded', init, false document.removeEventListener 'DOMContentLoaded', init, false

@ -48,3 +48,8 @@ class app.Collection
findAllBy: (attr, value) -> findAllBy: (attr, value) ->
model for model in @models when model[attr] is value model for model in @models when model[attr] is value
countAllBy: (attr, value) ->
i = 0
i += 1 for model in @models when model[attr] is value
i

@ -4,11 +4,19 @@ class app.collections.Docs extends app.Collection
findBySlug: (slug) -> findBySlug: (slug) ->
@findBy('slug', slug) or @findBy('slug_without_version', slug) @findBy('slug', slug) or @findBy('slug_without_version', slug)
NORMALIZE_VERSION_RGX = /\.(\d)$/
NORMALIZE_VERSION_SUB = '.0$1'
sort: -> sort: ->
@models.sort (a, b) -> @models.sort (a, b) ->
a = a.name.toLowerCase() if a.name is b.name
b = b.name.toLowerCase() if not a.version or a.version.replace(NORMALIZE_VERSION_RGX, NORMALIZE_VERSION_SUB) > b.version.replace(NORMALIZE_VERSION_RGX, NORMALIZE_VERSION_SUB)
if a < b then -1 else if a > b then 1 else 0 -1
else
1
else if a.name.toLowerCase() > b.name.toLowerCase()
1
else
-1
# Load models concurrently. # Load models concurrently.
# It's not pretty but I didn't want to import a promise library only for this. # It's not pretty but I didn't want to import a promise library only for this.

@ -7,10 +7,13 @@ class app.collections.Types extends app.Collection
(result[@_groupFor(type)] ||= []).push(type) (result[@_groupFor(type)] ||= []).push(type)
result.filter (e) -> e.length > 0 result.filter (e) -> e.length > 0
GUIDES_RGX = /(^|[\s\(])(guide|guides|tutorial|reference|playbooks|getting\ started|manual)($|[\s\):])/i GUIDES_RGX = /(^|\()(guides?|tutorials?|reference|book|getting\ started|manual|examples)($|[\):])/i
APPENDIX_RGX = /appendix/i
_groupFor: (type) -> _groupFor: (type) ->
if GUIDES_RGX.test(type.name) if GUIDES_RGX.test(type.name)
0 0
else if APPENDIX_RGX.test(type.name)
2
else else
1 1

@ -70,13 +70,16 @@ app.Searcher.prototype = _proto
# View tree # View tree
# #
@viewTree = (view = app.document, level = 0) -> @viewTree = (view = app.document, level = 0, visited = []) ->
console.log "%c #{Array(level + 1).join(' ')}#{view.constructor.name}: #{view.activated}", return if visited.indexOf(view) >= 0
visited.push(view)
console.log "%c #{Array(level + 1).join(' ')}#{view.constructor.name}: #{!!view.activated}",
'color:' + (view.activated and 'green' or 'red') 'color:' + (view.activated and 'green' or 'red')
for key, value of view when key isnt 'view' and value for own key, value of view when key isnt 'view' and value
if typeof value is 'object' and value.setupElement if typeof value is 'object' and value.setupElement
@viewTree(value, level + 1) @viewTree(value, level + 1, visited)
else if value.constructor.toString().match(/Object\(\)/) else if value.constructor.toString().match(/Object\(\)/)
@viewTree(v, level + 1) for k, v of value when value and typeof value is 'object' and value.setupElement @viewTree(v, level + 1, visited) for own k, v of value when v and typeof v is 'object' and v.setupElement
return return

@ -29,6 +29,7 @@ ajax.defaults =
# data # data
# error # error
# headers # headers
# progress
# success # success
# url # url
@ -54,6 +55,7 @@ applyCallbacks = (xhr, options) ->
return unless options.async return unless options.async
xhr.timer = setTimeout onTimeout.bind(undefined, xhr, options), options.timeout * 1000 xhr.timer = setTimeout onTimeout.bind(undefined, xhr, options), options.timeout * 1000
xhr.onprogress = options.progress if options.progress
xhr.onreadystatechange = -> xhr.onreadystatechange = ->
if xhr.readyState is 4 if xhr.readyState is 4
clearTimeout(xhr.timer) clearTimeout(xhr.timer)

@ -0,0 +1,37 @@
class @CookieStore
INT = /^\d+$/
@onBlocked: ->
get: (key) ->
value = Cookies.get(key)
value = parseInt(value, 10) if value? and INT.test(value)
value
set: (key, value) ->
if value == false
@del(key)
return
value = 1 if value == true
Cookies.set(key, '' + value, path: '/', expires: 1e8)
@constructor.onBlocked(key, value, @get(key)) if @get(key) != value
return
del: (key) ->
Cookies.expire(key)
return
reset: ->
try
for cookie in document.cookie.split(/;\s?/)
Cookies.expire(cookie.split('=')[0])
return
catch
dump: ->
result = {}
for cookie in document.cookie.split(/;\s?/) when cookie[0] isnt '_'
cookie = cookie.split('=')
result[cookie[0]] = cookie[1]
result

@ -15,8 +15,10 @@
@ @
trigger: (event, args...) -> trigger: (event, args...) ->
@eventInProgress = { name: event, args: args }
if callbacks = @_callbacks?[event] if callbacks = @_callbacks?[event]
callback? args... for callback in callbacks.slice(0) callback? args... for callback in callbacks.slice(0)
@eventInProgress = null
@trigger 'all', event, args... unless event is 'all' @trigger 'all', event, args... unless event is 'all'
@ @

@ -1,5 +1,5 @@
### ###
* Copyright 2013-2016 Thibaut Courouble and other contributors * Copyright 2013-2017 Thibaut Courouble and other contributors
* *
* This source code is licensed under the terms of the Mozilla * This source code is licensed under the terms of the Mozilla
* Public License, v. 2.0, a copy of which may be obtained at: * Public License, v. 2.0, a copy of which may be obtained at:

@ -1,4 +1,4 @@
class @Store class @LocalStorageStore
get: (key) -> get: (key) ->
try try
JSON.parse localStorage.getItem(key) JSON.parse localStorage.getItem(key)
@ -16,7 +16,7 @@ class @Store
true true
catch catch
clear: -> reset: ->
try try
localStorage.clear() localStorage.clear()
true true

@ -38,10 +38,15 @@ page.stop = ->
page.show = (path, state) -> page.show = (path, state) ->
return if path is currentState?.path return if path is currentState?.path
context = new Context(path, state) context = new Context(path, state)
previousState = currentState
currentState = context.state currentState = context.state
page.dispatch(context) if res = page.dispatch(context)
context.pushState() currentState = previousState
track() location.assign(res)
else
context.pushState()
updateCanonicalLink()
track()
context context
page.replace = (path, state, skipDispatch, init) -> page.replace = (path, state, skipDispatch, init) ->
@ -50,16 +55,22 @@ page.replace = (path, state, skipDispatch, init) ->
currentState = context.state currentState = context.state
page.dispatch(context) unless skipDispatch page.dispatch(context) unless skipDispatch
context.replaceState() context.replaceState()
track() unless init or skipDispatch updateCanonicalLink()
track() unless skipDispatch
context context
page.dispatch = (context) -> page.dispatch = (context) ->
i = 0 i = 0
next = -> next = ->
fn(context, next) if fn = callbacks[i++] res = fn(context, next) if fn = callbacks[i++]
return return res
next() return next()
return
page.canGoBack = ->
not Context.isIntialState(currentState)
page.canGoForward = ->
not Context.isLastState(currentState)
currentPath = -> currentPath = ->
location.pathname + location.search + location.hash location.pathname + location.search + location.hash
@ -69,6 +80,12 @@ class Context
@sessionId: Date.now() @sessionId: Date.now()
@stateId: 0 @stateId: 0
@isIntialState: (state) ->
state.id == 0
@isLastState: (state) ->
state.id == @stateId - 1
@isInitialPopState: (state) -> @isInitialPopState: (state) ->
state.path is @initialPath and @stateId is 1 state.path is @initialPath and @stateId is 1
@ -102,10 +119,9 @@ class Route
(context, next) => (context, next) =>
if @match context.pathname, params = [] if @match context.pathname, params = []
context.params = params context.params = params
fn(context, next) return fn(context, next)
else else
next() return next()
return
match: (path, params) -> match: (path, params) ->
return unless matchData = @regexp.exec(path) return unless matchData = @regexp.exec(path)
@ -159,13 +175,24 @@ onclick = (event) ->
if link and not link.target and isSameOrigin(link.href) if link and not link.target and isSameOrigin(link.href)
event.preventDefault() event.preventDefault()
page.show link.pathname + link.search + link.hash path = link.pathname + link.search + link.hash
path = path.replace /^\/\/+/, '/' # IE11 bug
page.show(path)
return return
isSameOrigin = (url) -> isSameOrigin = (url) ->
url.indexOf("#{location.protocol}//#{location.hostname}") is 0 url.indexOf("#{location.protocol}//#{location.hostname}") is 0
updateCanonicalLink = ->
@canonicalLink ||= document.head.querySelector('link[rel="canonical"]')
@canonicalLink.setAttribute('href', "http://#{location.host}#{location.pathname}")
trackers = []
page.track = (fn) ->
trackers.push(fn)
return
track = -> track = ->
ga?('send', 'pageview', location.pathname + location.search + location.hash) tracker.call() for tracker in trackers
_gauges?.push(['track'])
return return

@ -167,27 +167,32 @@ $.scrollTo = (el, parent, position = 'center', options = {}) ->
return unless parent return unless parent
parentHeight = parent.clientHeight parentHeight = parent.clientHeight
return unless parent.scrollHeight > parentHeight parentScrollHeight = parent.scrollHeight
return unless parentScrollHeight > parentHeight
top = $.offset(el, parent).top top = $.offset(el, parent).top
offsetTop = parent.firstElementChild.offsetTop
switch position switch position
when 'top' when 'top'
parent.scrollTop = top - (if options.margin? then options.margin else 20) parent.scrollTop = top - offsetTop - (if options.margin? then options.margin else 0)
when 'center' when 'center'
parent.scrollTop = top - Math.round(parentHeight / 2 - el.offsetHeight / 2) parent.scrollTop = top - Math.round(parentHeight / 2 - el.offsetHeight / 2)
when 'continuous' when 'continuous'
scrollTop = parent.scrollTop scrollTop = parent.scrollTop
height = el.offsetHeight height = el.offsetHeight
lastElementOffset = parent.lastElementChild.offsetTop + parent.lastElementChild.offsetHeight
offsetBottom = if lastElementOffset > 0 then parentScrollHeight - lastElementOffset else 0
# If the target element is above the visible portion of its scrollable # If the target element is above the visible portion of its scrollable
# ancestor, move it near the top with a gap = options.topGap * target's height. # ancestor, move it near the top with a gap = options.topGap * target's height.
if top <= scrollTop + height * (options.topGap or 1) if top - offsetTop <= scrollTop + height * (options.topGap or 1)
parent.scrollTop = top - height * (options.topGap or 1) parent.scrollTop = top - offsetTop - height * (options.topGap or 1)
# If the target element is below the visible portion of its scrollable # If the target element is below the visible portion of its scrollable
# ancestor, move it near the bottom with a gap = options.bottomGap * target's height. # ancestor, move it near the bottom with a gap = options.bottomGap * target's height.
else if top >= scrollTop + parentHeight - height * ((options.bottomGap or 1) + 1) else if top + offsetBottom >= scrollTop + parentHeight - height * ((options.bottomGap or 1) + 1)
parent.scrollTop = top - parentHeight + height * ((options.bottomGap or 1) + 1) parent.scrollTop = top + offsetBottom - parentHeight + height * ((options.bottomGap or 1) + 1)
return return
$.scrollToWithImageLock = (el, parent, args...) -> $.scrollToWithImageLock = (el, parent, args...) ->
@ -223,6 +228,36 @@ $.lockScroll = (el, fn) ->
fn() fn()
return return
smoothScroll = smoothStart = smoothEnd = smoothDistance = smoothDuration = null
$.smoothScroll = (el, end) ->
unless window.requestAnimationFrame
el.scrollTop = end
return
smoothEnd = end
if smoothScroll
newDistance = smoothEnd - smoothStart
smoothDuration += Math.min 300, Math.abs(smoothDistance - newDistance)
smoothDistance = newDistance
return
smoothStart = el.scrollTop
smoothDistance = smoothEnd - smoothStart
smoothDuration = Math.min 300, Math.abs(smoothDistance)
startTime = Date.now()
smoothScroll = ->
p = Math.min 1, (Date.now() - startTime) / smoothDuration
y = Math.max 0, Math.floor(smoothStart + smoothDistance * (if p < 0.5 then 2 * p * p else p * (4 - p * 2) - 1))
el.scrollTop = y
if p is 1
smoothScroll = null
else
requestAnimationFrame(smoothScroll)
requestAnimationFrame(smoothScroll)
# #
# Utilities # Utilities
# #
@ -278,6 +313,19 @@ $.classify = (string) ->
string[i] = substr[0].toUpperCase() + substr[1..] string[i] = substr[0].toUpperCase() + substr[1..]
string.join('') string.join('')
$.framify = (fn, obj) ->
if window.requestAnimationFrame
(args...) -> requestAnimationFrame(fn.bind(obj, args...))
else
fn
$.requestAnimationFrame = (fn) ->
if window.requestAnimationFrame
requestAnimationFrame(fn)
else
setTimeout(fn, 0)
return
# #
# Miscellaneous # Miscellaneous
# #
@ -285,17 +333,38 @@ $.classify = (string) ->
$.noop = -> $.noop = ->
$.popup = (value) -> $.popup = (value) ->
open value.href or value, '_blank' try
win = window.open()
win.opener = null if win.opener
win.location = value.href or value
catch
window.open value.href or value, '_blank'
return return
$.isTouchScreen = -> isMac = null
typeof ontouchstart isnt 'undefined'
$.isWindows = ->
navigator.platform?.indexOf('Win') >= 0
$.isMac = -> $.isMac = ->
navigator.userAgent?.indexOf('Mac') >= 0 isMac ?= navigator.userAgent?.indexOf('Mac') >= 0
isIE = null
$.isIE = ->
isIE ?= navigator.userAgent?.indexOf('MSIE') >= 0 || navigator.userAgent?.indexOf('rv:11.0') >= 0
isAndroid = null
$.isAndroid = ->
isAndroid ?= navigator.userAgent?.indexOf('Android') >= 0
isIOS = null
$.isIOS = ->
isIOS ?= navigator.userAgent?.indexOf('iPhone') >= 0 || navigator.userAgent?.indexOf('iPad') >= 0
$.overlayScrollbarsEnabled = ->
return false unless $.isMac()
div = document.createElement('div')
div.setAttribute('style', 'width: 100px; height: 100px; overflow: scroll; position: absolute')
document.body.appendChild(div)
result = div.offsetWidth is div.clientWidth
document.body.removeChild(div)
result
HIGHLIGHT_DEFAULTS = HIGHLIGHT_DEFAULTS =
className: 'highlight' className: 'highlight'

@ -7,6 +7,7 @@ class app.models.Doc extends app.Model
@slug_without_version = @slug.split('~')[0] @slug_without_version = @slug.split('~')[0]
@fullName = "#{@name}" + if @version then " #{@version}" else '' @fullName = "#{@name}" + if @version then " #{@version}" else ''
@icon = @slug_without_version @icon = @slug_without_version
@short_version = @version.split(' ')[0] if @version
@text = @toEntry().text @text = @toEntry().text
reset: (data) -> reset: (data) ->
@ -29,19 +30,22 @@ class app.models.Doc extends app.Model
"/#{@slug}#{path}" "/#{@slug}#{path}"
fileUrl: (path) -> fileUrl: (path) ->
"#{app.config.docs_host}#{@fullPath(path)}?#{@mtime}" "#{app.config.docs_origin}#{@fullPath(path)}?#{@mtime}"
dbUrl: -> dbUrl: ->
"#{app.config.docs_host}/#{@slug}/#{app.config.db_filename}?#{@mtime}" "#{app.config.docs_origin}/#{@slug}/#{app.config.db_filename}?#{@mtime}"
indexUrl: -> indexUrl: ->
"#{app.indexHost()}/#{@slug}/#{app.config.index_filename}?#{@mtime}" "#{app.indexHost()}/#{@slug}/#{app.config.index_filename}?#{@mtime}"
toEntry: -> toEntry: ->
@entry ||= new app.models.Entry return @entry if @entry
@entry = new app.models.Entry
doc: @ doc: @
name: @fullName name: @fullName
path: 'index' path: 'index'
@entry.addAlias(@name) if @version
@entry
findEntryByPathAndHash: (path, hash) -> findEntryByPathAndHash: (path, hash) ->
if hash and entry = @entries.findBy 'path', "#{path}##{hash}" if hash and entry = @entries.findBy 'path', "#{path}##{hash}"
@ -66,7 +70,7 @@ class app.models.Doc extends app.Model
error: onError error: onError
clearCache: -> clearCache: ->
app.store.del @slug app.localStorage.del @slug
return return
_loadFromCache: (onSuccess) -> _loadFromCache: (onSuccess) ->
@ -81,7 +85,7 @@ class app.models.Doc extends app.Model
true true
_getCache: -> _getCache: ->
return unless data = app.store.get @slug return unless data = app.localStorage.get @slug
if data[0] is @mtime if data[0] is @mtime
return data[1] return data[1]
@ -90,10 +94,10 @@ class app.models.Doc extends app.Model
return return
_setCache: (data) -> _setCache: (data) ->
app.store.set @slug, [@mtime, data] app.localStorage.set @slug, [@mtime, data]
return return
install: (onSuccess, onError) -> install: (onSuccess, onError, onProgress) ->
return if @installing return if @installing
@installing = true @installing = true
@ -111,6 +115,7 @@ class app.models.Doc extends app.Model
url: @dbUrl() url: @dbUrl()
success: success success: success
error: error error: error
progress: onProgress
timeout: 3600 timeout: 3600
return return

@ -5,8 +5,13 @@ class app.models.Entry extends app.Model
constructor: -> constructor: ->
super super
@text = app.Searcher.normalizeString(@name) @text = applyAliases(app.Searcher.normalizeString(@name))
@text = applyAliases(@text)
addAlias: (name) ->
text = applyAliases(app.Searcher.normalizeString(name))
@text = [@text] unless Array.isArray(@text)
@text.push(if Array.isArray(text) then text[1] else text)
return
fullPath: -> fullPath: ->
@doc.fullPath if @isIndex() then '' else @path @doc.fullPath if @isIndex() then '' else @path
@ -45,8 +50,9 @@ class app.models.Entry extends app.Model
return string return string
@ALIASES = ALIASES = @ALIASES = ALIASES =
'angular': 'ng'
'angular.js': 'ng' 'angular.js': 'ng'
'backbone': 'bb' 'backbone.js': 'bb'
'c++': 'cpp' 'c++': 'cpp'
'coffeescript': 'cs' 'coffeescript': 'cs'
'elixir': 'ex' 'elixir': 'ex'
@ -59,10 +65,16 @@ class app.models.Entry extends app.Model
'markdown': 'md' 'markdown': 'md'
'modernizr': 'mdr' 'modernizr': 'mdr'
'moment.js': 'mt' 'moment.js': 'mt'
'openjdk': 'java'
'nginx': 'ngx' 'nginx': 'ngx'
'numpy': 'np'
'pandas': 'pd'
'postgresql': 'pg' 'postgresql': 'pg'
'python': 'py' 'python': 'py'
'ruby.on.rails': 'ror' 'ruby.on.rails': 'ror'
'ruby': 'rb' 'ruby': 'rb'
'rust': 'rs'
'sass': 'scss' 'sass': 'scss'
'tensorflow': 'tf'
'typescript': 'ts'
'underscore.js': '_' 'underscore.js': '_'

@ -1,5 +1,60 @@
[ [
[ [
"2017-07-23",
"New documentation: <a href=\"/godot/\">Godot</a>"
], [
"2017-06-04",
"New documentations: <a href=\"/electron/\">Electron</a>, <a href=\"/pug/\">Pug</a>, and <a href=\"/falcon/\">Falcon</a>"
], [
"2017-05-14",
"New documentations: <a href=\"/jest/\">Jest</a>, <a href=\"/jasmine/\">Jasmine</a> and <a href=\"/liquid/\">Liquid</a>"
], [
"2017-04-30",
"New documentation: <a href=\"/openjdk/\">OpenJDK</a>"
], [
"2017-02-26",
"Refreshed design.",
"Added <a href=\"/settings\">Preferences</a>."
], [
"2017-01-22",
"New <a href=\"/http/\">HTTP</a> documentation (thanks Mozilla)"
], [
"2016-12-04",
"New documentations: <a href=\"/sqlite/\">SQLite</a>, <a href=\"/codeception/\">Codeception</a> and <a href=\"/codeceptjs/\">CodeceptJS</a>"
], [
"2016-11-20",
"New documentations: <a href=\"/yarn/\">Yarn</a>, <a href=\"/immutable/\">Immutable.js</a> and <a href=\"/async/\">Async</a>"
], [
"2016-10-10",
"New documentations: <a href=\"/scikit_learn/\">scikit-learn</a> and <a href=\"/statsmodels/\">Statsmodels</a>"
], [
"2016-09-18",
"New documentations: <a href=\"/pandas/\">pandas</a> and <a href=\"/twig/\">Twig</a>"
], [
"2016-09-05",
"New documentations: <a href=\"/fish/\">Fish</a>, <a href=\"/bottle/\">Bottle</a> and <a href=\"/scikit_image/\">scikit-image</a>"
], [
"2016-08-07",
"New documentation: <a href=\"/docker/\">Docker</a>"
], [
"2016-07-31",
"New documentations: <a href=\"/bootstrap~3/\">Bootstrap 3</a> and <a href=\"/bootstrap~4/\">Bootstrap 4</a>"
], [
"2016-07-24",
"New documentations: <a href=\"/julia/\">Julia</a>, <a href=\"/crystal/\">Crystal</a> and <a href=\"/redux/\">Redux</a>"
], [
"2016-07-03",
"New documentations: <a href=\"/cmake/\">CMake</a> and <a href=\"/matplotlib/\">Matplotlib</a>"
], [
"2016-06-19",
"New documentation: <a href=\"/love/\">L&Ouml;VE</a>"
], [
"2016-06-12",
"New documentation: <a href=\"/angular/\">Angular 2</a>"
], [
"2016-06-05",
"New documentations: <a href=\"/kotlin/\">Kotlin</a> and <a href=\"/padrino/\">Padrino</a>"
], [
"2016-04-24", "2016-04-24",
"New documentations: <a href=\"/numpy/\">NumPy</a> and <a href=\"/apache_pig/\">Apache Pig</a>" "New documentations: <a href=\"/numpy/\">NumPy</a> and <a href=\"/apache_pig/\">Apache Pig</a>"
], [ ], [
@ -25,7 +80,7 @@
"New documentations: <a href=\"/erlang/\">Erlang</a> and <a href=\"/tcl_tk/\">Tcl/Tk</a>" "New documentations: <a href=\"/erlang/\">Erlang</a> and <a href=\"/tcl_tk/\">Tcl/Tk</a>"
], [ ], [
"2016-01-24", "2016-01-24",
"&ldquo;Multi-version support&rdquo; has landed!\nClick <a href=\"#\" data-pick-docs>Select documentation</a> to pick which versions to use. More versions will be added in the coming weeks.\nIf you notice any bugs, please report them on <a href=\"https://github.com/Thibaut/devdocs/issues\" target=\"_blank\">GitHub</a>." "&ldquo;Multi-version support&rdquo; has landed!"
], [ ], [
"2015-11-22", "2015-11-22",
"New documentations: <a href=\"/phoenix/\">Phoenix</a>, <a href=\"/dojo/\">Dojo</a>, <a href=\"/relay/\">Relay</a> and <a href=\"/flow/\">Flow</a>" "New documentations: <a href=\"/phoenix/\">Phoenix</a>, <a href=\"/dojo/\">Dojo</a>, <a href=\"/relay/\">Relay</a> and <a href=\"/flow/\">Flow</a>"
@ -49,11 +104,8 @@
"New documentations: <a href=\"/q/\">Q</a> and <a href=\"/opentsdb/\">OpenTSDB</a>" "New documentations: <a href=\"/q/\">Q</a> and <a href=\"/opentsdb/\">OpenTSDB</a>"
], [ ], [
"2015-07-26", "2015-07-26",
"Added search abbreviations (e.g. <code class=\"_label\">$</code> is an alias for <code class=\"_label\">jQuery</code>).\n<a href=\"/help#abbreviations\">Click here</a> to see the full list. Feel free to suggest more on <a href=\"https://github.com/Thibaut/devdocs/issues/new\" target=\"_blank\">GitHub</a>.", "Added search aliases (e.g. <code class=\"_label\">$</code> is an alias for <code class=\"_label\">jQuery</code>).\n<a href=\"/help#aliases\">Click here</a> to see the full list. Feel free to suggest more on <a href=\"https://github.com/Thibaut/devdocs/issues/new\" target=\"_blank\" rel=\"noopener\">GitHub</a>.",
"Added <code class=\"_label\">shift + &darr;/&uarr;</code> shortcut for scrolling (same as <code class=\"_label\">alt + &darr;/&uarr;</code>)." "Added <code class=\"_label\">shift + &darr;/&uarr;</code> shortcut for scrolling (same as <code class=\"_label\">alt + &darr;/&uarr;</code>)."
], [
"2015-07-12",
"New sponsors: <a href=\"http://out.devdocs.io/s/jetbrains\">JetBrains</a> and <a href=\"http://out.devdocs.io/s/code-school\">Code School</a>\nIf you like DevDocs, please take a moment to check out their products — they're awesome!"
], [ ], [
"2015-07-05", "2015-07-05",
"New documentations: <a href=\"/drupal/\">Drupal</a>, <a href=\"/vue/\">Vue.js</a>, <a href=\"/phaser/\">Phaser</a> and <a href=\"/webpack/\">webpack</a>" "New documentations: <a href=\"/drupal/\">Drupal</a>, <a href=\"/vue/\">Vue.js</a>, <a href=\"/phaser/\">Phaser</a> and <a href=\"/webpack/\">webpack</a>"
@ -78,10 +130,10 @@
"New <a href=\"/iojs/\">io.js</a>, <a href=\"/symfony/\">Symfony</a>, <a href=\"/clojure/\">Clojure</a>, <a href=\"/lua/\">Lua</a> and <a href=\"/yii1/\">Yii 1.1</a> documentations" "New <a href=\"/iojs/\">io.js</a>, <a href=\"/symfony/\">Symfony</a>, <a href=\"/clojure/\">Clojure</a>, <a href=\"/lua/\">Lua</a> and <a href=\"/yii1/\">Yii 1.1</a> documentations"
], [ ], [
"2015-02-08", "2015-02-08",
"New dark theme\nClick the icon in the bottom left corner to activate.\n<a href=\"https://github.com/Thibaut/devdocs/issues\" target=\"_blank\">Feedback</a> welcome :)" "New dark theme"
], [ ], [
"2015-01-13", "2015-01-13",
"<a href=\"/offline\">Offline mode</a> has landed!\nIf you notice any bugs, please report them on <a href=\"https://github.com/Thibaut/devdocs/issues\" target=\"_blank\">GitHub</a>." "<a href=\"/offline\">Offline mode</a> has landed!"
], [ ], [
"2014-12-21", "2014-12-21",
"New <a href=\"/react/\">React</a>, <a href=\"/rethinkdb/\">RethinkDB</a>, <a href=\"/socketio/\">Socket.IO</a>, <a href=\"/modernizr/\">Modernizr</a> and <a href=\"/bower/\">Bower</a> documentations" "New <a href=\"/react/\">React</a>, <a href=\"/rethinkdb/\">RethinkDB</a>, <a href=\"/socketio/\">Socket.IO</a>, <a href=\"/modernizr/\">Modernizr</a> and <a href=\"/bower/\">Bower</a> documentations"
@ -93,7 +145,7 @@
"New <a href=\"/python2/\">Python 2</a> documentation" "New <a href=\"/python2/\">Python 2</a> documentation"
], [ ], [
"2014-11-09", "2014-11-09",
"New design\nFeedback welcome on <a href=\"https://twitter.com/DevDocs\" target=\"_blank\">Twitter</a> and <a href=\"https://github.com/Thibaut/devdocs\" target=\"_blank\">GitHub</a>." "New design\nFeedback welcome on <a href=\"https://twitter.com/DevDocs\" target=\"_blank\" rel=\"noopener\">Twitter</a> and <a href=\"https://github.com/Thibaut/devdocs\" target=\"_blank\" rel=\"noopener\">GitHub</a>."
], [ ], [
"2014-10-19", "2014-10-19",
"New <a href=\"/svg/\">SVG</a>, <a href=\"/marionette/\">Marionette.js</a>, and <a href=\"/mongoose/\">Mongoose</a> documentations" "New <a href=\"/svg/\">SVG</a>, <a href=\"/marionette/\">Marionette.js</a>, and <a href=\"/mongoose/\">Mongoose</a> documentations"
@ -151,9 +203,6 @@
], [ ], [
"2014-02-12", "2014-02-12",
"The root/category pages are now included in the search index (e.g. <a href=\"/#q=CSS\">CSS</a>)" "The root/category pages are now included in the search index (e.g. <a href=\"/#q=CSS\">CSS</a>)"
], [
"2014-01-26",
"Updated <a href=\"/angular/\">Angular.js</a> documentation"
], [ ], [
"2014-01-19", "2014-01-19",
"New <a href=\"/d3/\">D3.js</a> and <a href=\"/knockout/\">Knockout.js</a> documentations" "New <a href=\"/d3/\">D3.js</a> and <a href=\"/knockout/\">Knockout.js</a> documentations"
@ -207,7 +256,7 @@
"URL search now automatically opens the first result." "URL search now automatically opens the first result."
], [ ], [
"2013-08-13", "2013-08-13",
"New <a href=\"/angular/\">Angular.js</a> documentation" "New <a href=\"/angularjs/\">Angular.js</a> documentation"
], [ ], [
"2013-08-11", "2013-08-11",
"New <a href=\"/sass/\">Sass</a> and <a href=\"/less/\">Less</a> documentations" "New <a href=\"/sass/\">Sass</a> and <a href=\"/less/\">Less</a> documentations"

@ -3,7 +3,7 @@ error = (title, text = '', links = '') ->
links = """<p class="_error-links">#{links}</p>""" if links links = """<p class="_error-links">#{links}</p>""" if links
"""<div class="_error"><h1 class="_error-title">#{title}</h1>#{text}#{links}</div>""" """<div class="_error"><h1 class="_error-title">#{title}</h1>#{text}#{links}</div>"""
back = '<a href="javascript:history.back()" class="_error-link">Go back</a>' back = '<a href="#" data-behavior="back" class="_error-link">Go back</a>'
app.templates.notFoundPage = -> app.templates.notFoundPage = ->
error """ Page not found. """, error """ Page not found. """,
@ -19,22 +19,37 @@ app.templates.pageLoadError = ->
app.templates.bootError = -> app.templates.bootError = ->
error """ The app failed to load. """, error """ The app failed to load. """,
""" Check your Internet connection and try <a href="javascript:location.reload()">reloading</a>.<br> """ Check your Internet connection and try <a href="#" data-behavior="reload">reloading</a>.<br>
If you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. """ If you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. """
app.templates.offlineError = (reason) -> app.templates.offlineError = (reason, exception) ->
if reason is 'cookie_blocked'
return error """ Cookies must be enabled to use offline mode. """
reason = switch reason reason = switch reason
when 'not_supported' when 'not_supported'
""" Unfortunately your browser either doesn't support it or does not make it available. """ """ DevDocs requires IndexedDB to cache documentations for offline access.<br>
Unfortunately your browser either doesn't support IndexedDB or doesn't make it available. """
when 'buggy'
""" DevDocs requires IndexedDB to cache documentations for offline access.<br>
Unfortunately your browser's implementation of IndexedDB contains bugs that prevent DevDocs from using it. """
when 'private_mode'
""" Your browser appears to be running in private mode.<br>
This prevents DevDocs from caching documentations for offline access."""
when 'exception'
""" An error occured when trying to open the IndexedDB database:<br>
<code class="_label">#{exception.name}: #{exception.message}</code> """
when 'cant_open' when 'cant_open'
""" Although your browser appears to support it, DevDocs couldn't open the database.<br> """ An error occured when trying to open the IndexedDB database:<br>
This could be because you're browsing in private mode and have disallowed offline storage on the domain. """ <code class="_label">#{exception.name}: #{exception.message}</code><br>
when 'apple' This could be because you're browsing in private mode or have disallowed offline storage on the domain. """
""" Unfortunately Safari's implementation of IndexedDB is <a href="https://bugs.webkit.org/show_bug.cgi?id=136937">badly broken</a>.<br> when 'version'
This message will automatically go away when Apple fix their code. """ """ The IndexedDB database was modified with a newer version of the app.<br>
<a href="#" data-behavior="reload">Reload the page</a> to use offline mode. """
error """ Offline mode is unavailable. """, when 'empty'
""" DevDocs requires IndexedDB to cache documentations for offline access.<br>#{reason} """ """ The IndexedDB database appears to be corrupted. Try <a href="#" data-behavior="reset">resetting the app</a>. """
error 'Offline mode is unavailable.', reason
app.templates.unsupportedBrowser = """ app.templates.unsupportedBrowser = """
<div class="_fail"> <div class="_fail">

@ -1,9 +1,9 @@
notice = (text) -> """<p class="_notice-text">#{text}</p>""" notice = (text) -> """<p class="_notice-text">#{text}</p>"""
app.templates.singleDocNotice = (doc) -> app.templates.singleDocNotice = (doc) ->
notice """ You're currently browsing the #{doc.fullName} documentation. To browse all docs, go to notice """ You're browsing the #{doc.fullName} documentation. To browse all docs, go to
<a href="http://#{app.config.production_host}" target="_top">#{app.config.production_host}</a> (or press <code>esc</code>). """ <a href="http://#{app.config.production_host}" target="_top">#{app.config.production_host}</a> (or press <code>esc</code>). """
app.templates.disabledDocNotice = -> app.templates.disabledDocNotice = ->
notice """ <strong>This documentation is currently disabled.</strong> notice """ <strong>This documentation is disabled.</strong>
To enable it, click <a class="_notice-link" data-pick-docs>Select documentation</a>. """ To enable it, go to <a href="/settings" class="_notice-link">Preferences</a>. """

@ -1,24 +1,28 @@
notif = (title, html) -> notif = (title, html) ->
html = html.replace /<a /g, '<a class="_notif-link" ' html = html.replace /<a /g, '<a class="_notif-link" '
"""<h5 class="_notif-title">#{title}</h5>#{html}<a href="#" class="_notif-close"></a>""" """<h5 class="_notif-title">#{title}</h5>#{html}<button type="button" class="_notif-close" title="Close">Close</a>"""
textNotif = (title, message) -> textNotif = (title, message) ->
notif title, """<p class="_notif-text">#{message}""" notif title, """<p class="_notif-text">#{message}"""
app.templates.notifUpdateReady = -> app.templates.notifUpdateReady = ->
textNotif """ DevDocs has been updated. """, textNotif """<span data-behavior="reboot">DevDocs has been updated.</span>""",
""" <a href="javascript:location='/'">Reload the page</a> to use the new version. """ """<span data-behavior="reboot"><a href="#" data-behavior="reboot">Reload the page</a> to use the new version.</span>"""
app.templates.notifError = -> app.templates.notifError = ->
textNotif """ Oops, an error occured. """, textNotif """ Oops, an error occured. """,
""" Try <a href="javascript:app.reload()">reloading</a>, and if the problem persists, """ Try <a href="#" data-behavior="hard-reload">reloading</a>, and if the problem persists,
<a href="javascript:if(confirm('Are you sure you want to reset DevDocs?'))app.reset()">resetting the app</a>.<br> <a href="#" data-behavior="reset">resetting the app</a>.<br>
You can also report this issue on <a href="https://github.com/Thibaut/devdocs/issues/new" target="_blank">GitHub</a>. """ You can also report this issue on <a href="https://github.com/Thibaut/devdocs/issues/new" target="_blank" rel="noopener">GitHub</a>. """
app.templates.notifQuotaExceeded = -> app.templates.notifQuotaExceeded = ->
textNotif """ The offline database has exceeded its size limitation. """, textNotif """ The offline database has exceeded its size limitation. """,
""" Unfortunately this quota can't be detected programmatically, and the database can't be opened while over the quota, so it had to be reset. """ """ Unfortunately this quota can't be detected programmatically, and the database can't be opened while over the quota, so it had to be reset. """
app.templates.notifCookieBlocked = ->
textNotif """ Please enable cookies. """,
""" DevDocs will not work properly if cookies are disabled. """
app.templates.notifInvalidLocation = -> app.templates.notifInvalidLocation = ->
textNotif """ DevDocs must be loaded from #{app.config.production_host} """, textNotif """ DevDocs must be loaded from #{app.config.production_host} """,
""" Otherwise things are likely to break. """ """ Otherwise things are likely to break. """
@ -43,16 +47,16 @@ app.templates.notifUpdates = (docs, disabledDocs) ->
for doc in disabledDocs for doc in disabledDocs
html += "<li>#{doc.name}" html += "<li>#{doc.name}"
html += " <code>&rarr;</code> #{doc.release}" if doc.release html += " <code>&rarr;</code> #{doc.release}" if doc.release
html += """<span class="_notif-info"><a data-pick-docs>Enable</a></span>""" html += """<span class="_notif-info"><a href="/settings">Enable</a></span>"""
html += '</ul></div>' html += '</ul></div>'
notif 'Updates', "#{html}</div>" notif 'Updates', "#{html}</div>"
app.templates.notifShare = -> app.templates.notifShare = ->
textNotif """ Hi there! """, textNotif """ Hi there! """,
""" Like DevDocs? Help us reach more developers by sharing the link with your friends, on """ Like DevDocs? Help us reach more developers by sharing the link with your friends on
<a href="http://out.devdocs.io/s/tw" target="_blank">Twitter</a>, <a href="http://out.devdocs.io/s/fb" target="_blank">Facebook</a>, <a href="http://out.devdocs.io/s/tw" target="_blank" rel="noopener">Twitter</a>, <a href="http://out.devdocs.io/s/fb" target="_blank" rel="noopener">Facebook</a>,
<a href="http://out.devdocs.io/s/re" target="_blank">Reddit</a>, etc.<br>Thanks :) """ <a href="http://out.devdocs.io/s/re" target="_blank" rel="noopener">Reddit</a>, etc.<br>Thanks :) """
app.templates.notifUpdateDocs = -> app.templates.notifUpdateDocs = ->
textNotif """ Documentation updates available. """, textNotif """ Documentation updates available. """,

@ -1,21 +1,21 @@
app.templates.aboutPage = -> """ app.templates.aboutPage = -> """
<div class="_toc"> <nav class="_toc" role="directory">
<h3 class="_toc-title">Table of Contents</h3> <h3 class="_toc-title">Table of Contents</h3>
<ul class="_toc-list"> <ul class="_toc-list">
<li><a href="#credits">Credits</a>
<li><a href="#faq">FAQ</a>
<li><a href="#copyright">Copyright</a> <li><a href="#copyright">Copyright</a>
<li><a href="#plugins">Plugins</a> <li><a href="#plugins">Plugins</a>
<li><a href="#faq">FAQ</a>
<li><a href="#credits">Credits</a>
<li><a href="#privacy">Privacy Policy</a> <li><a href="#privacy">Privacy Policy</a>
</ul> </ul>
</div> </nav>
<h1 class="_lined-heading">API Documentation Browser</h1> <h1 class="_lined-heading">DevDocs: API Documentation Browser</h1>
<p>DevDocs combines multiple API documentations in a fast, organized, and searchable interface. <p>DevDocs combines multiple API documentations in a fast, organized, and searchable interface.
<ul> <ul>
<li>Created and maintained by <a href="http://thibaut.me">Thibaut Courouble</a> <li>Created and maintained by <a href="http://thibaut.me">Thibaut Courouble</a>
<li>Free and <a href="https://github.com/Thibaut/devdocs">open source</a> <li>Free and <a href="https://github.com/Thibaut/devdocs">open source</a>
<iframe class="_github-btn" src="https://ghbtns.com/github-btn.html?user=Thibaut&repo=devdocs&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20"></iframe> <iframe class="_github-btn" src="https://ghbtns.com/github-btn.html?user=Thibaut&repo=devdocs&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20" tabindex="-1"></iframe>
</ul> </ul>
<p>To keep up-to-date with the latest news: <p>To keep up-to-date with the latest news:
<ul> <ul>
@ -26,26 +26,26 @@ app.templates.aboutPage = -> """
<p class="_note _note-green">If you like DevDocs, please consider supporting my work on <p class="_note _note-green">If you like DevDocs, please consider supporting my work on
<a href="https://gratipay.com/devdocs/">Gratipay</a>. Thanks!<br> <a href="https://gratipay.com/devdocs/">Gratipay</a>. Thanks!<br>
<h2 class="_lined-heading" id="credits">Credits</h2> <h2 class="_block-heading" id="copyright">Copyright and License</h2>
<p class="_note">
<strong>Copyright 2013&ndash;2017 Thibaut Courouble and <a href="https://github.com/Thibaut/devdocs/graphs/contributors">other contributors</a></strong><br>
This software is licensed under the terms of the Mozilla Public License v2.0.<br>
You may obtain a copy of the source code at <a href="https://github.com/Thibaut/devdocs">github.com/Thibaut/devdocs</a>.<br>
For more information, see the <a href="https://github.com/Thibaut/devdocs/blob/master/COPYRIGHT">COPYRIGHT</a>
and <a href="https://github.com/Thibaut/devdocs/blob/master/LICENSE">LICENSE</a> files.
<p><strong>Special thanks to:</strong> <h2 class="_block-heading" id="plugins">Plugins and Extensions</h2>
<ul> <ul>
<li><a href="http://out.devdocs.io/s/maxcdn">MaxCDN</a> and <a href="http://get.gaug.es/?utm_source=devdocs&utm_medium=referral&utm_campaign=sponsorships" title="Real Time Web Analytics">Gauges</a> for offering a free account to DevDocs <li><a href="https://chrome.google.com/webstore/detail/devdocs/mnfehgbmkapmjnhcnbodoamcioleeooe">Chrome web app</a>
<li><a href="http://out.devdocs.io/s/maxcdn">MaxCDN</a>, <a href="http://out.devdocs.io/s/shopify">Shopify</a>, <a href="http://out.devdocs.io/s/jetbrains">JetBrains</a> and <a href="http://out.devdocs.io/s/code-school">Code School</a> for sponsoring DevDocs in the past <li><a href="https://github.com/egoist/devdocs-app">Desktop app</a>
<li><a href="https://www.heroku.com">Heroku</a> and <a href="http://newrelic.com">New Relic</a> for providing awesome free service <li><a href="https://sublime.wbond.net/packages/DevDocs">Sublime Text package</a>
<li>Daniel Bruce for the <a href="http://www.entypo.com">Entypo</a> pictograms <li><a href="https://atom.io/packages/devdocs">Atom package</a>
<li><a href="http://www.jeremykratz.com/">Jeremy Kratz</a> for the C/C++ logo <li><a href="https://marketplace.visualstudio.com/items?itemName=deibit.devdocs">Visual Studio Code extension</a>
<li><a href="https://github.com/yannickglt/alfred-devdocs">Alfred workflow</a>
<li><a href="https://github.com/search?q=topic%3Adevdocs&type=Repositories">More</a>
</ul> </ul>
<table class="_credits"> <h2 class="_block-heading" id="faq">Questions & Answers</h2>
<tr>
<th>Documentation
<th>Copyright
<th>License
#{("<tr><td>#{c[0]}<td>&copy; #{c[1]}<td><a href=\"#{c[3]}\">#{c[2]}</a>" for c in credits).join('')}
</table>
<h2 class="_lined-heading" id="faq">Questions & Answers</h2>
<dl> <dl>
<dt>Where can I suggest new docs and features? <dt>Where can I suggest new docs and features?
<dd>You can suggest and vote for new docs on the <a href="https://trello.com/b/6BmTulfx/devdocs-documentation">Trello board</a>.<br> <dd>You can suggest and vote for new docs on the <a href="https://trello.com/b/6BmTulfx/devdocs-documentation">Trello board</a>.<br>
@ -56,25 +56,28 @@ app.templates.aboutPage = -> """
</dl> </dl>
<p>For anything else, feel free to email me at <a href="mailto:thibaut@devdocs.io">thibaut@devdocs.io</a>. <p>For anything else, feel free to email me at <a href="mailto:thibaut@devdocs.io">thibaut@devdocs.io</a>.
<h2 class="_lined-heading" id="copyright">Copyright and License</h2> <h2 class="_block-heading" id="credits">Credits</h2>
<p class="_note">
<strong>Copyright 2013&ndash;2016 Thibaut Courouble and <a href="https://github.com/Thibaut/devdocs/graphs/contributors">other contributors</a></strong><br>
This software is licensed under the terms of the Mozilla Public License v2.0.<br>
You may obtain a copy of the source code at <a href="https://github.com/Thibaut/devdocs">github.com/Thibaut/devdocs</a>.<br>
For more information, see the <a href="https://github.com/Thibaut/devdocs/blob/master/COPYRIGHT">COPYRIGHT</a>
and <a href="https://github.com/Thibaut/devdocs/blob/master/LICENSE">LICENSE</a> files.
<h2 class="_lined-heading" id="plugins">Plugins and Extensions</h2> <p><strong>Special thanks to:</strong>
<ul> <ul>
<li><a href="https://chrome.google.com/webstore/detail/devdocs/mnfehgbmkapmjnhcnbodoamcioleeooe">Chrome web app</a> <li><a href="http://out.devdocs.io/s/maxcdn">MaxCDN</a> and <a href="http://get.gaug.es/?utm_source=devdocs&utm_medium=referral&utm_campaign=sponsorships" title="Real Time Web Analytics">Gauges</a> for offering a free account to DevDocs
<li><a href="https://sublime.wbond.net/packages/DevDocs">Sublime Text plugin</a> <li><a href="http://out.devdocs.io/s/maxcdn">MaxCDN</a>, <a href="http://out.devdocs.io/s/shopify">Shopify</a>, <a href="http://out.devdocs.io/s/jetbrains">JetBrains</a> and <a href="http://out.devdocs.io/s/code-school">Code School</a> for sponsoring DevDocs in the past
<li><a href="https://atom.io/packages/devdocs">Atom plugin</a> <li><a href="https://www.heroku.com">Heroku</a> and <a href="http://newrelic.com">New Relic</a> for providing awesome free service
<li><a href="https://github.com/gruehle/dev-docs-viewer">Brackets extension</a> <li>Daniel Bruce for the <a href="http://www.entypo.com">Entypo</a> pictograms
<li><a href="https://github.com/xuchunyang/DevDocs.el">Emacs Package</a> <li><a href="http://www.jeremykratz.com/">Jeremy Kratz</a> for the C/C++ logo
</ul> </ul>
<p>You can also use <a href="http://fluidapp.com">Fluid</a> to turn DevDocs into a real OS X app, or <a href="https://apps.ubuntu.com/cat/applications/fogger/">Fogger</a> on Ubuntu.
<h2 class="_lined-heading" id="privacy">Privacy Policy</h2> <div class="_table">
<table class="_credits">
<tr>
<th>Documentation
<th>Copyright
<th>License
#{("<tr><td>#{c[0]}<td>&copy; #{c[1]}<td><a href=\"#{c[3]}\">#{c[2]}</a>" for c in credits).join('')}
</table>
</div>
<h2 class="_block-heading" id="privacy">Privacy Policy</h2>
<ul> <ul>
<li><a href="http://devdocs.io">devdocs.io</a> ("App") is operated by Thibaut Courouble ("We"). <li><a href="http://devdocs.io">devdocs.io</a> ("App") is operated by Thibaut Courouble ("We").
<li>We do not collect personal information. <li>We do not collect personal information.
@ -86,25 +89,40 @@ app.templates.aboutPage = -> """
""" """
credits = [ credits = [
[ 'Angular.js', [ 'Angular<br>Angular.js',
'2010-2016 Google, Inc.', '2010-2017 Google, Inc.',
'CC BY', 'CC BY',
'https://creativecommons.org/licenses/by/4.0/' 'https://creativecommons.org/licenses/by/4.0/'
], [ ], [
'Ansible', 'Ansible',
'2012-2016 Michael DeHaan<br>&copy; 2016 Red Hat, Inc.', '2012-2017 Michael DeHaan<br>&copy; 2017 Red Hat, Inc.',
'GPLv3', 'GPLv3',
'https://raw.githubusercontent.com/ansible/ansible/devel/COPYING' 'https://raw.githubusercontent.com/ansible/ansible/devel/COPYING'
], [ ], [
'Apache HTTP Server<br>Apache Pig', 'Apache HTTP Server<br>Apache Pig',
'2016 The Apache Software Foundation<br>Apache and the Apache feather logo are trademarks of The Apache Software Foundation.', '2017 The Apache Software Foundation<br>Apache and the Apache feather logo are trademarks of The Apache Software Foundation.',
'Apache', 'Apache',
'https://www.apache.org/licenses/LICENSE-2.0' 'https://www.apache.org/licenses/LICENSE-2.0'
], [
'Async',
'2010-2017 Caolan McMahon',
'MIT',
'https://raw.githubusercontent.com/caolan/async/master/LICENSE'
], [ ], [
'Backbone.js', 'Backbone.js',
'2010-2016 Jeremy Ashkenas, DocumentCloud', '2010-2016 Jeremy Ashkenas, DocumentCloud',
'MIT', 'MIT',
'https://raw.githubusercontent.com/jashkenas/backbone/master/LICENSE' 'https://raw.githubusercontent.com/jashkenas/backbone/master/LICENSE'
], [
'Bootstrap',
'2011-2017 Twitter, Inc.<br>2011-2017 The Bootstrap Authors',
'CC BY',
'https://creativecommons.org/licenses/by/3.0/'
], [
'Bottle',
'2009-2017 Marcel Hellkamp',
'MIT',
'https://raw.githubusercontent.com/bottlepy/bottle/master/LICENSE'
], [ ], [
'Bower', 'Bower',
'2016 Bower contributors', '2016 Bower contributors',
@ -117,14 +135,14 @@ credits = [
'http://en.cppreference.com/w/Cppreference:Copyright/CC-BY-SA' 'http://en.cppreference.com/w/Cppreference:Copyright/CC-BY-SA'
], [ ], [
'CakePHP', 'CakePHP',
'2005-2016 The Cake Software Foundation, Inc.', '2005-2017 The Cake Software Foundation, Inc.',
'MIT', 'MIT',
'https://raw.githubusercontent.com/cakephp/cakephp/master/LICENSE.txt' 'https://raw.githubusercontent.com/cakephp/cakephp/master/LICENSE.txt'
], [ ], [
'Chai', 'Chai',
'2011-2015 Jake Luer', '2016 Chai.js Assertion Library',
'MIT', 'MIT',
'https://github.com/chaijs/chai/blob/master/README.md#license' 'https://raw.githubusercontent.com/chaijs/chai/master/LICENSE'
], [ ], [
'Chef&trade;', 'Chef&trade;',
'Chef Software, Inc.', 'Chef Software, Inc.',
@ -135,39 +153,64 @@ credits = [
'Rich Hickey', 'Rich Hickey',
'EPL', 'EPL',
'https://github.com/clojure/clojure/blob/master/epl-v10.html' 'https://github.com/clojure/clojure/blob/master/epl-v10.html'
], [
'CMake',
'2000-2017 Kitware, Inc. and Contributors',
'BSD',
'https://cmake.org/licensing/'
], [
'Codeception',
'2011-2017 Michael Bodnarchuk and contributors',
'MIT',
'https://raw.githubusercontent.com/Codeception/Codeception/master/LICENSE'
], [
'CodeceptJS',
'2015 DavertMik',
'MIT',
'https://raw.githubusercontent.com/Codeception/CodeceptJS/master/LICENSE'
], [ ], [
'CodeIgniter', 'CodeIgniter',
'2014-2016 British Columbia Institute of Technology', '2014-2017 British Columbia Institute of Technology',
'MIT', 'MIT',
'https://raw.githubusercontent.com/bcit-ci/CodeIgniter/develop/license.txt' 'https://raw.githubusercontent.com/bcit-ci/CodeIgniter/develop/license.txt'
], [ ], [
'CoffeeScript', 'CoffeeScript',
'2009-2015 Jeremy Ashkenas', '2009-2017 Jeremy Ashkenas',
'MIT', 'MIT',
'https://raw.githubusercontent.com/jashkenas/coffee-script/master/LICENSE' 'https://raw.githubusercontent.com/jashkenas/coffee-script/master/LICENSE'
], [ ], [
'Cordova', 'Cordova',
'2012-2016 The Apache Software Foundation', '2012-2017 The Apache Software Foundation',
'Apache', 'Apache',
'https://raw.githubusercontent.com/apache/cordova-docs/master/LICENSE' 'https://raw.githubusercontent.com/apache/cordova-docs/master/LICENSE'
], [ ], [
'CSS<br>DOM<br>HTML<br>JavaScript<br>SVG<br>XPath', 'CSS<br>DOM<br>HTTP<br>HTML<br>JavaScript<br>SVG<br>XPath',
'2005-2016 Mozilla Developer Network and individual contributors', '2005-2017 Mozilla Developer Network and individual contributors',
'CC BY-SA', 'CC BY-SA',
'https://creativecommons.org/licenses/by-sa/2.5/' 'https://creativecommons.org/licenses/by-sa/2.5/'
], [
'Crystal',
'2012-2017 Manas Technology Solutions',
'Apache',
'https://raw.githubusercontent.com/crystal-lang/crystal/master/LICENSE'
], [ ], [
'D3.js', 'D3.js',
'2010-2016 Michael Bostock', '2010-2017 Michael Bostock',
'BSD', 'BSD',
'https://raw.githubusercontent.com/mbostock/d3/master/LICENSE' 'https://raw.githubusercontent.com/d3/d3/master/LICENSE'
], [ ], [
'Django', 'Django',
'Django Software Foundation and individual contributors', 'Django Software Foundation and individual contributors',
'BSD', 'BSD',
'https://raw.githubusercontent.com/django/django/master/LICENSE' 'https://raw.githubusercontent.com/django/django/master/LICENSE'
], [
'Docker',
'2013-2016 Docker, Inc.<br>Docker and the Docker logo are trademarks of Docker, Inc.',
'Apache',
'https://raw.githubusercontent.com/docker/docker/master/LICENSE'
], [ ], [
'Dojo', 'Dojo',
'2005-2015 The Dojo Foundation', '2005-2017 JS Foundation',
'BSD + AFL', 'BSD + AFL',
'http://dojotoolkit.org/license.html' 'http://dojotoolkit.org/license.html'
], [ ], [
@ -176,25 +219,40 @@ credits = [
'GPLv2', 'GPLv2',
'https://api.drupal.org/api/drupal/LICENSE.txt' 'https://api.drupal.org/api/drupal/LICENSE.txt'
], [ ], [
'Ember.js', 'Electron',
'2016 Yehuda Katz, Tom Dale and Ember.js contributors', '2013-2017 GitHub Inc.',
'MIT', 'MIT',
'https://raw.githubusercontent.com/emberjs/ember.js/master/LICENSE' 'https://raw.githubusercontent.com/electron/electron/master/LICENSE'
], [ ], [
'Elixir', 'Elixir',
'2012-2016 Plataformatec', '2012-2017 Plataformatec',
'Apache', 'Apache',
'https://raw.githubusercontent.com/elixir-lang/elixir/master/LICENSE' 'https://raw.githubusercontent.com/elixir-lang/elixir/master/LICENSE'
], [
'Ember.js',
'2017 Yehuda Katz, Tom Dale and Ember.js contributors',
'MIT',
'https://raw.githubusercontent.com/emberjs/ember.js/master/LICENSE'
], [ ], [
'Erlang', 'Erlang',
'1999-2016 Ericsson AB', '2010-2017 Ericsson AB',
'Apache', 'Apache',
'https://raw.githubusercontent.com/erlang/otp/maint/LICENSE.txt' 'https://raw.githubusercontent.com/erlang/otp/maint/LICENSE.txt'
], [ ], [
'Express', 'Express',
'2016 StrongLoop, IBM, and other expressjs.com contributors.', '2016 StrongLoop, IBM, and other expressjs.com contributors.',
'Unknown', 'CC BY-SA',
'https://github.com/expressjs/expressjs.com/issues/413' 'https://raw.githubusercontent.com/expressjs/expressjs.com/gh-pages/LICENSE.md'
], [
'Falcon',
'2012-2016 by Rackspace Hosting, Inc. and other contributors',
'Apache',
'https://raw.githubusercontent.com/falconry/falcon/master/LICENSE'
], [
'Fish',
'2005-2009 Axel Liljencrantz',
'GPLv2',
'https://fishshell.com/docs/current/license.html'
], [ ], [
'GCC<br>GNU Fortran', 'GCC<br>GNU Fortran',
'Free Software Foundation', 'Free Software Foundation',
@ -202,7 +260,7 @@ credits = [
'https://www.gnu.org/licenses/fdl-1.3.en.html' 'https://www.gnu.org/licenses/fdl-1.3.en.html'
], [ ], [
'Git', 'Git',
'2005-2016 Linus Torvalds and others', '2005-2017 Linus Torvalds and others',
'GPLv2', 'GPLv2',
'https://raw.githubusercontent.com/git/git/master/COPYING' 'https://raw.githubusercontent.com/git/git/master/COPYING'
], [ ], [
@ -210,6 +268,11 @@ credits = [
'Google, Inc.', 'Google, Inc.',
'CC BY', 'CC BY',
'https://creativecommons.org/licenses/by/3.0/' 'https://creativecommons.org/licenses/by/3.0/'
], [
'Godot',
'2014-2017 Juan Linietsky, Ariel Manzur, Godot Engine contributors',
'MIT',
'https://raw.githubusercontent.com/godotengine/godot/master/LICENSE.txt'
], [ ], [
'Grunt', 'Grunt',
'GruntJS Team', 'GruntJS Team',
@ -225,11 +288,26 @@ credits = [
'2005-2016 Haxe Foundation', '2005-2016 Haxe Foundation',
'MIT', 'MIT',
'http://haxe.org/foundation/open-source.html' 'http://haxe.org/foundation/open-source.html'
], [
'Immutable.js',
'2014-2016 Facebook, Inc.',
'BSD',
'https://raw.githubusercontent.com/facebook/immutable-js/master/LICENSE'
], [ ], [
'InfluxData', 'InfluxData',
'2015 InfluxData, Inc.', '2015 InfluxData, Inc.',
'MIT', 'MIT',
'https://github.com/influxdata/docs.influxdata.com/blob/master/LICENSE' 'https://github.com/influxdata/docs.influxdata.com/blob/master/LICENSE'
], [
'Jasmine',
'2008-2017 Pivotal Labs',
'MIT',
'https://raw.githubusercontent.com/jasmine/jasmine/master/MIT.LICENSE'
], [
'Jest',
'2014-present Facebook Inc.',
'BSD',
'https://raw.githubusercontent.com/facebook/jest/master/LICENSE'
], [ ], [
'jQuery', 'jQuery',
'Packt Publishing<br>&copy; jQuery Foundation and other contributors', 'Packt Publishing<br>&copy; jQuery Foundation and other contributors',
@ -245,11 +323,21 @@ credits = [
'jQuery Foundation and other contributors', 'jQuery Foundation and other contributors',
'MIT', 'MIT',
'https://raw.githubusercontent.com/jquery/api.jqueryui.com/master/LICENSE.txt' 'https://raw.githubusercontent.com/jquery/api.jqueryui.com/master/LICENSE.txt'
], [
'Julia',
'2009-2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors',
'MIT',
'https://raw.githubusercontent.com/JuliaLang/julia/master/LICENSE.md'
], [ ], [
'Knockout.js', 'Knockout.js',
'Steven Sanderson, the Knockout.js team, and other contributors', 'Steven Sanderson, the Knockout.js team, and other contributors',
'MIT', 'MIT',
'https://raw.githubusercontent.com/knockout/knockout/master/LICENSE' 'https://raw.githubusercontent.com/knockout/knockout/master/LICENSE'
], [
'Kotlin',
'2010-2017 JetBrains s.r.o.',
'Apache',
'https://raw.githubusercontent.com/JetBrains/kotlin/master/license/LICENSE.txt'
], [ ], [
'Laravel', 'Laravel',
'Taylor Otwell', 'Taylor Otwell',
@ -260,6 +348,11 @@ credits = [
'2009-2016 The Core Less Team', '2009-2016 The Core Less Team',
'CC BY', 'CC BY',
'https://creativecommons.org/licenses/by/3.0/' 'https://creativecommons.org/licenses/by/3.0/'
], [
'Liquid',
'2005, 2006 Tobias Luetke',
'MIT',
'https://raw.githubusercontent.com/Shopify/liquid/master/LICENSE'
], [ ], [
'Lo-Dash', 'Lo-Dash',
'2012-2016 The Dojo Foundation', '2012-2016 The Dojo Foundation',
@ -267,12 +360,17 @@ credits = [
'https://raw.githubusercontent.com/lodash/lodash/master/LICENSE.txt' 'https://raw.githubusercontent.com/lodash/lodash/master/LICENSE.txt'
], [ ], [
'Lua', 'Lua',
'19942015 Lua.org, PUC-Rio', '19942017 Lua.org, PUC-Rio',
'MIT', 'MIT',
'http://www.lua.org/license.html' 'http://www.lua.org/license.html'
], [
'L&Ouml;VE',
'2006-2016 L&Ouml;VE Development Team',
'GFDL',
'http://www.gnu.org/copyleft/fdl.html'
], [ ], [
'Marionette.js', 'Marionette.js',
'2016 Muted Solutions, LLC', '2017 Muted Solutions, LLC',
'MIT', 'MIT',
'https://mutedsolutions.mit-license.org/' 'https://mutedsolutions.mit-license.org/'
], [ ], [
@ -280,11 +378,16 @@ credits = [
'2004 John Gruber', '2004 John Gruber',
'BSD', 'BSD',
'https://daringfireball.net/projects/markdown/license' 'https://daringfireball.net/projects/markdown/license'
], [
'Matplotlib',
'2012-2017 Matplotlib Development Team. All rights reserved.',
'Custom',
'https://raw.githubusercontent.com/matplotlib/matplotlib/master/LICENSE/LICENSE'
], [ ], [
'Meteor', 'Meteor',
'2011-2016 Meteor Development Group', '2011-2017 Meteor Development Group, Inc.',
'MIT', 'MIT',
'https://raw.githubusercontent.com/meteor/meteor/master/LICENSE.txt' 'https://raw.githubusercontent.com/meteor/meteor/master/LICENSE'
], [ ], [
'Minitest', 'Minitest',
'Ryan Davis, seattle.rb', 'Ryan Davis, seattle.rb',
@ -292,17 +395,17 @@ credits = [
'https://github.com/seattlerb/minitest/blob/master/README.rdoc#license' 'https://github.com/seattlerb/minitest/blob/master/README.rdoc#license'
], [ ], [
'Mocha', 'Mocha',
'2011-2016 TJ Holowaychuk', '2016 JS Foundation and contributors',
'MIT', 'CC BY',
'https://raw.githubusercontent.com/mochajs/mocha/master/LICENSE' 'https://creativecommons.org/licenses/by/4.0/'
], [ ], [
'Modernizr', 'Modernizr',
'2009-2016 The Modernizr team', '2009-2017 The Modernizr team',
'MIT', 'MIT',
'https://modernizr.com/license/' 'https://modernizr.com/license/'
], [ ], [
'Moment.js', 'Moment.js',
'2011-2016 Tim Wood, Iskren Chernev, Moment.js contributors', 'JS Foundation and other contributors',
'MIT', 'MIT',
'https://raw.githubusercontent.com/moment/moment/master/LICENSE' 'https://raw.githubusercontent.com/moment/moment/master/LICENSE'
], [ ], [
@ -312,12 +415,12 @@ credits = [
'https://github.com/LearnBoost/mongoose/blob/master/README.md#license' 'https://github.com/LearnBoost/mongoose/blob/master/README.md#license'
], [ ], [
'nginx', 'nginx',
'2002-2016 Igor Sysoev<br>&copy; 2011-2016 Nginx, Inc.', '2002-2017 Igor Sysoev<br>&copy; 2011-2017 Nginx, Inc.',
'BSD', 'BSD',
'http://nginx.org/LICENSE' 'http://nginx.org/LICENSE'
], [ ], [
'nginx / Lua Module', 'nginx / Lua Module',
'2009-2016 Xiaozhe Wang (chaoslawful)<br>&copy; 2009-2016 Yichun "agentzh" Zhang (章亦春), CloudFlare Inc.', '2009-2016 Xiaozhe Wang (chaoslawful)<br>&copy; 2009-2017 Yichun "agentzh" Zhang (章亦春), OpenResty Inc.',
'BSD', 'BSD',
'https://github.com/openresty/lua-nginx-module#copyright-and-license' 'https://github.com/openresty/lua-nginx-module#copyright-and-license'
], [ ], [
@ -327,7 +430,7 @@ credits = [
'https://raw.githubusercontent.com/nodejs/node/master/LICENSE' 'https://raw.githubusercontent.com/nodejs/node/master/LICENSE'
], [ ], [
'Nokogiri', 'Nokogiri',
'2008-2014 2014 Aaron Patterson, Mike Dalessio, Charles Nutter, Sergio Arbeo, Patrick Mahoney, Yoko Harada, Akinori Musha', '2008-2016 Aaron Patterson, Mike Dalessio, Charles Nutter, Sergio Arbeo, Patrick Mahoney, Yoko Harada, Akinori Musha, John Shahid',
'MIT', 'MIT',
'https://raw.githubusercontent.com/sparklemotion/nokogiri/master/LICENSE.txt' 'https://raw.githubusercontent.com/sparklemotion/nokogiri/master/LICENSE.txt'
], [ ], [
@ -337,22 +440,37 @@ credits = [
'https://raw.githubusercontent.com/npm/npm/master/LICENSE' 'https://raw.githubusercontent.com/npm/npm/master/LICENSE'
], [ ], [
'NumPy', 'NumPy',
'2008-2016 NumPy Developers', '2008-2017 NumPy Developers',
'NumPy', 'NumPy',
'https://raw.githubusercontent.com/numpy/numpy/master/LICENSE.txt' 'https://raw.githubusercontent.com/numpy/numpy/master/LICENSE.txt'
], [
'OpenJDK',
'1993-2017, Oracle and/or its affiliates. All rights reserved.<br>Licensed under the GNU General Public License, version 2, with the Classpath Exception.<br>Various third party code in OpenJDK is licensed under different licenses.<br>Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.',
'GPLv2',
'http://openjdk.java.net/legal/gplv2+ce.html'
], [ ], [
'OpenTSDB', 'OpenTSDB',
'2010-2016 The OpenTSDB Authors', '2010-2016 The OpenTSDB Authors',
'LGPLv2.1', 'LGPLv2.1',
'https://raw.githubusercontent.com/OpenTSDB/opentsdb.net/gh-pages/COPYING.LESSER' 'https://raw.githubusercontent.com/OpenTSDB/opentsdb.net/gh-pages/COPYING.LESSER'
], [
'Padrino',
'2010-2016 Padrino',
'MIT',
'https://raw.githubusercontent.com/padrino/padrino-framework/master/padrino/LICENSE.txt'
], [
'pandas',
'2008-2012, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team',
'BSD',
'https://raw.githubusercontent.com/pydata/pandas/master/LICENSE'
], [ ], [
'Perl', 'Perl',
'1993-2016 Larry Wall and others', '1993-2016 Larry Wall and others',
'GPLv1', 'GPLv1',
'http://perldoc.perl.org/index-licence.html' 'https://perldoc.perl.org/index-licence.html'
], [ ], [
'Phalcon', 'Phalcon',
'2011-2015 Phalcon Framework Team', '2011-2017 Phalcon Framework Team',
'CC BY', 'CC BY',
'https://docs.phalconphp.com/en/latest/reference/license.html' 'https://docs.phalconphp.com/en/latest/reference/license.html'
], [ ], [
@ -367,39 +485,49 @@ credits = [
'https://raw.githubusercontent.com/phoenixframework/phoenix/master/LICENSE.md' 'https://raw.githubusercontent.com/phoenixframework/phoenix/master/LICENSE.md'
], [ ], [
'PHP', 'PHP',
'1997-2016 The PHP Documentation Group', '1997-2017 The PHP Documentation Group',
'CC BY', 'CC BY',
'https://creativecommons.org/licenses/by/3.0/' 'https://secure.php.net/manual/en/copyright.php'
], [ ], [
'PHPUnit', 'PHPUnit',
'2005-2016 Sebastian Bergmann', '2005-2017 Sebastian Bergmann',
'CC BY', 'CC BY',
'https://creativecommons.org/licenses/by/3.0/' 'https://creativecommons.org/licenses/by/3.0/'
], [ ], [
'PostgreSQL', 'PostgreSQL',
'1996-2016 The PostgreSQL Global Development Group<br>&copy; 1994 The Regents of the University of California', '1996-2017 The PostgreSQL Global Development Group<br>&copy; 1994 The Regents of the University of California',
'PostgreSQL', 'PostgreSQL',
'http://www.postgresql.org/about/licence/' 'https://www.postgresql.org/about/licence/'
], [ ], [
'Python', 'Python',
'1990-2015 Python Software Foundation<br>Python is a trademark of the Python Software Foundation.', '2001-2017 Python Software Foundation<br>Python is a trademark of the Python Software Foundation.',
'PSFL', 'PSFL',
'https://docs.python.org/3/license.html' 'https://docs.python.org/3/license.html'
], [ ], [
'Q', 'Q',
'2009-2015 Kristopher Michael Kowal and contributors', '2009-2017 Kristopher Michael Kowal',
'MIT',
'https://raw.githubusercontent.com/kriskowal/q/master/LICENSE'
], [
'Ramda',
'2013-2016 Scott Sauyet and Michael Hurley',
'MIT', 'MIT',
'https://raw.githubusercontent.com/kriskowal/q/v1/LICENSE' 'https://raw.githubusercontent.com/ramda/ramda/master/LICENSE.txt'
], [ ], [
'React, React Native, Flow, Relay', 'React, React Native, Flow, Relay',
'2013-2016 Facebook Inc.', '2013-2017 Facebook Inc.',
'CC BY', 'CC BY',
'https://raw.githubusercontent.com/facebook/react/master/LICENSE-docs' 'https://raw.githubusercontent.com/facebook/react/master/LICENSE-docs'
], [ ], [
'Redis', 'Redis',
'2009-2016 Salvatore Sanfilippo', '2009-2017 Salvatore Sanfilippo',
'CC BY-SA', 'CC BY-SA',
'https://creativecommons.org/licenses/by-sa/4.0/' 'https://creativecommons.org/licenses/by-sa/4.0/'
], [
'Redux',
'2015-2017 Dan Abramov',
'MIT',
'https://raw.githubusercontent.com/reactjs/redux/master/LICENSE.md'
], [ ], [
'RequireJS', 'RequireJS',
'jQuery Foundation and other contributors', 'jQuery Foundation and other contributors',
@ -412,17 +540,17 @@ credits = [
'https://raw.githubusercontent.com/rethinkdb/docs/master/LICENSE' 'https://raw.githubusercontent.com/rethinkdb/docs/master/LICENSE'
], [ ], [
'Ruby', 'Ruby',
'1993-2016 Yukihiro Matsumoto', '1993-2017 Yukihiro Matsumoto',
'Ruby', 'Ruby',
'https://www.ruby-lang.org/en/about/license.txt' 'https://www.ruby-lang.org/en/about/license.txt'
], [ ], [
'Ruby on Rails', 'Ruby on Rails',
'2004-2016 David Heinemeier Hansson<br>Rails, Ruby on Rails, and the Rails logo are trademarks of David Heinemeier Hansson.', '2004-2017 David Heinemeier Hansson<br>Rails, Ruby on Rails, and the Rails logo are trademarks of David Heinemeier Hansson.',
'MIT', 'MIT',
'https://raw.githubusercontent.com/rails/rails/master/activerecord/MIT-LICENSE' 'https://raw.githubusercontent.com/rails/rails/master/activerecord/MIT-LICENSE'
], [ ], [
'Rust', 'Rust',
'2016 The Rust Project Developers', '2010 The Rust Project Developers',
'MIT', 'MIT',
'https://raw.githubusercontent.com/rust-lang/rust/master/LICENSE-MIT' 'https://raw.githubusercontent.com/rust-lang/rust/master/LICENSE-MIT'
], [ ], [
@ -430,19 +558,39 @@ credits = [
'2006-2016 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein', '2006-2016 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein',
'MIT', 'MIT',
'https://raw.githubusercontent.com/sass/sass/stable/MIT-LICENSE' 'https://raw.githubusercontent.com/sass/sass/stable/MIT-LICENSE'
], [
'scikit-image',
'2011 the scikit-image team',
'BSD',
'http://scikit-image.org/docs/dev/license.html'
], [
'scikit-learn',
'2007-2016 The scikit-learn developers',
'BSD',
'https://raw.githubusercontent.com/scikit-learn/scikit-learn/master/COPYING'
], [ ], [
'Sinon', 'Sinon',
'2010-2016 Christian Johansen', '2010-2017 Christian Johansen',
'BSD', 'BSD',
'https://raw.githubusercontent.com/cjohansen/Sinon.JS/master/LICENSE' 'https://raw.githubusercontent.com/sinonjs/sinon/master/LICENSE'
], [ ], [
'Socket.io', 'Socket.io',
'2014-2015 Automattic', '2014-2015 Automattic',
'MIT', 'MIT',
'https://raw.githubusercontent.com/Automattic/socket.io/master/LICENSE' 'https://raw.githubusercontent.com/Automattic/socket.io/master/LICENSE'
], [
'SQLite',
'n/a',
'Public Domain',
'https://sqlite.org/copyright.html'
], [
'Statsmodels',
'2009-2012 Statsmodels Developers<br>&copy; 2006-2008 Scipy Developers<br>&copy; 2006 Jonathan E. Taylor',
'BSD',
'https://raw.githubusercontent.com/statsmodels/statsmodels/master/LICENSE.txt'
], [ ], [
'Symfony', 'Symfony',
'2004-2016 Fabien Potencier', '2004-2017 Fabien Potencier',
'MIT', 'MIT',
'https://symfony.com/doc/current/contributing/code/license.html' 'https://symfony.com/doc/current/contributing/code/license.html'
], [ ], [
@ -452,9 +600,14 @@ credits = [
'http://tcl.tk/software/tcltk/license.html' 'http://tcl.tk/software/tcltk/license.html'
], [ ], [
'TensorFlow', 'TensorFlow',
'2015 The TensorFlow Authors', '2017 The TensorFlow Authors',
'Apache', 'CC BY',
'https://raw.githubusercontent.com/tensorflow/tensorflow/master/LICENSE' 'https://creativecommons.org/licenses/by/3.0/'
], [
'Twig',
'2009-2017 The Twig Team',
'BSD',
'https://twig.sensiolabs.org/license'
], [ ], [
'TypeScript', 'TypeScript',
'Microsoft and other contributors', 'Microsoft and other contributors',
@ -467,22 +620,27 @@ credits = [
'https://raw.githubusercontent.com/jashkenas/underscore/master/LICENSE' 'https://raw.githubusercontent.com/jashkenas/underscore/master/LICENSE'
], [ ], [
'Vagrant', 'Vagrant',
'2010-2015 Mitchell Hashimoto', '2010-2017 Mitchell Hashimoto',
'MIT', 'MPL',
'https://raw.githubusercontent.com/mitchellh/vagrant/master/LICENSE' 'https://raw.githubusercontent.com/mitchellh/vagrant/master/website/LICENSE.md'
], [ ], [
'Vue.js', 'Vue.js',
'2013-2016 Evan You, Vue.js contributors', '2013-2017 Evan You, Vue.js contributors',
'MIT', 'MIT',
'https://raw.githubusercontent.com/vuejs/vue/master/LICENSE' 'https://raw.githubusercontent.com/vuejs/vue/master/LICENSE'
], [ ], [
'Webpack', 'webpack',
'2012-2016 Tobias Koppers', 'JS Foundation and other contributors',
'MIT', 'CC BY',
'https://raw.githubusercontent.com/webpack/webpack/master/LICENSE' 'https://creativecommons.org/licenses/by/4.0/'
], [
'Yarn',
'2016-2017 Yarn Contributors',
'BSD',
'https://raw.githubusercontent.com/yarnpkg/yarn/master/LICENSE'
], [ ], [
'Yii', 'Yii',
'2008-2016 by Yii Software LLC', '2008-2017 by Yii Software LLC',
'BSD', 'BSD',
'https://raw.githubusercontent.com/yiisoft/yii/master/LICENSE' 'https://raw.githubusercontent.com/yiisoft/yii/master/LICENSE'
] ]

@ -1,26 +1,27 @@
ctrlKey = if $.isMac() then 'cmd' else 'ctrl' ctrlKey = if $.isMac() then 'cmd' else 'ctrl'
navKey = if $.isWindows() then 'alt' else ctrlKey navKey = if $.isMac() then 'cmd' else 'alt'
app.templates.helpPage = """ app.templates.helpPage = """
<div class="_toc"> <nav class="_toc" role="directory">
<h3 class="_toc-title">Table of Contents</h3> <h3 class="_toc-title">Table of Contents</h3>
<ul class="_toc-list"> <ul class="_toc-list">
<li><a href="#search">Search</a> <li><a href="#search">Search</a>
<li><a href="#shortcuts">Keyboard Shortcuts</a> <li><a href="#shortcuts">Keyboard Shortcuts</a>
<li><a href="#abbreviations">Abbreviations</a> <li><a href="#aliases">Search Aliases</a>
</ul> </ul>
</div> </nav>
<h2 class="_lined-heading" id="search">Search</h2> <h1 class="_lined-heading">Help</h2>
<h2 class="_block-heading" id="search">Search</h2>
<p> <p>
The search is case-insensitive and supports fuzzy matching (for queries longer than two characters). The search is case-insensitive. It supports fuzzy matching
For example, searching <code class="_label">bgcp</code> brings up <code class="_label">background-clip</code>.<br> (e.g. <code class="_label">bgcp</code> matches <code class="_label">background-clip</code>)
Abbreviations are also supported (<a href="#abbreviations">full list</a> below). and aliases (<a href="#aliases">full list</a> below).
For example, <code class="_label">$</code> is an alias for <code class="_label">jQuery</code>.
<dl> <dl>
<dt id="doc_search">Searching a single documentation <dt id="doc_search">Searching a single documentation
<dd> <dd>
You can scope the search to a single documentation by typing its name (or an abbreviation), You can scope the search to a single documentation by typing its name (or an abbreviation)
and pressing <code class="_label">Tab</code> (<code class="_label">Space</code> on mobile devices). and pressing <code class="_label">Tab</code> (<code class="_label">Space</code> on mobile devices).
For example, to search the JavaScript documentation, enter <code class="_label">javascript</code> For example, to search the JavaScript documentation, enter <code class="_label">javascript</code>
or <code class="_label">js</code>, then <code class="_label">Tab</code>.<br> or <code class="_label">js</code>, then <code class="_label">Tab</code>.<br>
@ -28,22 +29,22 @@ app.templates.helpPage = """
<dt id="url_search">Prefilling the search field <dt id="url_search">Prefilling the search field
<dd> <dd>
The search field can be prefilled from the URL by visiting <a href="/#q=keyword" target="_top">devdocs.io/#q=keyword</a>. The search field can be prefilled from the URL by visiting <a href="/#q=keyword" target="_top">devdocs.io/#q=keyword</a>.
Characters after <code class="_label">#q=</code> will be used as search string.<br> Characters after <code class="_label">#q=</code> will be used as search query.<br>
To search a single documentation, add its name and a space before the keyword: To search a single documentation, add its name and a space before the keyword:
<a href="/#q=js%20date" target="_top">devdocs.io/#q=js date</a>. <a href="/#q=js%20date" target="_top">devdocs.io/#q=js date</a>.
<dt id="browser_search">Searching using the address bar <dt id="browser_search">Searching using the address bar
<dd> <dd>
DevDocs supports OpenSearch, meaning it can easily be installed as a search engine on most web browsers. DevDocs supports OpenSearch. It can easily be installed as a search engine on most web browsers:
<ul> <ul>
<li>On Chrome, the setup is done automatically. Simply press <code class="_label">Tab</code> when devdocs.io is autocompleted <li>On Chrome, the setup is done automatically. Simply press <code class="_label">Tab</code> when devdocs.io is autocompleted
in the omnibox (to set a custom keyword, click <em>Manage search engines\u2026</em> in Chrome's settings). in the omnibox (to set a custom keyword, click <em>Manage search engines\u2026</em> in Chrome's settings).
<li>On Firefox, open the search engine list (icon in the search bar) and select <em>Add "DevDocs Search"</em>. <li>On Firefox, open the search engine list (icon in the search bar) and click <em>Add "DevDocs Search"</em>.
DevDocs is now available in the search bar. You can also search from the location bar by following DevDocs is now available in the search bar. You can also search from the location bar by following
<a href="https://support.mozilla.org/en-US/kb/how-search-from-address-bar">these instructions</a>. <a href="https://support.mozilla.org/en-US/kb/how-search-from-address-bar">these instructions</a>.
</dl> </dl>
<h2 class="_lined-heading" id="shortcuts">Keyboard Shortcuts</h2> <h2 class="_block-heading" id="shortcuts">Keyboard Shortcuts</h2>
<h3 class="_shortcuts-title">Selection</h3> <h3 class="_shortcuts-title">Sidebar</h3>
<dl class="_shortcuts-dl"> <dl class="_shortcuts-dl">
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
<code class="_shortcut-code">&darr;</code> <code class="_shortcut-code">&darr;</code>
@ -59,8 +60,11 @@ app.templates.helpPage = """
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
<code class="_shortcut-code">#{ctrlKey} + enter</code> <code class="_shortcut-code">#{ctrlKey} + enter</code>
<dd class="_shortcuts-dd">Open selection in a new tab <dd class="_shortcuts-dd">Open selection in a new tab
<dt class="_shortcuts-dt">
<code class="_shortcut-code">alt + r</code>
<dd class="_shortcuts-dd">Reveal current page in sidebar
</dl> </dl>
<h3 class="_shortcuts-title">Navigation</h3> <h3 class="_shortcuts-title">Browsing</h3>
<dl class="_shortcuts-dl"> <dl class="_shortcuts-dl">
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
<code class="_shortcut-code">#{navKey} + &larr;</code> <code class="_shortcut-code">#{navKey} + &larr;</code>
@ -81,38 +85,44 @@ app.templates.helpPage = """
<code class="_shortcut-code">#{ctrlKey} + &uarr;</code> <code class="_shortcut-code">#{ctrlKey} + &uarr;</code>
<code class="_shortcut-code">#{ctrlKey} + &darr;</code> <code class="_shortcut-code">#{ctrlKey} + &darr;</code>
<dd class="_shortcuts-dd">Scroll to the top/bottom <dd class="_shortcuts-dd">Scroll to the top/bottom
</dl>
<h3 class="_shortcuts-title">Misc</h3>
<dl class="_shortcuts-dl">
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
<code class="_shortcut-code">alt + f</code> <code class="_shortcut-code">alt + f</code>
<dd class="_shortcuts-dd">Focus first link in the content area<br>(press tab to focus the other links) <dd class="_shortcuts-dd">Focus first link in the content area<br>(press tab to focus the other links)
</dl>
<h3 class="_shortcuts-title">App</h3>
<dl class="_shortcuts-dl">
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
<code class="_shortcut-code">alt + r</code> <code class="_shortcut-code">ctrl + ,</code>
<dd class="_shortcuts-dd">Reveal current page in sidebar <dd class="_shortcuts-dd">Open preferences
<dt class="_shortcuts-dt">
<code class="_shortcut-code">escape</code>
<dd class="_shortcuts-dd">Reset UI
<dt class="_shortcuts-dt">
<code class="_shortcut-code">?</code>
<dd class="_shortcuts-dd">Show this page
</dl>
<h3 class="_shortcuts-title">Miscellaneous</h3>
<dl class="_shortcuts-dl">
<dt class="_shortcuts-dt">
<code class="_shortcut-code">alt + o</code>
<dd class="_shortcuts-dd">Open original page
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
<code class="_shortcut-code">alt + g</code> <code class="_shortcut-code">alt + g</code>
<dd class="_shortcuts-dd">Search on Google <dd class="_shortcuts-dd">Search on Google
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
<code class="_shortcut-code">alt + s</code> <code class="_shortcut-code">alt + s</code>
<dd class="_shortcuts-dd">Search on Stack Overflow <dd class="_shortcuts-dd">Search on Stack Overflow
<dt class="_shortcuts-dt">
<code class="_shortcut-code">escape</code>
<dd class="_shortcuts-dd">Reset<br>(press twice in single doc mode)
<dt class="_shortcuts-dt">
<code class="_shortcut-code">?</code>
<dd class="_shortcuts-dd">Show this page
</dl> </dl>
<p class="_note"> <p class="_note _note-green">
<strong>Tip:</strong> If the cursor is no longer in the search field, press backspace or <strong>Tip:</strong> If the cursor is no longer in the search field, press <code class="_label">/</code> or
continue to type and it will refocus the search field and start showing new results. continue to type and it will refocus the search field and start showing new results.
<h2 class="_lined-heading" id="abbreviations">Abbreviations</h2> <h2 class="_block-heading" id="aliases">Search Aliases</h2>
<p>Feel free to suggest new abbreviations on <a href="https://github.com/Thibaut/devdocs/issues/new">GitHub</a>. <table>
<table class="_abbreviations">
<tr> <tr>
<th>Word <th>Word
<th>Alias <th>Alias
#{("<tr><td>#{key}<td>#{value}" for key, value of app.models.Entry.ALIASES).join('')} #{("<tr><td class=\"_code\">#{key}<td class=\"_code\">#{value}" for key, value of app.models.Entry.ALIASES).join('')}
</table> </table>
<p>Feel free to suggest new aliases on <a href="https://github.com/Thibaut/devdocs/issues/new">GitHub</a>.
""" """

@ -15,7 +15,7 @@ app.templates.newsList = (news, options = {}) ->
date = new Date(value[0]) date = new Date(value[0])
if options.years isnt false and year isnt date.getUTCFullYear() if options.years isnt false and year isnt date.getUTCFullYear()
year = date.getUTCFullYear() year = date.getUTCFullYear()
result += "<h4>#{year}</h4>" result += """<h2 class="_block-heading">#{year}</h2>"""
result += newsItem(date, value[1..]) result += newsItem(date, value[1..])
result result

@ -2,26 +2,27 @@ app.templates.offlinePage = (docs) -> """
<h1 class="_lined-heading">Offline Documentation</h1> <h1 class="_lined-heading">Offline Documentation</h1>
<div class="_docs-tools"> <div class="_docs-tools">
<label>
<input type="checkbox" name="autoUpdate" value="1" #{if app.settings.get('manualUpdate') then '' else 'checked'}>Install updates automatically
</label>
<div class="_docs-links"> <div class="_docs-links">
<a class="_docs-link" data-action-all="install">Install all</a><a class="_docs-link" data-action-all="update"><strong>Update all</strong></a><a class="_docs-link" data-action-all="uninstall">Uninstall all</a> <button type="button" class="_btn-link" data-action-all="install">Install all</button><button type="button" class="_btn-link" data-action-all="update"><strong>Update all</strong></button><button type="button" class="_btn-link" data-action-all="uninstall">Uninstall all</button>
</div> </div>
<label class="_docs-label">
<input type="checkbox" name="autoUpdate" value="1" #{if app.settings.get('autoUpdate') then 'checked' else ''}>
Check for and install updates automatically
</label>
</div> </div>
<table class="_docs"> <div class="_table">
<tr> <table class="_docs">
<th>Documentation</th> <tr>
<th class="_docs-size">Size</th> <th>Documentation</th>
<th>Status</th> <th class="_docs-size">Size</th>
<th>Action</th> <th>Status</th>
</tr> <th>Action</th>
#{docs} </tr>
</table> #{docs}
</table>
</div>
<p class="_note"><strong>Note:</strong> your browser may delete DevDocs's offline data if your computer is running low on disk space and you haven't used the app in a while. Load this page before going offline to make sure the data is still there. <p class="_note"><strong>Note:</strong> your browser may delete DevDocs's offline data if your computer is running low on disk space and you haven't used the app in a while. Load this page before going offline to make sure the data is still there.
<h1 class="_lined-heading">Questions & Answers</h1> <h2 class="_block-heading">Questions & Answers</h2>
<dl> <dl>
<dt>How does this work? <dt>How does this work?
<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> <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,24 +30,22 @@ app.templates.offlinePage = (docs) -> """
<dt>Can I close the tab/browser? <dt>Can I close the tab/browser?
<dd>#{canICloseTheTab()} <dd>#{canICloseTheTab()}
<dt>What if I don't update a documentation? <dt>What if I don't update a documentation?
<dd>You'll see outdated content and some pages will be missing or broken, since the rest of the app (including data for the search and sidebar) uses a different caching mechanism which is updated automatically. <dd>You'll see outdated content and some pages will be missing or broken, because the rest of the app (including data for the search and sidebar) uses a different caching mechanism that's updated automatically.
<dt>I found a bug, where do I report it? <dt>I found a bug, where do I report it?
<dd>In the <a href="https://github.com/Thibaut/devdocs/issues">issue tracker</a>. Thanks! <dd>In the <a href="https://github.com/Thibaut/devdocs/issues">issue tracker</a>. Thanks!
<dt>How do I uninstall/reset the app? <dt>How do I uninstall/reset the app?
<dd>Click <a href="javascript:if(confirm('Are you sure you want to reset DevDocs?'))app.reset()">here</a>. <dd>Click <a href="#" data-behavior="reset">here</a>.
<dt>Why aren't all documentations listed above? <dt>Why aren't all documentations listed above?
<dd>You have to <a href="#" data-pick-docs>enable</a> them first. <dd>You have to <a href="/settings">enable</a> them first.
</dl> </dl>
""" """
canICloseTheTab = -> canICloseTheTab = ->
if app.AppCache.isEnabled() if app.AppCache.isEnabled()
""" 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). """ """ 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). """
else if app.mozApp
""" Yes! Even offline, you can open the app and everything will work as if you were online (provided you installed all the documentations you want to use beforehand). """
else else
""" 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> """ 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>
The current tab will continue to work, though (provided you installed all the documentations you want to use beforehand). """ The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). """
app.templates.offlineDoc = (doc, status) -> app.templates.offlineDoc = (doc, status) ->
outdated = doc.isOutdated(status) outdated = doc.isOutdated(status)
@ -54,23 +53,23 @@ app.templates.offlineDoc = (doc, status) ->
html = """ html = """
<tr data-slug="#{doc.slug}"#{if outdated then ' class="_highlight"' else ''}> <tr data-slug="#{doc.slug}"#{if outdated then ' class="_highlight"' else ''}>
<td class="_docs-name _icon-#{doc.icon}">#{doc.fullName}</td> <td class="_docs-name _icon-#{doc.icon}">#{doc.fullName}</td>
<td class="_docs-size">#{Math.ceil(doc.db_size / 100000) / 10} MB</td> <td class="_docs-size">#{Math.ceil(doc.db_size / 100000) / 10}&nbsp;<small>MB</small></td>
""" """
html += if !(status and status.installed) html += if !(status and status.installed)
""" """
<td>-</td> <td>-</td>
<td><a data-action="install">Install</a></td> <td><button type="button" class="_btn-link" data-action="install">Install</button></td>
""" """
else if outdated else if outdated
""" """
<td><strong>Outdated</strong></td> <td><strong>Outdated</strong></td>
<td><a data-action="update">Update</a> - <a data-action="uninstall">Uninstall</a></td> <td><button type="button" class="_btn-link _bold" data-action="update">Update</button> - <button type="button" class="_btn-link" data-action="uninstall">Uninstall</button></td>
""" """
else else
""" """
<td>Up-to-date</td> <td>Up&#8209;to&#8209;date</td>
<td><a data-action="uninstall">Uninstall</a></td> <td><button type="button" class="_btn-link" data-action="uninstall">Uninstall</button></td>
""" """
html + '</tr>' html + '</tr>'

@ -3,7 +3,7 @@ app.templates.splash = """<div class="_splash-title">DevDocs</div>"""
<% if App.development? %> <% if App.development? %>
app.templates.intro = """ app.templates.intro = """
<div class="_intro"><div class="_intro-message"> <div class="_intro"><div class="_intro-message">
<a class="_intro-hide" data-hide-intro>Stop showing this message</a> <a href="#" class="_intro-hide" data-hide-intro>Stop showing this message</a>
<h2 class="_intro-title">Hi there!</h2> <h2 class="_intro-title">Hi there!</h2>
<p>Thanks for downloading DevDocs. Here are a few things you should know: <p>Thanks for downloading DevDocs. Here are a few things you should know:
<ol class="_intro-list"> <ol class="_intro-list">
@ -27,47 +27,39 @@ app.templates.intro = """
<% else %> <% else %>
app.templates.intro = """ app.templates.intro = """
<div class="_intro"><div class="_intro-message"> <div class="_intro"><div class="_intro-message">
<a class="_intro-hide" data-hide-intro>Stop showing this message</a> <a href="#" class="_intro-hide" data-hide-intro>Stop showing this message</a>
<h2 class="_intro-title">Welcome!</h2> <h2 class="_intro-title">Welcome!</h2>
<p>DevDocs combines multiple API documentations in a fast, organized, and searchable interface. <p>DevDocs combines multiple API documentations in a fast, organized, and searchable interface.
Here's what you should know before you start: Here's what you should know before you start:
<ol class="_intro-list"> <ol class="_intro-list">
<li>To enable more docs, click <a class="_intro-link" data-pick-docs>Select documentation</a> in the bottom left corner <li>Open the <a href="/settings">Preferences</a> to enable more docs and customize the UI.
<li>You don't have to use your mouse &mdash; see the list of <a href="/help#shortcuts">keyboard shortcuts</a> <li>You don't have to use your mouse &mdash; see the list of <a href="/help#shortcuts">keyboard shortcuts</a>.
<li>The search supports fuzzy matching (e.g. "bgcp" brings up "background-clip") <li>The search supports fuzzy matching (e.g. "bgcp" brings up "background-clip").
<li>To search a specific documentation, type its name (or an abbreviation), then Tab <li>To search a specific documentation, type its name (or an abbr.), then Tab.
<li>You can search using your browser's address bar &mdash; <a href="/help#browser_search">learn how</a> <li>You can search using your browser's address bar &mdash; <a href="/help#browser_search">learn how</a>.
<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>. <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>.
<li>For the latest news, follow <a href="https://twitter.com/DevDocs">@DevDocs</a> <li>For the latest news, follow <a href="https://twitter.com/DevDocs">@DevDocs</a>.
<li>DevDocs is free and <a href="https://github.com/Thibaut/devdocs">open source</a> <li>DevDocs is free and <a href="https://github.com/Thibaut/devdocs">open source</a>.
<iframe class="_github-btn" src="//ghbtns.com/github-btn.html?user=Thibaut&repo=devdocs&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20"></iframe> <iframe class="_github-btn" src="//ghbtns.com/github-btn.html?user=Thibaut&repo=devdocs&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20"></iframe>
<li>If you like the app, please consider supporting the project on <a href="https://gratipay.com/devdocs/">Gratipay</a>. Thanks! <li>If you like the app, please consider supporting the project on <a href="https://gratipay.com/devdocs/">Gratipay</a>. Thanks!
<li>And if you're new to coding, check out <a href="https://www.freecodecamp.org/">freeCodeCamp's open source curriculum</a>.
</ol> </ol>
<p>Happy coding! <p>Happy coding!
</div></div> </div></div>
""" """
<% end %> <% end %>
app.templates.mobileNav = """
<nav class="_mobile-nav">
<a href="/offline" class="_mobile-nav-link">Offline</a>
<a href="/about" class="_mobile-nav-link">About</a>
<a href="/news" class="_mobile-nav-link">News</a>
<a href="/help" class="_mobile-nav-link">Help</a>
</nav>
"""
app.templates.mobileIntro = """ app.templates.mobileIntro = """
<div class="_mobile-intro"> <div class="_mobile-intro">
<h2 class="_intro-title">Welcome!</h2> <h2 class="_intro-title">Welcome!</h2>
<p>DevDocs combines multiple API documentations in a fast, organized, and searchable interface. <p>DevDocs combines multiple API documentations in a fast, organized, and searchable interface.
Here's what you should know before you start: Here's what you should know before you start:
<ol class="_intro-list"> <ol class="_intro-list">
<li>To pick your docs, click <a data-pick-docs>Select documentation</a> at the bottom of the menu <li>Pick your docs in the <a href="/settings">Preferences</a>.
<li>The search supports fuzzy matching (e.g. "bgcp" matches "background-clip") <li>The search supports fuzzy matching.
<li>To search a specific documentation, type its name (or an abbreviation), then Space <li>To search a specific documentation, type its name (or an abbr.), then Space.
<li>For the latest news, follow <a href="https://twitter.com/DevDocs">@DevDocs</a> <li>For the latest news, follow <a href="https://twitter.com/DevDocs">@DevDocs</a>.
<li>DevDocs is <a href="https://github.com/Thibaut/devdocs">open source</a> <li>DevDocs is <a href="https://github.com/Thibaut/devdocs">open source</a>.
</ol> </ol>
<p>Happy coding! <p>Happy coding!
<a class="_intro-hide" data-hide-intro>Stop showing this message</a> <a class="_intro-hide" data-hide-intro>Stop showing this message</a>
@ -79,6 +71,6 @@ app.templates.androidWarning = """
<h2 class="_intro-title">Hi there</h2> <h2 class="_intro-title">Hi there</h2>
<p>DevDocs is running inside an Android WebView. Some features may not work properly. <p>DevDocs is running inside an Android WebView. Some features may not work properly.
<p>If you downloaded an app called DevDocs on the Play Store, please uninstall it — it's made by someone who is using (and profiting from) the name DevDocs without permission. <p>If you downloaded an app called DevDocs on the Play Store, please uninstall it — it's made by someone who is using (and profiting from) the name DevDocs without permission.
<p>To install DevDocs on your phone, visit <a href="http://devdocs.io" target="_blank">devdocs.io</a> in Chrome and select "Add to home screen" in the menu. <p>To install DevDocs on your phone, visit <a href="http://devdocs.io" target="_blank" rel="noopener">devdocs.io</a> in Chrome and select "Add to home screen" in the menu.
</div> </div>
""" """

@ -0,0 +1,40 @@
app.templates.settingsPage = (settings) -> """
<h1 class="_lined-heading">Preferences</h1>
<div class="_settings-fieldset">
<h2 class="_settings-legend">General:</h2>
<div class="_settings-inputs">
<label class="_settings-label">
<input type="checkbox" form="settings" name="dark" value="1"#{if settings.dark then ' checked' else ''}>Enable dark theme
</label>
<label class="_settings-label _setting-max-width">
<input type="checkbox" form="settings" name="layout" value="_max-width"#{if settings['_max-width'] then ' checked' else ''}>Enable fixed-width layout
</label>
<label class="_settings-label _hide-on-mobile">
<input type="checkbox" form="settings" name="layout" value="_sidebar-hidden"#{if settings['_sidebar-hidden'] then ' checked' else ''}>Automatically hide and show the sidebar
<small>Tip: drag the edge of the sidebar to resize it.</small>
</label>
</div>
</div>
<div class="_settings-fieldset _hide-on-mobile">
<h2 class="_settings-legend">Scrolling:</h2>
<div class="_settings-inputs">
<label class="_settings-label">
<input type="checkbox" form="settings" name="smoothScroll" value="1"#{if settings.smoothScroll then ' checked' else ''}>Use smooth scrolling
</label>
<label class="_settings-label _setting-native-scrollbar">
<input type="checkbox" form="settings" name="layout" value="_native-scrollbars"#{if settings['_native-scrollbars'] then ' checked' else ''}>Use native scrollbars
</label>
<label class="_settings-label">
<input type="checkbox" form="settings" name="arrowScroll" value="1"#{if settings.arrowScroll then ' checked' else ''}>Use arrow keys to scroll the main content area
<small>With this checked, use <code class="_label">alt</code> + <code class="_label">&uarr;</code><code class="_label">&darr;</code><code class="_label">&larr;</code><code class="_label">&rarr;</code> to navigate the sidebar.</small>
</label>
</div>
</div>
<p>
<a href="#" class="_reset-link" data-behavior="reset">Reset all preferences and data</a>
"""

@ -1,5 +1,7 @@
arrow = """<svg class="_path-arrow"><use xlink:href="#dir-icon"/></svg>"""
app.templates.path = (doc, type, entry) -> app.templates.path = (doc, type, entry) ->
html = """<a href="#{doc.fullPath()}" class="_path-item _icon-#{doc.icon}">#{doc.fullName}</a>""" html = """<a href="#{doc.fullPath()}" class="_path-item _icon-#{doc.icon}">#{doc.fullName}</a>"""
html += """<a href="#{type.fullPath()}" class="_path-item">#{type.name}</a>""" if type html += """#{arrow}<a href="#{type.fullPath()}" class="_path-item">#{type.name}</a>""" if type
html += """<span class="_path-item">#{$.escape entry.name}</span>""" if entry html += """#{arrow}<span class="_path-item">#{$.escape entry.name}</span>""" if entry
html html

@ -1,41 +1,43 @@
templates = app.templates templates = app.templates
arrow = """<svg class="_list-arrow"><use xlink:href="#dir-icon"/></svg>"""
templates.sidebarDoc = (doc, options = {}) -> templates.sidebarDoc = (doc, options = {}) ->
link = """<a href="#{doc.fullPath()}" class="_list-item _icon-#{doc.icon} """ link = """<a href="#{doc.fullPath()}" class="_list-item _icon-#{doc.icon} """
link += if options.disabled then '_list-disabled' else '_list-dir' link += if options.disabled then '_list-disabled' else '_list-dir'
link += """" data-slug="#{doc.slug}" title="#{doc.fullName}">""" link += """" data-slug="#{doc.slug}" title="#{doc.fullName}" tabindex="-1">"""
if options.disabled if options.disabled
link += """<span class="_list-enable" data-enable="#{doc.slug}">Enable</span>""" link += """<span class="_list-enable" data-enable="#{doc.slug}">Enable</span>"""
else else
link += """<span class="_list-arrow"></span>""" link += arrow
link += """<span class="_list-count">#{doc.release}</span>""" if doc.release link += """<span class="_list-count">#{doc.release}</span>""" if doc.release
link += """<span class="_list-text">#{doc.name}""" link += """<span class="_list-text">#{doc.name}"""
link += " #{doc.version}" if options.disabled and doc.version link += " #{doc.version}" if options.fullName or options.disabled and doc.version
link + "</span></a>" link + "</span></a>"
templates.sidebarType = (type) -> templates.sidebarType = (type) ->
"""<a href="#{type.fullPath()}" class="_list-item _list-dir" data-slug="#{type.slug}"><span class="_list-arrow"></span><span class="_list-count">#{type.count}</span><span class="_list-text">#{type.name}</span></a>""" """<a href="#{type.fullPath()}" class="_list-item _list-dir" data-slug="#{type.slug}" tabindex="-1">#{arrow}<span class="_list-count">#{type.count}</span><span class="_list-text">#{$.escape type.name}</span></a>"""
templates.sidebarEntry = (entry) -> templates.sidebarEntry = (entry) ->
"""<a href="#{entry.fullPath()}" class="_list-item _list-hover">#{$.escape entry.name}</a>""" """<a href="#{entry.fullPath()}" class="_list-item _list-hover" tabindex="-1">#{$.escape entry.name}</a>"""
templates.sidebarResult = (entry) -> templates.sidebarResult = (entry) ->
addons = if entry.isIndex() and app.disabledDocs.contains(entry.doc) addons = if entry.isIndex() and app.disabledDocs.contains(entry.doc)
"""<span class="_list-enable" data-enable="#{entry.doc.slug}">Enable</span>""" """<span class="_list-enable" data-enable="#{entry.doc.slug}">Enable</span>"""
else else
"""<span class="_list-reveal" data-reset-list title="Reveal in list"></span>""" """<span class="_list-reveal" data-reset-list title="Reveal in list"></span>"""
addons += """<span class="_list-count">#{entry.doc.version}</span>""" if entry.doc.version and not entry.isIndex() addons += """<span class="_list-count">#{entry.doc.short_version}</span>""" if entry.doc.version and not entry.isIndex()
"""<a href="#{entry.fullPath()}" class="_list-item _list-hover _list-result _icon-#{entry.doc.icon}">#{addons}<span class="_list-text">#{$.escape entry.name}</span></a>""" """<a href="#{entry.fullPath()}" class="_list-item _list-hover _list-result _icon-#{entry.doc.icon}" tabindex="-1">#{addons}<span class="_list-text">#{$.escape entry.name}</span></a>"""
templates.sidebarNoResults = -> templates.sidebarNoResults = ->
html = """ <div class="_list-note">No results.</div> """ html = """ <div class="_list-note">No results.</div> """
html += """ html += """
<div class="_list-note">Note: documentations must be <a class="_list-note-link" data-pick-docs>enabled</a> to appear in the search.</div> <div class="_list-note">Note: documentations must be <a href="/settings" class="_list-note-link">enabled</a> to appear in the search.</div>
""" unless app.isSingleDoc() or app.disabledDocs.isEmpty() """ unless app.isSingleDoc() or app.disabledDocs.isEmpty()
html html
templates.sidebarPageLink = (count) -> templates.sidebarPageLink = (count) ->
"""<span class="_list-item _list-pagelink">Show more\u2026 (#{count})</span>""" """<span role="link" class="_list-item _list-pagelink">Show more\u2026 (#{count})</span>"""
templates.sidebarLabel = (doc, options = {}) -> templates.sidebarLabel = (doc, options = {}) ->
label = """<label class="_list-item""" label = """<label class="_list-item"""
@ -47,30 +49,20 @@ templates.sidebarLabel = (doc, options = {}) ->
templates.sidebarVersionedDoc = (doc, versions, options = {}) -> templates.sidebarVersionedDoc = (doc, versions, options = {}) ->
html = """<div class="_list-item _list-dir _list-rdir _icon-#{doc.icon}""" html = """<div class="_list-item _list-dir _list-rdir _icon-#{doc.icon}"""
html += " open" if options.open html += " open" if options.open
html + """"><span class="_list-arrow"></span>#{doc.name}</div><div class="_list _list-sub">#{versions}</div>""" html + """" tabindex="0">#{arrow}#{doc.name}</div><div class="_list _list-sub">#{versions}</div>"""
templates.sidebarDisabled = (options) -> templates.sidebarDisabled = (options) ->
"""<h6 class="_list-title"><span class="_list-arrow"></span>Disabled (#{options.count})</h6>""" """<h6 class="_list-title">#{arrow}Disabled (#{options.count}) <a href="/settings" class="_list-title-link" tabindex="-1">Customize</a></h6>"""
templates.sidebarDisabledList = (html) -> templates.sidebarDisabledList = (html) ->
"""<div class="_disabled-list">#{html}</div>""" """<div class="_disabled-list">#{html}</div>"""
templates.sidebarDisabledVersionedDoc = (doc, versions) -> templates.sidebarDisabledVersionedDoc = (doc, versions) ->
"""<a class="_list-item _list-dir _icon-#{doc.icon} _list-disabled" data-slug="#{doc.slug_without_version}"><span class="_list-arrow"></span>#{doc.name}</a><div class="_list _list-sub">#{versions}</div>""" """<a class="_list-item _list-dir _icon-#{doc.icon} _list-disabled" data-slug="#{doc.slug_without_version}" tabindex="-1">#{arrow}#{doc.name}</a><div class="_list _list-sub">#{versions}</div>"""
templates.sidebarPickerNote = """
<div class="_list-note">Tip: for faster and better search results, select only the docs you need.</div>
<a href="https://trello.com/b/6BmTulfx/devdocs-documentation" class="_list-link" target="_blank">Vote for new documentation</a>
"""
sidebarFooter = (html) -> """<div class="_sidebar-footer">#{html}</div>""" templates.docPickerHeader = """<div class="_list-picker-head"><span>Documentation</span> <span>Enable</span></div>"""
templates.sidebarSettings = -> templates.docPickerNote = """
sidebarFooter """ <div class="_list-note">Tip: for faster and better search results, select only the docs you need.</div>
<a class="_sidebar-footer-link _sidebar-footer-light" title="Toggle light" data-light></a> <a href="https://trello.com/b/6BmTulfx/devdocs-documentation" class="_list-link" target="_blank" rel="noopener">Vote for new documentation</a>
<a class="_sidebar-footer-link _sidebar-footer-layout" title="Toggle layout" data-layout></a>
<a class="_sidebar-footer-link _sidebar-footer-edit" data-pick-docs>Select documentation</a>
""" """
templates.sidebarSave = ->
sidebarFooter """<a class="_sidebar-footer-link _sidebar-footer-save">Save</a>"""

@ -0,0 +1,28 @@
try {
if (app.config.env == 'production') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-5544833-12', 'devdocs.io');
page.track(function() {
ga('send', 'pageview', {
page: location.pathname + location.search + location.hash,
dimension1: app.router.context && app.router.context.doc && app.router.context.doc.slug_without_version
});
});
page.track(function() {
if (window._gauges)
_gauges.push(['track']);
else
(function() {
var _gauges=_gauges||[];!function(){var a=document.createElement("script");
a.type="text/javascript",a.async=!0,a.id="gauges-tracker",
a.setAttribute("data-site-id","51c15f82613f5d7819000067"),
a.src="https://secure.gaug.es/track.js";var b=document.getElementsByTagName("script")[0];
b.parentNode.insertBefore(a,b)}();
})();
});
}
} catch(e) { }

@ -1,141 +1,172 @@
/*! /*
* Cookies.js - 0.3.1 * Cookies.js - 1.2.3
* Wednesday, April 24 2013 @ 2:28 AM EST * https://github.com/ScottHamper/Cookies
* *
* Copyright (c) 2013, Scott Hamper * This is free and unencumbered software released into the public domain.
* Licensed under the MIT license,
* http://www.opensource.org/licenses/MIT
*/ */
(function (undefined) { (function (global, undefined) {
'use strict'; 'use strict';
var Cookies = function (key, value, options) { var factory = function (window) {
return arguments.length === 1 ? if (typeof window.document !== 'object') {
Cookies.get(key) : Cookies.set(key, value, options); throw new Error('Cookies.js requires a `window` with a `document` object');
}; }
// Allows for setter injection in unit tests var Cookies = function (key, value, options) {
Cookies._document = document; return arguments.length === 1 ?
Cookies._navigator = navigator; Cookies.get(key) : Cookies.set(key, value, options);
};
Cookies.defaults = { // Allows for setter injection in unit tests
path: '/' Cookies._document = window.document;
};
Cookies.get = function (key) { // Used to ensure cookie keys do not collide with
if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) { // built-in `Object` properties
Cookies._renewCache(); Cookies._cacheKeyPrefix = 'cookey.'; // Hurr hurr, :)
}
return Cookies._cache[key]; Cookies._maxExpireDate = new Date('Fri, 31 Dec 9999 23:59:59 UTC');
};
Cookies.set = function (key, value, options) { Cookies.defaults = {
options = Cookies._getExtendedOptions(options); path: '/',
options.expires = Cookies._getExpiresDate(value === undefined ? -1 : options.expires); secure: false
};
Cookies._document.cookie = Cookies._generateCookieString(key, value, options); Cookies.get = function (key) {
if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) {
Cookies._renewCache();
}
return Cookies; var value = Cookies._cache[Cookies._cacheKeyPrefix + key];
};
Cookies.expire = function (key, options) { return value === undefined ? undefined : decodeURIComponent(value);
return Cookies.set(key, undefined, options); };
};
Cookies.set = function (key, value, options) {
options = Cookies._getExtendedOptions(options);
options.expires = Cookies._getExpiresDate(value === undefined ? -1 : options.expires);
Cookies._document.cookie = Cookies._generateCookieString(key, value, options);
Cookies._getExtendedOptions = function (options) { return Cookies;
return {
path: options && options.path || Cookies.defaults.path,
domain: options && options.domain || Cookies.defaults.domain,
expires: options && options.expires || Cookies.defaults.expires,
secure: options && options.secure !== undefined ? options.secure : Cookies.defaults.secure
}; };
};
Cookies._isValidDate = function (date) { Cookies.expire = function (key, options) {
return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime()); return Cookies.set(key, undefined, options);
}; };
Cookies._getExpiresDate = function (expires, now) { Cookies._getExtendedOptions = function (options) {
now = now || new Date(); return {
switch (typeof expires) { path: options && options.path || Cookies.defaults.path,
case 'number': expires = new Date(now.getTime() + expires * 1000); break; domain: options && options.domain || Cookies.defaults.domain,
case 'string': expires = new Date(expires); break; expires: options && options.expires || Cookies.defaults.expires,
} secure: options && options.secure !== undefined ? options.secure : Cookies.defaults.secure
};
};
if (expires && !Cookies._isValidDate(expires)) { Cookies._isValidDate = function (date) {
throw new Error('`expires` parameter cannot be converted to a valid Date instance'); return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime());
} };
return expires; Cookies._getExpiresDate = function (expires, now) {
}; now = now || new Date();
Cookies._generateCookieString = function (key, value, options) { if (typeof expires === 'number') {
key = encodeURIComponent(key); expires = expires === Infinity ?
value = (value + '').replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent); Cookies._maxExpireDate : new Date(now.getTime() + expires * 1000);
options = options || {}; } else if (typeof expires === 'string') {
expires = new Date(expires);
}
var cookieString = key + '=' + value; if (expires && !Cookies._isValidDate(expires)) {
cookieString += options.path ? ';path=' + options.path : ''; throw new Error('`expires` parameter cannot be converted to a valid Date instance');
cookieString += options.domain ? ';domain=' + options.domain : ''; }
cookieString += options.expires ? ';expires=' + options.expires.toGMTString() : '';
cookieString += options.secure ? ';secure' : '';
return cookieString; return expires;
}; };
Cookies._getCookieObjectFromString = function (documentCookie) { Cookies._generateCookieString = function (key, value, options) {
var cookieObject = {}; key = key.replace(/[^#$&+\^`|]/g, encodeURIComponent);
var cookiesArray = documentCookie ? documentCookie.split('; ') : []; key = key.replace(/\(/g, '%28').replace(/\)/g, '%29');
value = (value + '').replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent);
options = options || {};
for (var i = 0; i < cookiesArray.length; i++) { var cookieString = key + '=' + value;
var cookieKvp = Cookies._getKeyValuePairFromCookieString(cookiesArray[i]); cookieString += options.path ? ';path=' + options.path : '';
cookieString += options.domain ? ';domain=' + options.domain : '';
cookieString += options.expires ? ';expires=' + options.expires.toUTCString() : '';
cookieString += options.secure ? ';secure' : '';
if (cookieObject[cookieKvp.key] === undefined) { return cookieString;
cookieObject[cookieKvp.key] = cookieKvp.value; };
Cookies._getCacheFromString = function (documentCookie) {
var cookieCache = {};
var cookiesArray = documentCookie ? documentCookie.split('; ') : [];
for (var i = 0; i < cookiesArray.length; i++) {
var cookieKvp = Cookies._getKeyValuePairFromCookieString(cookiesArray[i]);
if (cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] === undefined) {
cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] = cookieKvp.value;
}
} }
}
return cookieObject; return cookieCache;
}; };
Cookies._getKeyValuePairFromCookieString = function (cookieString) { Cookies._getKeyValuePairFromCookieString = function (cookieString) {
// "=" is a valid character in a cookie value according to RFC6265, so cannot `split('=')` // "=" is a valid character in a cookie value according to RFC6265, so cannot `split('=')`
var separatorIndex = cookieString.indexOf('='); var separatorIndex = cookieString.indexOf('=');
// IE omits the "=" when the cookie value is an empty string
separatorIndex = separatorIndex < 0 ? cookieString.length : separatorIndex;
var key = cookieString.substr(0, separatorIndex);
var decodedKey;
try {
decodedKey = decodeURIComponent(key);
} catch (e) {
if (console && typeof console.error === 'function') {
console.error('Could not decode cookie with key "' + key + '"', e);
}
}
// IE omits the "=" when the cookie value is an empty string return {
separatorIndex = separatorIndex < 0 ? cookieString.length : separatorIndex; key: decodedKey,
value: cookieString.substr(separatorIndex + 1) // Defer decoding value until accessed
};
};
return { Cookies._renewCache = function () {
key: decodeURIComponent(cookieString.substr(0, separatorIndex)), Cookies._cache = Cookies._getCacheFromString(Cookies._document.cookie);
value: decodeURIComponent(cookieString.substr(separatorIndex + 1)) Cookies._cachedDocumentCookie = Cookies._document.cookie;
}; };
};
Cookies._renewCache = function () { Cookies._areEnabled = function () {
Cookies._cache = Cookies._getCookieObjectFromString(Cookies._document.cookie); var testKey = 'cookies.js';
Cookies._cachedDocumentCookie = Cookies._document.cookie; var areEnabled = Cookies.set(testKey, 1).get(testKey) === '1';
}; Cookies.expire(testKey);
return areEnabled;
};
Cookies._areEnabled = function () { Cookies.enabled = Cookies._areEnabled();
return Cookies._navigator.cookieEnabled ||
Cookies.set('cookies.js', 1).get('cookies.js') === '1';
};
Cookies.enabled = Cookies._areEnabled(); return Cookies;
};
var cookiesExport = (global && typeof global.document === 'object') ? factory(global) : factory;
// AMD support // AMD support
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {
define(function () { return Cookies; }); define(function () { return cookiesExport; });
// CommonJS and Node.js module support. // CommonJS/Node.js support
} else if (typeof exports !== 'undefined') { } else if (typeof exports === 'object') {
// Support Node.js specific `module.exports` (which can be a function) // Support Node.js specific `module.exports` (which can be a function)
if (typeof module !== 'undefined' && module.exports) { if (typeof module === 'object' && typeof module.exports === 'object') {
exports = module.exports = Cookies; exports = module.exports = cookiesExport;
} }
// But always support CommonJS module 1.1.1 spec (`exports` cannot be a function) // But always support CommonJS module 1.1.1 spec (`exports` cannot be a function)
exports.Cookies = Cookies; exports.Cookies = cookiesExport;
} else { } else {
window.Cookies = Cookies; global.Cookies = cookiesExport;
} }
})(); })(typeof window === 'undefined' ? this : window);

@ -0,0 +1,20 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Adapted from: https://github.com/fred-wang/mathml.css */
(function () {
window.addEventListener("load", function() {
var box, div, link, namespaceURI;
// First check whether the page contains any <math> element.
namespaceURI = "http://www.w3.org/1998/Math/MathML";
// Create a div to test mspace, using Kuma's "offscreen" CSS
document.body.insertAdjacentHTML("afterbegin", "<div style='border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px;'><math xmlns='" + namespaceURI + "'><mspace height='23px' width='77px'></mspace></math></div>");
div = document.body.firstChild;
box = div.firstChild.firstChild.getBoundingClientRect();
document.body.removeChild(div);
if (Math.abs(box.height - 23) > 1 || Math.abs(box.width - 77) > 1) {
window.supportsMathML = false;
}
});
}());

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -6,13 +6,13 @@ class app.views.Content extends app.View
click: 'onClick' click: 'onClick'
@shortcuts: @shortcuts:
altUp: 'scrollStepUp' altUp: 'scrollStepUp'
altDown: 'scrollStepDown' altDown: 'scrollStepDown'
pageUp: 'scrollPageUp' pageUp: 'scrollPageUp'
pageDown: 'scrollPageDown' pageDown: 'scrollPageDown'
home: 'scrollToTop' pageTop: 'scrollToTop'
end: 'scrollToBottom' pageBottom: 'scrollToBottom'
altF: 'onAltF' altF: 'onAltF'
@routes: @routes:
before: 'beforeRoute' before: 'beforeRoute'
@ -23,11 +23,12 @@ class app.views.Content extends app.View
@scrollMap = {} @scrollMap = {}
@scrollStack = [] @scrollStack = []
@rootPage = new app.views.RootPage @rootPage = new app.views.RootPage
@staticPage = new app.views.StaticPage @staticPage = new app.views.StaticPage
@offlinePage = new app.views.OfflinePage @settingsPage = new app.views.SettingsPage
@typePage = new app.views.TypePage @offlinePage = new app.views.OfflinePage
@entryPage = new app.views.EntryPage @typePage = new app.views.TypePage
@entryPage = new app.views.EntryPage
@entryPage @entryPage
.on 'loading', @onEntryLoading .on 'loading', @onEntryLoading
@ -51,6 +52,9 @@ class app.views.Content extends app.View
@addClass @constructor.loadingClass @addClass @constructor.loadingClass
return return
isLoading: ->
@el.classList.contains @constructor.loadingClass
hideLoading: -> hideLoading: ->
@removeClass @constructor.loadingClass @removeClass @constructor.loadingClass
return return
@ -59,38 +63,45 @@ class app.views.Content extends app.View
@scrollEl.scrollTop = value or 0 @scrollEl.scrollTop = value or 0
return return
smoothScrollTo: (value) ->
if app.settings.get('fastScroll')
@scrollTo value
else
$.smoothScroll @scrollEl, value or 0
return
scrollBy: (n) -> scrollBy: (n) ->
@scrollEl.scrollTop += n @smoothScrollTo @scrollEl.scrollTop + n
return return
scrollToTop: => scrollToTop: =>
@scrollTo 0 @smoothScrollTo 0
return return
scrollToBottom: => scrollToBottom: =>
@scrollTo @scrollEl.scrollHeight @smoothScrollTo @scrollEl.scrollHeight
return return
scrollStepUp: => scrollStepUp: =>
@scrollBy -50 @scrollBy -80
return return
scrollStepDown: => scrollStepDown: =>
@scrollBy 50 @scrollBy 80
return return
scrollPageUp: => scrollPageUp: =>
@scrollBy 80 - @scrollEl.clientHeight @scrollBy 40 - @scrollEl.clientHeight
return return
scrollPageDown: => scrollPageDown: =>
@scrollBy @scrollEl.clientHeight - 80 @scrollBy @scrollEl.clientHeight - 40
return return
scrollToTarget: -> scrollToTarget: ->
if @routeCtx.hash and el = @findTargetByHash @routeCtx.hash if @routeCtx.hash and el = @findTargetByHash @routeCtx.hash
$.scrollToWithImageLock el, @scrollEl, 'top', $.scrollToWithImageLock el, @scrollEl, 'top',
margin: 20 + if @scrollEl is @el then 0 else $.offset(@el).top margin: if @scrollEl is @el then 0 else $.offset(@el).top
$.highlight el, className: '_highlight' $.highlight el, className: '_highlight'
else else
@scrollTo @scrollMap[@routeCtx.state.id] @scrollTo @scrollMap[@routeCtx.state.id]
@ -107,21 +118,28 @@ class app.views.Content extends app.View
onEntryLoading: => onEntryLoading: =>
@showLoading() @showLoading()
if @scrollToTargetTimeout
clearTimeout @scrollToTargetTimeout
@scrollToTargetTimeout = null
return return
onEntryLoaded: => onEntryLoaded: =>
@hideLoading() @hideLoading()
if @scrollToTargetTimeout
clearTimeout @scrollToTargetTimeout
@scrollToTargetTimeout = null
@scrollToTarget() @scrollToTarget()
return return
beforeRoute: (context) => beforeRoute: (context) =>
@cacheScrollPosition() @cacheScrollPosition()
@routeCtx = context @routeCtx = context
@delay @scrollToTarget @scrollToTargetTimeout = @delay @scrollToTarget
return return
cacheScrollPosition: -> cacheScrollPosition: ->
return if not @routeCtx or @routeCtx.hash return if not @routeCtx or @routeCtx.hash
return if @routeCtx.path is '/'
unless @scrollMap[@routeCtx.state.id]? unless @scrollMap[@routeCtx.state.id]?
@scrollStack.push @routeCtx.state.id @scrollStack.push @routeCtx.state.id
@ -139,6 +157,8 @@ class app.views.Content extends app.View
@show @entryPage @show @entryPage
when 'type' when 'type'
@show @typePage @show @typePage
when 'settings'
@show @settingsPage
when 'offline' when 'offline'
@show @offlinePage @show @offlinePage
else else

@ -5,6 +5,9 @@ class app.views.EntryPage extends app.View
@events: @events:
click: 'onClick' click: 'onClick'
@shortcuts:
altO: 'onAltO'
@routes: @routes:
before: 'beforeRoute' before: 'beforeRoute'
@ -37,14 +40,23 @@ class app.views.EntryPage extends app.View
if app.disabledDocs.findBy 'slug', @entry.doc.slug if app.disabledDocs.findBy 'slug', @entry.doc.slug
@hiddenView = new app.views.HiddenPage @el, @entry @hiddenView = new app.views.HiddenPage @el, @entry
@delay @polyfillMathML
@trigger 'loaded' @trigger 'loaded'
return return
CLIPBOARD_LINK = '<a class="_pre-clip" title="Copy to clipboard" tabindex="-1"></a>'
addClipboardLinks: -> addClipboardLinks: ->
for el in @findAllByTag('pre') unless @clipBoardLink
el.insertAdjacentHTML('afterbegin', CLIPBOARD_LINK) @clipBoardLink = document.createElement('a')
@clipBoardLink.className = '_pre-clip'
@clipBoardLink.title = 'Copy to clipboard'
@clipBoardLink.tabIndex = -1
el.appendChild(@clipBoardLink.cloneNode()) for el in @findAllByTag('pre')
return
polyfillMathML: ->
return unless window.supportsMathML is false and !@polyfilledMathML and @findByTag('math')
@polyfilledMathML = true
$.append document.head, """<link rel="stylesheet" href="#{app.config.mathml_stylesheet}">"""
return return
LINKS = LINKS =
@ -77,8 +89,8 @@ class app.views.EntryPage extends app.View
@entry.doc.fullName + if @entry.isIndex() then ' documentation' else " / #{@entry.name}" @entry.doc.fullName + if @entry.isIndex() then ' documentation' else " / #{@entry.name}"
beforeRoute: => beforeRoute: =>
@abort()
@cache() @cache()
@abort()
return return
onRoute: (context) -> onRoute: (context) ->
@ -95,7 +107,7 @@ class app.views.EntryPage extends app.View
abort: -> abort: ->
if @xhr if @xhr
@xhr.abort() @xhr.abort()
@xhr = null @xhr = @entry = null
return return
onSuccess: (response) => onSuccess: (response) =>
@ -113,7 +125,7 @@ class app.views.EntryPage extends app.View
return return
cache: -> cache: ->
return if not @entry or @cacheMap[path = @entry.filePath()] return if @xhr or not @entry or @cacheMap[path = @entry.filePath()]
@cacheMap[path] = @el.innerHTML @cacheMap[path] = @el.innerHTML
@cacheStack.push(path) @cacheStack.push(path)
@ -137,3 +149,8 @@ class app.views.EntryPage extends app.View
target.classList.add if $.copyToClipboard(target.parentNode.textContent) then '_pre-clip-success' else '_pre-clip-error' target.classList.add if $.copyToClipboard(target.parentNode.textContent) then '_pre-clip-success' else '_pre-clip-error'
setTimeout (-> target.className = '_pre-clip'), 2000 setTimeout (-> target.className = '_pre-clip'), 2000
return return
onAltO: =>
return unless link = @find('._attribution:last-child ._attribution-link')
$.popup(link.href + location.hash)
return

@ -11,10 +11,14 @@ class app.views.OfflinePage extends app.View
return return
render: -> render: ->
if app.cookieBlocked
@html @tmpl('offlineError', 'cookie_blocked')
return
app.docs.getInstallStatuses (statuses) => app.docs.getInstallStatuses (statuses) =>
return unless @activated return unless @activated
if statuses is false if statuses is false
@html @tmpl('offlineError', app.db.reason) @html @tmpl('offlineError', app.db.reason, app.db.error)
else else
html = '' html = ''
html += @renderDoc(doc, statuses[doc.slug]) for doc in app.docs.all() html += @renderDoc(doc, statuses[doc.slug]) for doc in app.docs.all()
@ -31,7 +35,7 @@ class app.views.OfflinePage extends app.View
refreshLinks: -> refreshLinks: ->
for action in ['install', 'update', 'uninstall'] for action in ['install', 'update', 'uninstall']
@find("a[data-action-all='#{action}']").classList[if @find("a[data-action='#{action}']") then 'add' else 'remove']('_show') @find("[data-action-all='#{action}']").classList[if @find("[data-action='#{action}']") then 'add' else 'remove']('_show')
return return
docByEl: (el) -> docByEl: (el) ->
@ -41,24 +45,20 @@ class app.views.OfflinePage extends app.View
docEl: (doc) -> docEl: (doc) ->
@find("[data-slug='#{doc.slug}']") @find("[data-slug='#{doc.slug}']")
onRoute: (route) -> onRoute: (context) ->
if app.isSingleDoc() @render()
window.location = "/#/#{route.path}"
else
@render()
return return
onClick: (event) => onClick: (event) =>
return unless link = $.closestLink(event.target) el = event.target
if action = link.getAttribute('data-action') if action = el.getAttribute('data-action')
$.stopEvent(event) doc = @docByEl(el)
doc = @docByEl(link)
action = 'install' if action is 'update' action = 'install' if action is 'update'
doc[action](@onInstallSuccess.bind(@, doc), @onInstallError.bind(@, doc)) doc[action](@onInstallSuccess.bind(@, doc), @onInstallError.bind(@, doc), @onInstallProgress.bind(@, doc))
link.parentNode.innerHTML = "#{link.textContent.replace(/e$/, '')}ing…" el.parentNode.innerHTML = "#{el.textContent.replace(/e$/, '')}ing…"
else if action = link.getAttribute('data-action-all') else if action = el.getAttribute('data-action-all')
$.stopEvent(event) app.db.migrate()
el.click() for el in @findAll("a[data-action='#{action}']") $.click(el) for el in @findAll("[data-action='#{action}']")
return return
onInstallSuccess: (doc) -> onInstallSuccess: (doc) ->
@ -78,7 +78,14 @@ class app.views.OfflinePage extends app.View
el.lastElementChild.textContent = 'Error' el.lastElementChild.textContent = 'Error'
return return
onInstallProgress: (doc, event) ->
return unless @activated and event.lengthComputable
if el = @docEl(doc)
percentage = Math.round event.loaded * 100 / event.total
el.lastElementChild.textContent = el.lastElementChild.textContent.replace(/(\s.+)?$/, " (#{percentage}%)")
return
onChange: (event) -> onChange: (event) ->
if event.target.name is 'autoUpdate' if event.target.name is 'autoUpdate'
app.settings.set 'autoUpdate', !!event.target.checked app.settings.set 'manualUpdate', !event.target.checked
return return

@ -9,7 +9,6 @@ class app.views.RootPage extends app.View
render: -> render: ->
@empty() @empty()
@append @tmpl('mobileNav') if app.isMobile()
if app.isAndroidWebview() if app.isAndroidWebview()
@append @tmpl('androidWarning') @append @tmpl('androidWarning')
else else

@ -0,0 +1,64 @@
class app.views.SettingsPage extends app.View
LAYOUTS = ['_max-width', '_sidebar-hidden', '_native-scrollbars']
SIDEBAR_HIDDEN_LAYOUT = '_sidebar-hidden'
@className: '_static'
@events:
change: 'onChange'
render: ->
@html @tmpl('settingsPage', @currentSettings())
return
currentSettings: ->
settings = {}
settings.dark = app.settings.get('dark')
settings.smoothScroll = !app.settings.get('fastScroll')
settings.arrowScroll = app.settings.get('arrowScroll')
settings[layout] = app.settings.hasLayout(layout) for layout in LAYOUTS
settings
getTitle: ->
'Preferences'
toggleDark: (enable) ->
css = $('link[rel="stylesheet"][data-alt]')
alt = css.getAttribute('data-alt')
css.setAttribute('data-alt', css.getAttribute('href'))
css.setAttribute('href', alt)
app.settings.set('dark', !!enable)
app.appCache?.updateInBackground()
return
toggleLayout: (layout, enable) ->
document.body.classList[if enable then 'add' else 'remove'](layout) unless layout is SIDEBAR_HIDDEN_LAYOUT
document.body.classList[if $.overlayScrollbarsEnabled() then 'add' else 'remove']('_overlay-scrollbars')
app.settings.setLayout(layout, enable)
app.appCache?.updateInBackground()
return
toggleSmoothScroll: (enable) ->
app.settings.set('fastScroll', !enable)
return
toggle: (name, enable) ->
app.settings.set(name, enable)
return
onChange: (event) =>
input = event.target
switch input.name
when 'dark'
@toggleDark input.checked
when 'layout'
@toggleLayout input.value, input.checked
when 'smoothScroll'
@toggleSmoothScroll input.checked
else
@toggle input.name, input.checked
return
onRoute: (context) ->
@render()
return

@ -1,80 +1,42 @@
class app.views.Document extends app.View class app.views.Document extends app.View
MAX_WIDTH_CLASS = '_max-width'
HIDE_SIDEBAR_CLASS = '_sidebar-hidden'
@el: document @el: document
@events: @events:
visibilitychange: 'onVisibilityChange' visibilitychange: 'onVisibilityChange'
@shortcuts: @shortcuts:
help: 'onHelp' help: 'onHelp'
escape: 'onEscape' preferences: 'onPreferences'
superLeft: 'onBack' escape: 'onEscape'
superRight: 'onForward' superLeft: 'onBack'
superRight: 'onForward'
@routes:
after: 'afterRoute'
init: -> init: ->
@addSubview @nav = new app.views.Nav, @addSubview @menu = new app.views.Menu,
@addSubview @sidebar = new app.views.Sidebar @addSubview @sidebar = new app.views.Sidebar
@addSubview @resizer = new app.views.Resizer if app.views.Resizer.isSupported() @addSubview @resizer = new app.views.Resizer if app.views.Resizer.isSupported()
@addSubview @content = new app.views.Content @addSubview @content = new app.views.Content
@addSubview @path = new app.views.Path unless app.isSingleDoc() or app.isMobile() @addSubview @path = new app.views.Path unless app.isSingleDoc() or app.isMobile()
@settings = new app.views.Settings unless app.isSingleDoc()
@sidebar.search $.on document.body, 'click', @onClick
.on 'searching', @onSearching
.on 'clear', @onSearchClear
@activate() @activate()
return return
toggleLight: -> setTitle: (title) ->
css = $('link[rel="stylesheet"][data-alt]') @el.title = if title then "DevDocs — #{title}" else 'DevDocs API Documentation'
alt = css.getAttribute('data-alt')
css.setAttribute('data-alt', css.getAttribute('href'))
css.setAttribute('href', alt)
app.settings.setDark(alt.indexOf('dark') > 0)
app.appCache?.updateInBackground()
return
toggleLayout: ->
wantsMaxWidth = !app.el.classList.contains(MAX_WIDTH_CLASS)
app.el.classList[if wantsMaxWidth then 'add' else 'remove'](MAX_WIDTH_CLASS)
app.settings.setLayout(MAX_WIDTH_CLASS, wantsMaxWidth)
app.appCache?.updateInBackground()
return
showSidebar: (options = {}) ->
@toggleSidebar(options, true)
return
hideSidebar: (options = {}) ->
@toggleSidebar(options, false)
return
toggleSidebar: (options = {}, shouldShow) ->
shouldShow ?= if options.save then !@hasSidebar() else app.el.classList.contains(HIDE_SIDEBAR_CLASS)
app.el.classList[if shouldShow then 'remove' else 'add'](HIDE_SIDEBAR_CLASS)
if options.save
app.settings.setLayout(HIDE_SIDEBAR_CLASS, !shouldShow)
app.appCache?.updateInBackground()
return
hasSidebar: ->
!app.settings.hasLayout(HIDE_SIDEBAR_CLASS)
onSearching: =>
unless @hasSidebar()
@showSidebar()
return
onSearchClear: => afterRoute: (route) =>
unless @hasSidebar() if route is 'settings'
@hideSidebar() @settings?.activate()
else
@settings?.deactivate()
return return
setTitle: (title) ->
@el.title = if title then "DevDocs - #{title}" else 'DevDocs API Documentation'
onVisibilityChange: => onVisibilityChange: =>
return unless @el.visibilityState is 'visible' return unless @el.visibilityState is 'visible'
@delay -> @delay ->
@ -85,6 +47,11 @@ class app.views.Document extends app.View
onHelp: -> onHelp: ->
app.router.show '/help#shortcuts' app.router.show '/help#shortcuts'
return
onPreferences: ->
app.router.show '/settings'
return
onEscape: -> onEscape: ->
path = if !app.isSingleDoc() or location.pathname is app.doc.fullPath() path = if !app.isSingleDoc() or location.pathname is app.doc.fullPath()
@ -93,9 +60,23 @@ class app.views.Document extends app.View
app.doc.fullPath() app.doc.fullPath()
app.router.show(path) app.router.show(path)
return
onBack: -> onBack: ->
history.back() history.back()
return
onForward: -> onForward: ->
history.forward() history.forward()
return
onClick: (event) ->
return unless event.target.hasAttribute('data-behavior')
$.stopEvent(event)
switch event.target.getAttribute('data-behavior')
when 'back' then history.back()
when 'reload' then window.location.reload()
when 'reboot' then window.location = '/'
when 'hard-reload' then app.reload()
when 'reset' then app.reset() if confirm('Are you sure you want to reset DevDocs?')
return

@ -0,0 +1,22 @@
class app.views.Menu extends app.View
@el: '._menu'
@activeClass: 'active'
@events:
click: 'onClick'
init: ->
$.on document.body, 'click', @onGlobalClick
return
onClick: (event) ->
event.target.blur() if event.target.tagName is 'A'
return
onGlobalClick: (event) =>
return if event.which isnt 1
if event.target.hasAttribute?('data-toggle-menu')
@toggleClass @constructor.activeClass
else if @hasClass @constructor.activeClass
@removeClass @constructor.activeClass
return

@ -2,9 +2,13 @@ class app.views.Mobile extends app.View
@className: '_mobile' @className: '_mobile'
@elements: @elements:
body: 'body' body: 'body'
content: '._container' content: '._container'
sidebar: '._sidebar' sidebar: '._sidebar'
docPicker: '._settings ._sidebar'
@shortcuts:
escape: 'onEscape'
@routes: @routes:
after: 'afterRoute' after: 'afterRoute'
@ -32,62 +36,120 @@ class app.views.Mobile extends app.View
super super
init: -> init: ->
if $.isTouchScreen() window.FastClick?.attach @body
FastClick.attach @body
app.shortcuts.stop()
$.on @body, 'click', @onClick
$.on $('._home-link'), 'click', @onClickHome
$.on $('._menu-link'), 'click', @onClickMenu
$.on $('._search'), 'touchend', @onTapSearch $.on $('._search'), 'touchend', @onTapSearch
@toggleSidebar = $('button[data-toggle-sidebar]')
@toggleSidebar.removeAttribute('hidden')
$.on @toggleSidebar, 'click', @onClickToggleSidebar
@back = $('button[data-back]')
@back.removeAttribute('hidden')
$.on @back, 'click', @onClickBack
@forward = $('button[data-forward]')
@forward.removeAttribute('hidden')
$.on @forward, 'click', @onClickForward
@docPickerTab = $('button[data-tab="doc-picker"]')
@docPickerTab.removeAttribute('hidden')
$.on @docPickerTab, 'click', @onClickDocPickerTab
@settingsTab = $('button[data-tab="settings"]')
@settingsTab.removeAttribute('hidden')
$.on @settingsTab, 'click', @onClickSettingsTab
app.document.sidebar.search app.document.sidebar.search
.on 'searching', @showSidebar .on 'searching', @showSidebar
.on 'clear', @hideSidebar
@activate() @activate()
return return
showSidebar: => showSidebar: =>
return if @isSidebarShown() if @isSidebarShown()
@contentTop = @body.scrollTop window.scrollTo 0, 0
return
@contentTop = window.scrollY
@content.style.display = 'none' @content.style.display = 'none'
@sidebar.style.display = 'block' @sidebar.style.display = 'block'
if selection = @findByClass app.views.ListSelect.activeClass if selection = @findByClass app.views.ListSelect.activeClass
$.scrollTo selection, @body, 'center' scrollContainer = if window.scrollY is @body.scrollTop then @body else document.documentElement
$.scrollTo selection, scrollContainer, 'center'
else else
@body.scrollTop = @findByClass(app.views.ListFold.activeClass) and @sidebarTop or 0 window.scrollTo 0, @findByClass(app.views.ListFold.activeClass) and @sidebarTop or 0
return return
hideSidebar: => hideSidebar: =>
return unless @isSidebarShown() return unless @isSidebarShown()
@sidebarTop = @body.scrollTop @sidebarTop = window.scrollY
@sidebar.style.display = 'none' @sidebar.style.display = 'none'
@content.style.display = 'block' @content.style.display = 'block'
@body.scrollTop = @contentTop or 0 window.scrollTo 0, @contentTop or 0
return return
isSidebarShown: -> isSidebarShown: ->
@sidebar.style.display isnt 'none' @sidebar.style.display isnt 'none'
onClick: (event) => onClickBack: =>
if event.target.hasAttribute 'data-pick-docs' history.back()
@showSidebar()
onClickForward: =>
history.forward()
onClickToggleSidebar: =>
if @isSidebarShown() then @hideSidebar() else @showSidebar()
return return
onClickHome: => onClickDocPickerTab: (event) =>
app.shortcuts.trigger 'escape' $.stopEvent(event)
@hideSidebar() @showDocPicker()
return return
onClickMenu: => onClickSettingsTab: (event) =>
if @isSidebarShown() then @hideSidebar() else @showSidebar() $.stopEvent(event)
@showSettings()
return
showDocPicker: ->
window.scrollTo 0, 0
@docPickerTab.classList.add 'active'
@settingsTab.classList.remove 'active'
@docPicker.style.display = 'block'
@content.style.display = 'none'
return
showSettings: ->
window.scrollTo 0, 0
@docPickerTab.classList.remove 'active'
@settingsTab.classList.add 'active'
@docPicker.style.display = 'none'
@content.style.display = 'block'
return return
onTapSearch: => onTapSearch: =>
@body.scrollTop = 0 window.scrollTo 0, 0
onEscape: =>
@hideSidebar()
afterRoute: => afterRoute: (route) =>
@hideSidebar() @hideSidebar()
if route is 'settings'
@showDocPicker()
else
@content.style.display = 'block'
if page.canGoBack()
@back.removeAttribute('disabled')
else
@back.setAttribute('disabled', 'disabled')
if page.canGoForward()
@forward.removeAttribute('disabled')
else
@forward.setAttribute('disabled', 'disabled')
return return

@ -1,24 +0,0 @@
class app.views.Nav extends app.View
@el: '._nav'
@activeClass: '_nav-current'
@routes:
after: 'afterRoute'
select: (href) ->
@deselect()
if @current = @find "a[href='#{href}']"
@current.classList.add @constructor.activeClass
return
deselect: ->
if @current
@current.classList.remove @constructor.activeClass
@current = null
return
afterRoute: (route, context) =>
if route in ['page', 'offline']
@select context.pathname
else
@deselect()

@ -1,5 +1,7 @@
class app.views.Path extends app.View class app.views.Path extends app.View
@className: '_path' @className: '_path'
@attributes:
role: 'complementary'
@events: @events:
click: 'onClick' click: 'onClick'
@ -8,17 +10,21 @@ class app.views.Path extends app.View
after: 'afterRoute' after: 'afterRoute'
render: (args...) -> render: (args...) ->
@show()
@html @tmpl 'path', args... @html @tmpl 'path', args...
@show()
return
show: -> show: ->
$.prepend $('._app'), @el unless @el.parentNode @prependTo app.el unless @el.parentNode
return
hide: -> hide: ->
$.remove @el if @el.parentNode $.remove @el if @el.parentNode
return
onClick: (event) => onClick: (event) =>
@clicked = true if link = $.closestLink event.target, @el @clicked = true if link = $.closestLink event.target, @el
return
afterRoute: (route, context) => afterRoute: (route, context) =>
if context.type if context.type
@ -34,3 +40,4 @@ class app.views.Path extends app.View
if @clicked if @clicked
@clicked = null @clicked = null
app.document.sidebar.reset() app.document.sidebar.reset()
return

@ -4,7 +4,6 @@ class app.views.Resizer extends app.View
@events: @events:
dragstart: 'onDragStart' dragstart: 'onDragStart'
dragend: 'onDragEnd' dragend: 'onDragEnd'
dblclick: 'onDblClick'
@isSupported: -> @isSupported: ->
'ondragstart' of document.createElement('div') and !app.isMobile() 'ondragstart' of document.createElement('div') and !app.isMobile()
@ -32,10 +31,6 @@ class app.views.Resizer extends app.View
app.appCache?.updateInBackground() app.appCache?.updateInBackground()
return return
onDblClick: (event) ->
app.document.toggleSidebar(save: true)
return
onDragStart: (event) => onDragStart: (event) =>
@style.removeAttribute('disabled') @style.removeAttribute('disabled')
event.dataTransfer.effectAllowed = 'link' event.dataTransfer.effectAllowed = 'link'

@ -0,0 +1,80 @@
class app.views.Settings extends app.View
SIDEBAR_HIDDEN_LAYOUT = '_sidebar-hidden'
@el: '._settings'
@elements:
sidebar: '._sidebar'
saveBtn: 'button[type="submit"]'
backBtn: 'button[data-back]'
@events:
change: 'onChange'
submit: 'onSubmit'
click: 'onClick'
@shortcuts:
enter: 'onEnter'
init: ->
@addSubview @docPicker = new app.views.DocPicker
return
activate: ->
if super
@render()
document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT)
app.appCache?.on 'progress', @onAppCacheProgress
return
deactivate: ->
if super
@resetClass()
@docPicker.detach()
document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT) if app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT)
app.appCache?.off 'progress', @onAppCacheProgress
return
render: ->
@docPicker.appendTo @sidebar
@refreshElements()
@addClass '_in'
return
save: ->
unless @saving
@saving = true
docs = @docPicker.getSelectedDocs()
app.settings.setDocs(docs)
@saveBtn.textContent = if app.appCache then 'Downloading\u2026' else 'Saving\u2026'
disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1)
disabledDocs.uninstall ->
app.db.migrate()
app.reload()
return
onChange: =>
@addClass('_dirty')
return
onEnter: =>
@save()
return
onSubmit: (event) =>
event.preventDefault()
@save()
return
onClick: (event) =>
return if event.which isnt 1
if event.target is @backBtn
$.stopEvent(event)
app.router.show '/'
return
onAppCacheProgress: (event) =>
if event.lengthComputable
percentage = Math.round event.loaded * 100 / event.total
@saveBtn.textContent = "Downloading\u2026 (#{percentage}%)"
return

@ -12,13 +12,15 @@ class app.views.ListFocus extends app.View
superEnter: 'onSuperEnter' superEnter: 'onSuperEnter'
escape: 'blur' escape: 'blur'
constructor: (@el) -> super constructor: (@el) ->
super
@focusOnNextFrame = $.framify(@focus, @)
focus: (el) -> focus: (el, options = {}) ->
if el and not el.classList.contains @constructor.activeClass if el and not el.classList.contains @constructor.activeClass
@blur() @blur()
el.classList.add @constructor.activeClass el.classList.add @constructor.activeClass
$.trigger el, 'focus' $.trigger el, 'focus' unless options.silent is true
return return
blur: => blur: =>
@ -38,7 +40,7 @@ class app.views.ListFocus extends app.View
$.click(next) $.click(next)
@findNext cursor @findNext cursor
else if next.tagName is 'DIV' # sub-list else if next.tagName is 'DIV' # sub-list
if cursor.className.indexOf('open') >= 0 if cursor.className.indexOf(' open') >= 0
@findFirst(next) or @findNext(next) @findFirst(next) or @findNext(next)
else else
@findNext(next) @findNext(next)
@ -85,22 +87,22 @@ class app.views.ListFocus extends app.View
onDown: => onDown: =>
if cursor = @getCursor() if cursor = @getCursor()
@focus @findNext(cursor) @focusOnNextFrame @findNext(cursor)
else else
@focus @findByTag('a') @focusOnNextFrame @findByTag('a')
return return
onUp: => onUp: =>
if cursor = @getCursor() if cursor = @getCursor()
@focus @findPrev(cursor) @focusOnNextFrame @findPrev(cursor)
else else
@focus @findLastByTag('a') @focusOnNextFrame @findLastByTag('a')
return return
onLeft: => onLeft: =>
cursor = @getCursor() cursor = @getCursor()
if cursor and not cursor.classList.contains(app.views.ListFold.activeClass) and cursor.parentElement isnt @el if cursor and not cursor.classList.contains(app.views.ListFold.activeClass) and cursor.parentElement isnt @el
@focus cursor.parentElement.previousSibling @focusOnNextFrame cursor.parentElement.previousSibling
return return
onEnter: => onEnter: =>
@ -116,5 +118,5 @@ class app.views.ListFocus extends app.View
onClick: (event) => onClick: (event) =>
return if event.which isnt 1 or event.metaKey or event.ctrlKey return if event.which isnt 1 or event.metaKey or event.ctrlKey
if event.target.tagName is 'A' if event.target.tagName is 'A'
@focus event.target @focus event.target, silent: true
return return

@ -55,10 +55,17 @@ class app.views.ListFold extends app.View
return if event.which isnt 1 or event.metaKey or event.ctrlKey return if event.which isnt 1 or event.metaKey or event.ctrlKey
return unless event.pageY # ignore fabricated clicks return unless event.pageY # ignore fabricated clicks
el = event.target el = event.target
el = el.parentElement if el.parentElement.tagName.toUpperCase() is 'SVG'
if el.classList.contains @constructor.handleClass if el.classList.contains @constructor.handleClass
$.stopEvent(event) $.stopEvent(event)
@toggle el.parentElement @toggle el.parentElement
else if el.classList.contains @constructor.targetClass else if el.classList.contains @constructor.targetClass
if el.hasAttribute('href') then @open(el) else @toggle(el) if el.hasAttribute('href')
if el.classList.contains(@constructor.activeClass)
@close(el) if el.classList.contains(app.views.ListSelect.activeClass)
else
@open(el)
else
@toggle(el)
return return

@ -1,5 +1,7 @@
class app.views.Notice extends app.View class app.views.Notice extends app.View
@className: '_notice' @className: '_notice'
@attributes:
role: 'alert'
constructor: (@type, @args...) -> super constructor: (@type, @args...) -> super
@ -16,9 +18,8 @@ class app.views.Notice extends app.View
return return
show: -> show: ->
@addClass '_top' if @type is 'disabledDoc'
@html @tmpl("#{@type}Notice", @args...) @html @tmpl("#{@type}Notice", @args...)
@prependTo $('._app') @prependTo app.el
return return
hide: -> hide: ->

@ -1,6 +1,8 @@
class app.views.Notif extends app.View class app.views.Notif extends app.View
@className: '_notif' @className: '_notif'
@activeClass: '_in' @activeClass: '_in'
@attributes:
role: 'alert'
@defautOptions: @defautOptions:
autoHide: 15000 autoHide: 15000
@ -49,6 +51,7 @@ class app.views.Notif extends app.View
onClick: (event) => onClick: (event) =>
return if event.which isnt 1 return if event.which isnt 1
return if event.target.hasAttribute('data-behavior')
if event.target.tagName isnt 'A' or event.target.classList.contains('_notif-close') if event.target.tagName isnt 'A' or event.target.classList.contains('_notif-close')
$.stopEvent(event) $.stopEvent(event)
@hide() @hide()

@ -10,7 +10,7 @@ class app.views.Updates extends app.views.Notif
@lastUpdateTime = @getLastUpdateTime() @lastUpdateTime = @getLastUpdateTime()
@updatedDocs = @getUpdatedDocs() @updatedDocs = @getUpdatedDocs()
@updatedDisabledDocs = @getUpdatedDisabledDocs() @updatedDisabledDocs = @getUpdatedDisabledDocs()
@show() if @updatedDocs.length @show() if @updatedDocs.length > 0 or @updatedDisabledDocs.length > 0
@markAllAsRead() @markAllAsRead()
return return

@ -1,18 +1,43 @@
class app.views.BasePage extends app.View class app.views.BasePage extends app.View
constructor: (@el, @entry) -> super constructor: (@el, @entry) -> super
deactivate: ->
if super
@highlightNodes = []
render: (content, fromCache = false) -> render: (content, fromCache = false) ->
@highlightNodes = []
@previousTiming = null
@addClass "_#{@entry.doc.type}" unless @constructor.className @addClass "_#{@entry.doc.type}" unless @constructor.className
@html content @html content
@prepare?() unless fromCache @highlightCode() unless fromCache
@activate() @activate()
@delay @afterRender if @afterRender @delay @afterRender if @afterRender
if @highlightNodes.length > 0
$.requestAnimationFrame => $.requestAnimationFrame(@paintCode)
return
highlightCode: ->
for el in @findAll('pre[data-language]')
language = el.getAttribute('data-language')
el.classList.add("language-#{language}")
@highlightNodes.push(el)
return return
highlightCode: (el, language) -> paintCode: (timing) =>
if $.isCollection(el) if @previousTiming
@highlightCode e, language for e in el if Math.round(1000 / (timing - @previousTiming)) > 50 # fps
else if el @nodesPerFrame = Math.round(Math.min(@nodesPerFrame * 1.25, 50))
el.classList.add "language-#{language}" else
@nodesPerFrame = Math.round(Math.max(@nodesPerFrame * .8, 10))
else
@nodesPerFrame = 10
for el in @highlightNodes.splice(0, @nodesPerFrame)
$.remove(clipEl) if clipEl = el.lastElementChild
Prism.highlightElement(el) Prism.highlightElement(el)
$.append(el, clipEl) if clipEl
$.requestAnimationFrame(@paintCode) if @highlightNodes.length > 0
@previousTiming = timing
return return

@ -1,6 +0,0 @@
#= require views/pages/base
class app.views.BowerPage extends app.views.BasePage
prepare: ->
@highlightCode @findAll('pre[data-lang="js"], pre[data-lang="javascript"], pre[data-lang="json"]'), 'javascript'
return

@ -1,7 +0,0 @@
#= require views/pages/base
class app.views.CPage extends app.views.BasePage
prepare: ->
@highlightCode @findAll('pre.source-c, .source-c > pre'), 'c'
@highlightCode @findAll('pre.source-cpp, .source-cpp > pre'), 'cpp'
return

@ -1,7 +0,0 @@
#= require views/pages/base
class app.views.CoffeescriptPage extends app.views.BasePage
prepare: ->
@highlightCode @findAll('.code > pre:first-child'), 'coffeescript'
@highlightCode @findAll('.code > pre:last-child'), 'javascript'
return

@ -1,6 +0,0 @@
#= require views/pages/base
class app.views.D3Page extends app.views.BasePage
prepare: ->
@highlightCode @findAll('.highlight > pre'), 'javascript'
return

@ -1,6 +0,0 @@
#= require views/pages/base
class app.views.DrupalPage extends app.views.BasePage
prepare: ->
@highlightCode @findAll('pre.php'), 'php'
return

@ -1,6 +0,0 @@
#= require views/pages/base
class app.views.ElixirPage extends app.views.BasePage
prepare: ->
@highlightCode @findAllByTag('pre'), 'elixir'
return

@ -1,9 +0,0 @@
#= require views/pages/base
class app.views.GithubPage extends app.views.BasePage
LANGUAGE_RGX = /highlight-source-(\w+)/
prepare: ->
for el in @findAll('pre.highlight')
@highlightCode(el, el.className.match(LANGUAGE_RGX)[1])
return

@ -1,25 +0,0 @@
#= require views/pages/base
class app.views.JavascriptPage extends app.views.BasePage
prepare: ->
@highlightCode @findAllByTag('pre'), 'javascript'
return
class app.views.JavascriptWithMarkupCheckPage extends app.views.BasePage
prepare: ->
for el in @findAllByTag('pre')
language = if el.textContent.match(/^\s*</)
'markup'
else
'javascript'
@highlightCode el, language
return
app.views.ChaiPage =
app.views.GruntPage =
app.views.MochaPage =
app.views.JavascriptPage
app.views.DojoPage =
app.views.RequirejsPage =
app.views.JavascriptWithMarkupCheckPage

@ -3,12 +3,6 @@
class app.views.JqueryPage extends app.views.BasePage class app.views.JqueryPage extends app.views.BasePage
@demoClassName: '_jquery-demo' @demoClassName: '_jquery-demo'
prepare: ->
for el in @findAllByClass 'syntaxhighlighter'
language = if el.classList.contains('javascript') then 'javascript' else 'markup'
@highlightCode el, language
return
afterRender: -> afterRender: ->
# Prevent jQuery Mobile's demo iframes from scrolling the page # Prevent jQuery Mobile's demo iframes from scrolling the page
for iframe in @findAllByTag 'iframe' for iframe in @findAllByTag 'iframe'
@ -45,7 +39,7 @@ class app.views.JqueryPage extends app.views.BasePage
fixIframeSource: (source) -> fixIframeSource: (source) ->
source = source.replace '"/resources/', '"https://api.jquery.com/resources/' # attr(), keydown() source = source.replace '"/resources/', '"https://api.jquery.com/resources/' # attr(), keydown()
source.replace '</head>', """ source = source.replace '</head>', """
<style> <style>
html, body { border: 0; margin: 0; padding: 0; } html, body { border: 0; margin: 0; padding: 0; }
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; }
@ -60,3 +54,4 @@ class app.views.JqueryPage extends app.views.BasePage
</script> </script>
</head> </head>
""" """
source.replace /<script>/gi, '<script nonce="devdocs">'

@ -1,8 +0,0 @@
#= require views/pages/base
class app.views.KnockoutPage extends app.views.BasePage
prepare: ->
for el in @findAll('pre')
language = if el.innerHTML.indexOf('data-bind="') > 0 then 'markup' else 'javascript'
@highlightCode el, language
return

@ -1,6 +0,0 @@
#= require views/pages/base
class app.views.LuaPage extends app.views.BasePage
prepare: ->
@highlightCode @findAllByTag('pre'), 'lua'
return

@ -1,6 +0,0 @@
#= require views/pages/base
class app.views.NginxPage extends app.views.BasePage
prepare: ->
@highlightCode @findAllByTag('pre'), 'nginx'
return

@ -1,6 +0,0 @@
#= require views/pages/base
class app.views.PhpunitPage extends app.views.BasePage
prepare: ->
@highlightCode @findAll('pre.programlisting'), 'php'
return

@ -4,11 +4,6 @@ class app.views.RdocPage extends app.views.BasePage
@events: @events:
click: 'onClick' click: 'onClick'
prepare: ->
@highlightCode @findAll('pre.ruby'), 'ruby'
@highlightCode @findAll('pre.c'), 'clike'
return
onClick: (event) -> onClick: (event) ->
return unless event.target.classList.contains 'method-click-advice' return unless event.target.classList.contains 'method-click-advice'
$.stopEvent(event) $.stopEvent(event)

@ -1,6 +0,0 @@
#= require views/pages/base
class app.views.RustPage extends app.views.BasePage
prepare: ->
@highlightCode @findAll('pre.rust'), 'rust'
return

@ -1,39 +0,0 @@
#= require views/pages/base
class app.views.SimplePage extends app.views.BasePage
prepare: ->
for el in @findAllByTag('pre') when el.hasAttribute('data-language')
@highlightCode el, el.getAttribute('data-language')
return
app.views.AngularPage =
app.views.CakephpPage =
app.views.EmberPage =
app.views.ExpressPage =
app.views.GoPage =
app.views.LaravelPage =
app.views.LodashPage =
app.views.MarionettePage =
app.views.MdnPage =
app.views.MeteorPage =
app.views.ModernizrPage =
app.views.MomentPage =
app.views.MongoosePage =
app.views.NodePage =
app.views.PerlPage =
app.views.PhalconPage =
app.views.PhaserPage =
app.views.PhpPage =
app.views.PostgresPage =
app.views.RamdaPage =
app.views.ReactPage =
app.views.RethinkdbPage =
app.views.SinonPage =
app.views.SocketioPage =
app.views.SphinxSimplePage =
app.views.TensorflowPage =
app.views.TypescriptPage =
app.views.UnderscorePage =
app.views.VuePage =
app.views.WebpackPage =
app.views.SimplePage

@ -1,8 +0,0 @@
#= require views/pages/base
class app.views.SphinxPage extends app.views.BasePage
prepare: ->
@highlightCode @findAll('pre.python'), 'python'
@highlightCode @findAll('pre.markup'), 'markup'
@highlightCode @findAll('pre.php'), 'php'
return

@ -0,0 +1,17 @@
#= require views/pages/base
class app.views.SqlitePage extends app.views.BasePage
@events:
click: 'onClick'
onClick: (event) =>
return unless id = event.target.getAttribute('data-toggle')
return unless el = @find("##{id}")
$.stopEvent(event)
if el.style.display == 'none'
el.style.display = 'block'
event.target.textContent = 'hide'
else
el.style.display = 'none'
event.target.textContent = 'show'
return

@ -1,5 +0,0 @@
#= require views/pages/base
class app.views.VagrantPage extends app.views.BasePage
prepare: ->
@highlightCode @findAll('pre.ruby'), 'ruby'

@ -14,13 +14,12 @@ class app.views.Search extends app.View
submit: 'onSubmit' submit: 'onSubmit'
@shortcuts: @shortcuts:
typing: 'autoFocus' typing: 'focus'
altG: 'google' altG: 'google'
altS: 'stackoverflow' altS: 'stackoverflow'
@routes: @routes:
root: 'onRoot' after: 'afterRoute'
after: 'autoFocus'
init: -> init: ->
@addSubview @scope = new app.views.SearchScope @el @addSubview @scope = new app.views.SearchScope @el
@ -32,31 +31,31 @@ class app.views.Search extends app.View
app.on 'ready', @onReady app.on 'ready', @onReady
$.on window, 'hashchange', @searchUrl $.on window, 'hashchange', @searchUrl
$.on window, 'focus', @autoFocus $.on window, 'focus', @onWindowFocus
return return
focus: -> focus: =>
@input.focus() unless document.activeElement is @input @input.focus() unless document.activeElement is @input
return return
autoFocus: => autoFocus: =>
@focus() unless $.isTouchScreen() unless app.isMobile() or $.isAndroid() or $.isIOS()
@input.focus() unless document.activeElement?.tagName is 'INPUT'
return return
reset: -> onWindowFocus: (event) =>
@autoFocus() if event.target is window
getScopeDoc: ->
@scope.getScope() if @scope.isActive()
reset: (force) ->
@scope.reset() if force or not @input.value
@el.reset() @el.reset()
@onInput() @onInput()
@autoFocus() @autoFocus()
return return
disable: ->
@input.setAttribute('disabled', 'disabled')
return
enable: ->
@input.removeAttribute('disabled')
return
onReady: => onReady: =>
@value = '' @value = ''
@delay @onInput @delay @onInput
@ -83,11 +82,14 @@ class app.views.Search extends app.View
return return
searchUrl: => searchUrl: =>
return unless app.router.isRoot() if location.pathname is '/'
@scope.searchUrl() @scope.searchUrl()
else if not app.router.isIndex()
return
return unless value = @extractHashValue() return unless value = @extractHashValue()
@input.value = @value = value @input.value = @value = value
@input.setSelectionRange(value.length, value.length)
@search true @search true
true true
@ -125,16 +127,18 @@ class app.views.Search extends app.View
if event.target is @resetLink if event.target is @resetLink
$.stopEvent(event) $.stopEvent(event)
@reset() @reset()
@focus() app.document.onEscape()
return return
onSubmit: (event) -> onSubmit: (event) ->
$.stopEvent(event) $.stopEvent(event)
return return
onRoot: (context) => afterRoute: (name, context) =>
@reset() unless context.init return if app.shortcuts.eventInProgress?.name is 'escape'
@reset(true) if not context.init and app.router.isIndex()
@delay @searchUrl if context.hash @delay @searchUrl if context.hash
$.requestAnimationFrame @autoFocus
return return
extractHashValue: -> extractHashValue: ->
@ -142,5 +146,7 @@ class app.views.Search extends app.View
app.router.replaceHash() app.router.replaceHash()
value value
HASH_RGX = new RegExp "^##{SEARCH_PARAM}=(.*)"
getHashValue: -> getHashValue: ->
try (new RegExp "##{SEARCH_PARAM}=(.*)").exec($.urlDecode location.hash)?[1] catch try HASH_RGX.exec($.urlDecode location.hash)?[1] catch

@ -11,9 +11,6 @@ class app.views.SearchScope extends app.View
@routes: @routes:
after: 'afterRoute' after: 'afterRoute'
@shortcuts:
escape: 'reset'
constructor: (@el) -> super constructor: (@el) -> super
init: -> init: ->
@ -29,22 +26,29 @@ class app.views.SearchScope extends app.View
getScope: -> getScope: ->
@doc or app @doc or app
isActive: ->
!!@doc
name: -> name: ->
@doc?.name @doc?.name
search: (value) -> search: (value, searchDisabled = false) ->
unless @doc return if @doc
@searcher.find app.docs.all(), 'text', value @searcher.find app.docs.all(), 'text', value
@searcher.find app.disabledDocs.all(), 'text', value if not @doc and searchDisabled
return return
searchUrl: -> searchUrl: ->
if value = @extractHashValue() if value = @extractHashValue()
@search value @search value, true
return return
onResults: (results) => onResults: (results) =>
if results.length return unless doc = results[0]
@selectDoc results[0] if app.docs.contains(doc)
@selectDoc(doc)
else
@redirectToDoc(doc)
return return
selectDoc: (doc) -> selectDoc: (doc) ->
@ -63,6 +67,12 @@ class app.views.SearchScope extends app.View
@trigger 'change', @doc, previousDoc @trigger 'change', @doc, previousDoc
return return
redirectToDoc: (doc) ->
hash = location.hash
app.router.replaceHash('')
location.assign doc.fullPath() + hash
return
reset: => reset: =>
return unless @doc return unless @doc
previousDoc = @doc previousDoc = @doc
@ -78,16 +88,16 @@ class app.views.SearchScope extends app.View
return return
onKeydown: (event) => onKeydown: (event) =>
return if event.ctrlKey or event.metaKey or event.altKey or event.shiftKey
if event.which is 8 # backspace if event.which is 8 # backspace
if @doc and not @input.value if @doc and not @input.value
$.stopEvent(event) $.stopEvent(event)
@reset() @reset()
else if event.which is 9 or # tab else if not @doc and @input.value
event.which is 32 and (app.isMobile() or $.isTouchScreen()) # space return if event.ctrlKey or event.metaKey or event.altKey or event.shiftKey
$.stopEvent(event) if event.which is 9 or # tab
@search @input.value[0...@input.selectionStart] (event.which is 32 and app.isMobile()) # space
@search @input.value[0...@input.selectionStart]
$.stopEvent(event) if @doc
return return
extractHashValue: -> extractHashValue: ->
@ -96,8 +106,10 @@ class app.views.SearchScope extends app.View
app.router.replaceHash(newHash) app.router.replaceHash(newHash)
value value
HASH_RGX = new RegExp "^##{SEARCH_PARAM}=(.+?) ."
getHashValue: -> getHashValue: ->
try (new RegExp "^##{SEARCH_PARAM}=(.+?) .").exec($.urlDecode location.hash)?[1] catch try HASH_RGX.exec($.urlDecode location.hash)?[1] catch
afterRoute: (name, context) => afterRoute: (name, context) =>
if !app.isSingleDoc() and context.init and context.doc if !app.isSingleDoc() and context.init and context.doc

@ -1,5 +1,7 @@
class app.views.DocList extends app.View class app.views.DocList extends app.View
@className: '_list' @className: '_list'
@attributes:
role: 'navigation'
@events: @events:
open: 'onOpen' open: 'onOpen'
@ -16,9 +18,9 @@ class app.views.DocList extends app.View
init: -> init: ->
@lists = {} @lists = {}
@addSubview @listSelect = new app.views.ListSelect @el @addSubview @listFocus = new app.views.ListFocus @el
@addSubview @listFocus = new app.views.ListFocus @el unless app.isMobile()
@addSubview @listFold = new app.views.ListFold @el @addSubview @listFold = new app.views.ListFold @el
@addSubview @listSelect = new app.views.ListSelect @el
app.on 'ready', @render app.on 'ready', @render
return return
@ -35,7 +37,10 @@ class app.views.DocList extends app.View
return return
render: => render: =>
@html @tmpl('sidebarDoc', app.docs.all()) html = ''
for doc in app.docs.all()
html += @tmpl('sidebarDoc', doc, fullName: app.docs.countAllBy('name', doc.name) > 1)
@html html
@renderDisabled() unless app.isSingleDoc() or app.disabledDocs.size() is 0 @renderDisabled() unless app.isSingleDoc() or app.disabledDocs.size() is 0
return return
@ -46,10 +51,9 @@ class app.views.DocList extends app.View
return return
renderDisabledList: -> renderDisabledList: ->
if (hidden = app.settings.get 'hideDisabled') is true if app.settings.get('hideDisabled')
@removeDisabledList() @removeDisabledList()
else else
app.settings.set 'hideDisabled', false unless hidden is false
@appendDisabledList() @appendDisabledList()
return return
@ -83,7 +87,7 @@ class app.views.DocList extends app.View
@listSelect.deselect() @listSelect.deselect()
@listFocus?.blur() @listFocus?.blur()
@listFold.reset() @listFold.reset()
@revealCurrent() if options.revealCurrent @revealCurrent() if options.revealCurrent || app.isSingleDoc()
return return
onOpen: (event) => onOpen: (event) =>
@ -147,7 +151,7 @@ class app.views.DocList extends app.View
return return
scrollTo: (model) -> scrollTo: (model) ->
$.scrollTo @find("a[href='#{model.fullPath()}']"), null, 'top', margin: 0 $.scrollTo @find("a[href='#{model.fullPath()}']"), null, 'top', margin: if app.isMobile() then 48 else 0
return return
toggleDisabled: -> toggleDisabled: ->
@ -160,13 +164,13 @@ class app.views.DocList extends app.View
return return
onClick: (event) => onClick: (event) =>
if @disabledTitle and $.hasChild(@disabledTitle, event.target) if @disabledTitle and $.hasChild(@disabledTitle, event.target) and event.target.tagName isnt 'A'
$.stopEvent(event) $.stopEvent(event)
@toggleDisabled() @toggleDisabled()
else if slug = event.target.getAttribute('data-enable') else if slug = event.target.getAttribute('data-enable')
$.stopEvent(event) $.stopEvent(event)
doc = app.disabledDocs.findBy('slug', slug) doc = app.disabledDocs.findBy('slug', slug)
app.enableDoc(doc, @onEnabled, @onEnabled) app.enableDoc(doc, @onEnabled, @onEnabled) if doc
return return
onEnabled: => onEnabled: =>

@ -1,14 +1,9 @@
class app.views.DocPicker extends app.View class app.views.DocPicker extends app.View
@className: '_list _list-picker' @className: '_list _list-picker'
@elements:
saveLink: '._sidebar-footer-save'
@events: @events:
click: 'onClick' mousedown: 'onMouseDown'
mouseup: 'onMouseUp'
@shortcuts:
enter: 'onEnter'
init: -> init: ->
@addSubview @listFold = new app.views.ListFold(@el) @addSubview @listFold = new app.views.ListFold(@el)
@ -17,20 +12,18 @@ class app.views.DocPicker extends app.View
activate: -> activate: ->
if super if super
@render() @render()
@findByTag('input')?.focus() $.on @el, 'focus', @onDOMFocus, true
app.appCache?.on 'progress', @onAppCacheProgress
$.on @el, 'focus', @onFocus, true
return return
deactivate: -> deactivate: ->
if super if super
@empty() @empty()
app.appCache?.off 'progress', @onAppCacheProgress $.off @el, 'focus', @onDOMFocus, true
$.off @el, 'focus', @onFocus, true @focusEl = null
return return
render: -> render: ->
html = '' html = @tmpl('docPickerHeader')
docs = app.docs.all().concat(app.disabledDocs.all()...) docs = app.docs.all().concat(app.disabledDocs.all()...)
while doc = docs.shift() while doc = docs.shift()
@ -40,12 +33,9 @@ class app.views.DocPicker extends app.View
else else
html += @tmpl('sidebarLabel', doc, checked: app.docs.contains(doc)) html += @tmpl('sidebarLabel', doc, checked: app.docs.contains(doc))
@html html + @tmpl('sidebarPickerNote') + @tmpl('sidebarSave') @html html + @tmpl('docPickerNote')
@refreshElements()
@delay -> # trigger animation $.requestAnimationFrame => @findByTag('input')?.focus()
@el.offsetWidth
@addClass '_in'
return return
renderVersions: (docs) -> renderVersions: (docs) ->
@ -65,38 +55,34 @@ class app.views.DocPicker extends app.View
super super
return return
save: ->
unless @saving
@saving = true
docs = @getSelectedDocs()
app.settings.setDocs(docs)
@saveLink.textContent = if app.appCache then 'Downloading\u2026' else 'Saving\u2026'
disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1)
disabledDocs.uninstall ->
app.db.migrate()
app.reload()
return
getSelectedDocs: -> getSelectedDocs: ->
for input in @findAllByTag 'input' when input?.checked for input in @findAllByTag 'input' when input?.checked
input.name input.name
onClick: (event) => onMouseDown: =>
return if event.which isnt 1 @mouseDown = Date.now()
if event.target is @saveLink
$.stopEvent(event)
@save()
return return
onFocus: (event) -> onMouseUp: =>
$.scrollTo event.target.parentNode, null, 'continuous', bottomGap: 2 @mouseUp = Date.now()
onEnter: =>
@save()
return return
onAppCacheProgress: (event) => onDOMFocus: (event) =>
if event.lengthComputable target = event.target
percentage = Math.round event.loaded * 100 / event.total if target.tagName is 'INPUT'
@saveLink.textContent = "Downloading\u2026 (#{percentage}%)" unless (@mouseDown and Date.now() < @mouseDown + 100) or (@mouseUp and Date.now() < @mouseUp + 100)
$.scrollTo target.parentNode, null, 'continuous'
else if target.classList.contains(app.views.ListFold.targetClass)
target.blur()
unless @mouseDown and Date.now() < @mouseDown + 100
if @focusEl is $('input', target.nextElementSibling)
@listFold.close(target) if target.classList.contains(app.views.ListFold.activeClass)
prev = target.previousElementSibling
prev = prev.previousElementSibling until prev.tagName is 'LABEL' or prev.classList.contains(app.views.ListFold.targetClass)
prev = $.makeArray($$('input', prev.nextElementSibling)).pop() if prev.classList.contains(app.views.ListFold.activeClass)
@delay -> prev.focus()
else
@listFold.open(target) unless target.classList.contains(app.views.ListFold.activeClass)
@delay -> $('input', target.nextElementSibling).focus()
@focusEl = target
return return

@ -15,8 +15,8 @@ class app.views.Results extends app.View
return return
init: -> init: ->
@addSubview @listFocus = new app.views.ListFocus @el
@addSubview @listSelect = new app.views.ListSelect @el @addSubview @listSelect = new app.views.ListSelect @el
@addSubview @listFocus = new app.views.ListFocus @el unless app.isMobile()
@search @search
.on 'results', @onResults .on 'results', @onResults
@ -42,7 +42,7 @@ class app.views.Results extends app.View
return return
focusFirst: -> focusFirst: ->
@listFocus?.focus @el.firstElementChild @listFocus?.focusOnNextFrame @el.firstElementChild unless app.isMobile()
return return
openFirst: -> openFirst: ->
@ -65,4 +65,4 @@ class app.views.Results extends app.View
if slug = event.target.getAttribute('data-enable') if slug = event.target.getAttribute('data-enable')
$.stopEvent(event) $.stopEvent(event)
doc = app.disabledDocs.findBy('slug', slug) doc = app.disabledDocs.findBy('slug', slug)
app.enableDoc(doc, @onDocEnabled.bind(@, doc), $.noop) app.enableDoc(doc, @onDocEnabled.bind(@, doc), $.noop) if doc

@ -3,59 +3,90 @@ class app.views.Sidebar extends app.View
@events: @events:
focus: 'onFocus' focus: 'onFocus'
select: 'onSelect'
click: 'onClick' click: 'onClick'
@routes:
after: 'afterRoute'
@shortcuts: @shortcuts:
altR: 'onAltR' altR: 'onAltR'
escape: 'onEscape' escape: 'onEscape'
init: -> init: ->
@addSubview @hover = new app.views.SidebarHover @el unless app.isMobile() or $.isTouchScreen() @addSubview @hover = new app.views.SidebarHover @el unless app.isMobile()
@addSubview @search = new app.views.Search @addSubview @search = new app.views.Search
@search @search
.on 'searching', @showResults .on 'searching', @onSearching
.on 'clear', @showDocList .on 'clear', @onSearchClear
.scope .scope
.on 'change', @onScopeChange .on 'change', @onScopeChange
@results = new app.views.Results @, @search @results = new app.views.Results @, @search
@docList = new app.views.DocList @docList = new app.views.DocList
@docPicker = new app.views.DocPicker unless app.isSingleDoc()
app.on 'ready', @showDocList app.on 'ready', @onReady
$.on document, 'click', @onGlobalClick if @docPicker
$.on document.documentElement, 'mouseleave', (event) => @display() if event.clientX < 10
$.on document.documentElement, 'mouseenter', => @resetDisplay(forceNoHover: false)
return
display: ->
@addClass 'show'
return
resetDisplay: (options = {}) ->
return unless @hasClass 'show'
@removeClass 'show'
unless options.forceNoHover is false or @hasClass 'no-hover'
@addClass 'no-hover'
$.on window, 'mousemove', @resetHoverOnMouseMove
return return
show: (view) -> resetHoverOnMouseMove: =>
$.off window, 'mousemove', @resetHoverOnMouseMove
$.requestAnimationFrame @resetHover
resetHover: =>
@removeClass 'no-hover'
showView: (view) ->
unless @view is view unless @view is view
@hover?.hide() @hover?.hide()
@saveScrollPosition() @saveScrollPosition()
@view?.deactivate() @view?.deactivate()
@html @view = view @view = view
@append @tmpl('sidebarSettings') if @view is @docList and @docPicker @render()
@view.activate() @view.activate()
@restoreScrollPosition() @restoreScrollPosition()
if view is @docPicker then @search.disable() else @search.enable()
return return
showDocList: (reset) => render: ->
@show @docList @html @view
if reset is true
@docList.reset(revealCurrent: true)
@search.reset()
return return
showDocPicker: => showDocList: ->
@show @docPicker @showView @docList
return return
showResults: => showResults: =>
@show @results @display()
@showView @results
return return
reset: -> reset: ->
@showDocList true @display()
@showDocList()
@docList.reset()
@search.reset()
return
onReady: =>
@view = @docList
@render()
@view.activate()
return return
onScopeChange: (newDoc, previousDoc) => onScopeChange: (newDoc, previousDoc) =>
@ -80,41 +111,50 @@ class app.views.Sidebar extends app.View
@el.scrollTop = 0 @el.scrollTop = 0
return return
onSearching: =>
@showResults()
return
onSearchClear: =>
@resetDisplay()
@showDocList()
return
onFocus: (event) => onFocus: (event) =>
$.scrollTo event.target, @el, 'continuous', bottomGap: 2 @display()
$.scrollTo event.target, @el, 'continuous', bottomGap: 2 unless event.target is @el
return return
onClick: (event) => onSelect: =>
return if event.which isnt 1 @resetDisplay()
if event.target.hasAttribute? 'data-reset-list'
$.stopEvent(event)
@reset()
else if event.target.hasAttribute? 'data-light'
$.stopEvent(event)
app.document.toggleLight()
else if event.target.hasAttribute? 'data-layout'
$.stopEvent(event)
app.document.toggleLayout()
return return
onGlobalClick: (event) => onClick: (event) =>
return if event.which isnt 1 return if event.which isnt 1
if event.target.hasAttribute? 'data-pick-docs' if event.target.hasAttribute? 'data-reset-list'
$.stopEvent(event) $.stopEvent(event)
@showDocPicker() @onAltR()
else if @view is @docPicker
@showDocList() unless $.hasChild @el, event.target
return return
onAltR: => onAltR: =>
@reset() @reset()
@docList.reset(revealCurrent: true)
@display()
return return
onEscape: => onEscape: =>
@reset() @reset()
@scrollToTop() @resetDisplay()
if doc = @search.getScopeDoc() then @docList.reveal(doc.toEntry()) else @scrollToTop()
return return
onDocEnabled: -> onDocEnabled: ->
@docList.onEnabled() @docList.onEnabled()
@reset() @reset()
return
afterRoute: (name, context) =>
return if app.shortcuts.eventInProgress?.name is 'escape'
@reset() if not context.init and app.router.isIndex()
@resetDisplay()
return

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save