Going open source

This commit is contained in:
Thibaut 2013-10-24 20:25:52 +02:00
commit 18986c1814
345 changed files with 18635 additions and 0 deletions

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
.DS_Store
.bundle
*.pxm
*.sketch
tmp
public/assets
public/fonts
public/docs/**/*
!public/docs/docs.json
!public/docs/**/index.json

84
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,84 @@
# Contributing to DevDocs
Wish to contribute? Great. Please review the following guidelines carefully and always search for existing issues before opening a new one. More time spent managing issues means less time spent improving the software.
_Note: DevDocs is my first open source project and one which I deeply care about. Please forgive my inexperience and the fact that I may push back on certain things in order to keep the project to my liking. Feedback and advice are always welcome._
**Table of Contents:**
1. [Reporting bugs](#reporting-bugs)
2. [Requesting new features](#requesting-new-features)
3. [Requesting new docs](#requesting-new-docs)
4. [Contributing code and features](#contributing-code-and-features)
5. [Contributing new docs](#contributing-new-docs)
6. [Other contributions](#other-contributions)
7. [Coding conventions](#coding-conventions)
8. [Questions?](#questions)
## Reporting bugs
1. Always update to the most recent master release; the bug may already be fixed.
2. Search for existing issues; it's possible someone has already encountered this bug.
3. Try to isolate the problem and include steps to reproduce it.
4. Share as much information as possible (e.g. browser/OS environment, log output, stack trace, screenshots, etc.).
## Requesting new features
1. Search for similar feature requests; someone may have already requested it.
2. Make sure your feature fits DevDocs's [vision and stated goals](https://github.com/Thibaut/devdocs/blob/master/README.md#vision).
3. Provide a clear and detailed explanation of the feature and why it's important to add it.
For general feedback and ideas, please use the [mailing list](https://groups.google.com/d/forum/devdocs).
## Requesting new docs
Please do not open issues to request new documentations.
Use the [Trello board](https://trello.com/b/6BmTulfx/devdocs-documentation) where everyone can vote and contributors can get a feel for what's wished for.
## Contributing code and features
1. Search for existing issues; someone may already be working on a similar feature.
2. Before embarking on any significant pull request, please open an issue describing the changes you intend to make. Otherwise you risk spending a lot of time working on something that I may not want to merge. This also tells other contributors that you're working on the feature.
3. Follow the [coding conventions](#coding-conventions).
4. If you're modifying the Ruby code, include tests and ensure they pass.
5. Try to keep your pull request small and simple.
6. When it makes sense, squash your commits into a single commit.
7. Describe all your changes in the commit message and/or pull request.
## Contributing new docs
**Note:** there is currently no documentation on how to create a scraper/documentation. I'm working on it.
**Important:** in order to keep things fast and manageable, only the documentation of popular open source projects will be accepted into DevDocs. As more projects find their way in, the required level of popularity will gradually decrease. Additionally, the documentation's license must permit alteration, redistribution, and commercial use of the work. Software vendors that wish to add commercial software documentation to DevDocs may contact me privately.
**Please open an issue before adding any new documentation.**
In addition to the [guidelines for contributing code](#contributing-code-and-features), the following guidelines apply to pull requests that add a new documentation:
* Your documentation must come with a clean and official icon, in both 1x and 2x resolutions (16x16 and 32x32 pixels). This is important because icons are the only thing differentiating search results inside the app. If a project doesn't have an official icon, it won't be accepted into DevDocs—sorry.
* DevDocs favors quality over quantity. Your documentation should only include API/reference documents that most developers may wish to read semi-regularly. By reducing the number of entries you make it easier for people to find other, more relevant entries. _(Note: you're more than welcome to submit pull requests removing seldom-used entries from existing documentations.)_
* Try to remove as much content and HTML markup as possible, particularly content which isn't associated with any entries (e.g. introduction, changelog, etc.).
* Names must be as short as possible and unique across the documentation.
* The number of types (categories) must be less than 50.
* It's ok to leave the CSS up to me.
* Don't modify the icon sprite. I'll do it when merging your pull request.
* Once your documentation is accepted into DevDocs, you'll be expected to maintain it on a regular basis.
## Other contributions
Besides new docs and features, here are other ways you can contribute:
* **Improve words and sentences.** English isn't my first language so if you notice grammatical or usage errors, feel free to submit a pull request—it'll be much appreciated. (Note: American English is the preferred form)
* **Write documentation.** Although this task is mainly up to me, any documentation you can write that may help other developers understand and contribute to the code is highly appreciated.
* **Participate in the issue tracker.** Your opinion matters—feel free to add comments to existing issues. You're also welcome to participate to the [mailing list](https://groups.google.com/d/forum/devdocs).
## Coding conventions
* two spaces; no tabs
* no trailing whitespace; blank lines should have no spaces
* use the same coding style as the rest of the codebase
## Questions?
If you have any questions, please feel free to ask on the [mailing list](https://groups.google.com/d/forum/devdocs).

14
COPYRIGHT Normal file
View file

@ -0,0 +1,14 @@
Copyright 2013 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
file, You can obtain one at http://mozilla.org/MPL/2.0/.
NOTE: DevDocs is considered a trademark. You may not use the name to
endorse or promote products derived from this software without
Thibaut Courouble's permission, except as may be necessary to
comply with the notice/attribution requirements.
ADDITIONALLY: it is expected that any documentation file generated
using this software must be attributed to DevDocs. Let's be fair
to all contributors by not stealing their hard work.

41
Gemfile Normal file
View file

@ -0,0 +1,41 @@
source 'https://rubygems.org'
ruby '2.0.0'
gem 'thor'
gem 'pry', '~> 0.9.12'
gem 'activesupport', '~> 4.0', require: false
gem 'yajl-ruby', require: false
group :app do
gem 'rack'
gem 'sinatra'
gem 'sinatra-contrib'
gem 'thin'
gem 'sprockets'
gem 'sprockets-helpers'
gem 'erubis'
gem 'browser'
gem 'sass'
gem 'coffee-script'
end
group :production do
gem 'uglifier'
end
group :development do
gem 'better_errors'
end
group :docs do
gem 'typhoeus'
gem 'nokogiri', '~> 1.6.0'
gem 'html-pipeline'
gem 'progress_bar'
gem 'unix_utils'
end
group :test do
gem 'minitest'
gem 'rr', require: false
end

131
Gemfile.lock Normal file
View file

@ -0,0 +1,131 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (4.0.0)
i18n (~> 0.6, >= 0.6.4)
minitest (~> 4.2)
multi_json (~> 1.3)
thread_safe (~> 0.1)
tzinfo (~> 0.3.37)
atomic (1.1.14)
backports (3.3.5)
better_errors (1.0.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
browser (0.2.1)
coderay (1.0.9)
coffee-script (2.2.0)
coffee-script-source
execjs
coffee-script-source (1.6.3)
daemons (1.1.9)
erubis (2.7.0)
escape_utils (0.3.2)
ethon (0.6.1)
ffi (>= 1.3.0)
mime-types (~> 1.18)
eventmachine (1.0.3)
execjs (2.0.2)
fattr (2.2.1)
ffi (1.9.0)
gemoji (1.4.0)
github-markdown (0.5.5)
highline (1.6.19)
hike (1.2.3)
html-pipeline (0.2.1)
escape_utils (~> 0.3)
gemoji (~> 1.0)
github-markdown (~> 0.5)
nokogiri (~> 1.4)
rinku (~> 1.7)
sanitize (~> 2.0)
i18n (0.6.5)
method_source (0.8.2)
mime-types (1.25)
mini_portile (0.5.1)
minitest (4.7.5)
multi_json (1.8.1)
nokogiri (1.6.0)
mini_portile (~> 0.5.0)
options (2.3.0)
fattr
progress_bar (1.0.0)
highline (~> 1.6.1)
options (~> 2.3.0)
pry (0.9.12.2)
coderay (~> 1.0.5)
method_source (~> 0.8)
slop (~> 3.4)
rack (1.5.2)
rack-protection (1.5.0)
rack
rack-test (0.6.2)
rack (>= 1.0)
rinku (1.7.3)
rr (1.1.2)
sanitize (2.0.6)
nokogiri (>= 1.4.4)
sass (3.2.12)
sinatra (1.4.3)
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
sinatra-contrib (1.4.1)
backports (>= 2.0)
multi_json
rack-protection
rack-test
sinatra (~> 1.4.0)
tilt (~> 1.3)
slop (3.4.6)
sprockets (2.10.0)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sprockets-helpers (1.0.1)
sprockets (~> 2.0)
thin (1.6.0)
daemons (>= 1.0.9)
eventmachine (>= 1.0.0)
rack (>= 1.5.0)
thor (0.18.1)
thread_safe (0.1.3)
atomic
tilt (1.4.1)
typhoeus (0.6.5)
ethon (~> 0.6.1)
tzinfo (0.3.38)
uglifier (2.2.1)
execjs (>= 0.3.0)
multi_json (~> 1.0, >= 1.0.2)
unix_utils (0.0.15)
yajl-ruby (1.1.0)
PLATFORMS
ruby
DEPENDENCIES
activesupport (~> 4.0)
better_errors
browser
coffee-script
erubis
html-pipeline
minitest
nokogiri (~> 1.6.0)
progress_bar
pry (~> 0.9.12)
rack
rr
sass
sinatra
sinatra-contrib
sprockets
sprockets-helpers
thin
thor
typhoeus
uglifier
unix_utils
yajl-ruby

373
LICENSE Normal file
View file

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
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/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

136
README.md Normal file
View file

@ -0,0 +1,136 @@
# [DevDocs](http://devdocs.io) — Documentation Browser
DevDocs combines multiple API documentations in a fast, organized, and searchable interface.
* Created by [Thibaut Courouble](http://thibaut.me)
* Sponsored by [MaxCDN](http://www.maxcdn.com)
Keep track of development and community news:
* Subscribe to the [newsletter](http://eepurl.com/HnLUz)
* Follow [@DevDocs](https://twitter.com/DevDocs) on Twitter
* Join the [mailing list](https://groups.google.com/d/forum/devdocs)
DevDocs is free and open source. If you use it and like it, please consider donating through [Gittip](https://www.gittip.com/Thibaut/) or [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4PTFAGT7K6QVG). Your support helps sustain the project and is highly appreciated.
**Table of Contents:** [Quick Start](#quick-start) · [Vision](#vision) · [App](#app) · [Scraper](#scraper) · [Commands](#available-commands) · [Contributing](#contributing) · [License](#copyright--license) · [Questions?](#questions)
**Note:** I'm in the process of writing more documentation. As DevDocs is quite big, it'll take time. Feel free to [contact me directly](mailto:thibaut@devdocs.io) in the meantime.
## Quick Start
Unless you wish to use DevDocs offline or contribute to the code, I recommend using the hosted version at [devdocs.io](http://devdocs.io). It's up-to-date and requires no setup.
DevDocs is made of two separate pieces of software: a Ruby scraper responsible for generating the documentation files and indexes, and a JavaScript front-end powered by on small Sinatra app.
DevDocs requires Ruby 2.0. Once you have it installed, run the following commands:
```
gem install bundler
bundle install
thor docs:download --all
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.
The `thor docs:download` command is used to download/update individual documentations (e.g. `thor docs:download html css`), or all at the same time (using the `--all` option). You can see the list of available documentations by running `thor docs:list`.
**Note:** there is currently no update mechanism other than using git to update the code and `thor docs:download` to download the latest version of the docs. To stay informed about new versions, be sure to subscribe to the [newsletter](http://eepurl.com/HnLUz).
## Vision
DevDocs aims to make reading and searching reference documentation fast, accessible and enjoyable, while aspiring to become the “one stop shop” for all open-source software documentations.
The app's main goals are to: keep boot and page load times as fast as possible; improve the quality, speed, and order of search results; maximize the use of caching and other performance optimizations; maintain a clean, readable user interface; support full keyboard navigation; reduce “context switch” by using a consistent typography and design across all documentations; reduce clutter by focusing on a specific category of content (API/reference) and indexing only the minimum useful to most developers.
**Note:** DevDocs is neither a programming guide nor a search engine. All content is pulled from third-party sources and the project does not intend to compete with full-text search engines. Its backbone is metadata: each piece of content must be identified by a unique, obvious and short string. Thus, tutorials, guides and other content that don't fit this requirement are outside the scope of the project.
## App
The web app is all JavaScript, written in [CoffeeScript](http://coffeescript.org), and powered by a small [Sinatra](http://www.sinatrarb.com)/[Sprockets](https://github.com/sstephenson/sprockets) application. It relies on files generated by the [scraper](#scraper).
Many of the code's design decisions were driven by the fact that the app uses XHR to load content directly into the main frame. This includes stripping the original documents of most of their HTML markup (e.g. scripts and stylesheets) to avoid polluting the main frame, and prefixing all CSS class names with an underscore to prevent conflicts.
Another driving factor is the requirement for speed. This is partially solved by maximizing caching (both `applicationCache`, which comes with its own set of constraints, and `localStorage` are used to their full extent), as well as by allowing the user to pick his/her own set of documentations. On the other hand, the search algorithm is currently not very sophisticated because it needs to be fast even searching through 100k entries.
DevDocs being a developer tool, the browser requirements are high:
1. On the desktop:
* Recent version of Chrome
* Recent version of Firefox
* Safari 5.1+
* Opera 12.1+
* Interner Explorer 10+
2. On mobile:
* iOS 6+
* Android 4.1+
* Windows Phone 8+
This allows the code to take advantage of the latest DOM and HTML5 APIs and make developing DevDocs a lot more fun!
## Scraper
The scraper is responsible for generating the documentation and index files (metadata) used by the [app](#app). It's written in Ruby under the `Docs` module.
There are currently two kinds of scrapers: `UrlScraper` which downloads files via HTTP and `FileScraper` which reads them from the local filesystem. They both make copies of HTML documents, recursively following links that match a given set of rules and applying all sorts of modifications along the way, in addition to building an index of the files and their metadata. Documents are parsed using [Nokogiri](http://nokogiri.org).
Modifications made to each document include:
* removing stuff, such as the document structure (`<html>`, `<head>`, etc.), comments, empty nodes, etc.
* fixing links (e.g. to remove duplicates)
* replacing all external (not copied) URLs with their fully qualified counterpart
* replacing all internal (copied) URLs with their unqualified and relative counterpart
* adding stuff, such as a title and link to the original document
These modifications are applied through a set of filters, with each scraper also applying custom filters specific to the documentation. Each document is also passed through a filter whose task is to figure out its metadata, namely its _name_ and _type_ (category).
The end result is a set of normalized HTML partials and a JSON index file. Because the index files are loaded separately by the [app](#app) following the user's preferences, the code also creates a JSON manifest file containing information about the documentations currently available on the system (such as their name, version, update date, etc.).
## Available Commands
The command-line interface uses [Thor](http://whatisthor.com). To see all commands and options, run `thor list` from the project's root.
```
# Server
rackup # Start the server (ctrl+c to stop)
rackup --help # List server options
# Docs
thor docs:list # List available documentations
thor docs:download # Download one or more documentations
thor docs:manifest # Create the manifest file used by the app
thor docs:generate # Generate/scrape a documentation
thor docs:page # Generate/scrape a documentation page
thor docs:package # Package a documentation for use with docs:download
# Console
thor console # Start a REPL
thor console:docs # Start a REPL in the "Docs" module
Note: tests can be run quickly from within the console using the "test" command. Run "help test"
for usage instructions.
# Tests
thor test:all # Run all tests
# Assets
thor assets:compile # Compile assets (not required in development mode)
thor assets:clean # Clean old assets
```
## Contributing
Contributions are welcome. Please read the [contributing guidelines](https://github.com/Thibaut/devdocs/blob/master/CONTRIBUTING.md).
## Copyright / License
Copyright 2013 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.
**Note:** I consider _DevDocs_ to be a trademark. You may not use the name to endorse or promote products derived from this software without my permission, except as may be necessary to comply with the notice/attribution requirements.
**Additionally**, I wish that any documentation file generated using this software be attributed to DevDocs. Let's be fair to all contributors by not stealing their hard work.
## Questions?
If you have any questions, please feel free to ask on the [mailing list](https://groups.google.com/d/forum/devdocs).

14
Rakefile Normal file
View file

@ -0,0 +1,14 @@
#!/usr/bin/env rake
require 'bundler/setup'
require 'thor'
$LOAD_PATH.unshift 'lib'
namespace :assets do
desc 'Compile all assets'
task :precompile do
load 'tasks/assets.thor'
AssetsCLI.new.compile
end
end

1
Thorfile Normal file
View file

@ -0,0 +1 @@
$LOAD_PATH.unshift 'lib'

BIN
assets/images/icons.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
assets/images/icons@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
assets/images/maxcdn-bw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
assets/images/maxcdn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/images/maxcdn@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View file

@ -0,0 +1,156 @@
@app =
$: $
collections: {}
models: {}
templates: {}
views: {}
init: ->
return unless @browserCheck()
@initErrorTracking()
@showLoading()
@store = new Store
@appCache = new app.AppCache if app.AppCache.isEnabled()
@settings = new app.Settings
@docs = new app.collections.Docs
@disabledDocs = new app.collections.Docs
@entries = new app.collections.Entries
@router = new app.Router
@shortcuts = new app.Shortcuts
@document = new app.views.Document
@mobile = new app.views.Mobile if @isMobile()
if @DOC
@bootOne()
else if @DOCS
@bootAll()
else
@onBootError()
return
browserCheck: ->
return true if @isSupportedBrowser()
@hideLoading()
document.body.innerHTML = app.templates.unsupportedBrowser
false
initErrorTracking: ->
# Show a warning message and don't track errors when the app is loaded
# from a domain other than our own, because things are likely to break.
# (e.g. cross-domain requests)
if @isInvalidLocation()
new app.views.Notif 'InvalidLocation'
else
Raven.config(@config.sentry_dsn).install() if @config.sentry_dsn
@previousErrorHandler = onerror
window.onerror = @onWindowError.bind(@)
return
bootOne: ->
@doc = new app.models.Doc @DOC
@docs.reset [@doc]
@doc.load @start.bind(@), @onBootError.bind(@), readCache: true
new app.views.Notice 'singleDoc', @doc
delete @DOC
return
bootAll: ->
docs = @settings.getDocs()
for doc in @DOCS
(if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc)
@docs.load @start.bind(@), @onBootError.bind(@), readCache: true, writeCache: true
delete @DOCS
return
start: ->
@entries.add doc.entries.all() for doc in @docs.all()
@trigger 'ready'
@router.start()
@hideLoading()
new app.views.News() unless @doc
@removeEvent 'ready bootError'
return
reload: ->
@docs.clearCache()
@disabledDocs.clearCache()
if @appCache then @appCache.reload() else window.location = '/'
return
reset: ->
@store.clear()
@settings.reset()
@appCache?.update()
window.location = '/'
return
showLoading: ->
document.body.classList.add '_loading'
return
hideLoading: ->
document.body.classList.remove '_booting'
document.body.classList.remove '_loading'
return
indexHost: ->
@config[if @appCache and @settings.hasDocs() then 'index_path' else 'docs_host']
onBootError: (args...) ->
@trigger 'bootError'
@hideLoading()
return
onWindowError: (args...) ->
if @isInjectionError args...
@onInjectionError()
else if @isAppError args...
@previousErrorHandler? args...
@hideLoading()
@errorNotif or= new app.views.Notif 'Error'
@errorNotif.show()
return
onInjectionError: ->
unless @injectionError
@injectionError = true
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'
return
isInjectionError: ->
# Some browser extensions expect the entire web to use jQuery.
# I gave up trying to fight back.
window.$ isnt app.$
isAppError: (error, file) ->
# Ignore errors from external scripts.
file and file.indexOf('devdocs') isnt -1 and file.indexOf('.js') is file.length - 3
isSupportedBrowser: ->
try
return true if Function::bind and
history.pushState and
window.matchMedia and
document.body.classList and
document.body.insertAdjacentHTML and
document.createEvent('CustomEvent').defaultPrevented is false and
getComputedStyle(document.querySelector('._header')).backgroundImage isnt 'none'
catch
isMobile: ->
# Need to sniff the user agent because some Android and Windows Phone devices don't take
# resolution (dpi) into account when reporting device width/height.
@_isMobile ?= (matchMedia('(max-device-width: 767px), (max-device-height: 767px)').matches) or
(navigator.userAgent.indexOf('Android') isnt -1 and navigator.userAgent.indexOf('Mobile') isnt -1) or
(navigator.userAgent.indexOf('IEMobile') isnt -1)
isInvalidLocation: ->
@config.env is 'production' and location.host.indexOf(app.config.production_host) isnt 0
$.extend app, Events

View file

@ -0,0 +1,42 @@
class app.AppCache
$.extend @prototype, Events
@isEnabled: ->
try
applicationCache and applicationCache.status isnt applicationCache.UNCACHED
catch
constructor: ->
@cache = applicationCache
@onUpdateReady() if @cache.status is @cache.UPDATEREADY
$.on @cache, 'progress', @onProgress
$.on @cache, 'updateready', @onUpdateReady
@lastCheck = Date.now()
$.on window, 'focus', @checkForUpdate
update: ->
try @cache.update() catch
return
reload: ->
@reloading = true
$.on @cache, 'updateready noupdate error', -> window.location = '/'
@update()
return
checkForUpdate: =>
if Date.now() - @lastCheck > 86400e3
@lastCheck = Date.now()
@update()
return
onProgress: (event) =>
@trigger 'progress', event
return
onUpdateReady: =>
new app.views.Notif 'UpdateReady' unless @reloading
@trigger 'updateready'
return

View file

@ -0,0 +1,10 @@
app.config =
default_docs: ['css', 'dom', 'dom_events', 'html', 'http', 'javascript']
docs_host: '<%= App.docs_host %>'
env: '<%= App.environment %>'
history_cache_size: 10
index_path: '/<%= App.docs_prefix %>'
max_results: 50
production_host: 'devdocs.io'
search_param: 'q'
sentry_dsn: '<%= App.sentry_dsn %>'

View file

@ -0,0 +1,114 @@
class app.Router
$.extend @prototype, Events
@routes: [
['*', 'before' ]
['/', 'root' ]
['/about', 'about' ]
['/news', 'news' ]
['/help', 'help' ]
['/:doc-:type/', 'type' ]
['/:doc/', 'doc' ]
['/:doc/:path(*)', 'entry' ]
['*', 'notFound']
]
constructor: ->
for [path, method] in @constructor.routes
page path, @[method].bind(@)
@setInitialPath()
start: ->
page.start()
return
show: (path) ->
page.show(path)
return
triggerRoute: (name) ->
@trigger name, @context
@trigger 'after', name, @context
return
before: (context, next) ->
@context = context
@trigger 'before', context
next()
return
doc: (context, next) ->
if doc = app.docs.findBy('slug', context.params.doc) or app.disabledDocs.findBy('slug', context.params.doc)
context.doc = doc
context.entry = doc.indexEntry()
@triggerRoute 'entry'
else
next()
return
type: (context, next) ->
doc = app.docs.findBy 'slug', context.params.doc
if type = doc?.types.findBy 'slug', context.params.type
context.doc = doc
context.type = type
@triggerRoute 'type'
else
next()
return
entry: (context, next) ->
doc = app.docs.findBy 'slug', context.params.doc
if entry = doc?.findEntryByPathAndHash(context.params.path, context.hash)
context.doc = doc
context.entry = entry
@triggerRoute 'entry'
else
next()
return
root: ->
@triggerRoute 'root'
return
about: (context) ->
context.page = 'about'
@triggerRoute 'page'
return
news: (context) ->
context.page = 'news'
@triggerRoute 'page'
return
help: (context) ->
context.page = 'help'
@triggerRoute 'page'
return
notFound: (context) ->
@triggerRoute 'notFound'
return
isRoot: ->
location.pathname is '/'
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
# When the path is "/#/path", replace it with "/path"
if @isRoot() and path = @getInitialPath()
page.replace path + location.search, null, true
return
getInitialPath: ->
try
(new RegExp "#/(.+)").exec(decodeURIComponent location.hash)?[1]
catch
replaceHash: (hash) ->
page.replace location.pathname + location.search + (hash or ''), null, true
return

View file

@ -0,0 +1,221 @@
class app.Searcher
$.extend @prototype, Events
CHUNK_SIZE = 10000
SEPARATOR = '.'
DEFAULTS =
max_results: app.config.max_results
fuzzy_min_length: 3
constructor: (options = {}) ->
@options = $.extend {}, DEFAULTS, options
find: (data, attr, query) ->
@kill()
@data = data
@attr = attr
@query = query
@setup()
if @isValid() then @match() else @end()
return
setup: ->
@query = @normalizeQuery @query
@queryLength = @query.length
@dataLength = @data.length
@matchers = ['exactMatch']
@totalResults = 0
@setupFuzzy()
return
setupFuzzy: ->
if @queryLength >= @options.fuzzy_min_length
@fuzzyRegexp = @queryToFuzzyRegexp @query
@matchers.push 'fuzzyMatch'
return
isValid: ->
@queryLength > 0
end: ->
@triggerResults [] unless @totalResults
@free()
return
kill: ->
if @timeout
clearTimeout @timeout
@free()
return
free: ->
@data = @attr = @query = @queryLength = @dataLength =
@fuzzyRegexp = @matchers = @totalResults = @scoreMap =
@cursor = @matcher = @timeout = null
return
match: =>
if not @foundEnough() and @matcher = @matchers.shift()
@setupMatcher()
@matchChunks()
else
@end()
return
setupMatcher: ->
@cursor = 0
@scoreMap = new Array(101)
return
matchChunks: =>
@matchChunk()
if @cursor is @dataLength or @scoredEnough()
@delay @match
@sendResults()
else
@delay @matchChunks
return
matchChunk: ->
for [0...@chunkSize()]
if score = @[@matcher](@data[@cursor][@attr])
@addResult @data[@cursor], score
@cursor++
return
chunkSize: ->
if @cursor + CHUNK_SIZE > @dataLength
@dataLength % CHUNK_SIZE
else
CHUNK_SIZE
scoredEnough: ->
@scoreMap[100]?.length >= @options.max_results
foundEnough: ->
@totalResults >= @options.max_results
addResult: (object, score) ->
(@scoreMap[Math.round(score)] or= []).push(object)
@totalResults++
return
getResults: ->
results = []
for objects in @scoreMap by -1 when objects
results.push.apply results, objects
results[0...@options.max_results]
sendResults: ->
results = @getResults()
@triggerResults results if results.length
return
triggerResults: (results) ->
@trigger 'results', results
return
delay: (fn) ->
@timeout = setTimeout(fn, 1)
normalizeQuery: (string) ->
string.replace(/\s/g, '').toLowerCase()
queryToFuzzyRegexp: (string) ->
chars = string.split ''
chars[i] = $.escapeRegexp(char) for char, i in chars
new RegExp chars.join('.*?') # abc -> /a.*?b.*?c.*?/
#
# Match functions
#
index = # position of the query in the string being matched
match = # regexp match data
score = # score for the current match
separators = # counter
i = null # cursor
exactMatch: (value) ->
index = value.indexOf @query
return unless index >= 0
# Remove one point for each unmatched character.
score = 100 - (value.length - @queryLength)
if index > 0
# If the character preceding the query is a dot, assign the same score
# as if the query was found at the beginning of the string, minus one.
if value[index - 1] is SEPARATOR
score += index - 1
# Don't match a single-character query unless it's found at the beginning
# of the string or is preceded by a dot.
else if @queryLength is 1
return
# (1) Remove one point for each unmatched character up to the nearest
# preceding dot or the beginning of the string.
# (2) Remove one point for each unmatched character following the query.
else
i = index - 2
i-- while i >= 0 and value[i] isnt SEPARATOR
score -= (index - i) + # (1)
(value.length - @queryLength - index) # (2)
# Remove one point for each dot preceding the query, except for the one
# immediately before the query.
separators = 0
i = index - 2
while i >= 0
separators++ if value[i] is SEPARATOR
i--
score -= separators
# Remove five points for each dot following the query.
separators = 0
i = value.length - @queryLength - index - 1
while i >= 0
separators++ if value[index + @queryLength + i] is SEPARATOR
i--
score -= separators * 5
Math.max 1, score
fuzzyMatch: (value) ->
return if value.length <= @queryLength or value.indexOf(@query) >= 0
return unless match = @fuzzyRegexp.exec(value)
# When the match is at the beginning of the string or preceded by a dot.
if match.index is 0 or value[match.index - 1] is SEPARATOR
Math.max 66, 100 - match[0].length
# When the match is at the end of the string.
else if match.index + match[0].length is value.length
Math.max 33, 67 - match[0].length
# When the match is in the middle of the string.
else
Math.max 1, 34 - match[0].length
class app.SynchronousSearcher extends app.Searcher
match: =>
if @matcher
@allResults or= []
@allResults.push.apply @allResults, @getResults()
super
free: ->
@allResults = null
super
end: ->
@sendResults true
super
sendResults: (end) ->
if end and @allResults?.length
@triggerResults @allResults
delay: (fn) ->
fn()

View file

@ -0,0 +1,25 @@
class app.Settings
hasDocs: ->
try
!!Cookies.get 'docs'
catch
getDocs: ->
try
Cookies.get('docs')?.split('/') or app.config.default_docs
catch
app.config.default_docs
setDocs: (docs) ->
try
Cookies.set 'docs', docs.join('/'),
path: '/'
expires: 1e8
catch
return
reset: ->
try
Cookies.expire 'docs'
catch
return

View file

@ -0,0 +1,108 @@
class app.Shortcuts
$.extend @prototype, Events
constructor: ->
@start()
start: ->
$.on document, 'keydown', @onKeydown
$.on document, 'keypress', @onKeypress
return
stop: ->
$.off document, 'keydown', @onKeydown
$.off document, 'keypress', @onKeypress
return
onKeydown: (event) =>
result = if event.ctrlKey or event.metaKey
@handleKeydownSuperEvent event unless event.altKey or event.shiftKey
else if event.shiftKey
@handleKeydownShiftEvent event unless event.altKey
else if event.altKey
@handleKeydownAltEvent event
else
@handleKeydownEvent event
event.preventDefault() if result is false
return
onKeypress: (event) =>
unless event.ctrlKey or event.metaKey
result = @handleKeypressEvent event
event.preventDefault() if result is false
return
handleKeydownEvent: (event) ->
if not event.target.form and 65 <= event.which <= 90
@trigger 'typing'
return
switch event.which
when 8
@trigger 'typing' unless event.target.form
when 13
@trigger 'enter'
when 27
@trigger 'escape'
when 32
@trigger 'pageDown'
false
when 33
@trigger 'pageUp'
when 34
@trigger 'pageDown'
when 35
@trigger 'end'
when 36
@trigger 'home'
when 37
@trigger 'left' unless event.target.value
when 38
@trigger 'up'
false
when 39
@trigger 'right' unless event.target.value
when 40
@trigger 'down'
false
handleKeydownSuperEvent: (event) ->
switch event.which
when 13
@trigger 'superEnter'
when 37
@trigger 'superLeft'
false
when 38
@trigger 'home'
false
when 39
@trigger 'superRight'
false
when 40
@trigger 'end'
false
handleKeydownShiftEvent: (event) ->
if not event.target.form and 65 <= event.which <= 90
@trigger 'typing'
return
if event.which is 32
@trigger 'pageUp'
false
handleKeydownAltEvent: (event) ->
switch event.which
when 38
@trigger 'altUp'
false
when 40
@trigger 'altDown'
false
handleKeypressEvent: (event) ->
if event.which is 63 and not event.target.value
@trigger 'help'
false

View file

@ -0,0 +1,25 @@
#= require_tree ./vendor
#= require lib/license
#= require_tree ./lib
#= require app/app
#= require app/config
#= require_tree ./app
#= require collections/collection
#= require_tree ./collections
#= require models/model
#= require_tree ./models
#= require views/view
#= require_tree ./views
#= require_tree ./templates
init = ->
document.removeEventListener 'DOMContentLoaded', init
app.init()
document.addEventListener 'DOMContentLoaded', init, false

View file

@ -0,0 +1,43 @@
class app.Collection
constructor: (objects = []) ->
@reset objects
model: ->
app.models[@constructor.model]
reset: (objects = []) ->
@models = []
@add object for object in objects
return
add: (object) ->
if object instanceof app.Model
@models.push object
else if object instanceof Array
@add obj for obj in object
else if object instanceof app.Collection
@models.push object.all()...
else
@models.push new (@model())(object)
return
size: ->
@models.length
isEmpty: ->
@models.length is 0
each: (fn) ->
fn(model) for model in @models
return
all: ->
@models
findBy: (attr, value) ->
for model in @models
return model if model[attr] is value
return
findAllBy: (attr, value) ->
model for model in @models when model[attr] is value

View file

@ -0,0 +1,30 @@
class app.collections.Docs extends app.Collection
@model: 'Doc'
# Load models concurrently.
# It's not pretty but I didn't want to import a promise library only for this.
CONCURRENCY = 3
load: (onComplete, onError, options) ->
i = 0
next = =>
if i < @models.length
@models[i].load(next, fail, options)
else if i is @models.length + CONCURRENCY - 1
onComplete()
i++
return
fail = (args...) ->
if onError
onError(args...)
onError = null
next()
return
next() for [0...CONCURRENCY]
return
clearCache: ->
doc.clearCache() for doc in @models
return

View file

@ -0,0 +1,2 @@
class app.collections.Entries extends app.Collection
@model: 'Entry'

View file

@ -0,0 +1,2 @@
class app.collections.Types extends app.Collection
@model: 'Type'

View file

@ -0,0 +1,81 @@
#
# App
#
_init = app.init
app.init = ->
console.time 'Init'
_init.call(app)
console.timeEnd 'Init'
console.time 'Load'
_start = app.start
app.start = ->
_start.call(app, arguments...)
console.timeEnd 'Load'
_super = app.Searcher
_proto = app.Searcher.prototype
#
# Searcher
#
app.Searcher = ->
_super.apply @, arguments
_setup = @setup.bind(@)
@setup = ->
console.groupCollapsed "Search: #{@query}"
console.time 'Total'
_setup()
_match = @match.bind(@)
@match = =>
if @matcher
console.timeEnd @matcher
if @matcher is 'exactMatch'
for entries, score in @scoreMap by -1 when entries
console.log '' + score + ': ' + entries.map((entry) -> entry.text).join("\n ")
_match()
_setupMatcher = @setupMatcher.bind(@)
@setupMatcher = ->
console.time @matcher
_setupMatcher()
_end = @end.bind(@)
@end = ->
console.log "Results: #{@totalResults}"
console.groupEnd()
console.timeEnd 'Total'
_end()
_kill = @kill.bind(@)
@kill = ->
if @timeout
console.timeEnd @matcher if @matcher
console.groupEnd()
console.timeEnd 'Total'
console.warn 'Killed'
_kill()
return
_proto.constructor = app.Searcher
app.Searcher.prototype = _proto
#
# View tree
#
@viewTree = (view = app.document, level = 0) ->
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
if typeof value is 'object' and value.setupElement
@viewTree(value, level + 1)
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
return

View file

@ -0,0 +1,2 @@
//= depend_on docs.json
app.DOCS = <%= File.read App.docs_manifest_path %>;

View file

@ -0,0 +1,122 @@
MIME_TYPES =
json: 'application/json'
html: 'text/html'
@ajax = (options) ->
applyDefaults(options)
serializeData(options)
xhr = new XMLHttpRequest()
xhr.open(options.type, options.url, options.async)
applyCallbacks(xhr, options)
applyHeaders(xhr, options)
xhr.send(options.data)
if options.async
abort: abort.bind(undefined, xhr)
else
parseResponse(xhr, options)
ajax.defaults =
async: true
dataType: 'json'
timeout: 30000
type: 'GET'
# contentType
# context
# data
# error
# headers
# success
# url
applyDefaults = (options) ->
for key of ajax.defaults
options[key] ?= ajax.defaults[key]
return
serializeData = (options) ->
return unless options.data
if options.type is 'GET'
options.url += '?' + serializeParams(options.data)
options.data = null
else
options.data = serializeParams(options.data)
return
serializeParams = (params) ->
("#{encodeURIComponent key}=#{encodeURIComponent value}" for key, value of params).join '&'
applyCallbacks = (xhr, options) ->
return unless options.async
xhr.timer = setTimeout onTimeout.bind(undefined, xhr, options), options.timeout
xhr.onreadystatechange = ->
if xhr.readyState is 4
clearTimeout(xhr.timer)
onComplete(xhr, options)
return
return
applyHeaders = (xhr, options) ->
options.headers or= {}
if options.contentType
options.headers['Content-Type'] = options.contentType
if not options.headers['Content-Type'] and options.data and options.type isnt 'GET'
options.headers['Content-Type'] = 'application/x-www-form-urlencoded'
if options.dataType
options.headers['Accept'] = MIME_TYPES[options.dataType] or options.dataType
if isSameOrigin(options.url)
options.headers['X-Requested-With'] = 'XMLHttpRequest'
for key, value of options.headers
xhr.setRequestHeader(key, value)
return
onComplete = (xhr, options) ->
if 200 <= xhr.status < 300
if (response = parseResponse(xhr, options))?
onSuccess response, xhr, options
else
onError 'invalid', xhr, options
else
onError 'error', xhr, options
return
onSuccess = (response, xhr, options) ->
options.success?.call options.context, response, xhr, options
return
onError = (type, xhr, options) ->
options.error?.call options.context, type, xhr, options
return
onTimeout = (xhr, options) ->
xhr.abort()
onError 'timeout', xhr, options
return
abort = (xhr) ->
clearTimeout(xhr.timer)
xhr.onreadystatechange = null
xhr.abort()
return
isSameOrigin = (url) ->
url.indexOf('http') isnt 0 or url.indexOf(location.origin) is 0
parseResponse = (xhr, options) ->
if options.dataType is 'json'
parseJSON(xhr.responseText)
else
xhr.responseText
parseJSON = (json) ->
try JSON.parse(json) catch

View file

@ -0,0 +1,26 @@
@Events =
on: (event, callback) ->
if event.indexOf(' ') >= 0
@on name, callback for name in event.split(' ')
else
((@_callbacks ?= {})[event] ?= []).push callback
@
off: (event, callback) ->
if event.indexOf(' ') >= 0
@off name, callback for name in event.split(' ')
else if (callbacks = @_callbacks?[event]) and (index = callbacks.indexOf callback) >= 0
callbacks.splice index, 1
delete @_callbacks[event] unless callbacks.length
@
trigger: (event, args...) ->
if callbacks = @_callbacks?[event]
callback? args... for callback in callbacks.slice(0)
@trigger 'all', event, args... unless event is 'all'
@
removeEvent: (event) ->
if @_callbacks?
delete @_callbacks[name] for name in event.split(' ')
@

View file

@ -0,0 +1,7 @@
###
* Copyright 2013 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:
* http://mozilla.org/MPL/2.0/
###

View file

@ -0,0 +1,161 @@
###
* Based on github.com/visionmedia/page.js
* Licensed under the MIT license
* Copyright 2012 TJ Holowaychuk <tj@vision-media.ca>
###
running = false
currentState = null
callbacks = []
@page = (value, fn) ->
if typeof value is 'function'
page '*', value
else if typeof fn is 'function'
route = new Route(value)
callbacks.push route.middleware(fn)
else if typeof value is 'string'
page.show(value, fn)
else
page.start(value)
return
page.start = (options = {}) ->
unless running
running = true
addEventListener 'popstate', onpopstate
addEventListener 'click', onclick
page.replace currentPath(), null, null, true
return
page.stop = ->
if running
running = false
removeEventListener 'click', onclick
removeEventListener 'popstate', onpopstate
return
page.show = (path, state) ->
return if path is currentState?.path
context = new Context(path, state)
currentState = context.state
page.dispatch(context)
context.pushState()
context
page.replace = (path, state, skipDispatch, init) ->
context = new Context(path, state or currentState)
context.init = init
currentState = context.state
page.dispatch(context) unless skipDispatch
context.replaceState()
context
page.dispatch = (context) ->
i = 0
next = ->
fn(context, next) if fn = callbacks[i++]
return
next()
return
currentPath = ->
location.pathname + location.search + location.hash
class Context
@initialPath: currentPath()
@sessionId: Date.now()
@stateId: 0
@isInitialPopState: (state) ->
state.path is @initialPath and @stateId is 1
@isSameSession: (state) ->
state.sessionId is @sessionId
constructor: (@path = '/', @state = {}) ->
@pathname = @path.replace /(?:\?([^#]*))?(?:#(.*))?$/, (_, query, hash) =>
@query = query
@hash = hash
''
@state.id ?= @constructor.stateId++
@state.sessionId ?= @constructor.sessionId
@state.path = @path
pushState: ->
history.pushState @state, '', @path
return
replaceState: ->
history.replaceState @state, '', @path
return
class Route
constructor: (@path, options = {}) ->
@keys = []
@regexp = pathtoRegexp @path, @keys
middleware: (fn) ->
(context, next) =>
if @match context.pathname, params = []
context.params = params
fn(context, next)
else
next()
return
match: (path, params) ->
return unless matchData = @regexp.exec(path)
for value, i in matchData[1..]
value = decodeURIComponent value if typeof value is 'string'
if key = @keys[i]
params[key.name] = value
else
params.push value
true
pathtoRegexp = (path, keys) ->
return path if path instanceof RegExp
path = "(#{path.join '|'})" if path instanceof Array
path = path
.replace(/\/\(/g, '(?:/')
.replace /(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, (_, slash = '', format = '', key, capture, optional) ->
keys.push name: key, optional: !!optional
str = if optional then '' else slash
str += '(?:'
str += slash if optional
str += format
str += capture or if format then '([^/.]+?)' else '([^/]+?)'
str += ')'
str += optional if optional
str
.replace(/([\/.])/g, '\\$1')
.replace(/\*/g, '(.*)')
new RegExp "^#{path}$"
onpopstate = (event) ->
return if not event.state or Context.isInitialPopState(event.state)
if Context.isSameSession(event.state)
page.replace(event.state.path, event.state)
else
location.reload()
return
onclick = (event) ->
return if event.which isnt 1 or event.metaKey or event.ctrlKey or event.shiftKey or event.defaultPrevented
link = event.target
link = link.parentElement while link and link.tagName isnt 'A'
if link and not link.target and isSameOrigin(link.href)
event.preventDefault()
page.show link.pathname + link.search + link.hash
return
isSameOrigin = (url) ->
url.indexOf("#{location.protocol}//#{location.hostname}") is 0

View file

@ -0,0 +1,23 @@
class @Store
get: (key) ->
try
JSON.parse localStorage.getItem(key)
catch
set: (key, value) ->
try
localStorage.setItem(key, JSON.stringify(value))
true
catch
del: (key) ->
try
localStorage.removeItem(key)
true
catch
clear: ->
try
localStorage.clear()
true
catch

View file

@ -0,0 +1,284 @@
#
# Traversing
#
@$ = (selector, el = document) ->
try el.querySelector(selector) catch
@$$ = (selector, el = document) ->
try el.querySelectorAll(selector) catch
$.id = (id) ->
document.getElementById(id)
$.hasChild = (parent, el) ->
loop
return true if el is parent
return if el is document.body
el = el.parentElement
$.closestLink = (el, parent = document.body) ->
loop
return el if el.tagName is 'A'
return if el is parent
el = el.parentElement
#
# Events
#
$.on = (el, event, callback) ->
if event.indexOf(' ') >= 0
$.on el, name, callback for name in event.split(' ')
else
el.addEventListener(event, callback)
return
$.off = (el, event, callback) ->
if event.indexOf(' ') >= 0
$.off el, name, callback for name in event.split(' ')
else
el.removeEventListener(event, callback)
return
$.trigger = (el, type, canBubble = true, cancelable = true) ->
event = document.createEvent 'Event'
event.initEvent(type, canBubble, cancelable)
el.dispatchEvent(event)
return
$.click = (el) ->
event = document.createEvent 'MouseEvent'
event.initMouseEvent 'click', true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null
el.dispatchEvent(event)
return
$.stopEvent = (event) ->
event.preventDefault()
event.stopPropagation()
event.stopImmediatePropagation()
return
#
# Manipulation
#
buildFragment = (value) ->
fragment = document.createDocumentFragment()
if $.isCollection(value)
fragment.appendChild(child) for child in $.makeArray(value)
else
fragment.innerHTML = value
fragment
$.append = (el, value) ->
if typeof value is 'string'
el.insertAdjacentHTML 'beforeend', value
else
value = buildFragment(value) if $.isCollection(value)
el.appendChild(value)
return
$.prepend = (el, value) ->
if not el.firstChild
$.append(value)
else if typeof value is 'string'
el.insertAdjacentHTML 'afterbegin', value
else
value = buildFragment(value) if $.isCollection(value)
el.insertBefore(value, el.firstChild)
return
$.before = (el, value) ->
if typeof value is 'string' or $.isCollection(value)
value = buildFragment(value)
el.parentElement.insertBefore(value, el)
return
$.after = (el, value) ->
if typeof value is 'string' or $.isCollection(value)
value = buildFragment(value)
if el.nextSibling
el.parentElement.insertBefore(value, el.nextSibling)
else
el.parentElement.appendChild(value)
return
$.remove = (value) ->
if $.isCollection(value)
el.parentElement.removeChild(el) for el in $.makeArray(value)
else
value.parentElement.removeChild(value)
return
$.empty = (el) ->
el.removeChild(el.firstChild) while el.firstChild
return
# Calls the function while the element is off the DOM to avoid triggering
# unecessary reflows and repaints.
$.batchUpdate = (el, fn) ->
parent = el.parentNode
sibling = el.nextSibling
parent.removeChild(el)
fn(el)
if (sibling)
parent.insertBefore(el, sibling)
else
parent.appendChild(el)
return
#
# Offset
#
$.rect = (el) ->
el.getBoundingClientRect()
$.offset = (el, container = document.body) ->
top = 0
left = 0
while el and el isnt container
top += el.offsetTop
left += el.offsetLeft
el = el.offsetParent
top: top
left: left
$.scrollParent = (el) ->
while el = el.parentElement
break if el.scrollTop > 0
break if getComputedStyle(el).overflowY in ['auto', 'scroll']
el
$.scrollTo = (el, parent, position = 'center', options = {}) ->
return unless el
parent ?= $.scrollParent(el)
return unless parent
parentHeight = parent.clientHeight
return unless parent.scrollHeight > parentHeight
top = $.offset(el, parent).top
switch position
when 'top'
parent.scrollTop = top - (options.margin or 20)
when 'center'
parent.scrollTop = top - Math.round(parentHeight / 2 - el.offsetHeight / 2)
when 'continuous'
scrollTop = parent.scrollTop
height = el.offsetHeight
# 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 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)
return
$.scrollToWithImageLock = (el, parent, args...) ->
parent ?= $.scrollParent(el)
return unless parent
$.scrollTo el, parent, args...
# Lock the scroll position on the target element for up to 3 seconds while
# nearby images are loaded and rendered.
for image in parent.getElementsByTagName('img') when not image.complete
do ->
onLoad = (event) ->
clearTimeout(timeout)
unbind(event.target)
$.scrollTo el, parent, args...
unbind = (target) ->
$.off target, 'load', onLoad
$.on image, 'load', onLoad
timeout = setTimeout unbind.bind(null, image), 3000
return
# Calls the function while locking the element's position relative to the window.
$.lockScroll = (el, fn) ->
if parent = $.scrollParent(el)
top = $.rect(el).top
top -= $.rect(parent).top unless parent in [document.body, document.documentElement]
fn()
parent.scrollTop = $.offset(el, parent).top - top
else
fn()
return
#
# Utilities
#
$.extend = (target, objects...) ->
for object in objects when object
for key, value of object
target[key] = value
target
$.makeArray = (object) ->
if Array.isArray(object)
object
else
Array::slice.apply(object)
# Returns true if the object is an array or a collection of DOM elements.
$.isCollection = (object) ->
Array.isArray(object) or typeof object?.item is 'function'
ESCAPE_HTML_MAP =
'&': '&amp;'
'<': '&lt;'
'>': '&gt;'
'"': '&quot;'
"'": '&#x27;'
'/': '&#x2F;'
ESCAPE_HTML_REGEXP = /[&<>"'\/]/g
$.escape = (string) ->
string.replace ESCAPE_HTML_REGEXP, (match) -> ESCAPE_HTML_MAP[match]
ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g
$.escapeRegexp = (string) ->
string.replace ESCAPE_REGEXP, "\\$1"
#
# Miscellaneous
#
$.noop = ->
$.popup = (value) ->
open value.href or value, '_blank'
return
$.isTouchScreen = ->
typeof ontouchstart isnt 'undefined'
HIGHLIGHT_DEFAULTS =
className: 'highlight'
delay: 1000
$.highlight = (el, options = {}) ->
options = $.extend {}, HIGHLIGHT_DEFAULTS, options
el.classList.add(options.className)
setTimeout (-> el.classList.remove(options.className)), options.delay
return

View file

@ -0,0 +1,85 @@
class app.models.Doc extends app.Model
# Attributes: name, slug, type, version, index_path, mtime
constructor: ->
super
@reset @
reset: (data) ->
@resetEntries data.entries
@resetTypes data.types
return
resetEntries: (entries) ->
@entries = new app.collections.Entries(entries)
@entries.each (entry) => entry.doc = @
return
resetTypes: (types) ->
@types = new app.collections.Types(types)
@types.each (type) => type.doc = @
return
fullPath: (path = '') ->
path = "/#{path}" unless path[0] is '/'
"/#{@slug}#{path}"
fileUrl: (path) ->
"#{app.config.docs_host}#{@fullPath(path)}"
indexUrl: ->
"#{app.indexHost()}/#{@index_path}"
indexEntry: ->
new app.models.Entry
doc: @
name: @name
path: 'index'
findEntryByPathAndHash: (path, hash) ->
if hash and entry = @entries.findBy 'path', "#{path}##{hash}"
entry
else if path is 'index'
@indexEntry()
else
@entries.findBy 'path', path
load: (onSuccess, onError, options = {}) ->
return if options.readCache and @_loadFromCache(onSuccess)
callback = (data) =>
@reset data
onSuccess()
@_setCache data if options.writeCache
ajax
url: @indexUrl()
success: callback
error: onError
clearCache: ->
app.store.del @slug
return
_loadFromCache: (onSuccess) ->
return unless data = @_getCache()
callback = =>
@reset data
onSuccess()
setTimeout callback, 0
true
_getCache: ->
return unless data = app.store.get @slug
if data[0] is @mtime
return data[1]
else
@clearCache()
return
_setCache: (data) ->
app.store.set @slug, [@mtime, data]
return

View file

@ -0,0 +1,46 @@
class app.models.Entry extends app.Model
# Attributes: name, type, path
SEPARATORS_REGEXP = /\ |#|::/g
PARANTHESES_REGEXP = /\(.*?\)$/
constructor: ->
super
@text = @searchValue()
searchValue: ->
@name
.toLowerCase()
.replace('...', ' ')
.replace(' event', '')
.replace(SEPARATORS_REGEXP, '.')
.replace(/\.+/g, '.')
.replace(PARANTHESES_REGEXP, '')
.trim()
fullPath: ->
@doc.fullPath if @isIndex() then '' else @path
filePath: ->
@doc.fullPath @_filePath()
fileUrl: ->
@doc.fileUrl @_filePath()
_filePath: ->
result = @path.replace /#.*/, ''
result += '.html' unless result[-5..-1] is '.html'
result
isIndex: ->
@path is 'index'
getType: ->
@doc.types.findBy 'name', @type
loadFile: (onSuccess, onError) ->
ajax
url: @fileUrl()
dataType: 'html'
success: onSuccess
error: onError

View file

@ -0,0 +1,3 @@
class app.Model
constructor: (attributes) ->
@[key] = value for key, value of attributes

View file

@ -0,0 +1,8 @@
class app.models.Type extends app.Model
# Attributes: name, slug, count
fullPath: ->
"/#{@doc.slug}-#{@slug}/"
entries: ->
@doc.entries.findAllBy 'type', @name

View file

@ -0,0 +1,11 @@
app.templates.render = (name, value, args...) ->
template = app.templates[name]
if Array.isArray(value)
result = ''
result += template(val, args...) for val in value
result
else if typeof template is 'function'
template(value, args...)
else
template

View file

@ -0,0 +1,44 @@
error = (title, text = '', links = '') ->
text = """<p class="_error-text">#{text}</p>""" if text
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>'
app.templates.notFoundPage = ->
error """ Oops, that page doesn't exist. """,
""" It may be missing from the source documentation or this could be a bug. """,
back
app.templates.pageLoadError = ->
error """ Oops, that page failed to load. """,
""" It may be missing from the server or you could be offline.<br>
If you keep seeing this, you're likely behind a proxy or firewall which blocks cross-domain requests. """,
""" #{back} &middot; <a href="#" class="_error-link" data-retry>Retry</a> """
app.templates.bootError = ->
error """ Oops, the app failed to load. """,
""" Check your Internet connection and try <a href="javascript:location.reload()">reloading</a>.<br>
If you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. """
app.templates.unsupportedBrowser = """
<div class="_fail">
<h1 class="_fail-title">Your browser is unsupported, sorry.</h1>
<p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:
<ul class="_fail-list">
<li>Recent version of Chrome
<li>Recent version of Firefox
<li>Safari 5.1+
<li>Opera 12.1+
<li>Internet Explorer 10+
<li>iOS 6+
<li>Android 4.1+
<li>Windows Phone 8+
</ul>
<p class="_fail-text">
If you're unable to upgrade, I apologize.
I decided to prioritize speed and new features over support for older browsers.
<p class="_fail-text">
Thibaut <a href="https://twitter.com/DevDocs" class="_fail-link">@DevDocs</a>
</div>
"""

View file

@ -0,0 +1,9 @@
notice = (text) -> """<p class="_notice-text">#{text}</p>"""
app.templates.singleDocNotice = (doc) ->
notice """ You're currently browsing the #{doc.name} documentation. To browse all docs, go to
<a href="http://#{app.config.production_host}" target="_top">#{app.config.production_host}</a>. """
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>. """

View file

@ -0,0 +1,23 @@
notif = (title, html) ->
html = html.replace /<a/g, '<a class="_notif-link"'
"""<h5 class="_notif-title">#{title}</h5>#{html}<div class="_notif-close"></div>"""
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. """
app.templates.notifError = ->
textNotif """ Oops, an error occured. """,
""" Try <a href="javascript:app.reload()">reloading</a>, and if the problem persists,
<a href="javascript:app.reset()">resetting the app</a>.<br>
I track these errors automatically but feel free to contact me. """
app.templates.notifInvalidLocation = ->
textNotif """ DevDocs must be loaded from #{app.config.production_host} """,
""" Otherwise things are likely to break. """
app.templates.notifNews = (news) ->
notif 'Changelog', app.templates.newsList(news)

View file

@ -0,0 +1,145 @@
app.templates.aboutPage = -> """
<div class="_toc">
<h3 class="_toc-title">Table of Contents</h3>
<ul class="_toc-list">
<li><a href="#credits">Credits</a>
<li><a href="#thanks">Special Thanks</a>
<li><a href="#faq">FAQ</a>
<li><a href="#copyright">Copyright</a>
</ul>
</div>
<h1 class="_lined-heading">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>Proudly sponsored by <a href="http://www.maxcdn.com">MaxCDN</a>content delivery that developers love
<li>Free and <a href="https://github.com/Thibaut/devdocs">open source</a>
<iframe class="_github-btn" src="http://ghbtns.com/github-btn.html?user=Thibaut&repo=devdocs&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20"></iframe>
</ul>
<p>To keep up-to-date with the latest development and community news:
<ul>
<li>Subscribe to the <a href="http://eepurl.com/HnLUz">newsletter</a>
<li>Follow <a href="https://twitter.com/DevDocs">@DevDocs</a> on Twitter
<li>Join the <a href="https://groups.google.com/d/forum/devdocs">mailing list</a>
</ul>
<p class="_note _note-green">If you use and like DevDocs, please consider donating through
<a href="https://www.gittip.com/Thibaut/">Gittip</a> or
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4PTFAGT7K6QVG">PayPal</a>.<br>
Your support helps sustain the project and is highly appreciated.
<h2 class="_lined-heading" id="credits">Credits</h2>
<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="thanks">Special Thanks</h2>
<ul>
<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
</ul>
<h2 class="_lined-heading" id="faq">Questions & Answsers</h2>
<dl>
<dt>Does it work offline?
<dd>Yes! DevDocs is open source. You can run <a href="https://github.com/Thibaut/devdocs">the code</a> locally on your computer.<br>
An offline version that requires no setup is planned for the future.
<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>
If you have a specific feature request, add it to the <a href="https://github.com/Thibaut/devdocs/issues">issue tracker</a>.<br>
Otherwise use the <a href="https://groups.google.com/d/forum/devdocs">mailing list</a>.
<dt>Where can I report bugs?
<dd>In the <a href="https://github.com/Thibaut/devdocs/issues">issue tracker</a>. Thanks!
</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 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.
"""
credits = [
[ 'Angular.js',
'2010-2013 Google, Inc.',
'CC BY',
'http://creativecommons.org/licenses/by/3.0/'
], [
'Backbone.js',
'2010-2013 Jeremy Ashkenas, DocumentCloud',
'MIT',
'https://raw.github.com/jashkenas/backbone/master/LICENSE'
], [
'CoffeeScript',
'2009-2013 Jeremy Ashkenas',
'MIT',
'https://raw.github.com/jashkenas/coffee-script/master/LICENSE'
], [
'CSS<br>DOM<br>HTML<br>JavaScript',
'2005-2013 Mozilla Developer Network and individual contributors',
'CC BY-SA',
'http://creativecommons.org/licenses/by-sa/2.5/'
], [
'Ember.js',
'2013 Yehuda Katz, Tom Dale and Ember.js contributors',
'MIT',
'https://raw.github.com/emberjs/ember.js/master/LICENSE'
], [
'HTTP',
'1999 The Internet Society',
'Custom',
'http://www.w3.org/Protocols/rfc2616/rfc2616-sec21.html#sec21'
], [
'jQuery',
'2009 Packt Publishing<br>&copy; 2013 jQuery Foundation',
'MIT',
'https://raw.github.com/jquery/api.jquery.com/master/LICENSE-MIT.txt'
], [
'jQuery Mobile',
'2013 jQuery Foundation',
'MIT',
'https://raw.github.com/jquery/api.jquerymobile.com/master/LICENSE-MIT.txt'
], [
'jQuery UI',
'2013 jQuery Foundation',
'MIT',
'https://raw.github.com/jquery/api.jqueryui.com/master/LICENSE-MIT.txt'
], [
'Less',
'2009-2013 Alexis Sellier &amp; The Core Less Team',
'Apache v2',
'https://raw.github.com/less/less.js/master/LICENSE'
], [
'Lo-Dash',
'2009-2013 The Dojo Foundation',
'MIT',
'https://raw.github.com/lodash/lodash/master/LICENSE.txt'
], [
'Node.js',
'Joyent, Inc. and other Node contributors',
'MIT',
'https://raw.github.com/joyent/node/master/LICENSE'
], [
'PHP',
'1997-2013 The PHP Documentation Group',
'CC BY',
'http://creativecommons.org/licenses/by/3.0/'
], [
'Sass',
'2006-2013 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein',
'MIT',
'https://raw.github.com/nex3/sass/master/MIT-LICENSE'
], [
'Underscore.js',
'2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors',
'MIT',
'https://raw.github.com/jashkenas/underscore/master/LICENSE'
]
]

View file

@ -0,0 +1,89 @@
ctrlKey = if navigator.userAgent.indexOf 'Mac OS X' then 'cmd' else 'ctrl'
app.templates.helpPage = """
<div class="_toc">
<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>
</ul>
</div>
<h2 class="_lined-heading" id="search">Search</h2>
<p>
The search is case-insensitive, ignores spaces, 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>.
<dl>
<dt id="doc_search">Searching a specific documentation
<dd>
You can scope the search to a specific 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>
To clear the current scope, empty the search field and hit <code class="_label">Backspace</code>.
<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>
To search a specific 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.
<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</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>.
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/search-bar-easily-choose-your-search-engine#w_keywords">these instructions</a>.
</dl>
<h2 class="_lined-heading" id="shortcuts">Keyboard Shortcuts</h2>
<h3 class="_shortcuts-title">Selection</h3>
<dl class="_shortcuts-dl">
<dt class="_shortcuts-dt">
<code class="_shortcut-code">&darr;</code>
<code class="_shortcut-code">&uarr;</code>
<dd class="_shortcuts-dd">Move selection
<dt class="_shortcuts-dt">
<code class="_shortcut-code">&rarr;</code>
<code class="_shortcut-code">&larr;</code>
<dd class="_shortcuts-dd">Show/hide sub-list
<dt class="_shortcuts-dt">
<code class="_shortcut-code">enter</code>
<dd class="_shortcuts-dd">Open selection
<dt class="_shortcuts-dt">
<code class="_shortcut-code">#{ctrlKey} + enter</code>
<dd class="_shortcuts-dd">Open selection in a new tab
</dl>
<h3 class="_shortcuts-title">Navigation</h3>
<dl class="_shortcuts-dl">
<dt class="_shortcuts-dt">
<code class="_shortcut-code">#{ctrlKey} + &larr;</code>
<code class="_shortcut-code">#{ctrlKey} + &rarr;</code>
<dd class="_shortcuts-dd">Go back/forward
<dt class="_shortcuts-dt">
<code class="_shortcut-code">alt + &darr;</code>
<code class="_shortcut-code">alt + &uarr;</code>
<dd class="_shortcuts-dd">Scroll step by step
<dt class="_shortcuts-dt">
<code class="_shortcut-code">space</code>
<code class="_shortcut-code">shift + space</code>
<dd class="_shortcuts-dd">Scroll screen by screen
<dt class="_shortcuts-dt">
<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">escape</code>
<dd class="_shortcuts-dd">Reset
<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, just press backspace or
continue to type and it will refocus the search field and start showing new results. """

View file

@ -0,0 +1,114 @@
app.templates.newsPage = ->
""" <h1 class="_lined-heading">Changelog</h1>
<p class="_note">For the latest news and updates,
subscribe to the <a href="http://eepurl.com/HnLUz">newsletter</a>
or follow <a href="https://twitter.com/DevDocs">@DevDocs</a>.
<div class="_news">#{app.templates.newsList app.news}</div> """
app.templates.newsList = (news) ->
result = ''
result += newsItem new Date(value[0]), value[1..] for value in news
result
MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
newsItem = (date, news) ->
date = """<span class="_news-date">#{MONTHS[date.getUTCMonth()]} #{date.getUTCDate()}</span>"""
result = ''
for text, i in news
text = text.split "\n"
title = """<span class="_news-title">#{text.shift()}</span>"""
result += """<div class="_news-row">#{if i is 0 then date else ''} #{title} #{text.join '<br>'}</div>"""
result
app.news = [
[ 1382572800000, # October 24, 2013
""" DevDocs is now <a href="https://github.com/Thibaut/devdocs">open source</a>. """
], [
1381276800000, # October 9, 2013
""" DevDocs is now available as a <a href="https://chrome.google.com/webstore/detail/devdocs/mnfehgbmkapmjnhcnbodoamcioleeooe">Chrome web app</a>. """
], [ 1379808000000, # September 22, 2013
""" New <a href="/php/">PHP</a> documentation """
], [
1378425600000, # September 6, 2013
""" New <a href="/lodash/">Lo-Dash</a> documentation """,
""" On mobile devices you can now search a specific documentation by typing its name and <code class="_label">Space</code>. """
], [
1377993600000, # September 1, 2013
""" New <a href="/jqueryui/">jQuery UI</a> and <a href="/jquerymobile/">jQuery Mobile</a> documentations """
], [
1377648000000, # August 28, 2013
""" New smartphone interface
Tested on iOS 6+ and Android 4.1+ """
], [
1377388800000, # August 25, 2013
""" New <a href="/ember/">Ember.js</a> documentation """
], [
1376784000000, # August 18, 2013
""" New <a href="/coffeescript/">CoffeeScript</a> documentation """,
""" URL search now automatically opens the first result. """
], [
1376352000000, # August 13, 2013
""" New <a href="/angular/">Angular.js</a> documentation """
], [
1376179200000, # August 11, 2013
""" New <a href="/sass/">Sass</a> and <a href="/less/">Less</a> documentations """
], [
1375660800000, # August 5, 2013
""" New <a href="/node/">Node.js</a> documentation """
], [
1375488000000, # August 3, 2013
""" Added support for OpenSearch """
], [
1375142400000, # July 30, 2013
""" New <a href="/backbone/">Backbone.js</a> documentation """
], [
1374883200000, # July 27, 2013
""" You can now customize the list of documentations.
New docs will be hidden by default, but you'll see a notification when there are new releases. """,
""" New <a href="/http/">HTTP</a> documentation """
], [
1373846400000, # July 15, 2013
""" URL search now works with single documentations: <a href="/#q=js%20sort">devdocs.io/#q=js sort</a> """
], [
1373673600000, # July 13, 2013
""" Added syntax highlighting """,
""" Added documentation versions """
], [
1373500800000, # July 11, 2013
""" New <a href="/underscore/">Underscore.js</a> documentation """,
""" Improved compatibility with tablets
A mobile version is planned as soon as other high priority features have been implemented. """
], [
1373414400000, # July 10, 2013
""" You can now search specific documentations.
Simply type the documentation's name and press <code class="_label">Tab</code>.
The name is fuzzy matched so you can use abbreviations like <code>js</code> for <code>JavaScript</code>. """
], [
1373241600000, # July 8, 2013
""" Improved search with fuzzy matching and better results
For example, searching <code>jqmka</code> now returns <code>jQuery.makeArray()</code>. """,
""" DevDocs finally has an icon. """,
""" <code class="_label">space</code> has replaced <code class="_label">alt + space</code> for scrolling down. """
], [
1373068800000, # July 6, 2013
""" New <a href="/dom/">DOM</a> and <a href="/dom_events/">DOM Events</a> documentations
DevDocs now includes almost all reference documents available on the Mozilla Developer Network.
Big thank you to Mozilla and all the people that contributed to MDN. """,
""" Implemented URL search: <a href="/#q=sort">devdocs.io/#q=sort</a> """
], [
1372723200000, # July 2, 2013
""" New <a href="/javascript/">JavaScript</a> documentation """
], [
1372377600000, # June 28, 2013
""" DevDocs made the front page of Hacker News!
Hi everyone thanks for trying DevDocs.
Please bear with me while I fix bugs and scramble to add more docs.
This is only v1. There's a lot more to come. """
], [
1371513600000, # June 18, 2013
""" Initial release """
]
]

View file

@ -0,0 +1,77 @@
app.templates.splash = """
<div class="_splash-title">DevDocs</div>
<a href="http://www.maxcdn.com" class="_splash-maxcdn">Sponsored by<span class="_maxcdn-logo-bw"> MaxCDN</span></a>
"""
<% 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>
<h2 class="_intro-title">Hi there!</h2>
<p>Thanks for downloading DevDocs. Here are a few things you should know:
<ol class="_intro-list">
<li>Your local version of DevDocs will not self-update. Unless you're offline or modifying the code,
I recommend using the hosted version at <a href="http://devdocs.io">devdocs.io</a>.
<li>Run <code class="_label">thor docs:list</code> to see all available documentations.
<li>Run <code class="_label">thor docs:download --all</code> to download/update all documentations.
<li>To be notified about new versions, don't forget to subscribe to the <a href="http://eepurl.com/HnLUz">newsletter</a>.
<li>The <a href="https://github.com/Thibaut/devdocs/issues">issue tracker</a> is the preferred channel for bug reports and
feature requests. For everything else, use the <a href="https://groups.google.com/d/forum/devdocs">mailing list</a>.
<li>Contributions are welcome. See the <a href="https://github.com/Thibaut/devdocs/blob/master/CONTRIBUTING.md">guidelines</a>.
<li>DevDocs is licensed under the terms of the Mozilla Public License v2.0. 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.
</ol>
<a href="http://www.maxcdn.com" class="_intro-maxcdn">Sponsored by<span class="_maxcdn-logo"> MaxCDN</span></a>
<p>That's all. Happy coding!
</div></div>
"""
<% else %>
app.templates.intro = """
<div class="_intro"><div class="_intro-message">
<a 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 pick your 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 — 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 — <a href="/help#browser_search">learn how</a>
<li>DevDocs works on mobile and is available as a <a href="https://chrome.google.com/webstore/detail/devdocs/mnfehgbmkapmjnhcnbodoamcioleeooe">Chrome web app</a>
<li>For the latest news, subscribe to the <a href="http://eepurl.com/HnLUz">newsletter</a> or 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="http://ghbtns.com/github-btn.html?user=Thibaut&repo=devdocs&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20"></iframe>
</ol>
<a href="http://www.maxcdn.com" class="_intro-maxcdn">Sponsored by<span class="_maxcdn-logo"> MaxCDN</span></a>
<p>That's all. Happy coding!
</div></div>
"""
<% end %>
app.templates.mobileNav = """
<nav class="_mobile-nav">
<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, subscribe to the <a href="http://eepurl.com/HnLUz">newsletter</a> or follow <a href="https://twitter.com/DevDocs">@DevDocs</a>
<li>DevDocs is <a href="https://github.com/Thibaut/devdocs">open source</a>
</ol>
<p>That's all. Happy coding!
<p class="_intro-maxcdn">Sponsored by <a href="http://www.maxcdn.com" class="_intro-maxcdn-logo">MaxCDN</a></p>
<a class="_intro-hide" data-hide-intro>Stop showing this message</a>
</div>
"""

View file

@ -0,0 +1,6 @@
app.templates.typePage = (type) ->
""" <h1>#{type.doc.name} / #{type.name}</h1>
<ul class="_entry-list">#{app.templates.render 'typePageEntry', type.entries()}</ul> """
app.templates.typePageEntry = (entry) ->
"""<li><a href="#{entry.fullPath()}">#{$.escape entry.name}</a></li>"""

View file

@ -0,0 +1,40 @@
templates = app.templates
templates.sidebarDoc = (doc, options = {}) ->
link = """<a href="#{doc.fullPath()}" class="_list-item _icon-#{doc.slug} """
link += if options.disabled then '_list-disabled' else '_list-dir'
link += """" data-slug="#{doc.slug}">"""
link += """<span class="_list-arrow"></span>""" unless options.disabled
link += """<span class="_list-count">#{doc.version}</span>""" if doc.version
link + "#{doc.name}</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>#{type.name}</a>"""
templates.sidebarEntry = (entry) ->
name = $.escape(entry.name)
"""<a href="#{entry.fullPath()}" class="_list-item" title="#{name}">#{name}</a>"""
templates.sidebarResult = (entry) ->
name = $.escape(entry.name)
"""<a href="#{entry.fullPath()}" class="_list-item _list-result _icon-#{entry.doc.slug}" title="#{entry.doc.name}: #{name}">#{name}</a>"""
templates.sidebarPageLink = (count) ->
"""<span class="_list-item _list-pagelink">Show more… (#{count})</span>"""
templates.sidebarLabel = (doc, options = {}) ->
label = """<label class="_list-item _list-label _icon-#{doc.slug}"""
label += ' _list-label-off' unless options.checked
label += """"><input type="checkbox" name="#{doc.slug}" class="_list-checkbox" """
label += 'checked' if options.checked
label + ">#{doc.name}</label>"
templates.sidebarVote = '<a href="https://trello.com/b/6BmTulfx/devdocs-documentation" class="_list-link" target="_blank">Vote for new documentation</a>'
sidebarFooter = (html) -> """<div class="_sidebar-footer">#{html}</div>"""
templates.sidebarSettings = ->
sidebarFooter """<a class="_sidebar-footer-link _sidebar-footer-edit" data-pick-docs>Select documentation</a>"""
templates.sidebarSave = ->
sidebarFooter """<a class="_sidebar-footer-link _sidebar-footer-save">Save</a>"""

141
assets/javascripts/vendor/cookies.js vendored Normal file
View file

@ -0,0 +1,141 @@
/*!
* Cookies.js - 0.3.1
* Wednesday, April 24 2013 @ 2:28 AM EST
*
* Copyright (c) 2013, Scott Hamper
* Licensed under the MIT license,
* http://www.opensource.org/licenses/MIT
*/
(function (undefined) {
'use strict';
var Cookies = function (key, value, options) {
return arguments.length === 1 ?
Cookies.get(key) : Cookies.set(key, value, options);
};
// Allows for setter injection in unit tests
Cookies._document = document;
Cookies._navigator = navigator;
Cookies.defaults = {
path: '/'
};
Cookies.get = function (key) {
if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) {
Cookies._renewCache();
}
return Cookies._cache[key];
};
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);
return Cookies;
};
Cookies.expire = function (key, options) {
return Cookies.set(key, undefined, 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
};
};
Cookies._isValidDate = function (date) {
return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime());
};
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;
}
if (expires && !Cookies._isValidDate(expires)) {
throw new Error('`expires` parameter cannot be converted to a valid Date instance');
}
return expires;
};
Cookies._generateCookieString = function (key, value, options) {
key = encodeURIComponent(key);
value = (value + '').replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent);
options = options || {};
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' : '';
return cookieString;
};
Cookies._getCookieObjectFromString = function (documentCookie) {
var cookieObject = {};
var cookiesArray = documentCookie ? documentCookie.split('; ') : [];
for (var i = 0; i < cookiesArray.length; i++) {
var cookieKvp = Cookies._getKeyValuePairFromCookieString(cookiesArray[i]);
if (cookieObject[cookieKvp.key] === undefined) {
cookieObject[cookieKvp.key] = cookieKvp.value;
}
}
return cookieObject;
};
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;
return {
key: decodeURIComponent(cookieString.substr(0, separatorIndex)),
value: decodeURIComponent(cookieString.substr(separatorIndex + 1))
};
};
Cookies._renewCache = function () {
Cookies._cache = Cookies._getCookieObjectFromString(Cookies._document.cookie);
Cookies._cachedDocumentCookie = Cookies._document.cookie;
};
Cookies._areEnabled = function () {
return Cookies._navigator.cookieEnabled ||
Cookies.set('cookies.js', 1).get('cookies.js') === '1';
};
Cookies.enabled = Cookies._areEnabled();
// AMD support
if (typeof define === 'function' && define.amd) {
define(function () { return Cookies; });
// CommonJS and Node.js module support.
} else if (typeof exports !== 'undefined') {
// Support Node.js specific `module.exports` (which can be a function)
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = Cookies;
}
// But always support CommonJS module 1.1.1 spec (`exports` cannot be a function)
exports.Cookies = Cookies;
} else {
window.Cookies = Cookies;
}
})();

740
assets/javascripts/vendor/fastclick.js vendored Normal file
View file

@ -0,0 +1,740 @@
/**
* @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
*
* @version 0.6.9
* @codingstandard ftlabs-jsv2
* @copyright The Financial Times Limited [All Rights Reserved]
* @license MIT License (see LICENSE.txt)
*/
/*jslint browser:true, node:true*/
/*global define, Event, Node*/
/**
* Instantiate fast-clicking listeners on the specificed layer.
*
* @constructor
* @param {Element} layer The layer to listen on
*/
function FastClick(layer) {
'use strict';
var oldOnClick, self = this;
/**
* Whether a click is currently being tracked.
*
* @type boolean
*/
this.trackingClick = false;
/**
* Timestamp for when when click tracking started.
*
* @type number
*/
this.trackingClickStart = 0;
/**
* The element being tracked for a click.
*
* @type EventTarget
*/
this.targetElement = null;
/**
* X-coordinate of touch start event.
*
* @type number
*/
this.touchStartX = 0;
/**
* Y-coordinate of touch start event.
*
* @type number
*/
this.touchStartY = 0;
/**
* ID of the last touch, retrieved from Touch.identifier.
*
* @type number
*/
this.lastTouchIdentifier = 0;
/**
* Touchmove boundary, beyond which a click will be cancelled.
*
* @type number
*/
this.touchBoundary = 10;
/**
* The FastClick layer.
*
* @type Element
*/
this.layer = layer;
if (!layer || !layer.nodeType) {
throw new TypeError('Layer must be a document node');
}
/** @type function() */
this.onClick = function() { return FastClick.prototype.onClick.apply(self, arguments); };
/** @type function() */
this.onMouse = function() { return FastClick.prototype.onMouse.apply(self, arguments); };
/** @type function() */
this.onTouchStart = function() { return FastClick.prototype.onTouchStart.apply(self, arguments); };
/** @type function() */
this.onTouchEnd = function() { return FastClick.prototype.onTouchEnd.apply(self, arguments); };
/** @type function() */
this.onTouchCancel = function() { return FastClick.prototype.onTouchCancel.apply(self, arguments); };
if (FastClick.notNeeded(layer)) {
return;
}
// Set up event handlers as required
if (this.deviceIsAndroid) {
layer.addEventListener('mouseover', this.onMouse, true);
layer.addEventListener('mousedown', this.onMouse, true);
layer.addEventListener('mouseup', this.onMouse, true);
}
layer.addEventListener('click', this.onClick, true);
layer.addEventListener('touchstart', this.onTouchStart, false);
layer.addEventListener('touchend', this.onTouchEnd, false);
layer.addEventListener('touchcancel', this.onTouchCancel, false);
// Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
// which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
// layer when they are cancelled.
if (!Event.prototype.stopImmediatePropagation) {
layer.removeEventListener = function(type, callback, capture) {
var rmv = Node.prototype.removeEventListener;
if (type === 'click') {
rmv.call(layer, type, callback.hijacked || callback, capture);
} else {
rmv.call(layer, type, callback, capture);
}
};
layer.addEventListener = function(type, callback, capture) {
var adv = Node.prototype.addEventListener;
if (type === 'click') {
adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
if (!event.propagationStopped) {
callback(event);
}
}), capture);
} else {
adv.call(layer, type, callback, capture);
}
};
}
// If a handler is already declared in the element's onclick attribute, it will be fired before
// FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
// adding it as listener.
if (typeof layer.onclick === 'function') {
// Android browser on at least 3.2 requires a new reference to the function in layer.onclick
// - the old one won't work if passed to addEventListener directly.
oldOnClick = layer.onclick;
layer.addEventListener('click', function(event) {
oldOnClick(event);
}, false);
layer.onclick = null;
}
}
/**
* Android requires exceptions.
*
* @type boolean
*/
FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
/**
* iOS requires exceptions.
*
* @type boolean
*/
FastClick.prototype.deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
/**
* iOS 4 requires an exception for select elements.
*
* @type boolean
*/
FastClick.prototype.deviceIsIOS4 = FastClick.prototype.deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
/**
* iOS 6.0(+?) requires the target element to be manually derived
*
* @type boolean
*/
FastClick.prototype.deviceIsIOSWithBadTarget = FastClick.prototype.deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent);
/**
* Determine whether a given element requires a native click.
*
* @param {EventTarget|Element} target Target DOM element
* @returns {boolean} Returns true if the element needs a native click
*/
FastClick.prototype.needsClick = function(target) {
'use strict';
switch (target.nodeName.toLowerCase()) {
// Don't send a synthetic click to disabled inputs (issue #62)
case 'button':
case 'select':
case 'textarea':
if (target.disabled) {
return true;
}
break;
case 'input':
// File inputs need real clicks on iOS 6 due to a browser bug (issue #68)
if ((this.deviceIsIOS && target.type === 'file') || target.disabled) {
return true;
}
break;
case 'label':
case 'video':
return true;
}
return (/\bneedsclick\b/).test(target.className);
};
/**
* Determine whether a given element requires a call to focus to simulate click into element.
*
* @param {EventTarget|Element} target Target DOM element
* @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
*/
FastClick.prototype.needsFocus = function(target) {
'use strict';
switch (target.nodeName.toLowerCase()) {
case 'textarea':
case 'select':
return true;
case 'input':
switch (target.type) {
case 'button':
case 'checkbox':
case 'file':
case 'image':
case 'radio':
case 'submit':
return false;
}
// No point in attempting to focus disabled inputs
return !target.disabled && !target.readOnly;
default:
return (/\bneedsfocus\b/).test(target.className);
}
};
/**
* Send a click event to the specified element.
*
* @param {EventTarget|Element} targetElement
* @param {Event} event
*/
FastClick.prototype.sendClick = function(targetElement, event) {
'use strict';
var clickEvent, touch;
// On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
if (document.activeElement && document.activeElement !== targetElement) {
document.activeElement.blur();
}
touch = event.changedTouches[0];
// Synthesise a click event, with an extra attribute so it can be tracked
clickEvent = document.createEvent('MouseEvents');
clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
clickEvent.forwardedTouchEvent = true;
targetElement.dispatchEvent(clickEvent);
};
/**
* @param {EventTarget|Element} targetElement
*/
FastClick.prototype.focus = function(targetElement) {
'use strict';
var length;
if (this.deviceIsIOS && targetElement.setSelectionRange) {
length = targetElement.value.length;
targetElement.setSelectionRange(length, length);
} else {
targetElement.focus();
}
};
/**
* Check whether the given target element is a child of a scrollable layer and if so, set a flag on it.
*
* @param {EventTarget|Element} targetElement
*/
FastClick.prototype.updateScrollParent = function(targetElement) {
'use strict';
var scrollParent, parentElement;
scrollParent = targetElement.fastClickScrollParent;
// Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the
// target element was moved to another parent.
if (!scrollParent || !scrollParent.contains(targetElement)) {
parentElement = targetElement;
do {
if (parentElement.scrollHeight > parentElement.offsetHeight) {
scrollParent = parentElement;
targetElement.fastClickScrollParent = parentElement;
break;
}
parentElement = parentElement.parentElement;
} while (parentElement);
}
// Always update the scroll top tracker if possible.
if (scrollParent) {
scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
}
};
/**
* @param {EventTarget} targetElement
* @returns {Element|EventTarget}
*/
FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
'use strict';
// On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.
if (eventTarget.nodeType === Node.TEXT_NODE) {
return eventTarget.parentNode;
}
return eventTarget;
};
/**
* On touch start, record the position and scroll offset.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchStart = function(event) {
'use strict';
var targetElement, touch, selection;
// Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
if (event.targetTouches.length > 1) {
return true;
}
targetElement = this.getTargetElementFromEventTarget(event.target);
touch = event.targetTouches[0];
if (this.deviceIsIOS) {
// Only trusted events will deselect text on iOS (issue #49)
selection = window.getSelection();
if (selection.rangeCount && !selection.isCollapsed) {
return true;
}
if (!this.deviceIsIOS4) {
// Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
// when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
// with the same identifier as the touch event that previously triggered the click that triggered the alert.
// Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an
// immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.
if (touch.identifier === this.lastTouchIdentifier) {
event.preventDefault();
return false;
}
this.lastTouchIdentifier = touch.identifier;
// If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
// 1) the user does a fling scroll on the scrollable layer
// 2) the user stops the fling scroll with another tap
// then the event.target of the last 'touchend' event will be the element that was under the user's finger
// when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
// is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
this.updateScrollParent(targetElement);
}
}
this.trackingClick = true;
this.trackingClickStart = event.timeStamp;
this.targetElement = targetElement;
this.touchStartX = touch.pageX;
this.touchStartY = touch.pageY;
// Prevent phantom clicks on fast double-tap (issue #36)
if ((event.timeStamp - this.lastClickTime) < 200) {
event.preventDefault();
}
return true;
};
/**
* Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.touchHasMoved = function(event) {
'use strict';
var touch = event.changedTouches[0], boundary = this.touchBoundary;
if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
return true;
}
return false;
};
/**
* Attempt to find the labelled control for the given label element.
*
* @param {EventTarget|HTMLLabelElement} labelElement
* @returns {Element|null}
*/
FastClick.prototype.findControl = function(labelElement) {
'use strict';
// Fast path for newer browsers supporting the HTML5 control attribute
if (labelElement.control !== undefined) {
return labelElement.control;
}
// All browsers under test that support touch events also support the HTML5 htmlFor attribute
if (labelElement.htmlFor) {
return document.getElementById(labelElement.htmlFor);
}
// If no for attribute exists, attempt to retrieve the first labellable descendant element
// the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
};
/**
* On touch end, determine whether to send a click event at once.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchEnd = function(event) {
'use strict';
var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
// If the touch has moved, cancel the click tracking
if (this.touchHasMoved(event)) {
this.trackingClick = false;
this.targetElement = null;
}
if (!this.trackingClick) {
return true;
}
// Prevent phantom clicks on fast double-tap (issue #36)
if ((event.timeStamp - this.lastClickTime) < 200) {
this.cancelNextClick = true;
return true;
}
this.lastClickTime = event.timeStamp;
trackingClickStart = this.trackingClickStart;
this.trackingClick = false;
this.trackingClickStart = 0;
// On some iOS devices, the targetElement supplied with the event is invalid if the layer
// is performing a transition or scroll, and has to be re-detected manually. Note that
// for this to function correctly, it must be called *after* the event target is checked!
// See issue #57; also filed as rdar://13048589 .
if (this.deviceIsIOSWithBadTarget) {
touch = event.changedTouches[0];
// In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null
targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
}
targetTagName = targetElement.tagName.toLowerCase();
if (targetTagName === 'label') {
forElement = this.findControl(targetElement);
if (forElement) {
this.focus(targetElement);
if (this.deviceIsAndroid) {
return false;
}
targetElement = forElement;
}
} else if (this.needsFocus(targetElement)) {
// Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through.
// Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37).
if ((event.timeStamp - trackingClickStart) > 100 || (this.deviceIsIOS && window.top !== window && targetTagName === 'input')) {
this.targetElement = null;
return false;
}
this.focus(targetElement);
// Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
if (!this.deviceIsIOS4 || targetTagName !== 'select') {
this.targetElement = null;
event.preventDefault();
}
return false;
}
if (this.deviceIsIOS && !this.deviceIsIOS4) {
// Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
// and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
scrollParent = targetElement.fastClickScrollParent;
if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
return true;
}
}
// Prevent the actual click from going though - unless the target node is marked as requiring
// real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
if (!this.needsClick(targetElement)) {
event.preventDefault();
this.sendClick(targetElement, event);
}
return false;
};
/**
* On touch cancel, stop tracking the click.
*
* @returns {void}
*/
FastClick.prototype.onTouchCancel = function() {
'use strict';
this.trackingClick = false;
this.targetElement = null;
};
/**
* Determine mouse events which should be permitted.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onMouse = function(event) {
'use strict';
// If a target element was never set (because a touch event was never fired) allow the event
if (!this.targetElement) {
return true;
}
if (event.forwardedTouchEvent) {
return true;
}
// Programmatically generated events targeting a specific element should be permitted
if (!event.cancelable) {
return true;
}
// Derive and check the target element to see whether the mouse event needs to be permitted;
// unless explicitly enabled, prevent non-touch click events from triggering actions,
// to prevent ghost/doubleclicks.
if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
// Prevent any user-added listeners declared on FastClick element from being fired.
if (event.stopImmediatePropagation) {
event.stopImmediatePropagation();
} else {
// Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
event.propagationStopped = true;
}
// Cancel the event
event.stopPropagation();
event.preventDefault();
return false;
}
// If the mouse event is permitted, return true for the action to go through.
return true;
};
/**
* On actual clicks, determine whether this is a touch-generated click, a click action occurring
* naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
* an actual click which should be permitted.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onClick = function(event) {
'use strict';
var permitted;
// It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
if (this.trackingClick) {
this.targetElement = null;
this.trackingClick = false;
return true;
}
// Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
if (event.target.type === 'submit' && event.detail === 0) {
return true;
}
permitted = this.onMouse(event);
// Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through.
if (!permitted) {
this.targetElement = null;
}
// If clicks are permitted, return true for the action to go through.
return permitted;
};
/**
* Remove all FastClick's event listeners.
*
* @returns {void}
*/
FastClick.prototype.destroy = function() {
'use strict';
var layer = this.layer;
if (this.deviceIsAndroid) {
layer.removeEventListener('mouseover', this.onMouse, true);
layer.removeEventListener('mousedown', this.onMouse, true);
layer.removeEventListener('mouseup', this.onMouse, true);
}
layer.removeEventListener('click', this.onClick, true);
layer.removeEventListener('touchstart', this.onTouchStart, false);
layer.removeEventListener('touchend', this.onTouchEnd, false);
layer.removeEventListener('touchcancel', this.onTouchCancel, false);
};
/**
* Check whether FastClick is needed.
*
* @param {Element} layer The layer to listen on
*/
FastClick.notNeeded = function(layer) {
'use strict';
var metaViewport;
// Devices that don't support touch don't need FastClick
if (typeof window.ontouchstart === 'undefined') {
return true;
}
if ((/Chrome\/[0-9]+/).test(navigator.userAgent)) {
// Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89)
if (FastClick.prototype.deviceIsAndroid) {
metaViewport = document.querySelector('meta[name=viewport]');
if (metaViewport && metaViewport.content.indexOf('user-scalable=no') !== -1) {
return true;
}
// Chrome desktop doesn't need FastClick (issue #15)
} else {
return true;
}
}
// IE10 with -ms-touch-action: none, which disables double-tap-to-zoom (issue #97)
if (layer.style.msTouchAction === 'none') {
return true;
}
return false;
};
/**
* Factory method for creating a FastClick object
*
* @param {Element} layer The layer to listen on
*/
FastClick.attach = function(layer) {
'use strict';
return new FastClick(layer);
};
if (typeof define !== 'undefined' && define.amd) {
// AMD. Register as an anonymous module.
define(function() {
'use strict';
return FastClick;
});
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = FastClick.attach;
module.exports.FastClick = FastClick;
} else {
window.FastClick = FastClick;
}

510
assets/javascripts/vendor/prism.js vendored Normal file
View file

@ -0,0 +1,510 @@
/**
* Prism: Lightweight, robust, elegant syntax highlighting
* MIT license http://www.opensource.org/licenses/mit-license.php/
* @author Lea Verou http://lea.verou.me
*/
(function(){
// Private helper vars
var lang = /\blang(?:uage)?-(?!\*)(\w+)\b/i;
var _ = self.Prism = {
util: {
type: function (o) {
return Object.prototype.toString.call(o).match(/\[object (\w+)\]/)[1];
},
// Deep clone a language definition (e.g. to extend it)
clone: function (o) {
var type = _.util.type(o);
switch (type) {
case 'Object':
var clone = {};
for (var key in o) {
if (o.hasOwnProperty(key)) {
clone[key] = _.util.clone(o[key]);
}
}
return clone;
case 'Array':
return o.slice();
}
return o;
}
},
languages: {
extend: function (id, redef) {
var lang = _.util.clone(_.languages[id]);
for (var key in redef) {
lang[key] = redef[key];
}
return lang;
},
// Insert a token before another token in a language literal
insertBefore: function (inside, before, insert, root) {
root = root || _.languages;
var grammar = root[inside];
var ret = {};
for (var token in grammar) {
if (grammar.hasOwnProperty(token)) {
if (token == before) {
for (var newToken in insert) {
if (insert.hasOwnProperty(newToken)) {
ret[newToken] = insert[newToken];
}
}
}
ret[token] = grammar[token];
}
}
return root[inside] = ret;
},
// Traverse a language definition with Depth First Search
DFS: function(o, callback) {
for (var i in o) {
callback.call(o, i, o[i]);
if (_.util.type(o) === 'Object') {
_.languages.DFS(o[i], callback);
}
}
}
},
highlightAll: function(async, callback) {
var elements = document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code');
for (var i=0, element; element = elements[i++];) {
_.highlightElement(element, async === true, callback);
}
},
highlightElement: function(element, async, callback) {
// Find language
var language, grammar, parent = element;
while (parent && !lang.test(parent.className)) {
parent = parent.parentNode;
}
if (parent) {
language = (parent.className.match(lang) || [,''])[1];
grammar = _.languages[language];
}
if (!grammar) {
return;
}
// Set language on the element, if not present
element.className = element.className.replace(lang, '').replace(/\s+/g, ' ') + ' language-' + language;
// Set language on the parent, for styling
parent = element.parentNode;
if (/pre/i.test(parent.nodeName)) {
parent.className = parent.className.replace(lang, '').replace(/\s+/g, ' ') + ' language-' + language;
}
var code = element.textContent;
if(!code) {
return;
}
code = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ');
var env = {
element: element,
language: language,
grammar: grammar,
code: code
};
_.hooks.run('before-highlight', env);
// if (async && self.Worker) {
// var worker = new Worker(_.filename);
// worker.onmessage = function(evt) {
// env.highlightedCode = Token.stringify(JSON.parse(evt.data), language);
// _.hooks.run('before-insert', env);
// env.element.innerHTML = env.highlightedCode;
// callback && callback.call(env.element);
// _.hooks.run('after-highlight', env);
// };
// worker.postMessage(JSON.stringify({
// language: env.language,
// code: env.code
// }));
// }
// else {
env.highlightedCode = _.highlight(env.code, env.grammar, env.language)
_.hooks.run('before-insert', env);
env.element.innerHTML = env.highlightedCode;
callback && callback.call(element);
_.hooks.run('after-highlight', env);
// }
},
highlight: function (text, grammar, language) {
return Token.stringify(_.tokenize(text, grammar), language);
},
tokenize: function(text, grammar, language) {
var Token = _.Token;
var strarr = [text];
var rest = grammar.rest;
if (rest) {
for (var token in rest) {
grammar[token] = rest[token];
}
delete grammar.rest;
}
tokenloop: for (var token in grammar) {
if(!grammar.hasOwnProperty(token) || !grammar[token]) {
continue;
}
var pattern = grammar[token],
inside = pattern.inside,
lookbehind = !!pattern.lookbehind,
lookbehindLength = 0;
pattern = pattern.pattern || pattern;
for (var i=0; i<strarr.length; i++) { // Dont cache length as it changes during the loop
var str = strarr[i];
if (strarr.length > text.length) {
// Something went terribly wrong, ABORT, ABORT!
break tokenloop;
}
if (str instanceof Token) {
continue;
}
pattern.lastIndex = 0;
var match = pattern.exec(str);
if (match) {
if(lookbehind) {
lookbehindLength = match[1].length;
}
var from = match.index - 1 + lookbehindLength,
match = match[0].slice(lookbehindLength),
len = match.length,
to = from + len,
before = str.slice(0, from + 1),
after = str.slice(to + 1);
var args = [i, 1];
if (before) {
args.push(before);
}
var wrapped = new Token(token, inside? _.tokenize(match, inside) : match);
args.push(wrapped);
if (after) {
args.push(after);
}
Array.prototype.splice.apply(strarr, args);
}
}
}
return strarr;
},
hooks: {
all: {},
add: function (name, callback) {
var hooks = _.hooks.all;
hooks[name] = hooks[name] || [];
hooks[name].push(callback);
},
run: function (name, env) {
var callbacks = _.hooks.all[name];
if (!callbacks || !callbacks.length) {
return;
}
for (var i=0, callback; callback = callbacks[i++];) {
callback(env);
}
}
}
};
var Token = _.Token = function(type, content) {
this.type = type;
this.content = content;
};
Token.stringify = function(o, language, parent) {
if (typeof o == 'string') {
return o;
}
if (Object.prototype.toString.call(o) == '[object Array]') {
return o.map(function(element) {
return Token.stringify(element, language, o);
}).join('');
}
var env = {
type: o.type,
content: Token.stringify(o.content, language, parent),
tag: 'span',
classes: ['token', o.type],
attributes: {},
language: language,
parent: parent
};
if (env.type == 'comment') {
env.attributes['spellcheck'] = 'true';
}
_.hooks.run('wrap', env);
var attributes = '';
for (var name in env.attributes) {
attributes += name + '="' + (env.attributes[name] || '') + '"';
}
return '<' + env.tag + ' class="' + env.classes.join(' ') + '" ' + attributes + '>' + env.content + '</' + env.tag + '>';
};
// if (!self.document) {
// // In worker
// self.addEventListener('message', function(evt) {
// var message = JSON.parse(evt.data),
// lang = message.language,
// code = message.code;
// self.postMessage(JSON.stringify(_.tokenize(code, _.languages[lang])));
// self.close();
// }, false);
// return;
// }
// Get current script and highlight
// var script = document.getElementsByTagName('script');
// script = script[script.length - 1];
// if (script) {
// _.filename = script.src;
// if (document.addEventListener && !script.hasAttribute('data-manual')) {
// document.addEventListener('DOMContentLoaded', _.highlightAll);
// }
// }
})();
Prism.languages.markup = {
'comment': /&lt;!--[\w\W]*?-->/g,
'prolog': /&lt;\?.+?\?>/,
'doctype': /&lt;!DOCTYPE.+?>/,
'cdata': /&lt;!\[CDATA\[[\w\W]*?]]>/i,
'tag': {
pattern: /&lt;\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|\w+))?\s*)*\/?>/gi,
inside: {
'tag': {
pattern: /^&lt;\/?[\w:-]+/i,
inside: {
'punctuation': /^&lt;\/?/,
'namespace': /^[\w-]+?:/
}
},
'attr-value': {
pattern: /=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,
inside: {
'punctuation': /=|>|"/g
}
},
'punctuation': /\/?>/g,
'attr-name': {
pattern: /[\w:-]+/g,
inside: {
'namespace': /^[\w-]+?:/
}
}
}
},
'entity': /&amp;#?[\da-z]{1,8};/gi
};
// Plugin to make entity title show the real entity, idea by Roman Komarov
Prism.hooks.add('wrap', function(env) {
if (env.type === 'entity') {
env.attributes['title'] = env.content.replace(/&amp;/, '&');
}
});
Prism.languages.css = {
'comment': /\/\*[\w\W]*?\*\//g,
'atrule': {
pattern: /@[\w-]+?.*?(;|(?=\s*{))/gi,
inside: {
'punctuation': /[;:]/g
}
},
'url': /url\((["']?).*?\1\)/gi,
'selector': /[^\{\}\s][^\{\};]*(?=\s*\{)/g,
'property': /(\b|\B)[\w-]+(?=\s*:)/ig,
'string': /("|')(\\?.)*?\1/g,
'important': /\B!important\b/gi,
'ignore': /&(lt|gt|amp);/gi,
'punctuation': /[\{\};:]/g
};
if (Prism.languages.markup) {
Prism.languages.insertBefore('markup', 'tag', {
'style': {
pattern: /(&lt;|<)style[\w\W]*?(>|&gt;)[\w\W]*?(&lt;|<)\/style(>|&gt;)/ig,
inside: {
'tag': {
pattern: /(&lt;|<)style[\w\W]*?(>|&gt;)|(&lt;|<)\/style(>|&gt;)/ig,
inside: Prism.languages.markup.tag.inside
},
rest: Prism.languages.css
}
}
});
};
Prism.languages.css.selector = {
pattern: /[^\{\}\s][^\{\}]*(?=\s*\{)/g,
inside: {
'pseudo-element': /:(?:after|before|first-letter|first-line|selection)|::[-\w]+/g,
'pseudo-class': /:[-\w]+(?:\(.*\))?/g,
'class': /\.[-:\.\w]+/g,
'id': /#[-:\.\w]+/g
}
};
Prism.languages.insertBefore('css', 'ignore', {
'hexcode': /#[\da-f]{3,6}/gi,
'entity': /\\[\da-f]{1,8}/gi,
'number': /[\d%\.]+/g,
'function': /(attr|calc|cross-fade|cycle|element|hsla?|image|lang|linear-gradient|matrix3d|matrix|perspective|radial-gradient|repeating-linear-gradient|repeating-radial-gradient|rgba?|rotatex|rotatey|rotatez|rotate3d|rotate|scalex|scaley|scalez|scale3d|scale|skewx|skewy|skew|steps|translatex|translatey|translatez|translate3d|translate|url|var)/ig
});
Prism.languages.clike = {
'comment': {
pattern: /(^|[^\\])(\/\*[\w\W]*?\*\/|(^|[^:])\/\/.*?(\r?\n|$))/g,
lookbehind: true
},
'string': /("|')(\\?.)*?\1/g,
'class-name': {
pattern: /((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/ig,
lookbehind: true,
inside: {
punctuation: /(\.|\\)/
}
},
'keyword': /\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/g,
'boolean': /\b(true|false)\b/g,
'function': {
pattern: /[a-z0-9_]+\(/ig,
inside: {
punctuation: /\(/
}
},
'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/g,
'operator': /[-+]{1,2}|!|&lt;=?|>=?|={1,3}|(&amp;){1,2}|\|?\||\?|\*|\/|\~|\^|\%/g,
'ignore': /&(lt|gt|amp);/gi,
'punctuation': /[{}[\];(),.:]/g
};
Prism.languages.javascript = Prism.languages.extend('clike', {
'keyword': /\b(var|let|if|else|while|do|for|return|in|instanceof|function|new|with|typeof|try|throw|catch|finally|null|break|continue)\b/g,
'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?|NaN|-?Infinity)\b/g
});
Prism.languages.insertBefore('javascript', 'keyword', {
'regex': {
pattern: /(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g,
lookbehind: true
}
});
if (Prism.languages.markup) {
Prism.languages.insertBefore('markup', 'tag', {
'script': {
pattern: /(&lt;|<)script[\w\W]*?(>|&gt;)[\w\W]*?(&lt;|<)\/script(>|&gt;)/ig,
inside: {
'tag': {
pattern: /(&lt;|<)script[\w\W]*?(>|&gt;)|(&lt;|<)\/script(>|&gt;)/ig,
inside: Prism.languages.markup.tag.inside
},
rest: Prism.languages.javascript
}
}
});
};
Prism.languages.coffeescript = Prism.languages.extend('javascript', {
'block-comment': /([#]{3}\s*\r?\n(.*\s*\r*\n*)\s*?\r?\n[#]{3})/g,
'comment': /(\s|^)([#]{1}[^#^\r^\n]{2,}?(\r?\n|$))/g,
'keyword': /\b(this|window|delete|class|extends|namespace|extend|ar|let|if|else|while|do|for|each|of|return|in|instanceof|new|with|typeof|try|catch|finally|null|undefined|break|continue)\b/g
});
Prism.languages.insertBefore('coffeescript', 'keyword', {
'function': {
pattern: /[a-z|A-z]+\s*[:|=]\s*(\([.|a-z\s|,|:|{|}|\"|\'|=]*\))?\s*-&gt;/gi,
inside: {
'function-name': /[_?a-z-|A-Z-]+(\s*[:|=])| @[_?$?a-z-|A-Z-]+(\s*)| /g,
'operator': /[-+]{1,2}|!|=?&lt;|=?&gt;|={1,2}|(&amp;){1,2}|\|?\||\?|\*|\//g
}
},
'attr-name': /[_?a-z-|A-Z-]+(\s*:)| @[_?$?a-z-|A-Z-]+(\s*)| /g
});

1587
assets/javascripts/vendor/raven.js vendored Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,154 @@
class app.views.Content extends app.View
@el: '._content'
@loadingClass: '_content-loading'
@events:
click: 'onClick'
@shortcuts:
altUp: 'scrollStepUp'
altDown: 'scrollStepDown'
pageUp: 'scrollPageUp'
pageDown: 'scrollPageDown'
home: 'scrollToTop'
end: 'scrollToBottom'
@routes:
before: 'beforeRoute'
after: 'afterRoute'
init: ->
@scrollEl = if app.isMobile() then document.body else @el
@scrollMap = {}
@scrollStack = []
@rootPage = new app.views.RootPage
@staticPage = new app.views.StaticPage
@typePage = new app.views.TypePage
@entryPage = new app.views.EntryPage
@entryPage
.on('loading', @onEntryLoading)
.on('loaded', @onEntryLoaded)
app
.on('ready', @onReady)
.on('bootError', @onBootError)
return
show: (view) ->
unless view is @view
@view?.deactivate()
@html @view = view
@view.activate()
return
showLoading: ->
@addClass @constructor.loadingClass
return
hideLoading: ->
@removeClass @constructor.loadingClass
return
scrollTo: (value) ->
@scrollEl.scrollTop = value or 0
return
scrollBy: (n) ->
@scrollEl.scrollTop += n
return
scrollToTop: =>
@scrollTo 0
return
scrollToBottom: =>
@scrollTo @scrollEl.scrollHeight
return
scrollStepUp: =>
@scrollBy -50
return
scrollStepDown: =>
@scrollBy 50
return
scrollPageUp: =>
@scrollBy 80 - @scrollEl.clientHeight
return
scrollPageDown: =>
@scrollBy @scrollEl.clientHeight - 80
return
scrollToTarget: ->
if @routeCtx.hash and el = $.id @routeCtx.hash
$.scrollToWithImageLock el, @scrollEl, 'top',
margin: 20 + if @scrollEl is @el then 0 else $.offset(@el).top
$.highlight el, className: '_highlight'
else
@scrollTo @scrollMap[@routeCtx.state.id]
return
onReady: =>
@hideLoading()
return
onBootError: =>
@hideLoading()
@html @tmpl('bootError')
return
onEntryLoading: =>
@showLoading()
return
onEntryLoaded: =>
@hideLoading()
@scrollToTarget()
return
beforeRoute: (context) =>
@cacheScrollPosition()
@routeCtx = context
@delay @scrollToTarget
return
cacheScrollPosition: ->
return if not @routeCtx or @routeCtx.hash
unless @scrollMap[@routeCtx.state.id]?
@scrollStack.push @routeCtx.state.id
while @scrollStack.length > app.config.history_cache_size
delete @scrollMap[@scrollStack.shift()]
@scrollMap[@routeCtx.state.id] = @scrollEl.scrollTop
return
afterRoute: (route, context) =>
switch route
when 'root'
@show @rootPage
when 'entry'
@show @entryPage
when 'type'
@show @typePage
else
@show @staticPage
@view.onRoute(context)
app.document.setTitle @view.getTitle?()
return
onClick: (event) =>
link = $.closestLink event.target, @el
if link and @isExternalUrl link.getAttribute('href')
$.stopEvent(event)
$.popup(link)
return
isExternalUrl: (url) ->
url?[0..3] is 'http'

View file

@ -0,0 +1,108 @@
class app.views.EntryPage extends app.View
@className: '_page'
@loadingClass: '_page-loading'
@events:
click: 'onClick'
@routes:
before: 'beforeRoute'
init: ->
@cacheMap = {}
@cacheStack = []
return
deactivate: ->
if super
@empty()
@entry = null
return
loading: ->
@empty()
@trigger 'loading'
return
render: (content = '') ->
@empty()
@subview = new (@subViewClass()) @el, @entry
@subview.render(content)
if app.disabledDocs.findBy 'slug', @entry.doc.slug
@hiddenView = new app.views.HiddenPage @el, @entry
@trigger 'loaded'
return
empty: ->
@subview?.deactivate()
@subview = null
@hiddenView?.deactivate()
@hiddenView = null
@resetClass()
super
return
subViewClass: ->
docType = @entry.doc.type
app.views["#{docType[0].toUpperCase()}#{docType[1..]}Page"] or app.views.BasePage
getTitle: ->
@entry.doc.name + if @entry.isIndex() then '' else "/#{@entry.name}"
beforeRoute: =>
@abort()
@cache()
return
onRoute: (context) ->
isSameFile = context.entry.filePath() is @entry?.filePath()
@entry = context.entry
@restore() or @load() unless isSameFile
return
load: ->
@loading()
@xhr = @entry.loadFile @onSuccess, @onError
return
abort: ->
if @xhr
@xhr.abort()
@xhr = null
return
onSuccess: (response) =>
@xhr = null
@render response
return
onError: =>
@xhr = null
@render @tmpl('pageLoadError')
return
cache: ->
return if not @entry or @cacheMap[path = @entry.filePath()]
@cacheMap[path] = @el.innerHTML
@cacheStack.push(path)
while @cacheStack.length > app.config.history_cache_size
delete @cacheMap[@cacheStack.shift()]
return
restore: ->
if @cacheMap[path = @entry.filePath()]
@render @cacheMap[path]
true
onClick: (event) =>
if event.target.hasAttribute 'data-retry'
$.stopEvent(event)
@load()
return

View file

@ -0,0 +1,34 @@
class app.views.RootPage extends app.View
@events:
click: 'onClick'
init: ->
@setHidden false unless @isHidden() # reserve space in local storage
@render()
return
render: ->
@empty()
@append @tmpl('mobileNav') if app.isMobile()
@append @tmpl if @isHidden() then 'splash' else if app.isMobile() then 'mobileIntro' else 'intro'
return
hideIntro: ->
@setHidden true
@render()
return
setHidden: (value) ->
app.store.set 'hideIntro', value
return
isHidden: ->
app.doc or app.store.get 'hideIntro'
onRoute: ->
onClick: (event) =>
if event.target.hasAttribute 'data-hide-intro'
$.stopEvent(event)
@hideIntro()
return

View file

@ -0,0 +1,26 @@
class app.views.StaticPage extends app.View
@className: '_static'
@titles:
about: 'About'
news: 'News'
help: 'Help'
notFound: '404'
deactivate: ->
if super
@empty()
@page = null
return
render: (page) ->
@page = page
@html @tmpl("#{@page}Page")
return
getTitle: ->
@constructor.titles[@page]
onRoute: (context) ->
@render context.page or 'notFound'
return

View file

@ -0,0 +1,20 @@
class app.views.TypePage extends app.View
@className: '_page'
deactivate: ->
if super
@empty()
@type = null
return
render: (@type) ->
@type = type
@html @tmpl('typePage', @type)
return
getTitle: ->
"#{@type.doc.name}/#{@type.name}"
onRoute: (context) ->
@render context.type
return

View file

@ -0,0 +1,32 @@
class app.views.Document extends app.View
@el: document
@shortcuts:
help: 'onHelp'
escape: 'onEscape'
superLeft: 'onBack'
superRight: 'onForward'
init: ->
@addSubview @nav = new app.views.Nav,
@addSubview @sidebar = new app.views.Sidebar
@addSubview @content = new app.views.Content
@setTitle()
@activate()
return
setTitle: (title) ->
@el.title = if title then "DevDocs/#{title}" else 'DevDocs'
onHelp: ->
app.router.show '/help#shortcuts'
onEscape: ->
if app.doc then window.location = '/' else app.router.show '/'
onBack: ->
history.back()
onForward: ->
history.forward()

View file

@ -0,0 +1,74 @@
class app.views.Mobile extends app.View
@className: '_mobile'
@elements:
body: 'body'
content: '._container'
sidebar: '._sidebar'
@routes:
after: 'afterRoute'
constructor: ->
@el = document.documentElement
super
init: ->
FastClick.attach @body
app.shortcuts.stop()
$.on @body, 'click', @onClick
$.on $('._home-link'), 'click', @onClickHome
$.on $('._menu-link'), 'click', @onClickMenu
$.on $('._search'), 'touchend', @onTapSearch
app.document.sidebar.search
.on('searching', @showSidebar)
.on('clear', @hideSidebar)
@activate()
return
showSidebar: =>
return if @isSidebarShown()
@contentTop = @body.scrollTop
@content.style.display = 'none'
@sidebar.style.display = 'block'
if selection = @findByClass app.views.ListSelect.activeClass
$.scrollTo selection, @body, 'center'
else
@body.scrollTop = @findByClass(app.views.ListFold.activeClass) and @sidebarTop or 0
return
hideSidebar: =>
return unless @isSidebarShown()
@sidebarTop = @body.scrollTop
@sidebar.style.display = 'none'
@content.style.display = 'block'
@body.scrollTop = @contentTop or 0
return
isSidebarShown: ->
@sidebar.style.display isnt 'none'
onClick: (event) =>
if event.target.hasAttribute 'data-pick-docs'
@showSidebar()
return
onClickHome: =>
app.shortcuts.trigger 'escape'
@hideSidebar()
return
onClickMenu: =>
if @isSidebarShown() then @hideSidebar() else @showSidebar()
return
onTapSearch: =>
@body.scrollTop = 0
afterRoute: =>
@hideSidebar()
return

View file

@ -0,0 +1,24 @@
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 is 'page'
@select context.pathname
else
@deselect()

View file

@ -0,0 +1,109 @@
class app.views.ListFocus extends app.View
@activeClass: 'focus'
@events:
click: 'onClick'
@shortcuts:
up: 'onUp'
down: 'onDown'
left: 'onLeft'
enter: 'onEnter'
superEnter: 'onSuperEnter'
escape: 'blur'
constructor: (@el) -> super
focus: (el) ->
if el and not el.classList.contains @constructor.activeClass
@blur()
el.classList.add @constructor.activeClass
$.trigger el, 'focus'
return
blur: =>
if cursor = @getCursor()
cursor.classList.remove @constructor.activeClass
$.trigger cursor, 'blur'
return
getCursor: ->
@findByClass(@constructor.activeClass) or @findByClass(app.views.ListSelect.activeClass)
findNext: (cursor) ->
if next = cursor.nextSibling
if next.tagName is 'A'
next
else if next.tagName is 'SPAN' # pagination link
$.click(next)
@findNext cursor
else # sub-list
@findFirst(next) or @findNext(next)
else if cursor.parentElement isnt @el
@findNext cursor.parentElement
findFirst: (cursor) ->
return unless first = cursor.firstChild
if first.tagName is 'A'
first
else if first.tagName is 'SPAN' # pagination link
$.click(first)
@findFirst cursor
findPrev: (cursor) ->
if prev = cursor.previousSibling
if prev.tagName is 'A'
prev
else if prev.tagName is 'SPAN' # pagination link
$.click(prev)
@findPrev cursor
else # sub-list
@findLast(prev) or @findPrev(prev)
else if cursor.parentElement isnt @el
@findPrev cursor.parentElement
findLast: (cursor) ->
return unless last = cursor.lastChild
if last.tagName is 'A'
last
else if last.tagName is 'SPAN' # pagination link
@findPrev last
else # sub-list
@findLast last
onDown: =>
if cursor = @getCursor()
@focus @findNext(cursor)
else
@focus @findByTag('a')
return
onUp: =>
if cursor = @getCursor()
@focus @findPrev(cursor)
else
@focus @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
return
onEnter: =>
if cursor = @getCursor()
$.click(cursor)
return
onSuperEnter: =>
if cursor = @getCursor()
$.popup(cursor)
return
onClick: (event) =>
if event.target.tagName is 'A'
@focus event.target
return

View file

@ -0,0 +1,64 @@
class app.views.ListFold extends app.View
@targetClass: '_list-dir'
@handleClass: '_list-arrow'
@activeClass: 'open'
@events:
click: 'onClick'
@shortcuts:
left: 'onLeft'
right: 'onRight'
escape: 'reset'
constructor: (@el) -> super
open: (el) ->
if el and not el.classList.contains @constructor.activeClass
el.classList.add @constructor.activeClass
$.trigger el, 'open'
return
close: (el) ->
if el and el.classList.contains @constructor.activeClass
el.classList.remove @constructor.activeClass
$.trigger el, 'close'
return
toggle: (el) ->
if el.classList.contains @constructor.activeClass
@close el
else
@open el
return
reset: =>
while el = @findByClass @constructor.activeClass
@close el
return
getCursor: ->
@findByClass(app.views.ListFocus.activeClass) or @findByClass(app.views.ListSelect.activeClass)
onLeft: =>
cursor = @getCursor()
if cursor?.classList.contains @constructor.activeClass
@close cursor
return
onRight: =>
cursor = @getCursor()
if cursor?.classList.contains @constructor.targetClass
@open cursor
return
onClick: (event) =>
return unless event.pageY # ignore fabricated clicks
el = event.target
if el.classList.contains @constructor.handleClass
$.stopEvent(event)
@toggle el.parentElement
else if el.classList.contains @constructor.targetClass
@open el
return

View file

@ -0,0 +1,41 @@
class app.views.ListSelect extends app.View
@activeClass: 'active'
@events:
click: 'onClick'
constructor: (@el) -> super
deactivate: ->
@deselect() if super
return
select: (el) ->
@deselect()
if el
el.classList.add @constructor.activeClass
$.trigger el, 'select'
return
deselect: ->
if selection = @getSelection()
selection.classList.remove @constructor.activeClass
$.trigger selection, 'deselect'
return
selectByHref: (href) ->
unless @getSelection()?.getAttribute('href') is href
@select @find("a[href='#{href}']")
return
selectCurrent: ->
@selectByHref location.pathname + location.hash
return
getSelection: ->
@findByClass @constructor.activeClass
onClick: (event) =>
if event.target.tagName is 'A'
@select event.target
return

View file

@ -0,0 +1,89 @@
class app.views.PaginatedList extends app.View
PER_PAGE = app.config.max_results
constructor: (@data) ->
(@constructor.events or= {}).click ?= 'onClick'
super
renderPaginated: ->
@page = 0
if @totalPages() > 1
@paginateNext()
else
@html @renderAll()
return
# render: (dataSlice) -> implemented by subclass
renderAll: ->
@render @data
renderPage: (page) ->
@render @data[((page - 1) * PER_PAGE)...(page * PER_PAGE)]
renderPageLink: (count) ->
@tmpl 'sidebarPageLink', count
renderPrevLink: (page) ->
@renderPageLink (page - 1) * PER_PAGE
renderNextLink: (page) ->
@renderPageLink @data.length - page * PER_PAGE
totalPages: ->
Math.ceil @data.length / PER_PAGE
paginate: (link) ->
$.lockScroll link.nextSibling or link.previousSibling, =>
$.batchUpdate @el, =>
if link.nextSibling then @paginatePrev link else @paginateNext link
return
return
return
paginateNext: ->
@remove @el.lastChild if @el.lastChild # remove link
@hideTopPage() if @page >= 2 # keep previous page into view
@page++
@append @renderPage(@page)
@append @renderNextLink(@page) if @page < @totalPages()
return
paginatePrev: ->
@remove @el.firstChild # remove link
@hideBottomPage()
@page--
@prepend @renderPage(@page - 1) # previous page is offset by one
@prepend @renderPrevLink(@page - 1) if @page >= 3
return
paginateTo: (object) ->
index = @data.indexOf(object)
if index >= PER_PAGE
@paginateNext() for [0...Math.floor(index / PER_PAGE)]
return
hideTopPage: ->
n = if @page <= 2
PER_PAGE
else
PER_PAGE + 1 # remove link
@remove @el.firstChild for [0...n]
@prepend @renderPrevLink(@page)
return
hideBottomPage: ->
n = if @page is @totalPages()
@data.length % PER_PAGE or PER_PAGE
else
PER_PAGE + 1 # remove link
@remove @el.lastChild for [0...n]
@append @renderNextLink(@page - 1)
return
onClick: (event) =>
if event.target.tagName is 'SPAN' # link
$.stopEvent(event)
@paginate event.target
return

View file

@ -0,0 +1,34 @@
#= require views/misc/notif
class app.views.News extends app.views.Notif
@className += ' _notif-news'
@defautOptions:
autoHide: null
init: ->
@unreadNews = @getUnreadNews()
@show() if @unreadNews.length
@markAllAsRead()
return
render: ->
@html app.templates.notifNews(@unreadNews)
return
getUnreadNews: ->
return [] unless time = @getLastReadTime()
for news in app.news
break if news[0] <= time
news
getLastNewsTime: ->
app.news[0][0]
getLastReadTime: ->
app.store.get 'news'
markAllAsRead: ->
app.store.set 'news', @getLastNewsTime()
return

View file

@ -0,0 +1,25 @@
class app.views.Notice extends app.View
@className: '_notice'
constructor: (@type, @args...) -> super
init: ->
@activate()
return
activate: ->
@show() if super
return
deactivate: ->
@hide() if super
return
show: ->
@html @tmpl("#{@type}Notice", @args...)
@prependTo $('._app')
return
hide: ->
$.remove @el
return

View file

@ -0,0 +1,54 @@
class app.views.Notif extends app.View
@className: '_notif'
@activeClass: '_in'
@defautOptions:
autoHide: 15000
@events:
click: 'onClick'
constructor: (@type, @options = {}) ->
@options = $.extend {}, @constructor.defautOptions, @options
super
init: ->
@show()
return
show: ->
if @timeout
clearTimeout @timeout
@timeout = @delay @hide, @options.autoHide
else
@render()
@position()
@activate()
@appendTo document.body
@el.offsetWidth # force reflow
@addClass @constructor.activeClass
@timeout = @delay @hide, @options.autoHide if @options.autoHide
return
hide: ->
clearTimeout @timeout
@timeout = null
@detach()
return
render: ->
@html @tmpl("notif#{@type}")
return
position: ->
notifications = $$ ".#{@constructor.className}"
if notifications.length
lastNotif = notifications[notifications.length - 1]
@el.style.top = lastNotif.offsetTop + lastNotif.offsetHeight + 16 + 'px'
return
onClick: (event) =>
unless event.target.tagName is 'A'
$.stopEvent(event)
@hide()
return

View file

@ -0,0 +1,6 @@
#= require views/pages/base
class app.views.AngularPage extends app.views.BasePage
afterRender: ->
@highlightCode @findAllByClass('prettyprint'), 'javascript'
return

View file

@ -0,0 +1,17 @@
class app.views.BasePage extends app.View
constructor: (@el, @entry) -> super
render: (content) ->
@addClass "_#{@entry.doc.type}" unless @constructor.className
@html content
@activate()
@delay @afterRender if @afterRender
return
highlightCode: (el, language) ->
if $.isCollection(el)
@highlightCode e, language for e in el
else if el
el.classList.add "language-#{language}"
Prism.highlightElement(el)
return

View file

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

View file

@ -0,0 +1,11 @@
#= require views/pages/base
class app.views.EmberPage extends app.views.BasePage
afterRender: ->
for el in @findAllByTag 'pre'
language = if el.classList.contains 'javascript'
'javascript'
else if el.classList.contains 'html'
'markup'
@highlightCode el, language if language
return

View file

@ -0,0 +1,16 @@
class app.views.HiddenPage extends app.View
@events:
click: 'onClick'
constructor: (@el, @entry) -> super
init: ->
@addSubview @notice = new app.views.Notice 'disabledDoc'
@activate()
return
onClick: (event) =>
if link = $.closestLink(event.target, @el)
$.stopEvent(event)
$.popup(link)
return

View file

@ -0,0 +1,61 @@
#= require views/pages/base
class app.views.JqueryPage extends app.views.BasePage
@demoClassName: '_jquery-demo'
afterRender: ->
# Prevent jQuery Mobile's demo iframes from scrolling the page
for iframe in @findAllByTag 'iframe'
iframe.style.display = 'none'
$.on iframe, 'load', @onIframeLoaded
@runExamples()
for el in @findAllByClass 'syntaxhighlighter'
language = if el.classList.contains('javascript') then 'javascript' else 'markup'
@highlightCode el, language
return
onIframeLoaded: (event) =>
event.target.style.display = ''
$.off event.target, 'load', @onIframeLoaded
return
runExamples: ->
for el in @findAllByClass 'entry-example'
try @runExample el catch
return
runExample: (el) ->
source = el.getElementsByClassName('syntaxhighlighter')[0]
return unless source and source.innerHTML.indexOf('!doctype') isnt -1
unless iframe = el.getElementsByClassName(@constructor.demoClassName)[0]
iframe = document.createElement 'iframe'
iframe.className = @constructor.demoClassName
iframe.width = '100%'
iframe.height = 200
el.appendChild(iframe)
doc = iframe.contentDocument
doc.write @fixIframeSource(source.textContent)
doc.close()
return
fixIframeSource: (source) ->
source = source.replace '"/resources/', '"http://api.jquery.com/resources/' # attr(), keydown()
source.replace '</head>', """
<style>
html, body { border: 0; margin: 0; padding: 0; }
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; }
</style>
<script>
$.ajaxPrefilter(function(opt, opt2, xhr) {
if (opt.url.indexOf('http') !== 0) {
xhr.abort();
document.body.innerHTML = "<p><strong>This demo cannot run inside DevDocs.</strong></p>";
}
});
</script>
</head>
"""

View file

@ -0,0 +1,4 @@
#= require views/pages/base
#= require views/pages/underscore
app.views.LodashPage = app.views.UnderscorePage

View file

@ -0,0 +1,15 @@
#= require views/pages/base
class app.views.MdnPage extends app.views.BasePage
@className: '_mdn'
LANGUAGE_REGEXP = /brush: ?(\w+)/
afterRender: ->
for el in @findAll 'pre[class^="brush"]'
language = el.className.match(LANGUAGE_REGEXP)[1]
.replace('html', 'markup')
.replace('js', 'javascript')
el.className = ''
@highlightCode el, language
return

View file

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

View file

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

View file

@ -0,0 +1,113 @@
class app.views.Search extends app.View
SEARCH_PARAM = app.config.search_param
@el: '._search'
@activeClass: '_search-active'
@elements:
input: '._search-input'
resetLink: '._search-clear'
@events:
input: 'onInput'
click: 'onClick'
submit: 'onSubmit'
@shortcuts:
typing: 'autoFocus'
escape: 'reset'
@routes:
root: 'onRoot'
after: 'autoFocus'
init: ->
@addSubview @scope = new app.views.SearchScope @el
@searcher = new app.Searcher
@searcher.on 'results', @onResults
app.on 'ready', @onReady
$.on window, 'hashchange', @searchUrl
$.on window, 'focus', @autoFocus
return
focus: ->
@input.focus() unless document.activeElement is @input
return
autoFocus: =>
@focus() unless $.isTouchScreen()
return
reset: =>
@el.reset()
@onInput()
@autoFocus()
return
onReady: =>
@value = ''
@delay @onInput
return
onInput: =>
return if not @value? or # ignore events pre-"ready"
@value is @input.value
@value = @input.value
if @value.length
@search()
else
@clear()
return
search: (url = false) ->
@addClass @constructor.activeClass
@trigger 'searching'
@flags = urlSearch: url, initialResults: true
@searcher.find @scope.getScope().entries.all(), 'text', @value
return
searchUrl: =>
return unless app.router.isRoot()
@scope.searchUrl()
return unless value = @extractHashValue()
@input.value = @value = value
@search true
true
clear: ->
@removeClass @constructor.activeClass
@trigger 'clear'
onResults: (results) =>
@trigger 'results', results, @flags
@flags.initialResults = false
return
onClick: (event) =>
if event.target is @resetLink
$.stopEvent(event)
@reset()
@focus()
return
onSubmit: (event) ->
$.stopEvent(event)
return
onRoot: (context) =>
@reset() unless context.init
@delay @searchUrl if context.hash
return
extractHashValue: ->
if (value = @getHashValue())?
app.router.replaceHash()
value
getHashValue: ->
try (new RegExp "##{SEARCH_PARAM}=(.*)").exec(decodeURIComponent location.hash)?[1] catch

View file

@ -0,0 +1,84 @@
class app.views.SearchScope extends app.View
SEARCH_PARAM = app.config.search_param
@elements:
input: '._search-input'
tag: '._search-tag'
@events:
keydown: 'onKeydown'
@shortcuts:
escape: 'reset'
constructor: (@el) -> super
init: ->
@placeholder = @input.getAttribute 'placeholder'
@searcher = new app.SynchronousSearcher
fuzzy_min_length: 2
max_results: 1
@searcher.on 'results', @onResults
return
getScope: ->
@doc or app
search: (value) ->
unless @doc
@searcher.find app.docs.all(), 'slug', value
return
searchUrl: ->
if value = @extractHashValue()
@search value
return
onResults: (results) =>
if results.length
@selectDoc results[0]
return
selectDoc: (doc) ->
@doc = doc
@tag.textContent = doc.name
@tag.style.display = 'block'
@input.removeAttribute 'placeholder'
@input.value = @input.value[@input.selectionStart..]
@input.style.paddingLeft = @tag.offsetWidth + 6 + 'px'
$.trigger @input, 'input'
reset: =>
@doc = null
@tag.textContent = ''
@tag.style.display = 'none'
@input.setAttribute 'placeholder', @placeholder
@input.style.paddingLeft = ''
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]
return
extractHashValue: ->
if value = @getHashValue()
newHash = decodeURIComponent(location.hash).replace "##{SEARCH_PARAM}=#{value} ", "##{SEARCH_PARAM}="
app.router.replaceHash(newHash)
value
getHashValue: ->
try (new RegExp "^##{SEARCH_PARAM}=(.+?) .").exec(decodeURIComponent location.hash)?[1] catch

View file

@ -0,0 +1,91 @@
class app.views.DocList extends app.View
@className: '_list'
@events:
open: 'onOpen'
close: 'onClose'
@routes:
after: 'afterRoute'
init: ->
@lists = {}
@addSubview @listSelect = new app.views.ListSelect @el
@addSubview @listFocus = new app.views.ListFocus @el unless $.isTouchScreen()
@addSubview @listFold = new app.views.ListFold @el
app.on 'ready', @render
return
activate: ->
if super
list.activate() for slug, list of @lists
@listSelect.selectCurrent()
return
deactivate: ->
if super
list.deactivate() for slug, list of @lists
return
render: =>
@html @tmpl('sidebarDoc', app.docs.all())
unless app.doc or app.settings.hasDocs()
@append @tmpl('sidebarDoc', app.disabledDocs.all(), disabled: true)
return
onOpen: (event) =>
$.stopEvent(event)
doc = app.docs.findBy 'slug', event.target.getAttribute('data-slug')
if doc and not @lists[doc.slug]
@lists[doc.slug] = if doc.types.isEmpty()
new app.views.EntryList doc.entries.all()
else
new app.views.TypeList doc
$.after event.target, @lists[doc.slug].el
return
onClose: (event) =>
$.stopEvent(event)
doc = app.docs.findBy 'slug', event.target.getAttribute('data-slug')
if doc and @lists[doc.slug]
@lists[doc.slug].detach()
delete @lists[doc.slug]
return
revealType: (type) ->
@openDoc type.doc
return
revealEntry: (entry) ->
@openDoc entry.doc
@openType entry.getType() if entry.type
@lists[entry.doc.slug]?.revealEntry(entry)
return
openDoc: (doc) ->
@listFold.open @find("[data-slug='#{doc.slug}']")
return
openType: (type) ->
@listFold.open @lists[type.doc.slug].find("[data-slug='#{type.slug}']")
return
afterRoute: (route, context) =>
if context.init
switch route
when 'type' then @revealType context.type
when 'entry' then @revealEntry context.entry
if route in ['type', 'entry']
@listSelect.selectByHref (context.type or context.entry).fullPath()
else
@listSelect.deselect()
if context.init
$.scrollTo @listSelect.getSelection()
return

View file

@ -0,0 +1,69 @@
class app.views.DocPicker extends app.View
@className: '_list'
@elements:
saveLink: '._sidebar-footer-save'
@events:
click: 'onClick'
@shortcuts:
enter: 'onEnter'
activate: ->
if super
@render()
app.appCache?.on 'progress', @onAppCacheProgress
return
deactivate: ->
if super
@empty()
app.appCache?.off 'progress', @onAppCacheProgress
return
render: ->
@html @tmpl('sidebarLabel', app.docs.all(), checked: true) +
@tmpl('sidebarLabel', app.disabledDocs.all()) +
@tmpl('sidebarVote') +
@tmpl('sidebarSave')
@refreshElements()
@delay -> # trigger animation
@el.offsetWidth
@addClass '_in'
return
empty: ->
@resetClass()
super
return
save: ->
unless @saving
@saving = true
app.settings.setDocs @getSelectedDocs()
@saveLink.textContent = if app.appCache then 'Downloading…' else 'Saving…'
app.reload()
return
getSelectedDocs: ->
for input in @findAllByTag 'input' when input?.checked
input.name
onClick: (event) =>
if event.target is @saveLink
$.stopEvent(event)
@save()
return
onEnter: =>
@save()
return
onAppCacheProgress: (event) =>
if event.lengthComputable
percentage = Math.round event.loaded * 100 / event.total
@saveLink.textContent = "Downloading… (#{percentage}%)"
return

View file

@ -0,0 +1,19 @@
#= require views/list/paginated_list
class app.views.EntryList extends app.views.PaginatedList
@tagName: 'div'
@className: '_list _list-sub'
constructor: (@entries) -> super
init: ->
@renderPaginated()
@activate()
return
render: (entries) ->
@tmpl 'sidebarEntry', entries
revealEntry: (entry) ->
@paginateTo entry
return

View file

@ -0,0 +1,48 @@
class app.views.Results extends app.View
@className: '_list'
@routes:
after: 'afterRoute'
constructor: (@search) -> super
deactivate: ->
if super
@empty()
return
init: ->
@addSubview @listSelect = new app.views.ListSelect @el
@addSubview @listFocus = new app.views.ListFocus @el unless $.isTouchScreen()
@search
.on('results', @onResults)
.on('clear', @onClear)
return
onResults: (entries, flags) =>
@empty() if flags.initialResults
@append @tmpl('sidebarResult', entries)
if flags.initialResults
if flags.urlSearch then @openFirst() else @focusFirst()
return
onClear: =>
@empty()
return
focusFirst: ->
@listFocus?.focus @el.firstChild
return
openFirst: ->
@el.firstChild?.click()
return
afterRoute: (route, context) =>
if route is 'entry'
@listSelect.selectByHref context.entry.fullPath()
else
@listSelect.deselect()
return

View file

@ -0,0 +1,79 @@
class app.views.Sidebar extends app.View
@el: '._sidebar'
@events:
focus: 'onFocus'
@shortcuts:
escape: 'onEscape'
init: ->
@addSubview @search = new app.views.Search
@search
.on('searching', @showResults)
.on('clear', @showDocList)
@results = new app.views.Results @search
@docList = new app.views.DocList
@docPicker = new app.views.DocPicker unless app.doc
app.on 'ready', @showDocList
$.on document, 'click', @onGlobalClick if @docPicker
return
show: (view) ->
unless @view is view
@saveScrollPosition()
@view?.deactivate()
@html @view = view
@append @tmpl('sidebarSettings') if @view is @docList and @docPicker
@view.activate()
@restoreScrollPosition()
return
showDocList: =>
@show @docList
return
showDocPicker: =>
@show @docPicker
return
showResults: =>
@show @results
return
saveScrollPosition: ->
if @view is @docList
@scrollTop = @el.scrollTop
return
restoreScrollPosition: ->
if @view is @docList and @scrollTop
@el.scrollTop = @scrollTop
@scrollTop = null
else
@scrollToTop()
return
scrollToTop: ->
@el.scrollTop = 0
return
onFocus: (event) =>
$.scrollTo event.target, @el, 'continuous', bottomGap: 2
return
onEscape: =>
@showDocList()
@scrollToTop()
return
onGlobalClick: (event) =>
if event.target.hasAttribute? 'data-pick-docs'
$.stopEvent(event)
@showDocPicker()
else if @view is @docPicker
@showDocList() unless $.hasChild @el, event.target
return

View file

@ -0,0 +1,51 @@
class app.views.TypeList extends app.View
@tagName: 'div'
@className: '_list _list-sub'
@events:
open: 'onOpen'
close: 'onClose'
constructor: (@doc) -> super
init: ->
@lists = {}
@render()
@activate()
return
activate: ->
if super
list.activate() for slug, list of @lists
return
deactivate: ->
if super
list.deactivate() for slug, list of @lists
return
render: ->
@html @tmpl('sidebarType', @doc.types.all())
onOpen: (event) =>
$.stopEvent(event)
type = @doc.types.findBy 'slug', event.target.getAttribute('data-slug')
if type and not @lists[type.slug]
@lists[type.slug] = new app.views.EntryList(type.entries())
$.after event.target, @lists[type.slug].el
return
onClose: (event) =>
$.stopEvent(event)
type = @doc.types.findBy 'slug', event.target.getAttribute('data-slug')
if type and @lists[type.slug]
@lists[type.slug].detach()
delete @lists[type.slug]
return
revealEntry: (entry) ->
if entry.type
@lists[entry.getType().slug]?.revealEntry(entry)
return

View file

@ -0,0 +1,161 @@
class app.View
$.extend @prototype, Events
constructor: ->
@setupElement()
@originalClassName = @el.className if @el.className
@resetClass() if @constructor.className
@refreshElements()
@init?()
@refreshElements()
setupElement: ->
@el ?= if typeof @constructor.el is 'string'
$ @constructor.el
else if @constructor.el
@constructor.el
else
document.createElement @constructor.tagName or 'div'
return
refreshElements: ->
if @constructor.elements
@[name] = @find selector for name, selector of @constructor.elements
return
addClass: (name) ->
@el.classList.add(name)
return
removeClass: (name) ->
@el.classList.remove(name)
return
resetClass: ->
@el.className = @originalClassName or ''
if @constructor.className
@addClass name for name in @constructor.className.split ' '
return
find: (selector) ->
$ selector, @el
findAll: (selector) ->
$$ selector, @el
findByClass: (name) ->
@findAllByClass(name)[0]
findLastByClass: (name) ->
all = @findAllByClass(name)[0]
all[all.length - 1]
findAllByClass: (name) ->
@el.getElementsByClassName(name)
findByTag: (tag) ->
@findAllByTag(tag)[0]
findLastByTag: (tag) ->
all = @findAllByTag(tag)
all[all.length - 1]
findAllByTag: (tag) ->
@el.getElementsByTagName(tag)
append: (value) ->
$.append @el, value.el or value
return
appendTo: (value) ->
$.append value.el or value, @el
return
prepend: (value) ->
$.prepend @el, value.el or value
return
prependTo: (value) ->
$.prepend value.el or value, @el
return
before: (value) ->
$.before @el, value.el or value
return
after: (value) ->
$.after @el, value.el or value
return
remove: (value) ->
$.remove value.el or value
return
empty: ->
$.empty @el
@refreshElements()
return
html: (value) ->
@empty()
@append value
return
tmpl: (args...) ->
app.templates.render(args...)
delay: (fn, args...) ->
delay = if typeof args[args.length - 1] is 'number' then args.pop() else 0
setTimeout fn.bind(@, args...), delay
onDOM: (event, callback) ->
$.on @el, event, callback
return
offDOM: (event, callback) ->
$.off @el, event, callback
return
bindEvents: ->
if @constructor.events
@onDOM name, @[method] for name, method of @constructor.events
if @constructor.routes
app.router.on name, @[method] for name, method of @constructor.routes
if @constructor.shortcuts
app.shortcuts.on name, @[method] for name, method of @constructor.shortcuts
return
unbindEvents: ->
if @constructor.events
@offDOM name, @[method] for name, method of @constructor.events
if @constructor.routes
app.router.off name, @[method] for name, method of @constructor.routes
if @constructor.shortcuts
app.shortcuts.off name, @[method] for name, method of @constructor.shortcuts
return
addSubview: (view) ->
(@subviews or= []).push(view)
activate: ->
return if @activated
@bindEvents()
view.activate() for view in @subviews if @subviews
@activated = true
true
deactivate: ->
return unless @activated
@unbindEvents()
view.deactivate() for view in @subviews if @subviews
@activated = false
true
detach: ->
@deactivate()
$.remove @el
return

View file

@ -0,0 +1,41 @@
//= depend_on icons.png
//= depend_on icons@2x.png
//= require vendor/open-sans
/*!
* Copyright 2013 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:
* http://mozilla.org/MPL/2.0/
*/
@import 'global/variables',
'global/icons',
'global/classes',
'global/base';
@import 'components/app',
'components/header',
'components/sidebar',
'components/content',
'components/page',
'components/fail',
'components/notice',
'components/notif',
'components/prism',
'components/mobile';
@import 'pages/angular',
'pages/coffeescript',
'pages/ember',
'pages/jquery',
'pages/less',
'pages/lodash',
'pages/mdn',
'pages/node',
'pages/php',
'pages/rfc',
'pages/underscore',
'pages/yard';

View file

@ -0,0 +1,34 @@
._app {
position: relative;
z-index: 1;
height: 100%;
padding-top: $headerHeight;
overflow: hidden;
-webkit-transition: opacity .2s;
transition: opacity .2s;
@extend %border-box;
._booting > & { opacity: 0; }
}
._booting {
opacity: 0;
-webkit-transition: opacity .1s .3s;
transition: opacity .1s .3s;
&._loading { opacity: 1; }
&:before {
content: 'Loading…';
position: absolute;
top: 50%;
left: 0;
right: 0;
line-height: 1;
margin-top: -.75em;
font-size: 4rem;
color: #ccc;
text-align: center;
letter-spacing: -.125rem;
}
}

View file

@ -0,0 +1,325 @@
//
// Content
//
._container {
position: relative;
z-index: $contentZ;
height: 100%;
margin-left: $sidebarWidth;
border-top: 1px solid #b4b7bf;
box-shadow: inset 0 1px rgba(black, .04), // top inner shadow
inset 1px 0 #f4f4f4; // left inner shadow
pointer-events: none;
@extend %border-box;
@media #{$mediumScreen} { margin-left: $sidebarMediumWidth; }
}
._content {
position: relative;
height: 100%;
overflow-y: scroll;
margin-left: 1rem;
padding: 1.25rem 1.5rem 0;
font-size: .875rem;
pointer-events: auto;
-webkit-overflow-scrolling: touch;
@extend %border-box;
-webkit-padding-start: .75rem;
@media (-moz-overlay-scrollbars) { padding-left: .75rem; }
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { margin-left: 0; }
&:after { // padding bottom
content: '';
display: block;
margin-bottom: 1.25rem;
}
}
._content-loading:before {
color: #e6e6e6;
@extend ._booting:before;
}
//
// Splash screen
//
._splash-title {
color: #ddd;
cursor: default;
@extend ._booting:before, %user-select-none;
}
._splash-maxcdn {
position: absolute;
bottom: 1.25rem;
left: 50%;
width: 16rem;
margin-left: -8rem;
line-height: 1rem;
color: #bbb;
text-align: center;
&:hover { color: $linkColor; }
> ._maxcdn-logo-bw { opacity: .2; }
&:hover > ._maxcdn-logo-bw { opacity: .5; }
}
//
// Intro
//
._intro { text-align: center; }
._intro-message {
position: relative;
display: inline-block;
vertical-align: top;
max-width: 37rem;
padding: 1rem 1.25rem;
text-align: left;
@extend %note, %note-green;
}
._intro-hide {
float: right;
line-height: 1.5rem;
cursor: pointer;
}
._intro-title {
margin: 0 0 1rem;
font-size: 1rem;
line-height: 1.5rem;
}
._intro-list {
margin: 1rem 0;
padding-left: 2.25rem;
}
._intro-link { cursor: pointer; }
._intro-maxcdn {
position: absolute;
bottom: 1rem;
right: 1rem;
margin: 0;
color: $textColorLight;
&:hover { color: $linkColor; }
}
//
// Error
//
._error {
position: absolute;
top: 50%;
left: 0;
right: 0;
padding: 0 2rem;
line-height: 1.5rem;
text-align: center;
}
._error-title {
margin: -5.5rem 0 .5rem;
line-height: 2;
font-size: 1.5rem;
}
._error-text {
margin: 0 0 1rem;
color: $textColorLight;
}
._error-links {
font-size: 1rem;
font-weight: bold;
}
._error-link { padding: 0 .5rem; }
//
// Heading
//
._lined-heading,
%lined-heading {
white-space: nowrap;
overflow: hidden;
overflow-wrap: normal;
word-wrap: normal;
&:after {
content: '';
display: inline-block;
vertical-align: middle;
width: 100%;
height: 1px;
line-height: 0;
margin-left: 1rem;
background: #dde3e8;
}
}
//
// Table of contents
//
._toc {
float: right;
max-width: 15em;
margin: .25rem 0 1.5rem 1.5rem;
padding: .75rem 1rem;
@extend %box;
+ ._lined-heading { margin-top: 0; }
}
._toc-title {
margin: 0 0 .75em;
font-size: inherit;
}
._toc-list {
margin: 0;
padding: 0 1em 0 0;
list-style: none;
}
//
// Static page
//
._static {
padding-bottom: 2em;
> ._lined-heading:first-child { margin-top: 0; }
}
//
// Credits table
//
._credits {
width: 100%;
td:first-child, td:last-child { white-space: nowrap; }
}
//
// News
//
._content {
._news-row {
position: relative;
padding-left: 10em;
font-size: .8125rem;
color: $textColorLight;
+ ._news-row { margin-top: 1em; }
}
._news-title {
display: block;
font-size: .875rem;
color: $textColor;
}
._news-date {
position: absolute;
top: 0;
left: 0;
font-size: .875rem;
}
}
//
// Keyboard shortcuts
//
._shortcuts-title {
width: 16rem;
max-width: 40%;
margin: 2rem 0 1rem;
font-size: 1rem;
text-align: right;
}
._shortcuts-dl { margin: 1rem 0; }
._shortcuts-dt {
float: left;
clear: left;
margin: 0;
width: 16rem;
max-width: 40%;
font-weight: normal;
text-align: right;
}
._shortcuts-dd {
display: block;
margin: 0 0 .75rem;
padding: 1px 0 1px .75rem;
overflow: hidden;
}
._shortcut-code {
display: inline-block;
vertical-align: top;
padding: 0 .5em;
@extend %label;
}
//
// Utilities
//
._note { @extend %note; }
._note-green { @extend %note-green; }
._label { @extend %label; }
._highlight { background: #fffdcd !important; }
._github-btn {
display: inline-block;
vertical-align: text-top;
margin-left: .25rem;
}
%maxcdn-logo {
display: inline-block;
vertical-align: top;
width: 6.25rem;
margin-left: .375rem;
overflow: hidden;
text-indent: -20rem;
background-position: center center;
background-repeat: no-repeat;
background-size: 6.25rem 1rem;
}
._maxcdn-logo {
background-image: image-url('maxcdn.png');
@extend %maxcdn-logo;
@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) {
background-image: image-url('maxcdn@2x.png');
}
}
._maxcdn-logo-bw {
background-image: image-url('maxcdn-bw.png');
@extend %maxcdn-logo;
@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) {
background-image: image-url('maxcdn-bw@2x.png');
}
}

View file

@ -0,0 +1,35 @@
._fail {
position: relative;
top: 1.5rem;
width: 24rem;
max-width: 90%;
margin: 0 auto;
padding: 1rem 1.5rem;
background: #eaefef;
border-radius: 5px;
@extend %border-box;
&:after { // margin
content: '';
position: relative;
top: 3rem;
float: left;
width: 1px;
height: 1px;
}
}
._fail-title {
margin: 0 0 1rem;
font-size: 1rem;
font-weight: bold;
}
._fail-text, ._fail-list {
margin: 0 0 1rem;
font-size: .875rem;
}
._fail-text:last-child { margin: 0; }
._fail-link { float: right; }

View file

@ -0,0 +1,157 @@
//
// Header
//
._header {
position: absolute;
z-index: $headerZ;
top: 0;
left: 0;
right: 0;
height: $headerHeight;
line-height: $headerHeight;
text-shadow: 0 1px rgba(white, .5);
background: -webkit-linear-gradient(top, #f6f6f8, #e4e4e6);
background: linear-gradient(to bottom, #f6f6f8, #e4e4e6);
box-shadow: inset 0 1px rgba(white, .8), // top inner glow
inset 0 -1px rgba(white, .3); // bottom inner glow
@extend %user-select-none;
}
//
// Navigation menu
//
._nav {
float: right;
margin-right: .5rem;
font-size: .875rem;
color: lighten($textColor, 5%);
}
._nav-link,
._nav-link:hover {
float: left;
padding: 0 1.25rem;
color: inherit;
text-decoration: none;
background-clip: padding-box;
border: solid transparent;
border-width: 0 1px;
}
._nav-current,
._nav-current:hover {
color: lighten($textColor, 8%);
background: -webkit-linear-gradient(top, #e1e1e4, #f2f2f5);
background: linear-gradient(to bottom, #e1e1e4, #f2f2f5);
border-color: rgba(black, .15);
box-shadow: inset 0 1px rgba(black, .07), // top border
inset 0 1px 2px rgba(black, .1), // top inner shadow
1px 0 rgba(white, .2), // right glow
-1px 0 rgba(white, .2); // left glow
}
//
// Logo
//
._logo {
position: relative;
float: left;
height: $headerHeight;
margin: 0;
line-height: inherit;
font-size: inherit;
&:before, &:after { // left border
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -1px;
width: 1px;
background: -webkit-linear-gradient(bottom, #b4b7bf, rgba(#b4b7bf, 0) 80%);
background: linear-gradient(to top, #b4b7bf, rgba(#b4b7bf, 0) 80%);
}
&:after { // left glow
left: -2px;
background: -webkit-linear-gradient(bottom, rgba(white, .4), rgba(white, 0) 80%);
background: linear-gradient(to top, rgba(white, .4), rgba(white, 0) 80%);
}
}
//
// Search form
//
._search {
position: relative;
float: left;
width: $sidebarWidth;
height: 100%;
padding: .625rem;
border-right: 1px solid transparent;
@extend %border-box;
@media #{$mediumScreen} { width: $sidebarMediumWidth; }
&:before {
position: absolute;
top: 1rem;
left: 1rem;
opacity: .5;
@extend %icon, %icon-search;
}
}
._search-input {
display: block;
width: 100%;
height: 100%;
padding: 0 .75rem 1px 1.625rem;
font-size: .875rem;
border: 1px solid;
border-color: #b2b5bb #babbc5 #bebfc6;
border-radius: 1rem;
box-shadow: inset 0 1px 1px rgba(black, .1), // top inner shadow
0 1px rgba(white, .3); // bottom glow
&:focus {
outline: 0;
border-color: #35b5f4 #35b5f4 #30aeee;
box-shadow: inset 0 0 1px rgba(#35b5f4, .5), // inner glow
0 0 2px rgba(#35b5f4, .8); // outer glow
}
}
._search-clear {
display: none;
position: absolute;
top: .5em;
right: .5em;
padding: .5em;
cursor: pointer;
opacity: .3;
&:hover { opacity: .5; }
&:before { @extend %icon, %icon-clear; }
._search-active > & { display: block; }
}
._search-tag {
display: none;
position: absolute;
top: .875rem;
left: .875rem;
margin: -1px 0 0 -1px;
padding: 0 .625rem;
line-height: 1.25rem;
font-size: .875rem;
background: #dfeafe;
border: 1px solid #98aed8;
border-radius: .75rem;
box-shadow: inset 0 1px rgba(white, .2), // top inner glow
0 1px rgba(black, .05); // bottom shadow
}

View file

@ -0,0 +1,201 @@
//
// Mobile overrides
//
._mobile {
font-size: 100%;
// Layout
body { -ms-overflow-style: -ms-autohiding-scrollbar; }
._app, ._container, ._content { overflow: visible; }
._container {
margin: 0;
border: 0;
box-shadow: none;
}
._content {
position: static;
height: auto;
margin: 0;
padding: .75rem 1rem 2.5rem;
}
._booting:before, ._content-loading:before {
font-size: 3rem;
color: #ccc;
}
// Header
._header {
position: fixed;
z-index: $contentZ + 1;
border-bottom: 1px solid #b4b7bf;
box-shadow: 0 1px rgba(black, .03);
}
._logo, ._nav { display: none; }
._home-link, ._menu-link { display: block; }
._search {
float: none;
width: auto;
overflow: hidden;
padding-left: 0;
padding-right: 0;
border-right: 0;
&:before { left: .5rem; }
}
._search-clear { padding-right: 0; }
._search-tag { left: .325rem; }
// Sidebar
._sidebar {
position: static;
min-height: 100%;
overflow: visible;
padding-bottom: 2rem;
box-shadow: none;
> ._list { padding-bottom: 0; }
}
._list, ._sidebar-footer {
width: 100%;
box-shadow: none;
}
._list-item { border-right-width: 0; }
._list-link { display: none; }
._sidebar-footer {
position: static;
margin-top: .5rem;
font-weight: bold;
&:before { content: none; }
}
._sidebar-footer-save {
margin-top: 1rem;
border-bottom: 1px solid #bac6d7;
}
// Notice
._notice {
position: fixed;
left: 0;
padding: 0 .5rem;
&:before { content: none; }
~ ._sidebar { padding-bottom: 4rem; }
}
._notice-text { font-size: .75em; }
// Notification
._notif { position: fixed; }
// Table of contents
._toc {
float: none;
max-width: none;
margin-left: 0;
}
// Splash
._splash-title { margin-top: -.5em; }
}
//
// Fix viewport on Windows Phone
//
@-ms-viewport { width: device-width; }
@media (orientation: portrait) and (min-device-width: 720px) and (max-device-width: 768px),
(orientation: landscape) and (device-width: 1280px) and (max-device-height: 768px) {
@-ms-viewport { width: 50%; }
}
//
// Header buttons
//
%mobile-link {
display: none;
position: relative;
float: left;
width: 2.5rem;
height: 100%;
&:before {
position: absolute;
top: 50%;
left: 50%;
margin: -.5rem 0 0 -.5rem;
@extend %icon;
}
}
._home-link {
@extend %mobile-link;
&:before { @extend %icon-home; }
}
._menu-link {
float: right;
@extend %mobile-link;
&:before { @extend %icon-menu; }
}
//
// Navigation menu
//
._mobile-nav {
margin: .25rem 0 1.25rem;
padding: 0;
line-height: 2.8;
overflow: hidden;
@extend %box;
}
._mobile-nav-link {
float: left;
width: 33%;
text-align: center;
font-weight: bold;
&:nth-child(2n) { width: 34%; }
}
//
// Intro
//
._mobile-intro {
> ._intro-list { padding-left: 1.5rem; }
> ._intro-hide,
> ._intro-maxcdn {
position: static;
float: none;
display: block;
margin-top: 1.25rem;
text-align: center;
}
}

View file

@ -0,0 +1,41 @@
._notice {
position: absolute;
z-index: $noticeZ;
bottom: 0;
left: $sidebarWidth;
right: 0;
height: 2.5rem;
padding: 0 1.25rem;
text-shadow: 0 1px rgba(white, .5);
background: #faf9e2;
box-shadow: inset 0 1px #dddaaa, // top border
inset 0 2px rgba(white, .7), // top inner glow
inset 1px 0 rgba(black, .03); // left inner shadow
@media #{$mediumScreen} { left: $sidebarMediumWidth; }
~ ._container { padding-bottom: 2.5rem; }
&:before {
content: '';
position: absolute;
bottom: 100%;
left: 1.5rem;
right: 1.5rem;
height: 1.5rem;
background-image: -webkit-linear-gradient(top, rgba(white, 0), rgba(white, .95));
background-image: linear-gradient(to bottom, rgba(white, 0), rgba(white, .95));
pointer-events: none;
}
}
._notice-text {
display: table-cell;
vertical-align: middle;
margin: 0;
height: 2.5rem;
line-height: 1rem;
font-size: .875rem;
}
._notice-link { cursor: pointer; }

View file

@ -0,0 +1,97 @@
._notif {
position: absolute;
z-index: 2;
top: 1rem;
right: 1rem;
width: 25rem;
max-width: 90%;
padding: .75rem 1rem;
font-size: .75rem;
color: white;
text-shadow: 0 1px 1px rgba(black, .4);
background: -webkit-linear-gradient(top, rgba(#3a3a3a, .9), rgba(#202020, .9));
background: linear-gradient(to bottom, rgba(#3a3a3a, .9), rgba(#202020, .9));
background-clip: padding-box;
border: 1px solid black;
border-radius: .25rem;
box-shadow: inset 0 1px rgba(white, .1), // top inner glow
inset 0 0 0 1px rgba(white, .1), // inner glow
0 1px 3px rgba(black, .5); // drop shadow
transition: opacity .2s;
opacity: 0;
cursor: default;
@extend %border-box, %user-select-none;
&._in { opacity: 1; }
}
._notif-title {
margin: 0 0 .375rem;
line-height: 1rem;
font-size: inherit;
}
._notif-text { margin-bottom: 0; }
._notif-link,
._notif-link:hover {
color: inherit;
text-decoration: underline;
}
._notif-close {
position: absolute;
top: 0;
right: 0;
padding: .625rem;
opacity: .9;
cursor: pointer;
&:before { @extend %icon, %icon-close-white; }
}
._notif-news {
width: 20rem;
max-height: 85%;
overflow-y: auto;
> ._notif-title {
margin: -.125rem 0 1em;
text-align: center;
}
> ._news-row {
line-height: 1.125rem;
font-size: .6875rem;
color: #bbb;
+ ._news-row { margin-top: .75rem; }
}
._news-title {
display: block;
margin-bottom: .25rem;
font-size: .75rem;
font-weight: normal;
color: white;
}
._news-date {
float: right;
margin-left: 1rem;
font-weight: bold;
}
code {
display: inline-block;
vertical-align: baseline;
line-height: 0;
margin: 0;
padding: 0;
color: inherit;
text-shadow: inherit;
background: none;
border: 0;
box-shadow: none;
}
}

View file

@ -0,0 +1,58 @@
//
// Page
//
._page {
> h1 { @extend ._lined-heading; }
> h1:first-child { margin-top: 0; }
a[href^="http:"], a[href^="https:"] { @extend %external-link; }
a:not([href]) {
color: inherit;
text-decoration: none;
}
iframe {
display: block;
padding: 1px;
border: 1px dotted #ddd;
border-radius: 3px;
}
}
//
// Attribution box
//
._attribution {
clear: both;
margin: 2rem 0 1.5rem;
font-size: .75rem;
color: $textColorLight;
text-align: center;
-webkit-font-smoothing: subpixel-antialiased;
& + & { margin-top: 1.5rem; }
& + & > ._attribution-link { display: none; }
}
._attribution-p {
display: inline-block;
margin: 0;
padding: .25rem .75rem;
text-shadow: 0 1px rgba(white, .3);
background: #f2f2f2;
border-radius: 3px;
}
._attribution-link { @extend %internal-link; }
//
// Entry list
//
._entry-list {
padding-left: 1em;
list-style: none;
}

View file

@ -0,0 +1,52 @@
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata,
.token.punctuation {
color: $textColorLight;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string {
color: #5e8e01;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #a67f59;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #0070a3;
}
.token.regex,
.token.important {
color: #e90;
}
.token.important {
font-weight: bold;
}
.token.entity {
cursor: help;
}

View file

@ -0,0 +1,285 @@
//
// Sidebar
//
._sidebar {
position: absolute;
z-index: $sidebarZ;
top: $headerHeight;
bottom: 0;
left: 0;
overflow-x: hidden;
overflow-y: scroll;
text-shadow: 0 1px rgba(white, .3);
background: #e5eaf4;
box-shadow: inset 0 1px #b4b7bf, // top border
inset 0 2px rgba(black, .03); // top inner shadow
-webkit-overflow-scrolling: touch;
-ms-overflow-style: none; // IE 10 doesn't support pointer-events
@extend %user-select-none;
&::-webkit-scrollbar { width: 10px; }
&::-webkit-scrollbar-button { display: none; }
&::-webkit-scrollbar-track { background: white; }
&::-webkit-scrollbar-thumb {
min-height: 1rem;
background: #ccc;
background-clip: padding-box;
border: 3px solid white;
border-radius: 5px;
&:active {
background-color: #999;
border-width: 2px;
}
}
}
//
// List
//
._list {
margin: 0;
padding: 0;
list-style: none;
width: $sidebarWidth;
@media #{$mediumScreen} { width: $sidebarMediumWidth; }
._sidebar > & {
min-height: 100%;
padding-bottom: 3.5rem;
box-shadow: inset -1px 0 #bbc1cc, // right border
inset -2px 0 rgba(white, .2); // right inner glow
@extend %border-box;
}
}
._list-item {
display: block;
position: relative;
overflow: hidden;
padding: 0 .75rem;
line-height: 1.75rem;
font-size: .875rem;
white-space: nowrap;
text-overflow: ellipsis;
border: solid transparent;
border-width: 1px 1px 1px 0;
cursor: default;
&, &:hover {
color: inherit;
text-decoration: none;
}
&.focus,
&.focus:hover,
&.active,
&.active:hover {
color: white;
text-shadow: 0 1px rgba(black, .2);
background: -webkit-linear-gradient(top, #bfc6dd, #949eb8);
background: linear-gradient(to bottom, #bfc6dd, #949eb8);
border-color: #96a1c6 #8e99b7 #7f87a4;
box-shadow: inset 0 1px rgba(white, .15), // top inner glow
inset 0 0 0 1px rgba(white, .08), // inner glow
0 1px rgba(black, .05); // drop shadow
}
&.active,
&.active:hover {
background: -webkit-linear-gradient(top, #65b2fb, #3492e9);
background: linear-gradient(to bottom, #65b2fb, #3492e9);
border-color: #318ce4 #2b82db #2878c7;
}
&:before {
float: left;
margin: .375rem .625rem 0 0;
@extend %icon;
}
}
._list-count {
float: right;
font-size: .75rem;
color: $textColorLighter;
pointer-events: none;
.focus > &,
.active > & {
color: inherit;
}
}
//
// List hierarchy
//
._list-dir,
%_list-dir {
line-height: 2rem;
padding-left: 2.25rem;
&:before { margin-top: .5rem; }
}
._list-disabled {
@extend %_list-dir;
&, &:hover { color: $textColorLight; }
&:before { opacity: .7; }
}
._list-arrow {
position: absolute;
top: 0;
left: .25rem;
padding: .5rem;
cursor: pointer;
opacity: .45;
&:hover { opacity: .7; }
&:before {
@extend %icon, %icon-dir;
.open > & {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
}
}
._list-sub {
> ._list-item { padding-left: 2.75rem; }
> ._list-item:before { content: none; }
> ._list-dir { line-height: 1.75rem; }
._list-arrow {
left: 1rem;
padding: .375rem;
}
}
//
// List pagination
//
._list-pagelink {
color: $linkColor;
cursor: pointer;
&:hover {
color: $linkColorHover;
text-decoration: underline;
}
}
//
// List picker
//
._list-checkbox {
position: absolute;
top: .5rem;
right: -1rem;
width: 1rem;
height: 1rem;
transition: .2s;
}
._list-label {
transition: .2s;
@extend %_list-dir;
._in > & { padding-left: .75rem; }
._in > & > ._list-checkbox { right: .5rem }
}
._list-label-off {
opacity: 0;
padding-left: .75rem;
._in > & { opacity: 1; }
> ._list-checkbox { right: .5rem; }
}
._list-link {
display: block;
margin-top: .75rem;
font-size: .8125rem;
text-align: center;
@extend %external-link;
&:after { visibility: hidden; }
&:hover:after { visibility: visible; }
}
//
// Footer
//
._sidebar-footer {
position: fixed;
bottom: 0;
left: 0;
width: $sidebarWidth;
background: #e5eaf4;
box-shadow: inset -1px 0 #bbc1cc, // right border
inset -2px 0 rgba(white, .2); // right inner glow
@media #{$mediumScreen} { width: $sidebarMediumWidth; }
&:before {
content: '';
position: absolute;
bottom: 100%;
left: 0;
right: 1px;
height: 1em;
background-image: -webkit-linear-gradient(top, rgba(#e5eaf4, 0), rgba(#e5eaf4, .95));
background-image: linear-gradient(to bottom, rgba(#e5eaf4, 0), rgba(#e5eaf4, .95));
pointer-events: none;
}
}
._sidebar-footer-link {
position: relative;
display: block;
height: 2.5rem;
line-height: 1rem;
padding: .75rem;
font-size: .875em;
cursor: pointer;
@extend %border-box;
&, &:hover {
color: inherit;
text-decoration: none;
}
&:before {
float: left;
margin-right: .625rem;
@extend %icon;
}
}
._sidebar-footer-edit {
&:before { @extend %icon-settings; }
}
._sidebar-footer-save {
margin-right: 1px;
font-weight: bold;
background-image: -webkit-linear-gradient(top, #fbfbfe, #f5f5f8 50%, #eeeef1 51%, #e8e8ec);
background-image: linear-gradient(to bottom, #fbfbfe, #f5f5f8 50%, #eeeef1 51%, #e8e8ec);
box-shadow: inset 0 1px white, // top inner glow
inset 0 0 0 1px rgba(white, .2), // inner glow
0 -1px #c4cfde; // top border
&:before { @extend %icon-check; }
}

View file

@ -0,0 +1,183 @@
html {
height: 100%;
font-size: 100%;
background: white;
@media #{$mediumScreen} { font-size: 93.75%; }
}
body {
margin: 0;
height: 100%;
font: normal 1em/1.7 $baseFont;
color: $textColor;
overflow-wrap: break-word;
word-wrap: break-word;
-webkit-tap-highlight-color: rgba(black, 0);
-webkit-touch-callout: none;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
a {
color: $linkColor;
text-decoration: none;
&:hover {
color: $linkColorHover;
text-decoration: underline;
}
&:focus { outline: 0; }
}
img {
max-width: 100%;
height: auto;
border: 0;
}
h1, h2, h3, h4, h5, h6 {
margin: 1.5em 0 1em;
line-height: 1.3;
font-weight: bold;
}
h1 { font-size: 1.5em; }
h2 { font-size: 1.375em; }
h3 { font-size: 1.25em; }
h4 { font-size: 1.125em; }
h5 { font-size: 1em; }
h6 { font-size: .9375em; }
p { margin: 0 0 1em; }
p:last-child { margin-bottom: 0; }
b, strong { font-weight: bold; }
small { font-size: .9em; }
ul, ol {
margin: 1.5em 0;
padding: 0 0 0 2em;
list-style: disc outside;
}
ul ul { list-style-type: circle; }
ol { list-style-type: decimal; }
ol ol { list-style-type: lower-alpha; }
ol ol ol { list-style-type: lower-roman; }
li + li { margin-top: .25em; }
li > ul, li > ol, dd > ul, dd > ol { margin: .5em 0; }
dl { margin: 1.5em 0; }
dt { font-weight: bold; }
dd {
margin: .375em;
padding-left: 1em;
+ dt { margin-top: 1em; }
}
dfn, var { font-style: normal; }
abbr, acronym, dfn {
cursor: help;
border-bottom: 1px dotted $textColor;
}
pre, code, samp {
font-family: $monoFont;
font-weight: normal;
font-style: normal;
color: $textColor;
white-space: pre-wrap;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
}
pre {
margin: 1.5em 0;
padding: .375rem .75rem;
line-height: 1.5;
overflow: auto;
font-size: .9em;
@extend %box;
}
a > code { color: inherit; }
table {
margin: 1.5em 0;
background: none;
border: 1px solid #d5d5d5;
border-collapse: separate;
border-spacing: 0;
border-radius: 3px;
box-shadow: 0 1px 1px rgba(black, .04);
}
th, td {
vertical-align: top;
padding: .3em .7em;
padding-bottom: -webkit-calc(.3em + 1px);
padding-bottom: calc(.3em + 1px);
text-align: left;
}
th {
border: 0;
border-bottom: 1px solid #d5d5d5;
border-radius: 0;
@extend %heading-box;
&:empty {
background: none;
box-shadow: none;
}
+ th { border-left: 1px solid rgba(black, .12); }
+ td { border-left: 1px solid #d5d5d5; }
tr:first-child > &:first-child { border-top-left-radius: 3px; }
tr:first-child > &:last-child { border-top-right-radius: 3px; }
tr:last-child > &:first-child { border-bottom-left-radius: 3px; }
thead > tr:last-child > &:first-child { border-bottom-left-radius: 0; }
tr:last-child > & { border-bottom-width: 0; }
thead > tr:last-child > & { border-bottom-width: 1px; }
}
td {
border-bottom: 1px solid #e5e5e5;
+ td { border-left: 1px solid #e5e5e5; }
tr:last-child > & { border-bottom: 0; }
}
input {
margin: 0;
font-family: inherit;
font-size: 100%;
line-height: normal;
@extend %border-box;
}
input[type="search"] { -webkit-appearance: textfield; }
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-ms-clear { display: none; }
::-moz-focus-inner {
padding: 0 !important;
border: 0 !important;
}
::-webkit-input-placeholder { color: #aaa; }
::-moz-placeholder { color: #aaa; opacity: 1; }
:-ms-input-placeholder { color: #aaa; }

View file

@ -0,0 +1,143 @@
//
// Utilities
//
%border-box {
-moz-box-sizing: border-box;
box-sizing: border-box;
}
%user-select-none {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
//
// Boxes
//
%box {
text-shadow: 0 1px rgba(white, .3);
background: #f8f8f8;
border: 1px solid;
border-color: #d5d5d5 #d5d5d5 #d1d1d1;
border-radius: 3px;
box-shadow: inset 0 1px rgba(white, .3), // top inner glow
0 1px 1px rgba(black, .04); // drop shadow
}
%heading-box {
text-shadow: 0 1px rgba(white, .3);
background: -webkit-linear-gradient(top, #f7f7fa, #f0f0f2);
background: linear-gradient(to bottom, #f7f7fa, #f0f0f2);
border: 1px solid;
border-color: #d5d5d5 #d5d5d5 #d1d1d1;
border-radius: 3px;
box-shadow: inset 0 1px rgba(white, .3), // top inner glow
inset 0 0 0 1px rgba(white, .2), // inner glow
0 1px 1px rgba(black, .04); // drop shadow
}
%block-heading {
line-height: 1.25rem;
margin: 2em 0 1em;
padding: .5em .75em;
font-size: 1rem;
overflow: hidden;
@extend %heading-box;
}
//
// Notes
//
%note {
margin: 1.5rem 0;
padding: .5rem .875rem;
text-shadow: 0 1px rgba(white, .3);
background: #faf9e2;
border: 1px solid;
border-color: #dddaaa #dddaaa #d7d7a9;
border-radius: 3px;
box-shadow: inset 0 1px rgba(white, .2), // top inner glow
0 1px 1px rgba(black, .04); // drop shadow
}
%note-green {
background: #f1faeb;
border-color: #b3dba8 #b3dba8 #aed7a5;
}
%note-blue {
background: #f2f2ff;
border-color: #c6cde9 #c6cde9 #c3cce7;
}
%note-gold {
background: #fff0aa;
border-color: #ddce81 #ddce81 #d9ca7f;
}
%note-red {
background: #fed5d3;
border-color: #dc7874 #dc7874 #d47474;
}
//
// Labels
//
%label {
margin: 0 1px;
padding: 0 .3em 1px;
text-shadow: 0 1px rgba(white, .3);
background: #f2f2f2;
border: 1px solid;
border-color: #d2d2d2 #d2d2d2 #ccc;
border-radius: 2px;
box-shadow: 0 1px rgba(black, .04);
}
%block-label {
display: block;
margin: 2em 0 1em;
padding-left: .5em;
padding-right: .5em;
line-height: inherit;
font-size: inherit;
@extend %label;
}
%label-blue {
background: #d5daff;
border-color: #a3a4d4;
}
%label-yellow {
background: #ffdfb2;
border-color: #c2a16f;
}
%label-red {
background: #fed5d3;
border-color: #dc7874;
}
//
// External links
//
%external-link {
&:after {
display: inline-block;
width: .5rem;
height: .4375rem;
margin: .125rem 0 0 .0625rem;
vertical-align: top;
@extend %icon, %icon-link;
}
}
%internal-link:after { content: none !important; }

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