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.
**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:

@ -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
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
MAINTAINER Conor Heine <conor.heine@gmail.com>
WORKDIR /devdocs
RUN apt-get update
RUN apt-get -y install git nodejs
RUN git clone https://github.com/Thibaut/devdocs.git /devdocs
RUN gem install bundler
RUN apt-get update && \
apt-get -y install git nodejs && \
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
RUN thor docs:download --all
COPY . /devdocs
RUN thor docs:download --all && \
thor assets:compile && \
rm -rf /tmp
EXPOSE 9292
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'
ruby '2.3.0'
ruby '2.4.1'
gem 'rake'
gem 'thor'
gem 'pry', '~> 0.10.0'
gem 'activesupport', '~> 4.2', require: false
gem 'activesupport', '~> 5.0', require: false
gem 'yajl-ruby', require: false
group :app do
@ -14,7 +14,7 @@ group :app do
gem 'thin'
gem 'sprockets'
gem 'sprockets-helpers'
gem 'erubis'
gem 'erubi'
gem 'browser'
gem 'sass'
gem 'coffee-script'
@ -32,6 +32,8 @@ group :docs do
gem 'typhoeus'
gem 'nokogiri'
gem 'html-pipeline'
gem 'image_optim'
gem 'image_optim_pack', platforms: :ruby
gem 'progress_bar', require: false
gem 'unix_utils', require: false
gem 'tty-pager', require: false

@ -1,111 +1,136 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (4.2.6)
activesupport (5.1.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
backports (3.6.8)
better_errors (2.1.1)
backports (3.8.0)
better_errors (2.3.0)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
erubi (>= 1.0.0)
rack (>= 0.9.0)
browser (2.1.0)
browser (2.4.0)
coderay (1.1.1)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.10.0)
concurrent-ruby (1.0.2)
daemons (1.2.3)
erubis (2.7.0)
ethon (0.9.0)
coffee-script-source (1.12.2)
concurrent-ruby (1.0.5)
daemons (1.2.4)
erubi (1.6.1)
ethon (0.10.1)
ffi (>= 1.3.0)
eventmachine (1.2.0.1)
execjs (2.6.0)
ffi (1.9.10)
eventmachine (1.2.5)
execjs (2.7.0)
exifr (1.3.1)
ffi (1.9.18)
fspath (3.1.0)
highline (1.7.8)
html-pipeline (2.4.1)
activesupport (>= 2, < 5)
html-pipeline (2.6.0)
activesupport (>= 2)
nokogiri (>= 1.4)
i18n (0.7.0)
json (1.8.3)
i18n (0.8.6)
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)
mini_portile2 (2.0.0)
minitest (5.8.4)
multi_json (1.12.0)
nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
mini_portile2 (2.2.0)
minitest (5.10.3)
multi_json (1.12.1)
mustermann (1.0.0)
nokogiri (1.8.0)
mini_portile2 (~> 2.2.0)
options (2.3.2)
progress_bar (1.0.5)
progress (3.3.1)
progress_bar (1.1.0)
highline (~> 1.6)
options (~> 2.3.0)
pry (0.10.3)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
rack (1.6.4)
rack-protection (1.5.3)
rack (2.0.3)
rack-protection (2.0.0)
rack
rack-test (0.6.3)
rack (>= 1.0)
rake (11.1.2)
rr (1.1.2)
sass (3.4.22)
sinatra (1.4.7)
rack (~> 1.5)
rack-protection (~> 1.4)
tilt (>= 1.3, < 3)
sinatra-contrib (1.4.7)
rack-test (0.7.0)
rack (>= 1.0, < 3)
rake (12.0.0)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rr (1.2.1)
sass (3.5.1)
sass-listen (~> 4.0.0)
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)
multi_json
rack-protection
rack-test
sinatra (~> 1.4.0)
mustermann (~> 1.0)
rack-protection (= 2.0.0)
sinatra (= 2.0.0)
tilt (>= 1.3, < 3)
slop (3.6.0)
sprockets (3.6.0)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-helpers (1.2.1)
sprockets (>= 2.2)
thin (1.6.4)
thin (1.7.2)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (~> 1.0)
thor (0.19.1)
thread_safe (0.3.5)
tilt (2.0.3)
tty-pager (0.4.0)
rack (>= 1, < 3)
thor (0.19.4)
thread_safe (0.3.6)
tilt (2.0.8)
tty-pager (0.8.0)
tty-screen (~> 0.5.0)
tty-which (~> 0.1.0)
verse (~> 0.4.0)
tty-which (~> 0.3.0)
verse (~> 0.5.0)
tty-screen (0.5.0)
tty-which (0.1.0)
typhoeus (1.0.2)
tty-which (0.3.0)
typhoeus (1.1.2)
ethon (>= 0.9.0)
tzinfo (1.2.2)
tzinfo (1.2.3)
thread_safe (~> 0.1)
uglifier (3.0.0)
uglifier (3.2.0)
execjs (>= 0.3.0, < 3)
unicode-display_width (1.1.3)
unicode_utils (1.4.0)
unix_utils (0.0.15)
verse (0.4.0)
verse (0.5.0)
unicode-display_width (~> 1.1.0)
unicode_utils (~> 1.4.0)
yajl-ruby (1.2.1)
yajl-ruby (1.3.0)
PLATFORMS
ruby
DEPENDENCIES
activesupport (~> 4.2)
activesupport (~> 5.0)
better_errors
browser
coffee-script
erubis
erubi
html-pipeline
image_optim
image_optim_pack
minitest
nokogiri
progress_bar
@ -127,5 +152,8 @@ DEPENDENCIES
unix_utils
yajl-ruby
RUBY VERSION
ruby 2.4.1p111
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 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
gem install bundler
bundle install
thor docs:download --default
rackup
bundle exec thor docs:download --default
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.
@ -130,6 +130,8 @@ thor assets:compile # Compile assets (not required in development mode)
thor assets:clean # Clean old assets
```
If multiple versions of Ruby are installed on your system, commands must be run through `bundle exec`.
## Contributing
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 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.

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

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

@ -1,7 +1,7 @@
app.config =
db_filename: 'db.json'
default_docs: <%= App.default_docs.to_json %>
docs_host: '<%= App.docs_host %>'
docs_origin: '<%= App.docs_origin %>'
env: '<%= App.environment %>'
history_cache_size: 10
index_filename: 'index.json'
@ -11,3 +11,5 @@ app.config =
search_param: 'q'
sentry_dsn: '<%= App.sentry_dsn %>'
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
NAME = 'docs'
VERSION = 15
constructor: ->
@versionMultipler = if $.isIE() then 1e5 else 1e9
@useIndexedDB = @useIndexedDB()
@appVersion = @appVersion()
@callbacks = []
db: (fn) ->
@ -13,45 +14,91 @@ class app.DB
try
@open = true
req = indexedDB.open(NAME, @schemaVersion())
req = indexedDB.open(NAME, VERSION * @versionMultipler + @userVersion())
req.onsuccess = @onOpenSuccess
req.onerror = @onOpenError
req.onupgradeneeded = @onUpgradeNeeded
catch
@onOpenError()
catch error
@fail 'exception', error
return
onOpenSuccess: (event) =>
try
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
db = event.target.result
@runCallbacks(db)
@open = false
db.close()
if db.objectStoreNames.length is 0
try 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
onOpenError: (event) =>
event?.preventDefault()
event.preventDefault()
@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'
@reset()
@db()
app.onQuotaExceeded()
fail: (reason, error) ->
@cachedDocs = null
@useIndexedDB = false
@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
@useIndexedDB = false
@reason or= 'cant_open'
@runCallbacks()
@setUserVersion actualVersion - VERSION * @versionMultipler
@db()
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) ->
fn(db) while fn = @callbacks.shift()
return
@ -62,7 +109,7 @@ class app.DB
objectStoreNames = $.makeArray(db.objectStoreNames)
unless $.arrayDelete(objectStoreNames, 'docs')
db.createObjectStore('docs')
try db.createObjectStore('docs')
for doc in app.docs.all() when not $.arrayDelete(objectStoreNames, doc.slug)
try db.createObjectStore(doc.slug)
@ -71,7 +118,7 @@ class app.DB
try db.deleteObjectStore(name)
return
store: (doc, data, onSuccess, onError) ->
store: (doc, data, onSuccess, onError, _retry = true) ->
@db (db) =>
unless db
onError()
@ -82,9 +129,15 @@ class app.DB
@cachedDocs?[doc.slug] = doc.mtime
onSuccess()
return
txn.onerror = (event) ->
txn.onerror = (event) =>
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
store = txn.objectStore(doc.slug)
@ -96,7 +149,7 @@ class app.DB
return
return
unstore: (doc, onSuccess, onError) ->
unstore: (doc, onSuccess, onError, _retry = true) ->
@db (db) =>
unless db
onError()
@ -109,14 +162,20 @@ class app.DB
return
txn.onerror = (event) ->
event.preventDefault()
onError(event)
if txn.error?.name is 'NotFoundError' and _retry
@migrate()
setTimeout =>
@unstore(doc, onSuccess, onError, false)
, 0
else
onError(event)
return
store = txn.objectStore(doc.slug)
store.clear()
store = txn.objectStore('docs')
store.delete(doc.slug)
store = txn.objectStore(doc.slug)
store.clear()
return
return
@ -227,9 +286,11 @@ class app.DB
@cachedDocs = {}
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) =>
return unless cursor = event.target.result
@cachedDocs[cursor.key] = cursor.value
@ -240,6 +301,45 @@ class app.DB
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) ->
@useIndexedDB and (not @cachedDocs or @cachedDocs[entry.doc.slug])
@ -274,11 +374,9 @@ class app.DB
app.settings.set('schema', @userVersion() + 1)
return
schemaVersion: ->
@appVersion * 10 + @userVersion()
setUserVersion: (version) ->
app.settings.set('schema', version)
return
userVersion: ->
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
@routes: [
['*', 'before' ]
['/', 'root' ]
['/offline', 'offline' ]
['/about', 'about' ]
['/news', 'news' ]
['/help', 'help' ]
['/:doc-:type/', 'type' ]
['/:doc/', 'doc' ]
['/:doc/:path(*)', 'entry' ]
['*', 'notFound']
['*', 'before' ]
['/', 'root' ]
['/settings', 'settings' ]
['/offline', 'offline' ]
['/about', 'about' ]
['/news', 'news' ]
['/help', 'help' ]
['/:doc-:type/', 'type' ]
['/:doc/', 'doc' ]
['/:doc/:path(*)', 'entry' ]
['*', 'notFound' ]
]
constructor: ->
@ -33,19 +34,24 @@ class app.Router
return
before: (context, next) ->
previousContext = @context
@context = context
@trigger 'before', context
next()
return
if res = next()
@context = previousContext
return res
else
return
doc: (context, next) ->
if doc = app.docs.findBySlug(context.params.doc) or app.disabledDocs.findBySlug(context.params.doc)
context.doc = doc
context.entry = doc.toEntry()
@triggerRoute 'entry'
return
else
next()
return
return next()
type: (context, next) ->
doc = app.docs.findBySlug(context.params.doc)
@ -54,9 +60,9 @@ class app.Router
context.doc = doc
context.type = type
@triggerRoute 'type'
return
else
next()
return
return next()
entry: (context, next) ->
doc = app.docs.findBySlug(context.params.doc)
@ -65,32 +71,39 @@ class app.Router
context.doc = doc
context.entry = entry
@triggerRoute 'entry'
return
else
next()
return
return next()
root: ->
if app.isSingleDoc()
setTimeout (-> window.location = '/'), 0
else
@triggerRoute 'root'
return '/' if app.isSingleDoc()
@triggerRoute 'root'
return
settings: (context) ->
return "/#/#{context.path}" if app.isSingleDoc()
@triggerRoute 'settings'
return
offline: ->
offline: (context)->
return "/#/#{context.path}" if app.isSingleDoc()
@triggerRoute 'offline'
return
about: (context) ->
return "/#/#{context.path}" if app.isSingleDoc()
context.page = 'about'
@triggerRoute 'page'
return
news: (context) ->
return "/#/#{context.path}" if app.isSingleDoc()
context.page = 'news'
@triggerRoute 'page'
return
help: (context) ->
return "/#/#{context.path}" if app.isSingleDoc()
context.page = 'help'
@triggerRoute 'page'
return
@ -99,15 +112,15 @@ class app.Router
@triggerRoute 'notFound'
return
isRoot: ->
location.pathname is '/'
isIndex: ->
@context?.path is '/' or (app.isSingleDoc() and @context?.entry?.isIndex())
setInitialPath: ->
# Remove superfluous forward slashes at the beginning of the path
if (path = location.pathname.replace /^\/{2,}/g, '/') isnt location.pathname
page.replace path + location.search + location.hash, null, true
if @isRoot()
if location.pathname is '/'
if path = @getInitialPathFromHash()
page.replace path + location.search, null, true
else if path = @getInitialPathFromCookie()

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

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

@ -2,7 +2,7 @@ class app.Shortcuts
$.extend @prototype, Events
constructor: ->
@isWindows = $.isWindows()
@isMac = $.isMac()
@start()
start: ->
@ -15,11 +15,15 @@ class app.Shortcuts
$.off document, 'keypress', @onKeypress
return
swapArrowKeysBehavior: ->
app.settings.get('arrowScroll')
showTip: ->
app.showTip('KeyNav')
@showTip = null
onKeydown: (event) =>
return if @buggyEvent(event)
result = if event.ctrlKey or event.metaKey
@handleKeydownSuperEvent event unless event.altKey or event.shiftKey
else if event.shiftKey
@ -33,12 +37,15 @@ class app.Shortcuts
return
onKeypress: (event) =>
return if @buggyEvent(event)
unless event.ctrlKey or event.metaKey
result = @handleKeypressEvent event
event.preventDefault() if result is false
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)
@trigger 'typing'
return
@ -50,8 +57,9 @@ class app.Shortcuts
@trigger 'enter'
when 27
@trigger 'escape'
false
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'
false
when 33
@ -59,9 +67,9 @@ class app.Shortcuts
when 34
@trigger 'pageDown'
when 35
@trigger 'end'
@trigger 'pageBottom' unless event.target.form
when 36
@trigger 'home'
@trigger 'pageTop' unless event.target.form
when 37
@trigger 'left' unless event.target.value
when 38
@ -74,27 +82,36 @@ class app.Shortcuts
@trigger 'down'
@showTip?()
false
when 191
unless event.target.form
@trigger 'typing'
false
handleKeydownSuperEvent: (event) ->
switch event.which
when 13
@trigger 'superEnter'
when 37
unless @isWindows
if @isMac
@trigger 'superLeft'
false
when 38
@trigger 'home'
@trigger 'pageTop'
false
when 39
unless @isWindows
if @isMac
@trigger 'superRight'
false
when 40
@trigger 'end'
@trigger 'pageBottom'
false
when 188
@trigger 'preferences'
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
@trigger 'typing'
return
@ -112,19 +129,21 @@ class app.Shortcuts
@trigger 'altDown'
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
when 9
@trigger 'altRight', event
when 37
if @isWindows
unless @isMac
@trigger 'superLeft'
false
when 38
@trigger 'altUp'
false
when 39
if @isWindows
unless @isMac
@trigger 'superRight'
false
when 40
@ -135,6 +154,9 @@ class app.Shortcuts
when 71
@trigger 'altG'
false
when 79
@trigger 'altO'
false
when 82
@trigger 'altR'
false
@ -148,3 +170,12 @@ class app.Shortcuts
false
else
@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: ->
@lastCheck = Date.now()
$.on window, 'focus', @checkForUpdate
$.on window, 'focus', @onFocus
app.appCache.on 'updateready', @onUpdateReady if app.appCache
@checkDocs()
setTimeout @checkDocs, 0
check: ->
if app.appCache
@ -21,8 +21,8 @@ class app.UpdateChecker
new app.views.Notif 'UpdateReady', autoHide: null
return
checkDocs: ->
if app.settings.get('autoUpdate')
checkDocs: =>
unless app.settings.get('manualUpdate')
app.docs.updateInBackground()
else
app.docs.checkForUpdates (i) => @onDocsUpdateReady() if i > 0

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

@ -48,3 +48,8 @@ class app.Collection
findAllBy: (attr, 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) ->
@findBy('slug', slug) or @findBy('slug_without_version', slug)
NORMALIZE_VERSION_RGX = /\.(\d)$/
NORMALIZE_VERSION_SUB = '.0$1'
sort: ->
@models.sort (a, b) ->
a = a.name.toLowerCase()
b = b.name.toLowerCase()
if a < b then -1 else if a > b then 1 else 0
if a.name is b.name
if not a.version or a.version.replace(NORMALIZE_VERSION_RGX, NORMALIZE_VERSION_SUB) > b.version.replace(NORMALIZE_VERSION_RGX, NORMALIZE_VERSION_SUB)
-1
else
1
else if a.name.toLowerCase() > b.name.toLowerCase()
1
else
-1
# Load models concurrently.
# 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.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) ->
if GUIDES_RGX.test(type.name)
0
else if APPENDIX_RGX.test(type.name)
2
else
1

@ -70,13 +70,16 @@ app.Searcher.prototype = _proto
# View tree
#
@viewTree = (view = app.document, level = 0) ->
console.log "%c #{Array(level + 1).join(' ')}#{view.constructor.name}: #{view.activated}",
@viewTree = (view = app.document, level = 0, visited = []) ->
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')
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
@viewTree(value, level + 1)
@viewTree(value, level + 1, visited)
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

@ -29,6 +29,7 @@ ajax.defaults =
# data
# error
# headers
# progress
# success
# url
@ -54,6 +55,7 @@ applyCallbacks = (xhr, options) ->
return unless options.async
xhr.timer = setTimeout onTimeout.bind(undefined, xhr, options), options.timeout * 1000
xhr.onprogress = options.progress if options.progress
xhr.onreadystatechange = ->
if xhr.readyState is 4
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...) ->
@eventInProgress = { name: event, args: args }
if callbacks = @_callbacks?[event]
callback? args... for callback in callbacks.slice(0)
@eventInProgress = null
@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
* Public License, v. 2.0, a copy of which may be obtained at:

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

@ -38,10 +38,15 @@ page.stop = ->
page.show = (path, state) ->
return if path is currentState?.path
context = new Context(path, state)
previousState = currentState
currentState = context.state
page.dispatch(context)
context.pushState()
track()
if res = page.dispatch(context)
currentState = previousState
location.assign(res)
else
context.pushState()
updateCanonicalLink()
track()
context
page.replace = (path, state, skipDispatch, init) ->
@ -50,16 +55,22 @@ page.replace = (path, state, skipDispatch, init) ->
currentState = context.state
page.dispatch(context) unless skipDispatch
context.replaceState()
track() unless init or skipDispatch
updateCanonicalLink()
track() unless skipDispatch
context
page.dispatch = (context) ->
i = 0
next = ->
fn(context, next) if fn = callbacks[i++]
return
next()
return
res = fn(context, next) if fn = callbacks[i++]
return res
return next()
page.canGoBack = ->
not Context.isIntialState(currentState)
page.canGoForward = ->
not Context.isLastState(currentState)
currentPath = ->
location.pathname + location.search + location.hash
@ -69,6 +80,12 @@ class Context
@sessionId: Date.now()
@stateId: 0
@isIntialState: (state) ->
state.id == 0
@isLastState: (state) ->
state.id == @stateId - 1
@isInitialPopState: (state) ->
state.path is @initialPath and @stateId is 1
@ -102,10 +119,9 @@ class Route
(context, next) =>
if @match context.pathname, params = []
context.params = params
fn(context, next)
return fn(context, next)
else
next()
return
return next()
match: (path, params) ->
return unless matchData = @regexp.exec(path)
@ -159,13 +175,24 @@ onclick = (event) ->
if link and not link.target and isSameOrigin(link.href)
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
isSameOrigin = (url) ->
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 = ->
ga?('send', 'pageview', location.pathname + location.search + location.hash)
_gauges?.push(['track'])
tracker.call() for tracker in trackers
return

@ -167,27 +167,32 @@ $.scrollTo = (el, parent, position = 'center', options = {}) ->
return unless parent
parentHeight = parent.clientHeight
return unless parent.scrollHeight > parentHeight
parentScrollHeight = parent.scrollHeight
return unless parentScrollHeight > parentHeight
top = $.offset(el, parent).top
offsetTop = parent.firstElementChild.offsetTop
switch position
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'
parent.scrollTop = top - Math.round(parentHeight / 2 - el.offsetHeight / 2)
when 'continuous'
scrollTop = parent.scrollTop
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
# ancestor, move it near the top with a gap = options.topGap * target's height.
if top <= scrollTop + height * (options.topGap or 1)
parent.scrollTop = top - height * (options.topGap or 1)
if top - offsetTop <= scrollTop + 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
# 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)
parent.scrollTop = top - parentHeight + height * ((options.bottomGap or 1) + 1)
else if top + offsetBottom >= scrollTop + parentHeight - height * ((options.bottomGap or 1) + 1)
parent.scrollTop = top + offsetBottom - parentHeight + height * ((options.bottomGap or 1) + 1)
return
$.scrollToWithImageLock = (el, parent, args...) ->
@ -223,6 +228,36 @@ $.lockScroll = (el, fn) ->
fn()
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
#
@ -278,6 +313,19 @@ $.classify = (string) ->
string[i] = substr[0].toUpperCase() + substr[1..]
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
#
@ -285,17 +333,38 @@ $.classify = (string) ->
$.noop = ->
$.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
$.isTouchScreen = ->
typeof ontouchstart isnt 'undefined'
$.isWindows = ->
navigator.platform?.indexOf('Win') >= 0
isMac = null
$.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 =
className: 'highlight'

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

@ -5,8 +5,13 @@ class app.models.Entry extends app.Model
constructor: ->
super
@text = app.Searcher.normalizeString(@name)
@text = applyAliases(@text)
@text = applyAliases(app.Searcher.normalizeString(@name))
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: ->
@doc.fullPath if @isIndex() then '' else @path
@ -45,8 +50,9 @@ class app.models.Entry extends app.Model
return string
@ALIASES = ALIASES =
'angular': 'ng'
'angular.js': 'ng'
'backbone': 'bb'
'backbone.js': 'bb'
'c++': 'cpp'
'coffeescript': 'cs'
'elixir': 'ex'
@ -59,10 +65,16 @@ class app.models.Entry extends app.Model
'markdown': 'md'
'modernizr': 'mdr'
'moment.js': 'mt'
'openjdk': 'java'
'nginx': 'ngx'
'numpy': 'np'
'pandas': 'pd'
'postgresql': 'pg'
'python': 'py'
'ruby.on.rails': 'ror'
'ruby': 'rb'
'rust': 'rs'
'sass': 'scss'
'tensorflow': 'tf'
'typescript': 'ts'
'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",
"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>"
], [
"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",
"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>"
], [
"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>)."
], [
"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",
"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"
], [
"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",
"<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",
"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"
], [
"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",
"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",
"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",
"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."
], [
"2013-08-13",
"New <a href=\"/angular/\">Angular.js</a> documentation"
"New <a href=\"/angularjs/\">Angular.js</a> documentation"
], [
"2013-08-11",
"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
"""<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 = ->
error """ Page not found. """,
@ -19,22 +19,37 @@ app.templates.pageLoadError = ->
app.templates.bootError = ->
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. """
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
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'
""" Although your browser appears to support it, DevDocs couldn't open the database.<br>
This could be because you're browsing in private mode and have disallowed offline storage on the domain. """
when 'apple'
""" Unfortunately Safari's implementation of IndexedDB is <a href="https://bugs.webkit.org/show_bug.cgi?id=136937">badly broken</a>.<br>
This message will automatically go away when Apple fix their code. """
error """ Offline mode is unavailable. """,
""" DevDocs requires IndexedDB to cache documentations for offline access.<br>#{reason} """
""" An error occured when trying to open the IndexedDB database:<br>
<code class="_label">#{exception.name}: #{exception.message}</code><br>
This could be because you're browsing in private mode or have disallowed offline storage on the domain. """
when 'version'
""" 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. """
when 'empty'
""" 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 = """
<div class="_fail">

@ -1,9 +1,9 @@
notice = (text) -> """<p class="_notice-text">#{text}</p>"""
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>). """
app.templates.disabledDocNotice = ->
notice """ <strong>This documentation is currently disabled.</strong>
To enable it, click <a class="_notice-link" data-pick-docs>Select documentation</a>. """
notice """ <strong>This documentation is disabled.</strong>
To enable it, go to <a href="/settings" class="_notice-link">Preferences</a>. """

@ -1,24 +1,28 @@
notif = (title, html) ->
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) ->
notif title, """<p class="_notif-text">#{message}"""
app.templates.notifUpdateReady = ->
textNotif """ DevDocs has been updated. """,
""" <a href="javascript:location='/'">Reload the page</a> to use the new version. """
textNotif """<span data-behavior="reboot">DevDocs has been updated.</span>""",
"""<span data-behavior="reboot"><a href="#" data-behavior="reboot">Reload the page</a> to use the new version.</span>"""
app.templates.notifError = ->
textNotif """ Oops, an error occured. """,
""" Try <a href="javascript:app.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>
You can also report this issue on <a href="https://github.com/Thibaut/devdocs/issues/new" target="_blank">GitHub</a>. """
""" Try <a href="#" data-behavior="hard-reload">reloading</a>, and if the problem persists,
<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" rel="noopener">GitHub</a>. """
app.templates.notifQuotaExceeded = ->
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. """
app.templates.notifCookieBlocked = ->
textNotif """ Please enable cookies. """,
""" DevDocs will not work properly if cookies are disabled. """
app.templates.notifInvalidLocation = ->
textNotif """ DevDocs must be loaded from #{app.config.production_host} """,
""" Otherwise things are likely to break. """
@ -43,16 +47,16 @@ app.templates.notifUpdates = (docs, disabledDocs) ->
for doc in disabledDocs
html += "<li>#{doc.name}"
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>'
notif 'Updates', "#{html}</div>"
app.templates.notifShare = ->
textNotif """ Hi there! """,
""" 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/re" target="_blank">Reddit</a>, etc.<br>Thanks :) """
""" 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" 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" rel="noopener">Reddit</a>, etc.<br>Thanks :) """
app.templates.notifUpdateDocs = ->
textNotif """ Documentation updates available. """,

@ -1,21 +1,21 @@
app.templates.aboutPage = -> """
<div class="_toc">
<nav class="_toc" role="directory">
<h3 class="_toc-title">Table of Contents</h3>
<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="#plugins">Plugins</a>
<li><a href="#faq">FAQ</a>
<li><a href="#credits">Credits</a>
<li><a href="#privacy">Privacy Policy</a>
</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.
<ul>
<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>
<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>
<p>To keep up-to-date with the latest news:
<ul>
@ -26,26 +26,26 @@ app.templates.aboutPage = -> """
<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>
<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>
<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="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://www.heroku.com">Heroku</a> and <a href="http://newrelic.com">New Relic</a> for providing awesome free service
<li>Daniel Bruce for the <a href="http://www.entypo.com">Entypo</a> pictograms
<li><a href="http://www.jeremykratz.com/">Jeremy Kratz</a> for the C/C++ logo
<li><a href="https://chrome.google.com/webstore/detail/devdocs/mnfehgbmkapmjnhcnbodoamcioleeooe">Chrome web app</a>
<li><a href="https://github.com/egoist/devdocs-app">Desktop app</a>
<li><a href="https://sublime.wbond.net/packages/DevDocs">Sublime Text package</a>
<li><a href="https://atom.io/packages/devdocs">Atom package</a>
<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>
<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>
<h2 class="_lined-heading" id="faq">Questions & Answers</h2>
<h2 class="_block-heading" id="faq">Questions & Answers</h2>
<dl>
<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>
@ -56,25 +56,28 @@ app.templates.aboutPage = -> """
</dl>
<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>
<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="_block-heading" id="credits">Credits</h2>
<h2 class="_lined-heading" id="plugins">Plugins and Extensions</h2>
<p><strong>Special thanks to:</strong>
<ul>
<li><a href="https://chrome.google.com/webstore/detail/devdocs/mnfehgbmkapmjnhcnbodoamcioleeooe">Chrome web app</a>
<li><a href="https://sublime.wbond.net/packages/DevDocs">Sublime Text plugin</a>
<li><a href="https://atom.io/packages/devdocs">Atom plugin</a>
<li><a href="https://github.com/gruehle/dev-docs-viewer">Brackets extension</a>
<li><a href="https://github.com/xuchunyang/DevDocs.el">Emacs Package</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="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://www.heroku.com">Heroku</a> and <a href="http://newrelic.com">New Relic</a> for providing awesome free service
<li>Daniel Bruce for the <a href="http://www.entypo.com">Entypo</a> pictograms
<li><a href="http://www.jeremykratz.com/">Jeremy Kratz</a> for the C/C++ logo
</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>
<li><a href="http://devdocs.io">devdocs.io</a> ("App") is operated by Thibaut Courouble ("We").
<li>We do not collect personal information.
@ -86,25 +89,40 @@ app.templates.aboutPage = -> """
"""
credits = [
[ 'Angular.js',
'2010-2016 Google, Inc.',
[ 'Angular<br>Angular.js',
'2010-2017 Google, Inc.',
'CC BY',
'https://creativecommons.org/licenses/by/4.0/'
], [
'Ansible',
'2012-2016 Michael DeHaan<br>&copy; 2016 Red Hat, Inc.',
'2012-2017 Michael DeHaan<br>&copy; 2017 Red Hat, Inc.',
'GPLv3',
'https://raw.githubusercontent.com/ansible/ansible/devel/COPYING'
], [
'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',
'https://www.apache.org/licenses/LICENSE-2.0'
], [
'Async',
'2010-2017 Caolan McMahon',
'MIT',
'https://raw.githubusercontent.com/caolan/async/master/LICENSE'
], [
'Backbone.js',
'2010-2016 Jeremy Ashkenas, DocumentCloud',
'MIT',
'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',
'2016 Bower contributors',
@ -117,14 +135,14 @@ credits = [
'http://en.cppreference.com/w/Cppreference:Copyright/CC-BY-SA'
], [
'CakePHP',
'2005-2016 The Cake Software Foundation, Inc.',
'2005-2017 The Cake Software Foundation, Inc.',
'MIT',
'https://raw.githubusercontent.com/cakephp/cakephp/master/LICENSE.txt'
], [
'Chai',
'2011-2015 Jake Luer',
'2016 Chai.js Assertion Library',
'MIT',
'https://github.com/chaijs/chai/blob/master/README.md#license'
'https://raw.githubusercontent.com/chaijs/chai/master/LICENSE'
], [
'Chef&trade;',
'Chef Software, Inc.',
@ -135,39 +153,64 @@ credits = [
'Rich Hickey',
'EPL',
'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',
'2014-2016 British Columbia Institute of Technology',
'2014-2017 British Columbia Institute of Technology',
'MIT',
'https://raw.githubusercontent.com/bcit-ci/CodeIgniter/develop/license.txt'
], [
'CoffeeScript',
'2009-2015 Jeremy Ashkenas',
'2009-2017 Jeremy Ashkenas',
'MIT',
'https://raw.githubusercontent.com/jashkenas/coffee-script/master/LICENSE'
], [
'Cordova',
'2012-2016 The Apache Software Foundation',
'2012-2017 The Apache Software Foundation',
'Apache',
'https://raw.githubusercontent.com/apache/cordova-docs/master/LICENSE'
], [
'CSS<br>DOM<br>HTML<br>JavaScript<br>SVG<br>XPath',
'2005-2016 Mozilla Developer Network and individual contributors',
'CSS<br>DOM<br>HTTP<br>HTML<br>JavaScript<br>SVG<br>XPath',
'2005-2017 Mozilla Developer Network and individual contributors',
'CC BY-SA',
'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',
'2010-2016 Michael Bostock',
'2010-2017 Michael Bostock',
'BSD',
'https://raw.githubusercontent.com/mbostock/d3/master/LICENSE'
'https://raw.githubusercontent.com/d3/d3/master/LICENSE'
], [
'Django',
'Django Software Foundation and individual contributors',
'BSD',
'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',
'2005-2015 The Dojo Foundation',
'2005-2017 JS Foundation',
'BSD + AFL',
'http://dojotoolkit.org/license.html'
], [
@ -176,25 +219,40 @@ credits = [
'GPLv2',
'https://api.drupal.org/api/drupal/LICENSE.txt'
], [
'Ember.js',
'2016 Yehuda Katz, Tom Dale and Ember.js contributors',
'Electron',
'2013-2017 GitHub Inc.',
'MIT',
'https://raw.githubusercontent.com/emberjs/ember.js/master/LICENSE'
'https://raw.githubusercontent.com/electron/electron/master/LICENSE'
], [
'Elixir',
'2012-2016 Plataformatec',
'2012-2017 Plataformatec',
'Apache',
'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',
'1999-2016 Ericsson AB',
'2010-2017 Ericsson AB',
'Apache',
'https://raw.githubusercontent.com/erlang/otp/maint/LICENSE.txt'
], [
'Express',
'2016 StrongLoop, IBM, and other expressjs.com contributors.',
'Unknown',
'https://github.com/expressjs/expressjs.com/issues/413'
'CC BY-SA',
'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',
'Free Software Foundation',
@ -202,7 +260,7 @@ credits = [
'https://www.gnu.org/licenses/fdl-1.3.en.html'
], [
'Git',
'2005-2016 Linus Torvalds and others',
'2005-2017 Linus Torvalds and others',
'GPLv2',
'https://raw.githubusercontent.com/git/git/master/COPYING'
], [
@ -210,6 +268,11 @@ credits = [
'Google, Inc.',
'CC BY',
'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',
'GruntJS Team',
@ -225,11 +288,26 @@ credits = [
'2005-2016 Haxe Foundation',
'MIT',
'http://haxe.org/foundation/open-source.html'
], [
'Immutable.js',
'2014-2016 Facebook, Inc.',
'BSD',
'https://raw.githubusercontent.com/facebook/immutable-js/master/LICENSE'
], [
'InfluxData',
'2015 InfluxData, Inc.',
'MIT',
'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',
'Packt Publishing<br>&copy; jQuery Foundation and other contributors',
@ -245,11 +323,21 @@ credits = [
'jQuery Foundation and other contributors',
'MIT',
'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',
'Steven Sanderson, the Knockout.js team, and other contributors',
'MIT',
'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',
'Taylor Otwell',
@ -260,6 +348,11 @@ credits = [
'2009-2016 The Core Less Team',
'CC BY',
'https://creativecommons.org/licenses/by/3.0/'
], [
'Liquid',
'2005, 2006 Tobias Luetke',
'MIT',
'https://raw.githubusercontent.com/Shopify/liquid/master/LICENSE'
], [
'Lo-Dash',
'2012-2016 The Dojo Foundation',
@ -267,12 +360,17 @@ credits = [
'https://raw.githubusercontent.com/lodash/lodash/master/LICENSE.txt'
], [
'Lua',
'19942015 Lua.org, PUC-Rio',
'19942017 Lua.org, PUC-Rio',
'MIT',
'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',
'2016 Muted Solutions, LLC',
'2017 Muted Solutions, LLC',
'MIT',
'https://mutedsolutions.mit-license.org/'
], [
@ -280,11 +378,16 @@ credits = [
'2004 John Gruber',
'BSD',
'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',
'2011-2016 Meteor Development Group',
'2011-2017 Meteor Development Group, Inc.',
'MIT',
'https://raw.githubusercontent.com/meteor/meteor/master/LICENSE.txt'
'https://raw.githubusercontent.com/meteor/meteor/master/LICENSE'
], [
'Minitest',
'Ryan Davis, seattle.rb',
@ -292,17 +395,17 @@ credits = [
'https://github.com/seattlerb/minitest/blob/master/README.rdoc#license'
], [
'Mocha',
'2011-2016 TJ Holowaychuk',
'MIT',
'https://raw.githubusercontent.com/mochajs/mocha/master/LICENSE'
'2016 JS Foundation and contributors',
'CC BY',
'https://creativecommons.org/licenses/by/4.0/'
], [
'Modernizr',
'2009-2016 The Modernizr team',
'2009-2017 The Modernizr team',
'MIT',
'https://modernizr.com/license/'
], [
'Moment.js',
'2011-2016 Tim Wood, Iskren Chernev, Moment.js contributors',
'JS Foundation and other contributors',
'MIT',
'https://raw.githubusercontent.com/moment/moment/master/LICENSE'
], [
@ -312,12 +415,12 @@ credits = [
'https://github.com/LearnBoost/mongoose/blob/master/README.md#license'
], [
'nginx',
'2002-2016 Igor Sysoev<br>&copy; 2011-2016 Nginx, Inc.',
'2002-2017 Igor Sysoev<br>&copy; 2011-2017 Nginx, Inc.',
'BSD',
'http://nginx.org/LICENSE'
], [
'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',
'https://github.com/openresty/lua-nginx-module#copyright-and-license'
], [
@ -327,7 +430,7 @@ credits = [
'https://raw.githubusercontent.com/nodejs/node/master/LICENSE'
], [
'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',
'https://raw.githubusercontent.com/sparklemotion/nokogiri/master/LICENSE.txt'
], [
@ -337,22 +440,37 @@ credits = [
'https://raw.githubusercontent.com/npm/npm/master/LICENSE'
], [
'NumPy',
'2008-2016 NumPy Developers',
'2008-2017 NumPy Developers',
'NumPy',
'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',
'2010-2016 The OpenTSDB Authors',
'LGPLv2.1',
'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',
'1993-2016 Larry Wall and others',
'GPLv1',
'http://perldoc.perl.org/index-licence.html'
'https://perldoc.perl.org/index-licence.html'
], [
'Phalcon',
'2011-2015 Phalcon Framework Team',
'2011-2017 Phalcon Framework Team',
'CC BY',
'https://docs.phalconphp.com/en/latest/reference/license.html'
], [
@ -367,39 +485,49 @@ credits = [
'https://raw.githubusercontent.com/phoenixframework/phoenix/master/LICENSE.md'
], [
'PHP',
'1997-2016 The PHP Documentation Group',
'1997-2017 The PHP Documentation Group',
'CC BY',
'https://creativecommons.org/licenses/by/3.0/'
'https://secure.php.net/manual/en/copyright.php'
], [
'PHPUnit',
'2005-2016 Sebastian Bergmann',
'2005-2017 Sebastian Bergmann',
'CC BY',
'https://creativecommons.org/licenses/by/3.0/'
], [
'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',
'http://www.postgresql.org/about/licence/'
'https://www.postgresql.org/about/licence/'
], [
'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',
'https://docs.python.org/3/license.html'
], [
'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',
'https://raw.githubusercontent.com/kriskowal/q/v1/LICENSE'
'https://raw.githubusercontent.com/ramda/ramda/master/LICENSE.txt'
], [
'React, React Native, Flow, Relay',
'2013-2016 Facebook Inc.',
'2013-2017 Facebook Inc.',
'CC BY',
'https://raw.githubusercontent.com/facebook/react/master/LICENSE-docs'
], [
'Redis',
'2009-2016 Salvatore Sanfilippo',
'2009-2017 Salvatore Sanfilippo',
'CC BY-SA',
'https://creativecommons.org/licenses/by-sa/4.0/'
], [
'Redux',
'2015-2017 Dan Abramov',
'MIT',
'https://raw.githubusercontent.com/reactjs/redux/master/LICENSE.md'
], [
'RequireJS',
'jQuery Foundation and other contributors',
@ -412,17 +540,17 @@ credits = [
'https://raw.githubusercontent.com/rethinkdb/docs/master/LICENSE'
], [
'Ruby',
'1993-2016 Yukihiro Matsumoto',
'1993-2017 Yukihiro Matsumoto',
'Ruby',
'https://www.ruby-lang.org/en/about/license.txt'
], [
'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',
'https://raw.githubusercontent.com/rails/rails/master/activerecord/MIT-LICENSE'
], [
'Rust',
'2016 The Rust Project Developers',
'2010 The Rust Project Developers',
'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',
'MIT',
'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',
'2010-2016 Christian Johansen',
'2010-2017 Christian Johansen',
'BSD',
'https://raw.githubusercontent.com/cjohansen/Sinon.JS/master/LICENSE'
'https://raw.githubusercontent.com/sinonjs/sinon/master/LICENSE'
], [
'Socket.io',
'2014-2015 Automattic',
'MIT',
'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',
'2004-2016 Fabien Potencier',
'2004-2017 Fabien Potencier',
'MIT',
'https://symfony.com/doc/current/contributing/code/license.html'
], [
@ -452,9 +600,14 @@ credits = [
'http://tcl.tk/software/tcltk/license.html'
], [
'TensorFlow',
'2015 The TensorFlow Authors',
'Apache',
'https://raw.githubusercontent.com/tensorflow/tensorflow/master/LICENSE'
'2017 The TensorFlow Authors',
'CC BY',
'https://creativecommons.org/licenses/by/3.0/'
], [
'Twig',
'2009-2017 The Twig Team',
'BSD',
'https://twig.sensiolabs.org/license'
], [
'TypeScript',
'Microsoft and other contributors',
@ -467,22 +620,27 @@ credits = [
'https://raw.githubusercontent.com/jashkenas/underscore/master/LICENSE'
], [
'Vagrant',
'2010-2015 Mitchell Hashimoto',
'MIT',
'https://raw.githubusercontent.com/mitchellh/vagrant/master/LICENSE'
'2010-2017 Mitchell Hashimoto',
'MPL',
'https://raw.githubusercontent.com/mitchellh/vagrant/master/website/LICENSE.md'
], [
'Vue.js',
'2013-2016 Evan You, Vue.js contributors',
'2013-2017 Evan You, Vue.js contributors',
'MIT',
'https://raw.githubusercontent.com/vuejs/vue/master/LICENSE'
], [
'Webpack',
'2012-2016 Tobias Koppers',
'MIT',
'https://raw.githubusercontent.com/webpack/webpack/master/LICENSE'
'webpack',
'JS Foundation and other contributors',
'CC BY',
'https://creativecommons.org/licenses/by/4.0/'
], [
'Yarn',
'2016-2017 Yarn Contributors',
'BSD',
'https://raw.githubusercontent.com/yarnpkg/yarn/master/LICENSE'
], [
'Yii',
'2008-2016 by Yii Software LLC',
'2008-2017 by Yii Software LLC',
'BSD',
'https://raw.githubusercontent.com/yiisoft/yii/master/LICENSE'
]

@ -1,26 +1,27 @@
ctrlKey = if $.isMac() then 'cmd' else 'ctrl'
navKey = if $.isWindows() then 'alt' else ctrlKey
navKey = if $.isMac() then 'cmd' else 'alt'
app.templates.helpPage = """
<div class="_toc">
<nav class="_toc" role="directory">
<h3 class="_toc-title">Table of Contents</h3>
<ul class="_toc-list">
<li><a href="#search">Search</a>
<li><a href="#shortcuts">Keyboard Shortcuts</a>
<li><a href="#abbreviations">Abbreviations</a>
<li><a href="#aliases">Search Aliases</a>
</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>
The search is case-insensitive and supports fuzzy matching (for queries longer than two characters).
For example, searching <code class="_label">bgcp</code> brings up <code class="_label">background-clip</code>.<br>
Abbreviations are also supported (<a href="#abbreviations">full list</a> below).
For example, <code class="_label">$</code> is an alias for <code class="_label">jQuery</code>.
The search is case-insensitive. It supports fuzzy matching
(e.g. <code class="_label">bgcp</code> matches <code class="_label">background-clip</code>)
and aliases (<a href="#aliases">full list</a> below).
<dl>
<dt id="doc_search">Searching a single documentation
<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).
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>
@ -28,22 +29,22 @@ app.templates.helpPage = """
<dt id="url_search">Prefilling the search field
<dd>
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:
<a href="/#q=js%20date" target="_top">devdocs.io/#q=js date</a>.
<dt id="browser_search">Searching using the address bar
<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>
<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).
<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
<a href="https://support.mozilla.org/en-US/kb/how-search-from-address-bar">these instructions</a>.
</dl>
<h2 class="_lined-heading" id="shortcuts">Keyboard Shortcuts</h2>
<h3 class="_shortcuts-title">Selection</h3>
<h2 class="_block-heading" id="shortcuts">Keyboard Shortcuts</h2>
<h3 class="_shortcuts-title">Sidebar</h3>
<dl class="_shortcuts-dl">
<dt class="_shortcuts-dt">
<code class="_shortcut-code">&darr;</code>
@ -59,8 +60,11 @@ app.templates.helpPage = """
<dt class="_shortcuts-dt">
<code class="_shortcut-code">#{ctrlKey} + enter</code>
<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>
<h3 class="_shortcuts-title">Navigation</h3>
<h3 class="_shortcuts-title">Browsing</h3>
<dl class="_shortcuts-dl">
<dt class="_shortcuts-dt">
<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} + &darr;</code>
<dd class="_shortcuts-dd">Scroll to the top/bottom
</dl>
<h3 class="_shortcuts-title">Misc</h3>
<dl class="_shortcuts-dl">
<dt class="_shortcuts-dt">
<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)
</dl>
<h3 class="_shortcuts-title">App</h3>
<dl class="_shortcuts-dl">
<dt class="_shortcuts-dt">
<code class="_shortcut-code">alt + r</code>
<dd class="_shortcuts-dd">Reveal current page in sidebar
<code class="_shortcut-code">ctrl + ,</code>
<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">
<code class="_shortcut-code">alt + g</code>
<dd class="_shortcuts-dd">Search on Google
<dt class="_shortcuts-dt">
<code class="_shortcut-code">alt + s</code>
<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>
<p class="_note">
<strong>Tip:</strong> If the cursor is no longer in the search field, press backspace or
<p class="_note _note-green">
<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.
<h2 class="_lined-heading" id="abbreviations">Abbreviations</h2>
<p>Feel free to suggest new abbreviations on <a href="https://github.com/Thibaut/devdocs/issues/new">GitHub</a>.
<table class="_abbreviations">
<h2 class="_block-heading" id="aliases">Search Aliases</h2>
<table>
<tr>
<th>Word
<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>
<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])
if options.years isnt false and year isnt date.getUTCFullYear()
year = date.getUTCFullYear()
result += "<h4>#{year}</h4>"
result += """<h2 class="_block-heading">#{year}</h2>"""
result += newsItem(date, value[1..])
result

@ -2,26 +2,27 @@ app.templates.offlinePage = (docs) -> """
<h1 class="_lined-heading">Offline Documentation</h1>
<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">
<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>
<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>
<table class="_docs">
<tr>
<th>Documentation</th>
<th class="_docs-size">Size</th>
<th>Status</th>
<th>Action</th>
</tr>
#{docs}
</table>
<div class="_table">
<table class="_docs">
<tr>
<th>Documentation</th>
<th class="_docs-size">Size</th>
<th>Status</th>
<th>Action</th>
</tr>
#{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.
<h1 class="_lined-heading">Questions & Answers</h1>
<h2 class="_block-heading">Questions & Answers</h2>
<dl>
<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>
@ -29,24 +30,22 @@ app.templates.offlinePage = (docs) -> """
<dt>Can I close the tab/browser?
<dd>#{canICloseTheTab()}
<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?
<dd>In the <a href="https://github.com/Thibaut/devdocs/issues">issue tracker</a>. Thanks!
<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?
<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>
"""
canICloseTheTab = ->
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). """
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
""" 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). """
""" 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 function even when you go offline (provided you installed all the documentations beforehand). """
app.templates.offlineDoc = (doc, status) ->
outdated = doc.isOutdated(status)
@ -54,23 +53,23 @@ app.templates.offlineDoc = (doc, status) ->
html = """
<tr data-slug="#{doc.slug}"#{if outdated then ' class="_highlight"' else ''}>
<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)
"""
<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
"""
<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
"""
<td>Up-to-date</td>
<td><a data-action="uninstall">Uninstall</a></td>
<td>Up&#8209;to&#8209;date</td>
<td><button type="button" class="_btn-link" data-action="uninstall">Uninstall</button></td>
"""
html + '</tr>'

@ -3,7 +3,7 @@ app.templates.splash = """<div class="_splash-title">DevDocs</div>"""
<% if App.development? %>
app.templates.intro = """
<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>
<p>Thanks for downloading DevDocs. Here are a few things you should know:
<ol class="_intro-list">
@ -27,47 +27,39 @@ app.templates.intro = """
<% else %>
app.templates.intro = """
<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>
<p>DevDocs combines multiple API documentations in a fast, organized, and searchable interface.
Here's what you should know before you start:
<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>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>To search a specific documentation, type its name (or an abbreviation), then Tab
<li>You can search using your browser's address bar &mdash; <a href="/help#browser_search">learn how</a>
<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>The search supports fuzzy matching (e.g. "bgcp" brings up "background-clip").
<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>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>DevDocs is free and <a href="https://github.com/Thibaut/devdocs">open source</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>.
<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>And if you're new to coding, check out <a href="https://www.freecodecamp.org/">freeCodeCamp's open source curriculum</a>.
</ol>
<p>Happy coding!
</div></div>
"""
<% 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 = """
<div class="_mobile-intro">
<h2 class="_intro-title">Welcome!</h2>
<p>DevDocs combines multiple API documentations in a fast, organized, and searchable interface.
Here's what you should know before you start:
<ol class="_intro-list">
<li>To pick your docs, click <a data-pick-docs>Select documentation</a> at the bottom of the menu
<li>The search supports fuzzy matching (e.g. "bgcp" matches "background-clip")
<li>To search a specific documentation, type its name (or an abbreviation), then Space
<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>Pick your docs in the <a href="/settings">Preferences</a>.
<li>The search supports fuzzy matching.
<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>DevDocs is <a href="https://github.com/Thibaut/devdocs">open source</a>.
</ol>
<p>Happy coding!
<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>
<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>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>
"""

@ -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) ->
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 += """<span class="_path-item">#{$.escape entry.name}</span>""" if entry
html += """#{arrow}<a href="#{type.fullPath()}" class="_path-item">#{type.name}</a>""" if type
html += """#{arrow}<span class="_path-item">#{$.escape entry.name}</span>""" if entry
html

@ -1,41 +1,43 @@
templates = app.templates
arrow = """<svg class="_list-arrow"><use xlink:href="#dir-icon"/></svg>"""
templates.sidebarDoc = (doc, options = {}) ->
link = """<a href="#{doc.fullPath()}" class="_list-item _icon-#{doc.icon} """
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
link += """<span class="_list-enable" data-enable="#{doc.slug}">Enable</span>"""
else
link += """<span class="_list-arrow"></span>"""
link += arrow
link += """<span class="_list-count">#{doc.release}</span>""" if doc.release
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>"
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) ->
"""<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) ->
addons = if entry.isIndex() and app.disabledDocs.contains(entry.doc)
"""<span class="_list-enable" data-enable="#{entry.doc.slug}">Enable</span>"""
else
"""<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()
"""<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>"""
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}" tabindex="-1">#{addons}<span class="_list-text">#{$.escape entry.name}</span></a>"""
templates.sidebarNoResults = ->
html = """ <div class="_list-note">No results.</div> """
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()
html
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 = {}) ->
label = """<label class="_list-item"""
@ -47,30 +49,20 @@ templates.sidebarLabel = (doc, options = {}) ->
templates.sidebarVersionedDoc = (doc, versions, options = {}) ->
html = """<div class="_list-item _list-dir _list-rdir _icon-#{doc.icon}"""
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) ->
"""<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) ->
"""<div class="_disabled-list">#{html}</div>"""
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>"""
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>
"""
"""<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>"""
sidebarFooter = (html) -> """<div class="_sidebar-footer">#{html}</div>"""
templates.docPickerHeader = """<div class="_list-picker-head"><span>Documentation</span> <span>Enable</span></div>"""
templates.sidebarSettings = ->
sidebarFooter """
<a class="_sidebar-footer-link _sidebar-footer-light" title="Toggle light" data-light></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.docPickerNote = """
<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" rel="noopener">Vote for new 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
* Wednesday, April 24 2013 @ 2:28 AM EST
/*
* Cookies.js - 1.2.3
* https://github.com/ScottHamper/Cookies
*
* Copyright (c) 2013, Scott Hamper
* Licensed under the MIT license,
* http://www.opensource.org/licenses/MIT
* This is free and unencumbered software released into the public domain.
*/
(function (undefined) {
(function (global, undefined) {
'use strict';
var Cookies = function (key, value, options) {
return arguments.length === 1 ?
Cookies.get(key) : Cookies.set(key, value, options);
};
var factory = function (window) {
if (typeof window.document !== 'object') {
throw new Error('Cookies.js requires a `window` with a `document` object');
}
// Allows for setter injection in unit tests
Cookies._document = document;
Cookies._navigator = navigator;
var Cookies = function (key, value, options) {
return arguments.length === 1 ?
Cookies.get(key) : Cookies.set(key, value, options);
};
Cookies.defaults = {
path: '/'
};
// Allows for setter injection in unit tests
Cookies._document = window.document;
Cookies.get = function (key) {
if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) {
Cookies._renewCache();
}
// Used to ensure cookie keys do not collide with
// built-in `Object` properties
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) {
options = Cookies._getExtendedOptions(options);
options.expires = Cookies._getExpiresDate(value === undefined ? -1 : options.expires);
Cookies.defaults = {
path: '/',
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 Cookies.set(key, undefined, options);
};
return value === undefined ? undefined : decodeURIComponent(value);
};
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 {
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
return Cookies;
};
};
Cookies._isValidDate = function (date) {
return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime());
};
Cookies.expire = function (key, options) {
return Cookies.set(key, undefined, options);
};
Cookies._getExpiresDate = function (expires, now) {
now = now || new Date();
switch (typeof expires) {
case 'number': expires = new Date(now.getTime() + expires * 1000); break;
case 'string': expires = new Date(expires); break;
}
Cookies._getExtendedOptions = function (options) {
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
};
};
if (expires && !Cookies._isValidDate(expires)) {
throw new Error('`expires` parameter cannot be converted to a valid Date instance');
}
Cookies._isValidDate = function (date) {
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) {
key = encodeURIComponent(key);
value = (value + '').replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent);
options = options || {};
if (typeof expires === 'number') {
expires = expires === Infinity ?
Cookies._maxExpireDate : new Date(now.getTime() + expires * 1000);
} else if (typeof expires === 'string') {
expires = new Date(expires);
}
var cookieString = key + '=' + value;
cookieString += options.path ? ';path=' + options.path : '';
cookieString += options.domain ? ';domain=' + options.domain : '';
cookieString += options.expires ? ';expires=' + options.expires.toGMTString() : '';
cookieString += options.secure ? ';secure' : '';
if (expires && !Cookies._isValidDate(expires)) {
throw new Error('`expires` parameter cannot be converted to a valid Date instance');
}
return cookieString;
};
return expires;
};
Cookies._getCookieObjectFromString = function (documentCookie) {
var cookieObject = {};
var cookiesArray = documentCookie ? documentCookie.split('; ') : [];
Cookies._generateCookieString = function (key, value, options) {
key = key.replace(/[^#$&+\^`|]/g, encodeURIComponent);
key = key.replace(/\(/g, '%28').replace(/\)/g, '%29');
value = (value + '').replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent);
options = options || {};
for (var i = 0; i < cookiesArray.length; i++) {
var cookieKvp = Cookies._getKeyValuePairFromCookieString(cookiesArray[i]);
var cookieString = key + '=' + value;
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) {
cookieObject[cookieKvp.key] = cookieKvp.value;
return cookieString;
};
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) {
// "=" is a valid character in a cookie value according to RFC6265, so cannot `split('=')`
var separatorIndex = cookieString.indexOf('=');
Cookies._getKeyValuePairFromCookieString = function (cookieString) {
// "=" is a valid character in a cookie value according to RFC6265, so cannot `split('=')`
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
separatorIndex = separatorIndex < 0 ? cookieString.length : separatorIndex;
return {
key: decodedKey,
value: cookieString.substr(separatorIndex + 1) // Defer decoding value until accessed
};
};
return {
key: decodeURIComponent(cookieString.substr(0, separatorIndex)),
value: decodeURIComponent(cookieString.substr(separatorIndex + 1))
Cookies._renewCache = function () {
Cookies._cache = Cookies._getCacheFromString(Cookies._document.cookie);
Cookies._cachedDocumentCookie = Cookies._document.cookie;
};
};
Cookies._renewCache = function () {
Cookies._cache = Cookies._getCookieObjectFromString(Cookies._document.cookie);
Cookies._cachedDocumentCookie = Cookies._document.cookie;
};
Cookies._areEnabled = function () {
var testKey = 'cookies.js';
var areEnabled = Cookies.set(testKey, 1).get(testKey) === '1';
Cookies.expire(testKey);
return areEnabled;
};
Cookies._areEnabled = function () {
return Cookies._navigator.cookieEnabled ||
Cookies.set('cookies.js', 1).get('cookies.js') === '1';
};
Cookies.enabled = Cookies._areEnabled();
Cookies.enabled = Cookies._areEnabled();
return Cookies;
};
var cookiesExport = (global && typeof global.document === 'object') ? factory(global) : factory;
// AMD support
if (typeof define === 'function' && define.amd) {
define(function () { return Cookies; });
// CommonJS and Node.js module support.
} else if (typeof exports !== 'undefined') {
define(function () { return cookiesExport; });
// CommonJS/Node.js support
} else if (typeof exports === 'object') {
// Support Node.js specific `module.exports` (which can be a function)
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = Cookies;
if (typeof module === 'object' && typeof module.exports === 'object') {
exports = module.exports = cookiesExport;
}
// But always support CommonJS module 1.1.1 spec (`exports` cannot be a function)
exports.Cookies = Cookies;
exports.Cookies = cookiesExport;
} 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'
@shortcuts:
altUp: 'scrollStepUp'
altDown: 'scrollStepDown'
pageUp: 'scrollPageUp'
pageDown: 'scrollPageDown'
home: 'scrollToTop'
end: 'scrollToBottom'
altF: 'onAltF'
altUp: 'scrollStepUp'
altDown: 'scrollStepDown'
pageUp: 'scrollPageUp'
pageDown: 'scrollPageDown'
pageTop: 'scrollToTop'
pageBottom: 'scrollToBottom'
altF: 'onAltF'
@routes:
before: 'beforeRoute'
@ -23,11 +23,12 @@ class app.views.Content extends app.View
@scrollMap = {}
@scrollStack = []
@rootPage = new app.views.RootPage
@staticPage = new app.views.StaticPage
@offlinePage = new app.views.OfflinePage
@typePage = new app.views.TypePage
@entryPage = new app.views.EntryPage
@rootPage = new app.views.RootPage
@staticPage = new app.views.StaticPage
@settingsPage = new app.views.SettingsPage
@offlinePage = new app.views.OfflinePage
@typePage = new app.views.TypePage
@entryPage = new app.views.EntryPage
@entryPage
.on 'loading', @onEntryLoading
@ -51,6 +52,9 @@ class app.views.Content extends app.View
@addClass @constructor.loadingClass
return
isLoading: ->
@el.classList.contains @constructor.loadingClass
hideLoading: ->
@removeClass @constructor.loadingClass
return
@ -59,38 +63,45 @@ class app.views.Content extends app.View
@scrollEl.scrollTop = value or 0
return
smoothScrollTo: (value) ->
if app.settings.get('fastScroll')
@scrollTo value
else
$.smoothScroll @scrollEl, value or 0
return
scrollBy: (n) ->
@scrollEl.scrollTop += n
@smoothScrollTo @scrollEl.scrollTop + n
return
scrollToTop: =>
@scrollTo 0
@smoothScrollTo 0
return
scrollToBottom: =>
@scrollTo @scrollEl.scrollHeight
@smoothScrollTo @scrollEl.scrollHeight
return
scrollStepUp: =>
@scrollBy -50
@scrollBy -80
return
scrollStepDown: =>
@scrollBy 50
@scrollBy 80
return
scrollPageUp: =>
@scrollBy 80 - @scrollEl.clientHeight
@scrollBy 40 - @scrollEl.clientHeight
return
scrollPageDown: =>
@scrollBy @scrollEl.clientHeight - 80
@scrollBy @scrollEl.clientHeight - 40
return
scrollToTarget: ->
if @routeCtx.hash and el = @findTargetByHash @routeCtx.hash
$.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'
else
@scrollTo @scrollMap[@routeCtx.state.id]
@ -107,21 +118,28 @@ class app.views.Content extends app.View
onEntryLoading: =>
@showLoading()
if @scrollToTargetTimeout
clearTimeout @scrollToTargetTimeout
@scrollToTargetTimeout = null
return
onEntryLoaded: =>
@hideLoading()
if @scrollToTargetTimeout
clearTimeout @scrollToTargetTimeout
@scrollToTargetTimeout = null
@scrollToTarget()
return
beforeRoute: (context) =>
@cacheScrollPosition()
@routeCtx = context
@delay @scrollToTarget
@scrollToTargetTimeout = @delay @scrollToTarget
return
cacheScrollPosition: ->
return if not @routeCtx or @routeCtx.hash
return if @routeCtx.path is '/'
unless @scrollMap[@routeCtx.state.id]?
@scrollStack.push @routeCtx.state.id
@ -139,6 +157,8 @@ class app.views.Content extends app.View
@show @entryPage
when 'type'
@show @typePage
when 'settings'
@show @settingsPage
when 'offline'
@show @offlinePage
else

@ -5,6 +5,9 @@ class app.views.EntryPage extends app.View
@events:
click: 'onClick'
@shortcuts:
altO: 'onAltO'
@routes:
before: 'beforeRoute'
@ -37,14 +40,23 @@ class app.views.EntryPage extends app.View
if app.disabledDocs.findBy 'slug', @entry.doc.slug
@hiddenView = new app.views.HiddenPage @el, @entry
@delay @polyfillMathML
@trigger 'loaded'
return
CLIPBOARD_LINK = '<a class="_pre-clip" title="Copy to clipboard" tabindex="-1"></a>'
addClipboardLinks: ->
for el in @findAllByTag('pre')
el.insertAdjacentHTML('afterbegin', CLIPBOARD_LINK)
unless @clipBoardLink
@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
LINKS =
@ -77,8 +89,8 @@ class app.views.EntryPage extends app.View
@entry.doc.fullName + if @entry.isIndex() then ' documentation' else " / #{@entry.name}"
beforeRoute: =>
@abort()
@cache()
@abort()
return
onRoute: (context) ->
@ -95,7 +107,7 @@ class app.views.EntryPage extends app.View
abort: ->
if @xhr
@xhr.abort()
@xhr = null
@xhr = @entry = null
return
onSuccess: (response) =>
@ -113,7 +125,7 @@ class app.views.EntryPage extends app.View
return
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
@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'
setTimeout (-> target.className = '_pre-clip'), 2000
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
render: ->
if app.cookieBlocked
@html @tmpl('offlineError', 'cookie_blocked')
return
app.docs.getInstallStatuses (statuses) =>
return unless @activated
if statuses is false
@html @tmpl('offlineError', app.db.reason)
@html @tmpl('offlineError', app.db.reason, app.db.error)
else
html = ''
html += @renderDoc(doc, statuses[doc.slug]) for doc in app.docs.all()
@ -31,7 +35,7 @@ class app.views.OfflinePage extends app.View
refreshLinks: ->
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
docByEl: (el) ->
@ -41,24 +45,20 @@ class app.views.OfflinePage extends app.View
docEl: (doc) ->
@find("[data-slug='#{doc.slug}']")
onRoute: (route) ->
if app.isSingleDoc()
window.location = "/#/#{route.path}"
else
@render()
onRoute: (context) ->
@render()
return
onClick: (event) =>
return unless link = $.closestLink(event.target)
if action = link.getAttribute('data-action')
$.stopEvent(event)
doc = @docByEl(link)
el = event.target
if action = el.getAttribute('data-action')
doc = @docByEl(el)
action = 'install' if action is 'update'
doc[action](@onInstallSuccess.bind(@, doc), @onInstallError.bind(@, doc))
link.parentNode.innerHTML = "#{link.textContent.replace(/e$/, '')}ing…"
else if action = link.getAttribute('data-action-all')
$.stopEvent(event)
el.click() for el in @findAll("a[data-action='#{action}']")
doc[action](@onInstallSuccess.bind(@, doc), @onInstallError.bind(@, doc), @onInstallProgress.bind(@, doc))
el.parentNode.innerHTML = "#{el.textContent.replace(/e$/, '')}ing…"
else if action = el.getAttribute('data-action-all')
app.db.migrate()
$.click(el) for el in @findAll("[data-action='#{action}']")
return
onInstallSuccess: (doc) ->
@ -78,7 +78,14 @@ class app.views.OfflinePage extends app.View
el.lastElementChild.textContent = 'Error'
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) ->
if event.target.name is 'autoUpdate'
app.settings.set 'autoUpdate', !!event.target.checked
app.settings.set 'manualUpdate', !event.target.checked
return

@ -9,7 +9,6 @@ class app.views.RootPage extends app.View
render: ->
@empty()
@append @tmpl('mobileNav') if app.isMobile()
if app.isAndroidWebview()
@append @tmpl('androidWarning')
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
MAX_WIDTH_CLASS = '_max-width'
HIDE_SIDEBAR_CLASS = '_sidebar-hidden'
@el: document
@events:
visibilitychange: 'onVisibilityChange'
@shortcuts:
help: 'onHelp'
escape: 'onEscape'
superLeft: 'onBack'
superRight: 'onForward'
help: 'onHelp'
preferences: 'onPreferences'
escape: 'onEscape'
superLeft: 'onBack'
superRight: 'onForward'
@routes:
after: 'afterRoute'
init: ->
@addSubview @nav = new app.views.Nav,
@addSubview @menu = new app.views.Menu,
@addSubview @sidebar = new app.views.Sidebar
@addSubview @resizer = new app.views.Resizer if app.views.Resizer.isSupported()
@addSubview @content = new app.views.Content
@addSubview @path = new app.views.Path unless app.isSingleDoc() or app.isMobile()
@settings = new app.views.Settings unless app.isSingleDoc()
@sidebar.search
.on 'searching', @onSearching
.on 'clear', @onSearchClear
$.on document.body, 'click', @onClick
@activate()
return
toggleLight: ->
css = $('link[rel="stylesheet"][data-alt]')
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
setTitle: (title) ->
@el.title = if title then "DevDocs — #{title}" else 'DevDocs API Documentation'
onSearchClear: =>
unless @hasSidebar()
@hideSidebar()
afterRoute: (route) =>
if route is 'settings'
@settings?.activate()
else
@settings?.deactivate()
return
setTitle: (title) ->
@el.title = if title then "DevDocs - #{title}" else 'DevDocs API Documentation'
onVisibilityChange: =>
return unless @el.visibilityState is 'visible'
@delay ->
@ -85,6 +47,11 @@ class app.views.Document extends app.View
onHelp: ->
app.router.show '/help#shortcuts'
return
onPreferences: ->
app.router.show '/settings'
return
onEscape: ->
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.router.show(path)
return
onBack: ->
history.back()
return
onForward: ->
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'
@elements:
body: 'body'
content: '._container'
sidebar: '._sidebar'
body: 'body'
content: '._container'
sidebar: '._sidebar'
docPicker: '._settings ._sidebar'
@shortcuts:
escape: 'onEscape'
@routes:
after: 'afterRoute'
@ -32,62 +36,120 @@ class app.views.Mobile extends app.View
super
init: ->
if $.isTouchScreen()
FastClick.attach @body
app.shortcuts.stop()
window.FastClick?.attach @body
$.on @body, 'click', @onClick
$.on $('._home-link'), 'click', @onClickHome
$.on $('._menu-link'), 'click', @onClickMenu
$.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
.on 'searching', @showSidebar
.on 'clear', @hideSidebar
@activate()
return
showSidebar: =>
return if @isSidebarShown()
@contentTop = @body.scrollTop
if @isSidebarShown()
window.scrollTo 0, 0
return
@contentTop = window.scrollY
@content.style.display = 'none'
@sidebar.style.display = 'block'
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
@body.scrollTop = @findByClass(app.views.ListFold.activeClass) and @sidebarTop or 0
window.scrollTo 0, @findByClass(app.views.ListFold.activeClass) and @sidebarTop or 0
return
hideSidebar: =>
return unless @isSidebarShown()
@sidebarTop = @body.scrollTop
@sidebarTop = window.scrollY
@sidebar.style.display = 'none'
@content.style.display = 'block'
@body.scrollTop = @contentTop or 0
window.scrollTo 0, @contentTop or 0
return
isSidebarShown: ->
@sidebar.style.display isnt 'none'
onClick: (event) =>
if event.target.hasAttribute 'data-pick-docs'
@showSidebar()
onClickBack: =>
history.back()
onClickForward: =>
history.forward()
onClickToggleSidebar: =>
if @isSidebarShown() then @hideSidebar() else @showSidebar()
return
onClickHome: =>
app.shortcuts.trigger 'escape'
@hideSidebar()
onClickDocPickerTab: (event) =>
$.stopEvent(event)
@showDocPicker()
return
onClickMenu: =>
if @isSidebarShown() then @hideSidebar() else @showSidebar()
onClickSettingsTab: (event) =>
$.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
onTapSearch: =>
@body.scrollTop = 0
window.scrollTo 0, 0
onEscape: =>
@hideSidebar()
afterRoute: =>
afterRoute: (route) =>
@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

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

@ -4,7 +4,6 @@ class app.views.Resizer extends app.View
@events:
dragstart: 'onDragStart'
dragend: 'onDragEnd'
dblclick: 'onDblClick'
@isSupported: ->
'ondragstart' of document.createElement('div') and !app.isMobile()
@ -32,10 +31,6 @@ class app.views.Resizer extends app.View
app.appCache?.updateInBackground()
return
onDblClick: (event) ->
app.document.toggleSidebar(save: true)
return
onDragStart: (event) =>
@style.removeAttribute('disabled')
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'
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
@blur()
el.classList.add @constructor.activeClass
$.trigger el, 'focus'
$.trigger el, 'focus' unless options.silent is true
return
blur: =>
@ -38,7 +40,7 @@ class app.views.ListFocus extends app.View
$.click(next)
@findNext cursor
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)
else
@findNext(next)
@ -85,22 +87,22 @@ class app.views.ListFocus extends app.View
onDown: =>
if cursor = @getCursor()
@focus @findNext(cursor)
@focusOnNextFrame @findNext(cursor)
else
@focus @findByTag('a')
@focusOnNextFrame @findByTag('a')
return
onUp: =>
if cursor = @getCursor()
@focus @findPrev(cursor)
@focusOnNextFrame @findPrev(cursor)
else
@focus @findLastByTag('a')
@focusOnNextFrame @findLastByTag('a')
return
onLeft: =>
cursor = @getCursor()
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
onEnter: =>
@ -116,5 +118,5 @@ class app.views.ListFocus extends app.View
onClick: (event) =>
return if event.which isnt 1 or event.metaKey or event.ctrlKey
if event.target.tagName is 'A'
@focus event.target
@focus event.target, silent: true
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 unless event.pageY # ignore fabricated clicks
el = event.target
el = el.parentElement if el.parentElement.tagName.toUpperCase() is 'SVG'
if el.classList.contains @constructor.handleClass
$.stopEvent(event)
@toggle el.parentElement
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

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

@ -1,6 +1,8 @@
class app.views.Notif extends app.View
@className: '_notif'
@activeClass: '_in'
@attributes:
role: 'alert'
@defautOptions:
autoHide: 15000
@ -49,6 +51,7 @@ class app.views.Notif extends app.View
onClick: (event) =>
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')
$.stopEvent(event)
@hide()

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

@ -1,18 +1,43 @@
class app.views.BasePage extends app.View
constructor: (@el, @entry) -> super
deactivate: ->
if super
@highlightNodes = []
render: (content, fromCache = false) ->
@highlightNodes = []
@previousTiming = null
@addClass "_#{@entry.doc.type}" unless @constructor.className
@html content
@prepare?() unless fromCache
@highlightCode() unless fromCache
@activate()
@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
highlightCode: (el, language) ->
if $.isCollection(el)
@highlightCode e, language for e in el
else if el
el.classList.add "language-#{language}"
paintCode: (timing) =>
if @previousTiming
if Math.round(1000 / (timing - @previousTiming)) > 50 # fps
@nodesPerFrame = Math.round(Math.min(@nodesPerFrame * 1.25, 50))
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)
$.append(el, clipEl) if clipEl
$.requestAnimationFrame(@paintCode) if @highlightNodes.length > 0
@previousTiming = timing
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
@demoClassName: '_jquery-demo'
prepare: ->
for el in @findAllByClass 'syntaxhighlighter'
language = if el.classList.contains('javascript') then 'javascript' else 'markup'
@highlightCode el, language
return
afterRender: ->
# Prevent jQuery Mobile's demo iframes from scrolling the page
for iframe in @findAllByTag 'iframe'
@ -45,7 +39,7 @@ class app.views.JqueryPage extends app.views.BasePage
fixIframeSource: (source) ->
source = source.replace '"/resources/', '"https://api.jquery.com/resources/' # attr(), keydown()
source.replace '</head>', """
source = source.replace '</head>', """
<style>
html, body { border: 0; margin: 0; padding: 0; }
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; }
@ -60,3 +54,4 @@ class app.views.JqueryPage extends app.views.BasePage
</script>
</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:
click: 'onClick'
prepare: ->
@highlightCode @findAll('pre.ruby'), 'ruby'
@highlightCode @findAll('pre.c'), 'clike'
return
onClick: (event) ->
return unless event.target.classList.contains 'method-click-advice'
$.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'
@shortcuts:
typing: 'autoFocus'
typing: 'focus'
altG: 'google'
altS: 'stackoverflow'
@routes:
root: 'onRoot'
after: 'autoFocus'
after: 'afterRoute'
init: ->
@addSubview @scope = new app.views.SearchScope @el
@ -32,31 +31,31 @@ class app.views.Search extends app.View
app.on 'ready', @onReady
$.on window, 'hashchange', @searchUrl
$.on window, 'focus', @autoFocus
$.on window, 'focus', @onWindowFocus
return
focus: ->
focus: =>
@input.focus() unless document.activeElement is @input
return
autoFocus: =>
@focus() unless $.isTouchScreen()
unless app.isMobile() or $.isAndroid() or $.isIOS()
@input.focus() unless document.activeElement?.tagName is 'INPUT'
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()
@onInput()
@autoFocus()
return
disable: ->
@input.setAttribute('disabled', 'disabled')
return
enable: ->
@input.removeAttribute('disabled')
return
onReady: =>
@value = ''
@delay @onInput
@ -83,11 +82,14 @@ class app.views.Search extends app.View
return
searchUrl: =>
return unless app.router.isRoot()
@scope.searchUrl()
if location.pathname is '/'
@scope.searchUrl()
else if not app.router.isIndex()
return
return unless value = @extractHashValue()
@input.value = @value = value
@input.setSelectionRange(value.length, value.length)
@search true
true
@ -125,16 +127,18 @@ class app.views.Search extends app.View
if event.target is @resetLink
$.stopEvent(event)
@reset()
@focus()
app.document.onEscape()
return
onSubmit: (event) ->
$.stopEvent(event)
return
onRoot: (context) =>
@reset() unless context.init
afterRoute: (name, context) =>
return if app.shortcuts.eventInProgress?.name is 'escape'
@reset(true) if not context.init and app.router.isIndex()
@delay @searchUrl if context.hash
$.requestAnimationFrame @autoFocus
return
extractHashValue: ->
@ -142,5 +146,7 @@ class app.views.Search extends app.View
app.router.replaceHash()
value
HASH_RGX = new RegExp "^##{SEARCH_PARAM}=(.*)"
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:
after: 'afterRoute'
@shortcuts:
escape: 'reset'
constructor: (@el) -> super
init: ->
@ -29,22 +26,29 @@ class app.views.SearchScope extends app.View
getScope: ->
@doc or app
isActive: ->
!!@doc
name: ->
@doc?.name
search: (value) ->
unless @doc
@searcher.find app.docs.all(), 'text', value
search: (value, searchDisabled = false) ->
return if @doc
@searcher.find app.docs.all(), 'text', value
@searcher.find app.disabledDocs.all(), 'text', value if not @doc and searchDisabled
return
searchUrl: ->
if value = @extractHashValue()
@search value
@search value, true
return
onResults: (results) =>
if results.length
@selectDoc results[0]
return unless doc = results[0]
if app.docs.contains(doc)
@selectDoc(doc)
else
@redirectToDoc(doc)
return
selectDoc: (doc) ->
@ -63,6 +67,12 @@ class app.views.SearchScope extends app.View
@trigger 'change', @doc, previousDoc
return
redirectToDoc: (doc) ->
hash = location.hash
app.router.replaceHash('')
location.assign doc.fullPath() + hash
return
reset: =>
return unless @doc
previousDoc = @doc
@ -78,16 +88,16 @@ class app.views.SearchScope extends app.View
return
onKeydown: (event) =>
return if event.ctrlKey or event.metaKey or event.altKey or event.shiftKey
if event.which is 8 # backspace
if @doc and not @input.value
$.stopEvent(event)
@reset()
else if event.which is 9 or # tab
event.which is 32 and (app.isMobile() or $.isTouchScreen()) # space
$.stopEvent(event)
@search @input.value[0...@input.selectionStart]
else if not @doc and @input.value
return if event.ctrlKey or event.metaKey or event.altKey or event.shiftKey
if event.which is 9 or # tab
(event.which is 32 and app.isMobile()) # space
@search @input.value[0...@input.selectionStart]
$.stopEvent(event) if @doc
return
extractHashValue: ->
@ -96,8 +106,10 @@ class app.views.SearchScope extends app.View
app.router.replaceHash(newHash)
value
HASH_RGX = new RegExp "^##{SEARCH_PARAM}=(.+?) ."
getHashValue: ->
try (new RegExp "^##{SEARCH_PARAM}=(.+?) .").exec($.urlDecode location.hash)?[1] catch
try HASH_RGX.exec($.urlDecode location.hash)?[1] catch
afterRoute: (name, context) =>
if !app.isSingleDoc() and context.init and context.doc

@ -1,5 +1,7 @@
class app.views.DocList extends app.View
@className: '_list'
@attributes:
role: 'navigation'
@events:
open: 'onOpen'
@ -16,9 +18,9 @@ class app.views.DocList extends app.View
init: ->
@lists = {}
@addSubview @listSelect = new app.views.ListSelect @el
@addSubview @listFocus = new app.views.ListFocus @el unless app.isMobile()
@addSubview @listFocus = new app.views.ListFocus @el
@addSubview @listFold = new app.views.ListFold @el
@addSubview @listSelect = new app.views.ListSelect @el
app.on 'ready', @render
return
@ -35,7 +37,10 @@ class app.views.DocList extends app.View
return
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
return
@ -46,10 +51,9 @@ class app.views.DocList extends app.View
return
renderDisabledList: ->
if (hidden = app.settings.get 'hideDisabled') is true
if app.settings.get('hideDisabled')
@removeDisabledList()
else
app.settings.set 'hideDisabled', false unless hidden is false
@appendDisabledList()
return
@ -83,7 +87,7 @@ class app.views.DocList extends app.View
@listSelect.deselect()
@listFocus?.blur()
@listFold.reset()
@revealCurrent() if options.revealCurrent
@revealCurrent() if options.revealCurrent || app.isSingleDoc()
return
onOpen: (event) =>
@ -147,7 +151,7 @@ class app.views.DocList extends app.View
return
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
toggleDisabled: ->
@ -160,13 +164,13 @@ class app.views.DocList extends app.View
return
onClick: (event) =>
if @disabledTitle and $.hasChild(@disabledTitle, event.target)
if @disabledTitle and $.hasChild(@disabledTitle, event.target) and event.target.tagName isnt 'A'
$.stopEvent(event)
@toggleDisabled()
else if slug = event.target.getAttribute('data-enable')
$.stopEvent(event)
doc = app.disabledDocs.findBy('slug', slug)
app.enableDoc(doc, @onEnabled, @onEnabled)
app.enableDoc(doc, @onEnabled, @onEnabled) if doc
return
onEnabled: =>

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

@ -15,8 +15,8 @@ class app.views.Results extends app.View
return
init: ->
@addSubview @listFocus = new app.views.ListFocus @el
@addSubview @listSelect = new app.views.ListSelect @el
@addSubview @listFocus = new app.views.ListFocus @el unless app.isMobile()
@search
.on 'results', @onResults
@ -42,7 +42,7 @@ class app.views.Results extends app.View
return
focusFirst: ->
@listFocus?.focus @el.firstElementChild
@listFocus?.focusOnNextFrame @el.firstElementChild unless app.isMobile()
return
openFirst: ->
@ -65,4 +65,4 @@ class app.views.Results extends app.View
if slug = event.target.getAttribute('data-enable')
$.stopEvent(event)
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:
focus: 'onFocus'
select: 'onSelect'
click: 'onClick'
@routes:
after: 'afterRoute'
@shortcuts:
altR: 'onAltR'
escape: 'onEscape'
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
@search
.on 'searching', @showResults
.on 'clear', @showDocList
.on 'searching', @onSearching
.on 'clear', @onSearchClear
.scope
.on 'change', @onScopeChange
@results = new app.views.Results @, @search
@docList = new app.views.DocList
@docPicker = new app.views.DocPicker unless app.isSingleDoc()
app.on 'ready', @showDocList
$.on document, 'click', @onGlobalClick if @docPicker
app.on 'ready', @onReady
$.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
show: (view) ->
resetHoverOnMouseMove: =>
$.off window, 'mousemove', @resetHoverOnMouseMove
$.requestAnimationFrame @resetHover
resetHover: =>
@removeClass 'no-hover'
showView: (view) ->
unless @view is view
@hover?.hide()
@saveScrollPosition()
@view?.deactivate()
@html @view = view
@append @tmpl('sidebarSettings') if @view is @docList and @docPicker
@view = view
@render()
@view.activate()
@restoreScrollPosition()
if view is @docPicker then @search.disable() else @search.enable()
return
showDocList: (reset) =>
@show @docList
if reset is true
@docList.reset(revealCurrent: true)
@search.reset()
render: ->
@html @view
return
showDocPicker: =>
@show @docPicker
showDocList: ->
@showView @docList
return
showResults: =>
@show @results
@display()
@showView @results
return
reset: ->
@showDocList true
@display()
@showDocList()
@docList.reset()
@search.reset()
return
onReady: =>
@view = @docList
@render()
@view.activate()
return
onScopeChange: (newDoc, previousDoc) =>
@ -80,41 +111,50 @@ class app.views.Sidebar extends app.View
@el.scrollTop = 0
return
onSearching: =>
@showResults()
return
onSearchClear: =>
@resetDisplay()
@showDocList()
return
onFocus: (event) =>
$.scrollTo event.target, @el, 'continuous', bottomGap: 2
@display()
$.scrollTo event.target, @el, 'continuous', bottomGap: 2 unless event.target is @el
return
onClick: (event) =>
return if event.which isnt 1
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()
onSelect: =>
@resetDisplay()
return
onGlobalClick: (event) =>
onClick: (event) =>
return if event.which isnt 1
if event.target.hasAttribute? 'data-pick-docs'
if event.target.hasAttribute? 'data-reset-list'
$.stopEvent(event)
@showDocPicker()
else if @view is @docPicker
@showDocList() unless $.hasChild @el, event.target
@onAltR()
return
onAltR: =>
@reset()
@docList.reset(revealCurrent: true)
@display()
return
onEscape: =>
@reset()
@scrollToTop()
@resetDisplay()
if doc = @search.getScopeDoc() then @docList.reveal(doc.toEntry()) else @scrollToTop()
return
onDocEnabled: ->
@docList.onEnabled()
@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