├── .gitignore ├── lib ├── styles │ ├── default │ │ ├── github-icon.png │ │ ├── disclosure-indicator.png │ │ ├── config.rb │ │ ├── docPage.jade │ │ ├── _codestyles-pygments.sass │ │ ├── _codestyles.scss │ │ ├── _codestyles-highlight.scss │ │ ├── behavior.coffee │ │ ├── style.sass │ │ └── style.css │ ├── base.coffee │ └── default.coffee ├── styles.coffee ├── utils │ ├── compatibility_helpers.coffee │ ├── humanize.coffee │ ├── logger.coffee │ ├── cli_helpers.coffee │ └── style_helpers.coffee ├── project.coffee ├── doc_tags.coffee ├── languages.coffee ├── cli.coffee └── utils.coffee ├── bin └── groc ├── .groc.json ├── index.js ├── MIT-LICENSE.txt ├── package.json ├── scripts └── publish-git-pages.sh └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .sass-cache 3 | doc 4 | node_modules 5 | npm-debug.log -------------------------------------------------------------------------------- /lib/styles/default/github-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nevir/groc/HEAD/lib/styles/default/github-icon.png -------------------------------------------------------------------------------- /lib/styles/default/disclosure-indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nevir/groc/HEAD/lib/styles/default/disclosure-indicator.png -------------------------------------------------------------------------------- /lib/styles/default/config.rb: -------------------------------------------------------------------------------- 1 | output_style = :compressed 2 | preferred_syntax = :sass 3 | css_dir = '.' 4 | sass_dir = '.' 5 | images_dir = '.' 6 | -------------------------------------------------------------------------------- /lib/styles.coffee: -------------------------------------------------------------------------------- 1 | Base = require './styles/base' 2 | 3 | module.exports = styles = 4 | Base: Base 5 | Default: require('./styles/default') Base 6 | -------------------------------------------------------------------------------- /bin/groc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../').CLI(process.argv.slice(2), function(error) { 4 | if (error) { 5 | process.exit(1) 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /.groc.json: -------------------------------------------------------------------------------- 1 | { 2 | "glob": ["lib/**/*.coffee", "README.md", "lib/styles/*/style.sass", "lib/styles/*/*.jade", "scripts/**/*.sh", ".groc.json", "package.json"], 3 | "github": false, 4 | "repository-url": "https://github.com/nevir/groc" 5 | } 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('coffee-script/register'); 2 | 3 | module.exports = { 4 | PACKAGE_INFO: require('./package.json'), 5 | CLI: require('./lib/cli'), 6 | LANGUAGES: require('./lib/languages'), 7 | DOC_TAGS: require('./lib/doc_tags'), 8 | Project: require('./lib/project'), 9 | styles: require('./lib/styles') 10 | }; 11 | -------------------------------------------------------------------------------- /lib/utils/compatibility_helpers.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | util = require 'util' 3 | 4 | FORMAT_REGEXP = /%[sdj%]/g 5 | 6 | module.exports = CompatibilityHelpers = 7 | # `path.sep` was introduced with Node 0.8, so make sure we have it in 0.6. 8 | pathSep: path.sep || (if process.platform == 'win32' then '\\' else '/') 9 | 10 | # A backport of Node 0.6's util.format 11 | format: (args...) -> 12 | return util.format args... if util.format? 13 | 14 | if typeof args[0] != 'string' 15 | return args.map( (o) -> util.inspect o ).join ' ' 16 | 17 | i = 1 18 | len = args.length 19 | str = String(args[0]).replace FORMAT_REGEXP, (x) -> 20 | return x if i >= len 21 | 22 | switch x 23 | when '%s' then String args[i++] 24 | when '%d' then Number args[i++] 25 | when '%j' then JSON.stringify args[i++] 26 | when '%%' then '%' 27 | else x 28 | 29 | while i < len 30 | x = args[i++] 31 | if x == null or typeof x != 'object' 32 | str += ' ' + x 33 | else 34 | str += ' ' + util.inspect x 35 | 36 | str 37 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Ian MacLeod 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 4 | associated documentation files (the "Software"), to deal in the Software without restriction, 5 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 6 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or 10 | substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 13 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 16 | OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "groc", 3 | "version": "0.8.0", 4 | "description": "Documentation generation, in the spirit of literate programming.", 5 | "keywords": ["documentation", "docs", "generator"], 6 | "homepage": "http://nevir.github.com/groc/", 7 | "author": "Ian MacLeod (https://github.com/nevir)", 8 | 9 | "licenses": [{ 10 | "type": "MIT", 11 | "url": "https://github.com/nevir/groc/MIT-LICENSE.txt" 12 | }], 13 | 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/nevir/groc" 17 | }, 18 | 19 | "main": "./index", 20 | "directories": { 21 | "lib": "./lib" 22 | }, 23 | "bin": { 24 | "groc": "./bin/groc" 25 | }, 26 | 27 | "engines": { 28 | "node": ">= 0.8" 29 | }, 30 | "dependencies": { 31 | "coffee-script": "^1.6.3", 32 | "colors": "^0.6.2", 33 | "fs-tools": "^0.2.10", 34 | "glob": "^3.2.7", 35 | "highlight.js": "^8.0.0", 36 | "jade": "^0.35.0", 37 | "marked": "^0.2.10", 38 | "optimist": "^0.6.0", 39 | "showdown": "^0.3.1", 40 | "spate": "^0.1.0", 41 | "uglify-js": "^2.4.6", 42 | "underscore": "^1.6.0" 43 | }, 44 | "devDependencies": {}, 45 | 46 | "scripts": {} 47 | } 48 | -------------------------------------------------------------------------------- /lib/utils/humanize.coffee: -------------------------------------------------------------------------------- 1 | module.exports = humanize = 2 | joinSentence: (parts, conjunctive='and') -> 3 | if parts.length > 2 4 | [parts[0..-2].join(', '), parts[parts.length - 1]].join(", #{conjunctive} ") 5 | else 6 | parts.join(" #{conjunctive} ") 7 | 8 | capitalize: (sentence) -> 9 | if sentence.length 10 | parts = sentence.match /^(\s*)([_\*`]*)(\s*)(\w)(.*)$/m 11 | "#{parts[1]}#{parts[2]}#{parts[3]}#{parts[4].toUpperCase()}#{parts[5]}" 12 | else 13 | '' 14 | 15 | article: (word) -> 16 | if word[0].toLowerCase() in ['a', 'e', 'i', 'o', 'u'] 17 | 'an' 18 | else 19 | 'a' 20 | 21 | pluralizationRules: [ 22 | { regex: /([bcdfghjklmnpqrstvwxz])y$/, replacement: '$1ies' }, 23 | { regex: /(ch|sh|x|ss|s)$/, replacement: '$1es' }, 24 | { regex: /$/, replacement: 's' } 25 | ] 26 | 27 | pluralize: (word) -> 28 | return word.replace(rule.regex, rule.replacement) for rule in humanize.pluralizationRules when rule.regex.test word 29 | 30 | gutterify: (text, gutterWidth) -> 31 | extantMimimumGutterWidth = 0 32 | 33 | if text.match(/^ +/gm) 34 | extantMinimumGutterWidth = text.match(/^ +/gm).sort()[0].length 35 | 36 | gutter = ' '[0..gutterWidth].slice(1) 37 | regex = ///^\s{#{extantMinimumGutterWidth}}///gm 38 | text.replace regex, gutter -------------------------------------------------------------------------------- /lib/styles/default/docPage.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= pageTitle 5 | meta(http-equiv="Content-Type", content="text/html; charset=utf-8") 6 | meta(name="viewport", content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0") 7 | meta(name="groc-relative-root", content=relativeRoot) 8 | meta(name="groc-document-path", content=targetPath) 9 | meta(name="groc-project-path", content=projectPath) 10 | - if (project.githubURL) 11 | meta(name="groc-github-url", content=project.githubURL) 12 | link(rel="stylesheet", type="text/css", media="all", href=relativeRoot + "assets/style.css") 13 | script(type="text/javascript", src=relativeRoot + "assets/behavior.js") 14 | 15 | body 16 | #meta 17 | - if (project.githubURL) 18 | .file-path 19 | a(href=project.githubURL + '/blob/master/' + projectPath)= projectPath 20 | - else 21 | .file-path= projectPath 22 | #document 23 | - for (var i in segments) 24 | .segment 25 | - if (segments[i].markdownedComments != '') 26 | div(class="comments " + (segments[i].accessClasses || '')) 27 | .wrapper!= segments[i].markdownedComments 28 | 29 | - if (segments[i].highlightedCode != '') 30 | - if (segments[i].foldMarker != '') 31 | .code.folded 32 | .wrapper.marker 33 | span.c1!= segments[i].foldMarker 34 | .wrapper!= segments[i].highlightedCode 35 | - else 36 | .code 37 | .wrapper!= segments[i].highlightedCode 38 | -------------------------------------------------------------------------------- /lib/utils/logger.coffee: -------------------------------------------------------------------------------- 1 | # # groc.Logger 2 | 3 | colors = require 'colors' 4 | 5 | CompatibilityHelpers = require './compatibility_helpers' 6 | 7 | 8 | # We have pretty simple needs for a logger, and so far have been unable to find a reasonable 9 | # off-the-shelf solution that fits them without being too overbearing: 10 | module.exports = class Logger 11 | # * We want the standard levels of output, plus a few more. 12 | LEVELS: 13 | TRACE: 0 14 | DEBUG: 1 15 | INFO: 2 16 | PASS: 2 17 | WARN: 3 18 | ERROR: 4 19 | 20 | # * Full on level: labels are **extremely** heavy for what is primarily a command-line tool. The 21 | # common case - `INFO` - does not even expose a label. We only want it to call out uncommon 22 | # events with some slight symbolism. 23 | LEVEL_PREFIXES: 24 | TRACE: '∴ ' 25 | DEBUG: '‡ ' 26 | INFO: ' ' 27 | PASS: '✓ ' 28 | WARN: '» ' 29 | ERROR: '! ' 30 | 31 | # * Colors make the world better. 32 | LEVEL_COLORS: 33 | TRACE: 'grey' 34 | DEBUG: 'grey' 35 | INFO: 'black' 36 | PASS: 'green' 37 | WARN: 'yellow' 38 | ERROR: 'red' 39 | 40 | # * Don't forget the semantics of our output. 41 | LEVEL_STREAMS: 42 | TRACE: console.log 43 | DEBUG: console.log 44 | INFO: console.log 45 | PASS: console.log 46 | WARN: console.error 47 | ERROR: console.error 48 | 49 | constructor: (minLevel = @LEVELS.INFO) -> 50 | @minLevel = minLevel 51 | 52 | for name of @LEVELS 53 | do (name) => 54 | @[name.toLowerCase()] = (args...) -> 55 | @emit name, args... 56 | 57 | emit: (levelName, args...) -> 58 | if @LEVELS[levelName] >= @minLevel 59 | output = CompatibilityHelpers.format args... 60 | 61 | # * We like nicely indented output 62 | output = output.split(/\r?\n/).join('\n ') 63 | 64 | @LEVEL_STREAMS[levelName] colors[@LEVEL_COLORS[levelName]] "#{@LEVEL_PREFIXES[levelName]}#{output}" 65 | 66 | output 67 | 68 | # * Sometimes we just want one-off logging 69 | globalLogger = new Logger Logger::LEVELS.TRACE 70 | 71 | for level of globalLogger.LEVELS 72 | do (level) -> 73 | Logger[level.toLowerCase()] = (args...) -> globalLogger[level.toLowerCase()] args... 74 | -------------------------------------------------------------------------------- /scripts/publish-git-pages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e # Stop on the first failure that occurs 3 | 4 | DOCS_PATH=.git/groc-tmp 5 | TARGET_BRANCH=gh-pages 6 | [[ $1 ]] && TARGET_REMOTE=$1 || TARGET_REMOTE=origin 7 | 8 | # Git spits out status information on $stderr, and we don't want to relay that as an error to the 9 | # user. So we wrap git and do error handling ourselves... 10 | exec_git() { 11 | args='' 12 | for (( i = 1; i <= $#; i++ )); do 13 | eval arg=\$$i 14 | if [[ $arg == *\ * ]]; then 15 | # } We assume that double quotes will not be used as part of argument values. 16 | args="$args \"$arg\"" 17 | else 18 | args="$args $arg" 19 | fi 20 | done 21 | 22 | set +e 23 | # } Even though we wrap the arguments in quotes, bash is splitting on whitespace within. Why? 24 | result=`eval git $args 2>&1` 25 | status=$? 26 | set -e 27 | 28 | if [[ $status -ne 0 ]]; then 29 | echo "$result" >&2 30 | exit $status 31 | fi 32 | 33 | echo "$result" 34 | return 0 35 | } 36 | 37 | if [[ `git status -s` != "" ]]; then 38 | echo "Please commit or stash your changes before publishing documentation to github!" >&2 39 | exit 1 40 | fi 41 | 42 | CURRENT_BRANCH=`git branch 2>/dev/null| sed -n '/^\*/s/^\* //p'` 43 | CURRENT_COMMIT=`git rev-parse HEAD` 44 | 45 | [[ $2 ]] && COMMIT_MESSAGE=$2 || COMMIT_MESSAGE="Generated documentation for $CURRENT_COMMIT" 46 | 47 | # Preserve the project's .gitignore so that we don't check in or otherwise screw up hidden files 48 | if [[ -e .gitignore ]]; then 49 | cp .gitignore $DOCS_PATH/ 50 | fi 51 | 52 | if [[ `git branch --no-color | grep " $TARGET_BRANCH"` == "" ]]; then 53 | # Do a fetch from the target remote to see if it was created remotely 54 | exec_git fetch $TARGET_REMOTE 55 | 56 | # Does it exist remotely? 57 | if [[ `git branch -a --no-color | grep " remotes/$TARGET_REMOTE/$TARGET_BRANCH"` == "" ]]; then 58 | echo "No '$TARGET_BRANCH' branch exists. Creating one" 59 | exec_git symbolic-ref HEAD refs/heads/$TARGET_BRANCH 60 | rm .git/index 61 | 62 | # Preserve ignored files, but make sure they're actually ignored! 63 | if [[ -e $DOCS_PATH/.gitignore ]]; then 64 | cp $DOCS_PATH/.gitignore .gitignore 65 | exec_git add .gitignore 66 | fi 67 | 68 | exec_git clean -fdq 69 | else 70 | TARGET_REMOTE=origin 71 | echo "No local branch '$TARGET_BRANCH', checking out '$TARGET_REMOTE/$TARGET_BRANCH' and tracking that" 72 | exec_git checkout -b $TARGET_BRANCH $TARGET_REMOTE/$TARGET_BRANCH 73 | fi 74 | 75 | else 76 | exec_git checkout $TARGET_BRANCH 77 | fi 78 | 79 | # We want to keep in complete sync (deleting old docs, or cruft from previous documentation output) 80 | exec_git ls-files | xargs rm 81 | 82 | # The previous solution below fails to copy .dot-files, therefore we utilize 83 | # `find`, which in turn also made the `if`-statement obsolete. 84 | # 85 | # > cp -Rf $DOCS_PATH/* . 86 | # > if [[ -e $DOCS_PATH/.gitignore ]]; then 87 | # > cp $DOCS_PATH/.gitignore .gitignore 88 | # > fi 89 | # 90 | # Alternative solution using `tar` 91 | # 92 | # > ( cd $DOCS_PATH ; tar --excude .git cf - . ) | tar xkpvvf - 93 | # 94 | find $DOCS_PATH -maxdepth 1 -not -path $DOCS_PATH -and -not -path $DOCS_PATH/.git -exec cp -Rf "{}" . \; 95 | 96 | # Do nothing unless we actually have changes 97 | if [[ `git status -s` != "" ]]; then 98 | exec_git add -A 99 | exec_git commit -m $COMMIT_MESSAGE 100 | exec_git push $TARGET_REMOTE $TARGET_BRANCH 101 | fi 102 | 103 | # Clean up after ourselves 104 | rm -rf $DOCS_PATH 105 | 106 | exec_git checkout $CURRENT_BRANCH 107 | -------------------------------------------------------------------------------- /lib/styles/default/_codestyles-pygments.sass: -------------------------------------------------------------------------------- 1 | $code-color-keyword: #e0c090 2 | $code-color-attribute: #a9c2ba 3 | $code-color-property: #abd9cf 4 | $code-color-class: #cee4dd 5 | $code-color-variable: #b9d0af 6 | $code-color-string: #e9baba 7 | $code-color-interpolated: #cba8d6 8 | $code-color-punctuation: #ded3a1 9 | $code-color-comment: #b1bac4 10 | 11 | =codestyles-pygments 12 | .w // Whitespace 13 | .err // Error 14 | .x // Other 15 | 16 | .k // Keyword 17 | color: $code-color-keyword 18 | .kc // Keyword.Constant 19 | color: $code-color-keyword 20 | .kd // Keyword.Declaration 21 | color: $code-color-keyword 22 | .kp // Keyword.Pseudo 23 | color: $code-color-keyword 24 | .kr // Keyword.Reserved 25 | color: $code-color-keyword 26 | .kt // Keyword.Type 27 | color: $code-color-keyword 28 | 29 | .n, // Name 30 | .na // Name.Attribute 31 | color: $code-color-attribute 32 | .nb, // Name.Builtin 33 | .bp, // Name.Builtin.Pseudo 34 | .nc // Name.Class 35 | color: $code-color-class 36 | .no // Name.Constant 37 | color: $code-color-class 38 | .nd // Name.Decorator 39 | color: $code-color-class 40 | .ni, // Name.Entity 41 | .ne, // Name.Exception 42 | .nf // Name.Function 43 | color: $code-color-property 44 | .py // Name.Property 45 | color: $code-color-property 46 | .nl, // Name.Label 47 | .nn, // Name.Namespace 48 | .nx, // Name.Other 49 | .nt, // Name.Tag 50 | 51 | .nv // Name.Variable 52 | color: $code-color-variable 53 | .vc // Name.Variable.Class 54 | color: $code-color-variable 55 | .vg // Name.Variable.Global 56 | color: $code-color-variable 57 | .vi // Name.Variable.Instance 58 | color: $code-color-variable 59 | 60 | // .l // Literal 61 | // .ld // Literal.Date 62 | 63 | .s // String 64 | color: $code-color-string 65 | .sb // String.Backtick 66 | color: $code-color-string 67 | .sc // String.Char 68 | color: $code-color-string 69 | .sd // String.Doc 70 | color: $code-color-string 71 | .s2 // String.Double 72 | color: $code-color-string 73 | .se // String.Escape 74 | color: $code-color-string 75 | .sh // String.Heredoc 76 | color: $code-color-string 77 | .si // String.Interpol 78 | color: $code-color-string 79 | .sx // String.Other 80 | color: $code-color-string 81 | .sr // String.Regex 82 | color: $code-color-interpolated 83 | .s1 // String.Single 84 | color: $code-color-string 85 | .ss // String.Symbol 86 | color: $code-color-interpolated 87 | 88 | .m // Number 89 | color: $code-color-interpolated 90 | .mf // Number.Float 91 | color: $code-color-interpolated 92 | .mh // Number.Hex 93 | color: $code-color-interpolated 94 | .mi // Number.Integer 95 | color: $code-color-interpolated 96 | .il // Number.Integer.Long 97 | color: $code-color-interpolated 98 | .mo // Number.Oct 99 | color: $code-color-interpolated 100 | 101 | .o // Operator 102 | color: $code-color-punctuation 103 | .ow // Operator.Word 104 | color: $code-color-punctuation 105 | 106 | .p // Punctuation 107 | color: $code-color-punctuation 108 | 109 | .c, .cm, .cp, .c1, .cs // Comment.* 110 | font-style: italic 111 | 112 | .c // Comment 113 | color: $code-color-comment 114 | .cm // Comment.Multiline 115 | color: $code-color-comment 116 | .cp // Comment.Preproc 117 | color: $code-color-comment 118 | .c1 // Comment.Single 119 | color: $code-color-comment 120 | .cs // Comment.Special 121 | color: $code-color-comment 122 | 123 | .g // Generic 124 | .gd // Generic.Deleted 125 | .ge // Generic.Emph 126 | .gr // Generic.Error 127 | .gh // Generic.Heading 128 | .gi // Generic.Inserted 129 | .go // Generic.Output 130 | .gp // Generic.Prompt 131 | .gs // Generic.Strong 132 | .gu // Generic.Subheading 133 | .gt // Generic.Traceback 134 | -------------------------------------------------------------------------------- /lib/utils/cli_helpers.coffee: -------------------------------------------------------------------------------- 1 | childProcess = require 'child_process' 2 | path = require 'path' 3 | 4 | _ = require 'underscore' 5 | 6 | 7 | # # Command Line Helpers 8 | module.exports = CLIHelpers = 9 | 10 | # ## configureOptimist 11 | 12 | # [Optimist](https://github.com/substack/node-optimist) fails to provide a few conveniences, so we 13 | # layer on a little bit of additional structure when defining our options. 14 | configureOptimist: (opts, config, extraDefaults) -> 15 | for optName, optConfig of config 16 | # * We support two tiers of default values. First, we set up the hard-coded defaults specified 17 | # as part of `config`. 18 | # 19 | # Also, `default` is a reserved name, hence `defaultVal`. 20 | defaultVal = extraDefaults?[optName] ? optConfig.default 21 | 22 | # * We also want the ability to specify reactionary default values, so that the user can 23 | # inspect the current state of things by tacking on a `--help`. 24 | defaultVal = defaultVal opts if _.isFunction defaultVal 25 | 26 | # And set it all up with our key as the canonical option name. 27 | opts.options optName, _(optConfig).extend(default: defaultVal) 28 | 29 | # ## extractArgv 30 | 31 | # In addition to the extended configuration that we desire, we also want special handling for 32 | # generated values: 33 | extractArgv: (opts, config) -> 34 | argv = opts.argv 35 | 36 | # * With regular optimist parsing, you either get an individual value or an array. For 37 | # list-style options, we always want an array. 38 | for optName, optConfig of config 39 | if optConfig.type == 'list' and not _.isArray opts.argv[optName] 40 | argv[optName] = _.compact [ argv[optName] ] 41 | 42 | # * It's also handy to auto-resolve paths. 43 | for optName, optConfig of config 44 | argv[optName] = path.resolve argv[optName] if optConfig.type == 'path' 45 | 46 | argv 47 | 48 | 49 | # ## guessPrimaryGitHubURL 50 | 51 | guessPrimaryGitHubURL: (repository_url, callback) -> 52 | # `git config --list` provides information about branches and remotes - everything we need to 53 | # attempt to guess the project's GitHub repository. 54 | # 55 | # There are several states that a GitHub-based repository could be in, and we've probably missed 56 | # a few. We attempt to guess it through a few means: 57 | childProcess.exec 'git config --list', (error, stdout, stderr) => 58 | return error if error 59 | 60 | config = {} 61 | for line in stdout.split '\n' 62 | pieces = line.split '=' 63 | config[pieces[0]] = pieces[1] 64 | 65 | # * If the user has a tracked `gh-pages` branch, chances are extremely high that its tracked 66 | # remote is the correct github project. 67 | pagesRemote = config['branch.gh-pages.remote'] ? "origin" 68 | if repository_url? 69 | return callback null, repository_url, pagesRemote 70 | else 71 | if config["remote.#{pagesRemote}.url"] 72 | url = @extractGitHubURL config["remote.#{pagesRemote}.url"] 73 | return callback null, url, pagesRemote if url 74 | 75 | # * If that fails, we fall back to the origin remote if it is a GitHub repository. 76 | url = @extractGitHubURL config['remote.origin.url'] 77 | return callback null, url, pagesRemote if url 78 | 79 | # * We fall back to searching all remotes for a GitHub repository, and choose the first one 80 | # we encounter. 81 | for key, value of config 82 | url = @extractGitHubURL value 83 | return callback null, url, pagesRemote if url 84 | 85 | callback new Error "Could not guess a GitHub URL for the current repository :(" 86 | 87 | # A quick helper that extracts a GitHub project URL from its repository URL. 88 | extractGitHubURL: (url) -> 89 | match = url?.match /github\.com[:\/]([^\/]+)\/([^\/]+)/ 90 | return null unless match 91 | 92 | owner = match[1] 93 | repo = if match[2][-4..] == '.git' then match[2][0...-4] else match[2] 94 | 95 | "https://github.com/#{owner}/#{repo}" 96 | 97 | module.exports = CLIHelpers 98 | -------------------------------------------------------------------------------- /lib/styles/base.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | fsTools = require 'fs-tools' 5 | 6 | StyleHelpers = require '../utils/style_helpers' 7 | Utils = require '../utils' 8 | 9 | 10 | module.exports = class Base 11 | constructor: (project) -> 12 | @project = project 13 | @log = project.log 14 | @files = [] 15 | @outline = {} # Keyed on target path 16 | 17 | renderFile: (data, fileInfo, callback) -> 18 | @log.trace 'BaseStyle#renderFile(..., %j, ...)', fileInfo 19 | 20 | @files.push fileInfo 21 | 22 | segments = Utils.splitSource data, fileInfo.language, 23 | requireWhitespaceAfterToken: !!@project.options.requireWhitespaceAfterToken 24 | allowEmptyLines: !!@project.options.allowEmptyLines 25 | 26 | @log.debug 'Split %s into %d segments', fileInfo.sourcePath, segments.length 27 | 28 | Utils.parseDocTags segments, @project, (error) => 29 | if error 30 | @log.error 'Failed to parse doc tags %s: %s\n', fileInfo.sourcePath, error.message, error.stack 31 | return callback error 32 | 33 | Utils.markdownDocTags segments, @project, (error) => 34 | if error 35 | @log.error 'Failed to markdown doc tags %s: %s\n', fileInfo.sourcePath, error.message, error.stack 36 | return callback error 37 | 38 | @renderDocTags segments 39 | 40 | if @project.options.highlighter is 'pygments' 41 | highlightCode = Utils.highlightCodeUsingPygments 42 | else 43 | highlightCode = Utils.highlightCodeUsingHighlightJS 44 | 45 | highlightCode segments, fileInfo.language, (error) => 46 | if error 47 | if error.failedHighlights 48 | for highlight, i in error.failedHighlights 49 | @log.debug "highlight #{i}:" 50 | @log.warn segments[i]?.code.join '\n' 51 | @log.error highlight 52 | 53 | @log.error 'Failed to highlight %s as %s: %s', fileInfo.sourcePath, fileInfo.language.name, error.message or error 54 | return callback error 55 | 56 | Utils.markdownComments segments, @project, (error) => 57 | if error 58 | @log.error 'Failed to markdown %s: %s', fileInfo.sourcePath, error.message 59 | return callback error 60 | 61 | @outline[fileInfo.targetPath] = StyleHelpers.outlineHeaders segments 62 | 63 | # We also prefer to split out solo headers 64 | segments = StyleHelpers.segmentizeSoloHeaders segments 65 | 66 | @renderDocFile segments, fileInfo, callback 67 | 68 | # renderDocTags: # THIS METHOD MUST BE DEFINED BY SUBCLASSES 69 | 70 | renderDocFile: (segments, fileInfo, callback) -> 71 | @log.trace 'BaseStyle#renderDocFile(..., %j, ...)', fileInfo 72 | 73 | throw new Error "@templateFunc must be defined by subclasses!" unless @templateFunc 74 | 75 | docPath = path.resolve @project.outPath, "#{fileInfo.targetPath}.html" 76 | 77 | fsTools.mkdir path.dirname(docPath), '0755', (error) => 78 | if error 79 | @log.error 'Unable to create directory %s: %s', path.dirname(docPath), error.message 80 | return callback error 81 | 82 | for segment in segments 83 | segment.markdownedComments = Utils.trimBlankLines segment.markdownedComments 84 | segment.highlightedCode = Utils.trimBlankLines segment.highlightedCode 85 | segment.foldMarker = Utils.trimBlankLines(segment.foldMarker || '') 86 | 87 | templateContext = 88 | project: @project 89 | segments: segments 90 | pageTitle: fileInfo.pageTitle 91 | sourcePath: fileInfo.sourcePath 92 | targetPath: fileInfo.targetPath 93 | projectPath: fileInfo.projectPath 94 | 95 | # How many levels deep are we? 96 | pathChunks = path.dirname(fileInfo.targetPath).split(/[\/\\]/) 97 | if pathChunks.length == 1 && pathChunks[0] == '.' 98 | templateContext.relativeRoot = '' 99 | else 100 | templateContext.relativeRoot = "#{pathChunks.map(-> '..').join '/'}/" 101 | 102 | try 103 | data = @templateFunc templateContext 104 | 105 | catch error 106 | @log.error 'Rendering documentation template for %s failed: %s', docPath, error.message 107 | return callback error 108 | 109 | fs.writeFile docPath, data, 'utf-8', (error) => 110 | if error 111 | @log.error 'Failed to write documentation file %s: %s', docPath, error.message 112 | return callback error 113 | 114 | @log.pass docPath 115 | callback() 116 | 117 | renderCompleted: (callback) -> 118 | @log.trace 'BaseStyle#renderCompleted(...)' 119 | 120 | @tableOfContents = StyleHelpers.buildTableOfContents @files, @outline 121 | 122 | callback() 123 | -------------------------------------------------------------------------------- /lib/project.coffee: -------------------------------------------------------------------------------- 1 | # # groc API 2 | 3 | fs = require 'fs' 4 | path = require 'path' 5 | 6 | spate = require 'spate' 7 | 8 | CompatibilityHelpers = require './utils/compatibility_helpers' 9 | Logger = require './utils/logger' 10 | Utils = require './utils' 11 | styles = require './styles' 12 | 13 | 14 | # A core concept of `groc` is that your code is grouped into a project, and that there is a certain 15 | # amount of context that it lends to your documentation. 16 | # 17 | # A project: 18 | module.exports = class Project 19 | constructor: (root, outPath, minLogLevel=Logger::INFO) -> 20 | @options = {} 21 | @log = new Logger minLogLevel 22 | 23 | # * Has a single root directory that contains (most of) it. 24 | @root = path.resolve root 25 | # * Generally wants documented generated somewhere within its tree. We default the output path 26 | # to be relative to the project root, unless you pass an absolute path. 27 | @outPath = path.resolve @root, outPath 28 | # * Contains a set of files to generate documentation from, source code or otherwise. 29 | @files = [] 30 | # * Should strip specific prefixes of a file's path when generating relative paths for 31 | # documentation. For example, this could be used to ensure that `lib/some/source.file` maps 32 | # to `doc/some/source.file` and not `doc/lib/some/source.file`. 33 | @stripPrefixes = [] 34 | 35 | # Annoyingly, we seem to be hitting a race condition within Node 0.10's 36 | # emulation for old-style streams. For now, we're dropping concurrent doc 37 | # generation to play it safe. People are still using groc with 0.6. 38 | oldNode = process.version.match /v0\.[0-8]\./ 39 | # This is both a performance (over-)optimization and debugging aid. Instead of spamming the 40 | # system with file I/O and overhead all at once, we only process a certain number of source files 41 | # concurrently. This is similar to what [graceful-fs](https://github.com/isaacs/node-graceful-fs) 42 | # accomplishes. 43 | BATCH_SIZE: if oldNode then 10 else 1 44 | 45 | # Where the magic happens. 46 | # 47 | # Currently, the only supported option is: 48 | generate: (options, callback) -> 49 | @log.trace 'Project#Generate(%j, ...)', options 50 | @log.info 'Generating documentation...' 51 | 52 | # * style: The style prototype to use. Defaults to `styles.Default` 53 | style = new (options.style || styles.Default) @ 54 | 55 | # We need to ensure that the project root is a strip prefix so that we properly generate 56 | # relative paths for our files. Since strip prefixes are relative, it must be the first prefix, 57 | # so that they can strip from the remainder. 58 | @stripPrefixes = [@root + CompatibilityHelpers.pathSep].concat @stripPrefixes 59 | 60 | fileMap = Utils.mapFiles @root, @files, @stripPrefixes 61 | indexPath = path.resolve @root, @index 62 | 63 | pool = spate.pool (k for k of fileMap), maxConcurrency: @BATCH_SIZE, (currentFile, done) => 64 | @log.debug "Processing %s", currentFile 65 | 66 | language = Utils.getLanguage currentFile, @options.languages 67 | unless language? 68 | @log.warn '%s is not in a supported language, skipping.', currentFile 69 | return done() 70 | 71 | fileInfo = 72 | language: language 73 | sourcePath: currentFile 74 | projectPath: currentFile.replace ///^#{Utils.regexpEscape @root + CompatibilityHelpers.pathSep}///, '' 75 | targetPath: if currentFile == indexPath then 'index' else fileMap[currentFile] 76 | pageTitle: if currentFile == indexPath then (options.indexPageTitle || 'index') else fileMap[currentFile] 77 | 78 | targetFullPath = path.resolve @outPath, "#{fileInfo.targetPath}.html" 79 | 80 | # Only render files whose sources are newer than output? 81 | if options.onlyRenderNewer 82 | 83 | if fs.existsSync(currentFile) and fs.existsSync(targetFullPath) 84 | 85 | sourceStat = fs.statSync currentFile 86 | targetStat = fs.statSync targetFullPath 87 | 88 | # Compare timestamps. 89 | if targetStat.mtime.getTime() > sourceStat.mtime.getTime() 90 | 91 | # Mark the file as processed in the style, and return. 92 | # 93 | # TODO this is a bad API, "Base" style class should provide a 94 | # method to this effect. 95 | style.files.push fileInfo 96 | return done() 97 | 98 | fs.readFile currentFile, 'utf-8', (error, data) => 99 | if error 100 | @log.error "Failed to process %s: %s", currentFile, error.message 101 | return callback error 102 | 103 | style.renderFile data, fileInfo, done 104 | 105 | pool.exec (error) => 106 | return callback error if error 107 | 108 | style.renderCompleted (error) => 109 | return callback error if error 110 | 111 | @log.info '' 112 | @log.pass 'Documentation generated' 113 | callback() 114 | -------------------------------------------------------------------------------- /lib/styles/default/_codestyles.scss: -------------------------------------------------------------------------------- 1 | @mixin codestyles () { 2 | $code-color-keyword: #e0c090; 3 | $code-color-attribute: #a9c2ba; 4 | $code-color-property: #abd9cf; 5 | $code-color-class: #cee4dd; 6 | $code-color-variable: #b9d0af; 7 | $code-color-string: #e9baba; 8 | $code-color-interpolated: #cba8d6; 9 | $code-color-punctuation: #ded3a1; 10 | $code-color-comment: #b1bac4; 11 | $prefix: 'hljs-'; 12 | 13 | .hljs { 14 | display: block; padding: 0.5em; 15 | // background: #002b36; color: #839496; 16 | } 17 | 18 | .#{$prefix}comment, 19 | .#{$prefix}template_comment, 20 | .diff .#{$prefix}header, 21 | .#{$prefix}doctype, 22 | .#{$prefix}pi, 23 | .lisp .#{$prefix}string, 24 | .#{$prefix}javadoc { 25 | color: $code-color-comment; 26 | font-style: italic; 27 | } 28 | 29 | .#{$prefix}keyword, 30 | .#{$prefix}winutils, 31 | .method, 32 | .#{$prefix}addition, 33 | .css .#{$prefix}tag, 34 | .#{$prefix}request, 35 | .#{$prefix}status, 36 | .nginx .#{$prefix}title { 37 | color: $code-color-keyword; 38 | } 39 | 40 | .#{$prefix}string { 41 | color: $code-color-string; 42 | } 43 | 44 | .#{$prefix}property { 45 | color: $code-color-variable; 46 | } 47 | 48 | .#{$prefix}function { 49 | color: $code-color-property; 50 | } 51 | 52 | .#{$prefix}class { 53 | color: $code-color-class; 54 | } 55 | 56 | .#{$prefix}number, 57 | .#{$prefix}command, 58 | .#{$prefix}tag .#{$prefix}value, 59 | .#{$prefix}rules .#{$prefix}value, 60 | .#{$prefix}phpdoc, 61 | .tex .#{$prefix}formula, 62 | .#{$prefix}regexp, 63 | .#{$prefix}hexcolor { 64 | color: $code-color-interpolated; 65 | } 66 | 67 | .#{$prefix}title, 68 | .#{$prefix}localvars, 69 | .#{$prefix}chunk, 70 | .#{$prefix}decorator, 71 | .#{$prefix}built_in, 72 | .#{$prefix}identifier, 73 | .vhdl .#{$prefix}literal, 74 | .#{$prefix}id, 75 | .css .#{$prefix}function { 76 | color: $code-color-attribute; 77 | } 78 | 79 | .#{$prefix}attribute, 80 | .#{$prefix}variable, 81 | .lisp .#{$prefix}body, 82 | .smalltalk .#{$prefix}number, 83 | .#{$prefix}constant, 84 | .#{$prefix}class .#{$prefix}title, 85 | .#{$prefix}parent, 86 | .haskell .#{$prefix}type { 87 | color: $code-color-variable; 88 | } 89 | 90 | .#{$prefix}preprocessor, 91 | .#{$prefix}preprocessor .#{$prefix}keyword, 92 | .#{$prefix}pragma, 93 | .#{$prefix}shebang, 94 | .#{$prefix}symbol, 95 | .#{$prefix}symbol .#{$prefix}string, 96 | .diff .#{$prefix}change, 97 | .#{$prefix}special, 98 | .#{$prefix}attr_selector, 99 | .#{$prefix}important, 100 | .#{$prefix}subst, 101 | .#{$prefix}cdata, 102 | .clojure .#{$prefix}title, 103 | .css .#{$prefix}pseudo { 104 | color: $code-color-class; 105 | } 106 | 107 | .#{$prefix}deletion { 108 | color: #dc322f; 109 | } 110 | 111 | .tex .#{$prefix}formula { 112 | background: $code-color-string; 113 | } 114 | } 115 | 116 | @mixin inline-codestyles () { 117 | // Orginal Style from ethanschoonover.com/solarized 118 | // (c) Jeremy Hull 119 | $prefix: 'hljs-'; 120 | 121 | .#{$prefix}comment, 122 | .#{$prefix}template_comment, 123 | .diff .#{$prefix}header, 124 | .#{$prefix}doctype, 125 | .#{$prefix}pi, 126 | .lisp .#{$prefix}string, 127 | .#{$prefix}javadoc { 128 | color: #93a1a1; 129 | font-style: italic; 130 | } 131 | 132 | .#{$prefix}keyword, 133 | .#{$prefix}winutils, 134 | .method, 135 | .#{$prefix}addition, 136 | .css .#{$prefix}tag, 137 | .#{$prefix}request, 138 | .#{$prefix}status, 139 | .nginx .#{$prefix}title { 140 | color: #859900; 141 | } 142 | 143 | .#{$prefix}number, 144 | .#{$prefix}command, 145 | .#{$prefix}string, 146 | .#{$prefix}tag .#{$prefix}value, 147 | .#{$prefix}rules .#{$prefix}value, 148 | .#{$prefix}phpdoc, 149 | .tex .#{$prefix}formula, 150 | .#{$prefix}regexp, 151 | .#{$prefix}hexcolor { 152 | color: #2aa198; 153 | } 154 | 155 | .#{$prefix}title, 156 | .#{$prefix}localvars, 157 | .#{$prefix}chunk, 158 | .#{$prefix}decorator, 159 | .#{$prefix}built_in, 160 | .#{$prefix}identifier, 161 | .vhdl .#{$prefix}literal, 162 | .#{$prefix}id, 163 | .css .#{$prefix}function { 164 | color: #268bd2; 165 | } 166 | 167 | .#{$prefix}attribute, 168 | .#{$prefix}variable, 169 | .lisp .#{$prefix}body, 170 | .smalltalk .#{$prefix}number, 171 | .#{$prefix}constant, 172 | .#{$prefix}class .#{$prefix}title, 173 | .#{$prefix}parent, 174 | .haskell .#{$prefix}type { 175 | color: #b58900; 176 | } 177 | 178 | .#{$prefix}preprocessor, 179 | .#{$prefix}preprocessor .#{$prefix}keyword, 180 | .#{$prefix}pragma, 181 | .#{$prefix}shebang, 182 | .#{$prefix}symbol, 183 | .#{$prefix}symbol .#{$prefix}string, 184 | .diff .#{$prefix}change, 185 | .#{$prefix}special, 186 | .#{$prefix}attr_selector, 187 | .#{$prefix}important, 188 | .#{$prefix}subst, 189 | .#{$prefix}cdata, 190 | .clojure .#{$prefix}title, 191 | .css .#{$prefix}pseudo { 192 | color: #cb4b16; 193 | } 194 | 195 | .#{$prefix}deletion { 196 | color: #dc322f; 197 | } 198 | 199 | .tex .#{$prefix}formula { 200 | background: #eee8d5; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /lib/styles/default/_codestyles-highlight.scss: -------------------------------------------------------------------------------- 1 | @mixin codestyles-highlight () { 2 | $code-color-keyword: #e0c090; 3 | $code-color-attribute: #a9c2ba; 4 | $code-color-property: #abd9cf; 5 | $code-color-class: #cee4dd; 6 | $code-color-variable: #b9d0af; 7 | $code-color-string: #e9baba; 8 | $code-color-interpolated: #cba8d6; 9 | $code-color-punctuation: #ded3a1; 10 | $code-color-comment: #b1bac4; 11 | $prefix: 'hljs-'; // or '#{$prefix}' 12 | 13 | .hljs { 14 | display: block; padding: 0.5em; 15 | // background: #002b36; color: #839496; 16 | } 17 | 18 | .#{$prefix}comment, 19 | .#{$prefix}template_comment, 20 | .diff .#{$prefix}header, 21 | .#{$prefix}doctype, 22 | .#{$prefix}pi, 23 | .lisp .#{$prefix}string, 24 | .#{$prefix}javadoc { 25 | color: $code-color-comment; 26 | font-style: italic; 27 | } 28 | 29 | .#{$prefix}keyword, 30 | .#{$prefix}winutils, 31 | .method, 32 | .#{$prefix}addition, 33 | .css .#{$prefix}tag, 34 | .#{$prefix}request, 35 | .#{$prefix}status, 36 | .nginx .#{$prefix}title { 37 | color: $code-color-keyword; 38 | } 39 | 40 | .#{$prefix}string { 41 | color: $code-color-string; 42 | } 43 | 44 | .#{$prefix}property { 45 | color: $code-color-variable; 46 | } 47 | 48 | .#{$prefix}function { 49 | color: $code-color-property; 50 | } 51 | 52 | .#{$prefix}class { 53 | color: $code-color-class; 54 | } 55 | 56 | .#{$prefix}number, 57 | .#{$prefix}command, 58 | .#{$prefix}tag .#{$prefix}value, 59 | .#{$prefix}rules .#{$prefix}value, 60 | .#{$prefix}phpdoc, 61 | .tex .#{$prefix}formula, 62 | .#{$prefix}regexp, 63 | .#{$prefix}hexcolor { 64 | color: $code-color-interpolated; 65 | } 66 | 67 | .#{$prefix}title, 68 | .#{$prefix}localvars, 69 | .#{$prefix}chunk, 70 | .#{$prefix}decorator, 71 | .#{$prefix}built_in, 72 | .#{$prefix}identifier, 73 | .vhdl .#{$prefix}literal, 74 | .#{$prefix}id, 75 | .css .#{$prefix}function { 76 | color: $code-color-attribute; 77 | } 78 | 79 | .#{$prefix}attribute, 80 | .#{$prefix}variable, 81 | .lisp .#{$prefix}body, 82 | .smalltalk .#{$prefix}number, 83 | .#{$prefix}constant, 84 | .#{$prefix}class .#{$prefix}title, 85 | .#{$prefix}parent, 86 | .haskell .#{$prefix}type { 87 | color: $code-color-variable; 88 | } 89 | 90 | .#{$prefix}preprocessor, 91 | .#{$prefix}preprocessor .#{$prefix}keyword, 92 | .#{$prefix}pragma, 93 | .#{$prefix}shebang, 94 | .#{$prefix}symbol, 95 | .#{$prefix}symbol .#{$prefix}string, 96 | .diff .#{$prefix}change, 97 | .#{$prefix}special, 98 | .#{$prefix}attr_selector, 99 | .#{$prefix}important, 100 | .#{$prefix}subst, 101 | .#{$prefix}cdata, 102 | .clojure .#{$prefix}title, 103 | .css .#{$prefix}pseudo { 104 | color: $code-color-class; 105 | } 106 | 107 | .#{$prefix}deletion { 108 | color: #dc322f; 109 | } 110 | 111 | .tex .#{$prefix}formula { 112 | background: $code-color-string; 113 | } 114 | } 115 | 116 | @mixin inline-codestyles () { 117 | // Orginal Style from ethanschoonover.com/solarized 118 | // (c) Jeremy Hull 119 | $prefix: 'hljs-'; // or 'hljs-' 120 | 121 | .#{$prefix}comment, 122 | .#{$prefix}template_comment, 123 | .diff .#{$prefix}header, 124 | .#{$prefix}doctype, 125 | .#{$prefix}pi, 126 | .lisp .#{$prefix}string, 127 | .#{$prefix}javadoc { 128 | color: #93a1a1; 129 | font-style: italic; 130 | } 131 | 132 | .#{$prefix}keyword, 133 | .#{$prefix}winutils, 134 | .method, 135 | .#{$prefix}addition, 136 | .css .#{$prefix}tag, 137 | .#{$prefix}request, 138 | .#{$prefix}status, 139 | .nginx .#{$prefix}title { 140 | color: #859900; 141 | } 142 | 143 | .#{$prefix}number, 144 | .#{$prefix}command, 145 | .#{$prefix}string, 146 | .#{$prefix}tag .#{$prefix}value, 147 | .#{$prefix}rules .#{$prefix}value, 148 | .#{$prefix}phpdoc, 149 | .tex .#{$prefix}formula, 150 | .#{$prefix}regexp, 151 | .#{$prefix}hexcolor { 152 | color: #2aa198; 153 | } 154 | 155 | .#{$prefix}title, 156 | .#{$prefix}localvars, 157 | .#{$prefix}chunk, 158 | .#{$prefix}decorator, 159 | .#{$prefix}built_in, 160 | .#{$prefix}identifier, 161 | .vhdl .#{$prefix}literal, 162 | .#{$prefix}id, 163 | .css .#{$prefix}function { 164 | color: #268bd2; 165 | } 166 | 167 | .#{$prefix}attribute, 168 | .#{$prefix}variable, 169 | .lisp .#{$prefix}body, 170 | .smalltalk .#{$prefix}number, 171 | .#{$prefix}constant, 172 | .#{$prefix}class .#{$prefix}title, 173 | .#{$prefix}parent, 174 | .haskell .#{$prefix}type { 175 | color: #b58900; 176 | } 177 | 178 | .#{$prefix}preprocessor, 179 | .#{$prefix}preprocessor .#{$prefix}keyword, 180 | .#{$prefix}pragma, 181 | .#{$prefix}shebang, 182 | .#{$prefix}symbol, 183 | .#{$prefix}symbol .#{$prefix}string, 184 | .diff .#{$prefix}change, 185 | .#{$prefix}special, 186 | .#{$prefix}attr_selector, 187 | .#{$prefix}important, 188 | .#{$prefix}subst, 189 | .#{$prefix}cdata, 190 | .clojure .#{$prefix}title, 191 | .css .#{$prefix}pseudo { 192 | color: #cb4b16; 193 | } 194 | 195 | .#{$prefix}deletion { 196 | color: #dc322f; 197 | } 198 | 199 | .tex .#{$prefix}formula { 200 | background: #eee8d5; 201 | } 202 | } -------------------------------------------------------------------------------- /lib/utils/style_helpers.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | _ = require 'underscore' 4 | 5 | 6 | # # Style Helpers 7 | # 8 | # A collection of helpful functions to support styles and their behavior. 9 | module.exports = StyleHelpers = 10 | # Generate a table of contents as a tree of {node: {...}, children: []} objects. 11 | # 12 | # We want a pretty complex hierarchy in our table of contents: 13 | # buildTableOfContents: (files) -> 14 | 15 | # Given an array of markdowned segments, convert their list of headers into a hierarchical 16 | # outline (table of contents!) 17 | outlineHeaders: (segments) -> 18 | headers = segments.reduce ( (a, s) -> a.concat s.headers ), [] 19 | return [] unless headers.length > 0 20 | 21 | nodes = [] 22 | for header in headers 23 | nodes.push 24 | type: 'heading' 25 | data: header 26 | depth: header.level 27 | 28 | @buildNodeTree nodes 29 | 30 | # Generate a table of contents as a tree of {..., children: []} objects. 31 | # 32 | # We take a list of file info objects, and a map of outlines to fileInfo.targetPath. 33 | # 34 | # We want a pretty complex hierarchy in our table of contents: 35 | buildTableOfContents: (files, outlines) -> 36 | files = files.sort (a, b) -> 37 | # * The index is always first in the table of contents. 38 | return -1 if a.targetPath == 'index' 39 | return 1 if b.targetPath == 'index' 40 | 41 | return 0 if a.targetPath == b.targetPath 42 | # * files matching a directory name come directly before it. E.g. "foo" < "foo/bar" < "foz" 43 | if a.targetPath < b.targetPath then -1 else 1 44 | 45 | nodes = [] 46 | prevPath = [] 47 | for file in files 48 | targetChunks = file.targetPath.split path.join('/') 49 | pathChunks = targetChunks[0...-1] 50 | 51 | # * If a file has the same name as a directory, it takes ownership of that directory's folder. 52 | for chunk, i in pathChunks 53 | if prevPath[i] != chunk 54 | # * Otherwise we have a generic folder node that takes its place in the table of contents. 55 | # TODO: We probably want some way to rename folders! 56 | nodes.push 57 | type: 'folder' 58 | data: 59 | path: targetChunks[0..i].join '/' 60 | title: if chunk == 'index' and i is 0 then file.pageTitle else targetChunks[i] 61 | depth: i + 1 62 | 63 | prevPath = [] # Make sure that we don't match directories several levels in, after a fail. 64 | 65 | prevPath = targetChunks 66 | 67 | fileData = _(file).clone() 68 | 69 | # Annotate the file data with a title to represent it. 70 | # 71 | # If possible, we use the initial header in the file, 72 | if outlines[file.targetPath]?[0]?.data?.isFileHeader 73 | fileData.firstHeader = outlines[file.targetPath].shift() 74 | fileData.title = fileData.firstHeader.data.title 75 | 76 | if fileData.firstHeader.children?.length > 0 77 | outlines[file.targetPath] = fileData.firstHeader.children.concat outlines[file.targetPath] 78 | 79 | # Otherwise we just fall back to the file's target path... 80 | else 81 | fileData.title = if file.targetPath == 'index' then file.pageTitle else path.basename file.targetPath 82 | 83 | nodes.push 84 | type: 'file' 85 | data: fileData 86 | depth: file.targetPath.split( path.join('/') ).length 87 | outline: outlines[file.targetPath] 88 | 89 | @buildNodeTree nodes 90 | 91 | # Take a flat, though ordered, list of nodes and convert them into a tree. 92 | # 93 | # Nodes are expected to have a `depth` property. Each node is annotated with an array of 94 | # `children` if it does not exist. Children are appended, otherwise. 95 | buildNodeTree: (nodes) -> 96 | result = [] 97 | stack = [] 98 | 99 | for node in nodes 100 | # Unwind the stack until we get to the first node that is at a lower depth than us. We are 101 | # considered to be its child 102 | stack.pop() while _(stack).last()?.depth >= node.depth 103 | 104 | if stack.length == 0 105 | # Top level nodes are directly returned in the result list. 106 | result.push node 107 | else 108 | _(stack).last().children ||= [] 109 | _(stack).last().children.push node 110 | 111 | stack.push node 112 | 113 | result 114 | 115 | # It helps visually to separate headers that are not attached to other comments into their own 116 | # segment. 117 | segmentizeSoloHeaders: (segments) -> 118 | results = [] 119 | for segment in segments 120 | headerOnly = segment.markdownedComments.match /^\s*]*>[^<]*<\/h\d>\s*$/ 121 | if headerOnly and not segment.highlightedCode.match /^\s*$/ 122 | results.push 123 | code: [] 124 | comments: segment.comments 125 | highlightedCode: '' 126 | markdownedComments: segment.markdownedComments 127 | 128 | results.push 129 | code: segment.code 130 | comments: [] 131 | highlightedCode: segment.highlightedCode 132 | markdownedComments: '' 133 | 134 | else 135 | results.push segment 136 | 137 | results 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # groc 2 | 3 | Groc takes your _documented_ code, and in an admission that people aren't machines, generates 4 | documentation that follows the spirit of literate programming. Take a look at the 5 | [self-generated documentation](http://nevir.github.com/groc/), and see if it appeals to you! 6 | 7 | It is very heavily influenced by [Jeremy Ashkenas](https://github.com/jashkenas)' 8 | [docco](http://jashkenas.github.com/docco/), and is an attempt to further enhance the idea (thus, 9 | groc can't tout the same quick 'n dirty principles of docco). 10 | 11 | 12 | ## Maintainers 13 | 14 | Groc, unfortunately, does not have any active maintainers. If you are interested in picking up the 15 | torch, please toss me an email (ian@nevir.net). 16 | 17 | 18 | ## What does it give you? 19 | 20 | Groc will: 21 | 22 | * Generate documentation from your source code, by displaying your 23 | [Markdown](http://daringfireball.net/projects/markdown/) formatted comments next to the code 24 | fragments that they document. 25 | 26 | * Submit your project's documentation to the [github pages](http://pages.github.com/) for your 27 | project. 28 | 29 | * Generate a searchable table of contents for all documented files and headers within your project. 30 | 31 | * Gracefully handle complex hierarchies of source code across multiple folders. 32 | 33 | * Read a configuration file so that you don't have to think when you want your documentation built; 34 | you just type `groc`. 35 | 36 | 37 | ## How? 38 | 39 | ### Installing groc 40 | 41 | Groc depends on [Node.js](http://nodejs.org/). Once you have those installed - and assuming that 42 | your node install came with [npm](http://npmjs.org/) - you can install groc via: 43 | 44 | ```bash 45 | $ npm install -g groc 46 | ``` 47 | 48 | For those new to npm, `-g` indicates that you want groc installed as a global command for your 49 | environment. You may need to prefix the command with sudo, depending on how you installed node. 50 | 51 | 52 | ### Using groc (CLI) 53 | 54 | To generate documentation, just point groc to source files that you want docs for: 55 | 56 | ```bash 57 | $ groc *.rb 58 | ``` 59 | 60 | Groc will also handle extended globbing syntax if you quote arguments: 61 | 62 | ```bash 63 | $ groc "lib/**/*.coffee" README.md 64 | ``` 65 | 66 | By default, groc will drop the generated documentation in the `doc/` folder of your project, and it 67 | will treat `README.md` as the index. Take a look at your generated docs, and see if everything is 68 | in order! 69 | 70 | Once you are pleased with the output, you can push your docs to your github pages branch: 71 | 72 | ```bash 73 | $ groc --github "lib/**/*.coffee" README.md 74 | ``` 75 | 76 | Groc will automagically create and push the `gh-pages` branch if it is missing. 77 | 78 | There are [additional options](http://nevir.github.com/groc/cli.html#cli-options) supported by 79 | groc, if you are interested. 80 | 81 | 82 | ### Configuring groc (.groc.json) 83 | 84 | Groc supports a simple JSON configuration format once you know the config values that appeal to you. 85 | 86 | Create a `.groc.json` file in your project root, where each key maps to an option you would pass to 87 | the `groc` command. File names and globs are defined as an array with the key `glob`. For 88 | example, groc's own configuration is: 89 | 90 | ```json 91 | { 92 | "glob": ["lib/**/*.coffee", "README.md", "lib/styles/*/style.sass", "lib/styles/*/*.jade"], 93 | "github": true 94 | } 95 | ``` 96 | 97 | From now on, if you call `groc` without any arguments, it will use your pre-defined configuration. 98 | 99 | 100 | ## Literate programming? 101 | 102 | [Literate programming](http://en.wikipedia.org/wiki/Literate_programming) is a programming 103 | methodology coined by [Donald Knuth](http://en.wikipedia.org/wiki/Donald_Knuth). The primary tenet 104 | is that you write a program so that the structure of both the code and documentation align with 105 | your mental model of its behaviors and processes. 106 | 107 | Groc aims to provide a happy medium where you can freely write your source files as structured 108 | documents, while not going out of your way to restructure the code to fit the documentation. 109 | Here are some suggested guidelines to follow when writing your code: 110 | 111 | * Try to keep the size of each source file down. It is helpful if each file fulfills a specific 112 | feature of your application or library. 113 | 114 | * Rather than commenting individual lines of code, write comments that explain the _behavior_ of a 115 | given method or code block. Take advantage of the fact that comments can span that method. 116 | 117 | * Make gratuitous use of lists when explaining processes; step by step explanations are extremely 118 | easy to follow! 119 | 120 | * Break each source file into sections via headers. Don't be afraid to split source into even 121 | smaller files if it makes them more readable. 122 | 123 | Writing documentation is _hard_; hopefully groc helps to streamline the process for you! 124 | 125 | 126 | ## Known Issues 127 | 128 | * Groc does not fare well with files that have very long line lengths (minimized 129 | JavaScript being the prime offender). Make sure that you exclude them! 130 | 131 | 132 | ## What's in the works? 133 | 134 | Groc wants to: 135 | 136 | * [Fully support hand-held viewing of documentation](https://github.com/nevir/groc/issues/1). It 137 | can almost do this today, but the table of contents is horribly broken in the mobile view. 138 | -------------------------------------------------------------------------------- /lib/styles/default.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | _ = require 'underscore' 5 | coffeeScript = require 'coffee-script' 6 | fsTools = require 'fs-tools' 7 | jade = require 'jade' 8 | uglifyJs = require 'uglify-js' 9 | humanize = require '../utils/humanize' 10 | 11 | module.exports = (Base) -> class Default extends Base 12 | STATIC_ASSETS: ['style.css'] 13 | 14 | constructor: (args...) -> 15 | super(args...) 16 | 17 | @sourceAssets = path.join __dirname, 'default' 18 | @targetAssets = path.resolve @project.outPath, 'assets' 19 | 20 | templateData = fs.readFileSync path.join(@sourceAssets, 'docPage.jade'), 'utf-8' 21 | @templateFunc = jade.compile templateData 22 | 23 | renderCompleted: (callback) -> 24 | @log.trace 'styles.Default#renderCompleted(...)' 25 | 26 | super (error) => 27 | return error if error 28 | @copyAssets callback 29 | 30 | copyAssets: (callback) -> 31 | @log.trace 'styles.Default#copyAssets(...)' 32 | 33 | # Even though fsTools.copy creates directories if they're missing - we want a bit more control 34 | # over it (permissions), as well as wanting to avoid contention. 35 | fsTools.mkdir @targetAssets, '0755', (error) => 36 | if error 37 | @log.error 'Unable to create directory %s: %s', @targetAssets, error.message 38 | return callback error 39 | @log.trace 'mkdir: %s', @targetAssets 40 | 41 | numCopied = 0 42 | for asset in @STATIC_ASSETS 43 | do (asset) => 44 | assetTarget = path.join @targetAssets, asset 45 | fsTools.copy path.join(@sourceAssets, asset), assetTarget, (error) => 46 | if error 47 | @log.error 'Unable to copy %s: %s', assetTarget, error.message 48 | return callback error 49 | @log.trace 'Copied %s', assetTarget 50 | 51 | numCopied += 1 52 | @compileScript callback unless numCopied < @STATIC_ASSETS.length 53 | 54 | compileScript: (callback) -> 55 | @log.trace 'styles.Default#compileScript(...)' 56 | 57 | scriptPath = path.join @sourceAssets, 'behavior.coffee' 58 | fs.readFile scriptPath, 'utf-8', (error, data) => 59 | if error 60 | @log.error 'Failed to read %s: %s', scriptPath, error.message 61 | return callback error 62 | 63 | try 64 | scriptSource = _.template(data)(@) 65 | catch error 66 | @log.error 'Failed to interpolate %s: %s', scriptPath, error.message 67 | return callback error 68 | 69 | try 70 | scriptSource = coffeeScript.compile scriptSource 71 | @log.trace 'Compiled %s', scriptPath 72 | catch error 73 | @log.debug scriptSource 74 | @log.error 'Failed to compile %s: %s', scriptPath, error.message 75 | return callback error 76 | 77 | #@compressScript scriptSource, callback 78 | @concatenateScripts scriptSource, callback 79 | 80 | compressScript: (scriptSource, callback) -> 81 | @log.trace 'styles.Default#compressScript(..., ...)' 82 | 83 | try 84 | ast = uglifyJs.parser.parse scriptSource 85 | ast = uglifyJs.uglify.ast_mangle ast 86 | ast = uglifyJs.uglify.ast_squeeze ast 87 | 88 | compiledSource = uglifyJs.uglify.gen_code ast 89 | 90 | catch error 91 | @log.error 'Failed to compress assets/behavior.js: %s', error.message 92 | return callback error 93 | 94 | @concatenateScripts compiledSource, callback 95 | 96 | concatenateScripts: (scriptSource, callback) -> 97 | @log.trace 'styles.Default#concatenateScripts(..., ...)' 98 | 99 | jqueryPath = path.join @sourceAssets, 'jquery.min.js' 100 | fs.readFile jqueryPath, 'utf-8', (error, data) => 101 | if error 102 | @log.error 'Failed to read %s: %s', jqueryPath, error.message 103 | return callback error 104 | 105 | outputPath = path.join @targetAssets, 'behavior.js' 106 | fs.writeFile outputPath, data + scriptSource, (error) => 107 | if error 108 | @log.error 'Failed to write %s: %s', outputPath, error.message 109 | return callback error 110 | @log.trace 'Wrote %s', outputPath 111 | 112 | callback() 113 | 114 | renderDocTags: (segments) -> 115 | for segment, segmentIndex in segments when segment.tagSections? 116 | 117 | sections = segment.tagSections 118 | output = '' 119 | metaOutput = '' 120 | accessClasses = 'doc-section' 121 | 122 | accessClasses += " doc-section-#{tag.name}" for tag in sections.access if sections.access? 123 | 124 | segment.accessClasses = accessClasses 125 | 126 | firstPart = [] 127 | firstPart.push tag.markdown for tag in sections.access if sections.access? 128 | firstPart.push tag.markdown for tag in sections.special if sections.special? 129 | firstPart.push tag.markdown for tag in sections.type if sections.type? 130 | 131 | metaOutput += "#{humanize.capitalize firstPart.join(' ')}" 132 | if sections.flags? or sections.metadata? 133 | secondPart = [] 134 | secondPart.push tag.markdown for tag in sections.flags if sections.flags? 135 | secondPart.push tag.markdown for tag in sections.metadata if sections.metadata? 136 | metaOutput += " #{humanize.joinSentence secondPart}" 137 | 138 | output += "#{metaOutput}\n\n" if metaOutput isnt '' 139 | 140 | output += "#{tag.markdown}\n\n" for tag in sections.description if sections.description? 141 | 142 | output += "#{tag.markdown}\n\n" for tag in sections.todo if sections.todo? 143 | 144 | if sections.params? 145 | output += 'Parameters:\n\n' 146 | output += "#{tag.markdown}\n\n" for tag in sections.params 147 | 148 | if sections.returns? 149 | output += (humanize.capitalize(tag.markdown) for tag in sections.returns if sections.returns?).join('
**and** ') 150 | 151 | if sections.howto? 152 | output += "\n\nHow-To:\n\n#{humanize.gutterify tag.markdown, 0}" for tag in sections.howto 153 | 154 | if sections.example? 155 | output += "\n\nExample:\n\n#{humanize.gutterify tag.markdown, 4}" for tag in sections.example 156 | 157 | segment.comments = output.split '\n' 158 | -------------------------------------------------------------------------------- /lib/doc_tags.coffee: -------------------------------------------------------------------------------- 1 | # # Known Doc Tags 2 | 3 | humanize = require './utils/humanize' 4 | 5 | # This function collapses... spaces 6 | # 7 | # @private 8 | # @method collapse_space 9 | # 10 | # @param {String} value This is the value which will be collapsed. The primary 11 | # purpose of this method is to allow multiline parameter 12 | # descriptions, just like this one. 13 | # 14 | # @return {String} 15 | collapse_space = (value) -> 16 | value.replace /\s+/g, ' ' 17 | 18 | # This is a sample doc tagged block comment 19 | # 20 | # @public 21 | # @module DOC_TAGS 22 | # @type {Object} 23 | module.exports = DOC_TAGS = 24 | description: 25 | section: 'description' 26 | markdown: '{value}' 27 | 28 | internal: 29 | section: 'access' 30 | 'private': 31 | section: 'access' 32 | 'protected': 33 | section: 'access' 34 | 'public': 35 | valuePrefix: 'as' 36 | section: 'access' 37 | 'static': 38 | section: 'access' 39 | 40 | constructor: 41 | section: 'special' 42 | destructor: 43 | section: 'special' 44 | 45 | constant: 46 | section: 'type' 47 | method: 48 | section: 'type' 49 | module: 50 | section: 'type' 51 | 'package': 52 | section: 'type' 53 | property: 54 | section: 'type' 55 | 56 | accessor: 57 | section: 'flags' 58 | markdown: 'is an accessor' 59 | async: 60 | section: 'flags' 61 | markdown: 'is asynchronous' 62 | asynchronous: 'async' 63 | getter: 64 | section: 'flags' 65 | markdown: 'is a getter' 66 | recursive: 67 | section: 'flags' 68 | markdown: 'is recursive' 69 | refactor: 70 | section: 'flags' 71 | markdown: 'needs to be refactored' 72 | setter: 73 | section: 'flags' 74 | markdown: 'is a setter' 75 | 76 | alias: 77 | valuePrefix: 'as' 78 | section: 'metadata' 79 | markdown: 'is aliased as {value}' 80 | publishes: 81 | section: 'metadata' 82 | requests: 83 | section: 'metadata' 84 | markdown: 'makes an ajax request to {value}' 85 | subscribes: 86 | valuePrefix: 'to' 87 | section: 'metadata' 88 | markdown: 'subscribes to {value}' 89 | type: 90 | section: 'metadata' 91 | markdown: 'of type _{value}_' 92 | 93 | todo: 94 | section: 'todo' 95 | markdown: 'TODO: {value}' 96 | 97 | example: 98 | section: 'example' 99 | markdown: '{value}' 100 | examples: 'example' 101 | usage: 'example' 102 | 103 | howto: 104 | section: 'howto' 105 | markdown: '{value}' 106 | 107 | # A comment that does not have doc tags in it 108 | note: 109 | section: 'discard' 110 | notes: 'note' 111 | 112 | param: 113 | section: 'params' 114 | # parses function parameters 115 | # 116 | # @public 117 | # @method parseValue 118 | # 119 | # @param {String} value Text that follows @param 120 | # 121 | # @return {Object} 122 | parseValue: (value) -> 123 | parts = collapse_space(value).match /^\{([^\}]+)\}\s+(\[?)([\w\.\$]+)(?:=([^\s\]]+))?(\]?)\s*(.*)$/ 124 | types: (parts[1]?.split /\|{1,2}/g) 125 | isOptional: (parts[2] == '[' and parts[5] == ']') 126 | varName: parts[3] 127 | isSubParam: /\./.test parts[3] 128 | defaultValue: parts[4] 129 | description: parts[6] 130 | 131 | # converts parsed values to markdown text 132 | # 133 | # @private 134 | # @method markdown 135 | # 136 | # @param {Object} value 137 | # @param {String[]} value.types 138 | # @param {Boolean} value.isOptional=false 139 | # @param {String} value.varName 140 | # @param {Boolean} value.isSubParam=false 141 | # @param {String} [value.defaultValue] 142 | # @param {String} [value.description] 143 | # 144 | # @return {String} should be in markdown syntax 145 | markdown: (value) -> 146 | types = ( 147 | for type in value.types 148 | if type.match /^\.\.\.|\.\.\.$/ 149 | "any number of #{humanize.pluralize type.replace(/^\.\.\.|\.\.\.$/, "")}" 150 | else if type.match /\[\]$/ 151 | "an Array of #{humanize.pluralize type.replace(/\[\]$/, "")}" 152 | else 153 | "#{humanize.article type} #{type}" 154 | ) 155 | 156 | fragments = [] 157 | 158 | fragments.push 'is optional' if value.isOptional 159 | verb = 'must' 160 | 161 | if types.length > 1 162 | verb = 'can' 163 | else if types[0] == 'a Mixed' 164 | verb = 'can' 165 | types[0] = 'of any type' 166 | else if types[0] == 'an Array of Mixeds' 167 | verb = 'can' 168 | types[0] = 'an Array of any type' 169 | else if types[0] == 'any number of Mixeds' 170 | verb = 'can' 171 | types[0] = 'any number of arguments of any type' 172 | 173 | fragments.push "#{verb} be #{humanize.joinSentence types, 'or'}" 174 | fragments.push "has a default value of #{value.defaultValue}" if value.defaultValue? 175 | 176 | "#{if value.isSubParam then " *" else "*"} **#{value.varName} #{humanize.joinSentence fragments}.**#{if value.description.length then '
(' else ''}#{value.description}#{if value.description.length then ')' else ''}" 177 | params: 'param' 178 | parameters: 'param' 179 | 180 | 'return': 181 | section: 'returns' 182 | parseValue: (value) -> 183 | parts = collapse_space(value).match /^\{([^\}]+)\}\s*(.*)$/ 184 | types: parts[1].split /\|{1,2}/g 185 | description: parts[2] 186 | markdown: (value) -> 187 | types = ("#{humanize.article type} #{type}" for type in value.types) 188 | "**returns #{types.join ' or '}**#{if value.description.length then '
(' else ''}#{value.description}#{if value.description.length then ')' else ''}" 189 | returns: 'return' 190 | throw: 191 | section: 'returns' 192 | parseValue: (value) -> 193 | parts = collapse_space(value).match /^\{([^\}]+)\}\s*(.*)$/ 194 | types: parts[1].split /\|{1,2}/g 195 | description: parts[2] 196 | markdown: (value) -> 197 | types = ("#{humanize.article type} #{type}" for type in value.types) 198 | "**can throw #{types.join ' or '}**#{if value.description.length then '
(' else ''}#{value.description}#{if value.description.length then ')' else ''}" 199 | throws: 'throw' 200 | 201 | defaultNoValue: 202 | section: 'flags' 203 | defaultHasValue: 204 | section: 'metadata' 205 | -------------------------------------------------------------------------------- /lib/languages.coffee: -------------------------------------------------------------------------------- 1 | # # Supported Languages 2 | 3 | module.exports = LANGUAGES = 4 | Markdown: 5 | nameMatchers: ['.md', '.markdown','.mkd', '.mkdn', '.mdown'] 6 | commentsOnly: true 7 | 8 | C: 9 | nameMatchers: ['.c', '.h'] 10 | pygmentsLexer: 'c' 11 | highlightJS: 'cpp' 12 | multiLineComment: ['/*', '*', '*/'] 13 | singleLineComment: ['//'] 14 | ignorePrefix: '}' 15 | foldPrefix: '^' 16 | 17 | CSharp: 18 | nameMatchers: ['.cs'] 19 | pygmentsLexer: 'csharp' 20 | highlightJS: 'cs' 21 | multiLineComment: ['/*', '*', '*/'] 22 | singleLineComment: ['//'] 23 | ignorePrefix: '}' 24 | foldPrefix: '^' 25 | 26 | CSS: 27 | nameMatchers: ['.css'] 28 | pygmentsLexer: 'css' 29 | multiLineComment: ['/*', '*', '*/'] 30 | ignorePrefix: '}' 31 | foldPrefix: '^' 32 | 33 | 'C++': 34 | nameMatchers: ['.cpp', '.hpp', '.c++', '.h++', '.cc', '.hh', '.cxx', '.hxx'] 35 | pygmentsLexer: 'cpp' 36 | multiLineComment: ['/*', '*', '*/'] 37 | singleLineComment: ['//'] 38 | ignorePrefix: '}' 39 | foldPrefix: '^' 40 | 41 | Clojure: 42 | nameMatchers: ['.clj', '.cljs'] 43 | pygmentsLexer: 'clojure' 44 | singleLineComment: [';;'] 45 | ignorePrefix: '}' 46 | foldPrefix: '^' 47 | 48 | CoffeeScript: 49 | nameMatchers: ['.coffee', 'Cakefile'] 50 | pygmentsLexer: 'coffee-script' 51 | highlightJS: 'coffeescript' 52 | # **CoffeScript's multi-line block-comment styles.** 53 | 54 | # - Variant 1: 55 | # (Variant 3 is preferred over this syntax, as soon as the pull-request 56 | # mentioned below has been merged into coffee-script's codebase.) 57 | ###* } 58 | * Tip: use '-' or '+' for bullet-lists instead of '*' to distinguish 59 | * bullet-lists visually from this kind of block comments. The preceding 60 | * whitespaces in the line-matcher and end-matcher are required. Without 61 | * them this syntax makes no sense, as it is meant to produce comments 62 | * like the following in compiled javascript: 63 | * 64 | * /** 65 | * * A sample comment, having a preceding whitespace per line. Useful 66 | * * to embed `@doctags` in javascript compiled from coffeescript. 67 | * * <= COMBINE THESE TWO CHARS => / 68 | * 69 | * (The the final comment-mark above has been TWEAKED to not raise an error) 70 | ### 71 | # - Variant 2: 72 | ### } 73 | Uses the the below defined syntax, without preceding `#` per line. This is 74 | the syntax for what the definition is actually meant for ! 75 | ### 76 | # - Variant 3: 77 | # (This syntax produces arkward comments in the compiled javascript, if 78 | # the pull-request _“[Format block-comments 79 | # better](', # HTML block comments go first, for code highlighting / segment splitting purposes 120 | '{{!', '', '}}' # Actual handlebars block comments 121 | ] 122 | # See above for a description of this flag. 123 | strictMultiLineEnd:true 124 | # This one differs from the common `ignorePrefix` of all other languages ! 125 | ignorePrefix: '#' 126 | foldPrefix: '^' 127 | 128 | Haskell: 129 | nameMatchers: ['.hs'] 130 | pygmentsLexer: 'haskell' 131 | singleLineComment: ['--'] 132 | ignorePrefix: '}' 133 | foldPrefix: '^' 134 | 135 | HTML: 136 | nameMatchers: ['.htm', '.html'] 137 | pygmentsLexer: 'html' 138 | multiLineComment: [''] 139 | ignorePrefix: '}' 140 | foldPrefix: '^' 141 | 142 | Jade: 143 | nameMatchers: ['.jade'] 144 | pygmentsLexer: 'jade' 145 | # @todo 146 | highlightJS: 'AUTO' 147 | singleLineComment: ['//', '//-'] 148 | ignorePrefix: '}' 149 | foldPrefix: '^' 150 | 151 | Java: 152 | nameMatchers: ['.java'] 153 | pygmentsLexer: 'java' 154 | multiLineComment: ['/*', '*', '*/'] 155 | singleLineComment: ['//'] 156 | multiLineComment: ['/*', '*', '*/'] 157 | ignorePrefix: '}' 158 | foldPrefix: '^' 159 | 160 | JavaScript: 161 | nameMatchers: ['.js'] 162 | pygmentsLexer: 'javascript' 163 | multiLineComment: ['/*', '*', '*/'] 164 | singleLineComment: ['//'] 165 | ignorePrefix: '}' 166 | foldPrefix: '^' 167 | 168 | Jake: 169 | nameMatchers: ['.jake'] 170 | pygmentsLexer: 'javascript' 171 | singleLineComment: ['//'] 172 | ignorePrefix: '}' 173 | foldPrefix: '^' 174 | 175 | JSON : 176 | nameMatchers : ['.json'] 177 | pygmentsLexer : 'json' 178 | codeOnly : true 179 | 180 | JSP: 181 | nameMatchers: ['.jsp'] 182 | pygmentsLexer: 'jsp' 183 | multiLineComment: [ 184 | '', 185 | '<%--', '', '--%>' 186 | ] 187 | strictMultiLineEnd:true 188 | ignorePrefix: '#' 189 | foldPrefix: '^' 190 | 191 | LaTeX: 192 | nameMatchers: ['.tex', '.latex', '.sty'] 193 | pygmentsLexer: 'latex' 194 | highlightJS: 'tex' 195 | singleLineComment: ['%'] 196 | ignorePrefix: '}' 197 | foldPrefix: '^' 198 | 199 | LESS: 200 | nameMatchers: ['.less'] 201 | pygmentsLexer: 'sass' # TODO: is there a less lexer? No. Maybe in the future. 202 | highlightJS: 'scss' 203 | singleLineComment: ['//'] 204 | ignorePrefix: '}' 205 | foldPrefix: '^' 206 | 207 | LiveScript: 208 | nameMatchers: ['.ls', 'Slakefile'] 209 | pygmentsLexer: 'livescript' 210 | multiLineComment: ['/*', '*', '*/'] 211 | singleLineComment: ['#'] 212 | ignorePrefix: '}' 213 | foldPrefix: '^' 214 | 215 | Lua: 216 | nameMatchers: ['.lua'] 217 | pygmentsLexer: 'lua' 218 | singleLineComment: ['--'] 219 | ignorePrefix: '}' 220 | foldPrefix: '^' 221 | 222 | Make: 223 | nameMatchers: ['Makefile'] 224 | pygmentsLexer: 'make' 225 | singleLineComment: ['#'] 226 | ignorePrefix: '}' 227 | foldPrefix: '^' 228 | 229 | Mustache: 230 | nameMatchers: ['.mustache'] 231 | pygmentsLexer: 'html' # TODO: is there a handlebars/mustache lexer? Nope. Lame. 232 | highlightJS: 'handlebars' 233 | multiLineComment: ['{{!', '', '}}'] 234 | ignorePrefix: '#' 235 | foldPrefix: '^' 236 | 237 | 'Objective-C': 238 | nameMatchers: ['.m', '.mm'] 239 | pygmentsLexer: 'objc' 240 | highlightJS: 'objectivec' 241 | multiLineComment: ['/*', '*', '*/'] 242 | singleLineComment: ['//'] 243 | ignorePrefix: '}' 244 | foldPrefix: '^' 245 | 246 | Perl: 247 | nameMatchers: ['.pl', '.pm'] 248 | pygmentsLexer: 'perl' 249 | singleLineComment: ['#'] 250 | ignorePrefix: '}' 251 | foldPrefix: '^' 252 | 253 | PHP: 254 | nameMatchers: [/\.php\d?$/, '.fbp'] 255 | pygmentsLexer: 'php' 256 | singleLineComment: ['//'] 257 | ignorePrefix: '}' 258 | foldPrefix: '^' 259 | 260 | Puppet: 261 | nameMatchers: ['.pp'] 262 | pygmentsLexer: 'puppet' 263 | highlightJS: 'AUTO' 264 | singleLineComment: ['#'] 265 | ignorePrefix: '}' 266 | foldPrefix: '^' 267 | 268 | Python: 269 | nameMatchers: ['.py'] 270 | pygmentsLexer: 'python' 271 | singleLineComment: ['#'] 272 | ignorePrefix: '}' 273 | foldPrefix: '^' 274 | 275 | Ruby: 276 | nameMatchers: ['.rb', '.ru', '.gemspec'] 277 | pygmentsLexer: 'ruby' 278 | singleLineComment: ['#'] 279 | ignorePrefix: '}' 280 | foldPrefix: '^' 281 | 282 | Sass: 283 | nameMatchers: ['.sass'] 284 | pygmentsLexer: 'sass' 285 | highlightJS: 'AUTO' 286 | singleLineComment: ['//'] 287 | ignorePrefix: '}' 288 | foldPrefix: '^' 289 | 290 | SCSS: 291 | nameMatchers: ['.scss'] 292 | pygmentsLexer: 'scss' 293 | multiLineComment: ['/*', '*', '*/'] 294 | singleLineComment: ['//'] 295 | ignorePrefix: '}' 296 | foldPrefix: '^' 297 | 298 | Shell: 299 | nameMatchers: ['.sh'] 300 | pygmentsLexer: 'sh' 301 | highlightJS: 'bash' 302 | singleLineComment: ['#'] 303 | ignorePrefix: '}' 304 | foldPrefix: '^' 305 | 306 | SQL: 307 | nameMatchers: ['.sql'] 308 | pygmentsLexer: 'sql' 309 | singleLineComment: ['--'] 310 | ignorePrefix: '}' 311 | foldPrefix: '^' 312 | 313 | Swift: 314 | nameMatchers: ['.swift'] 315 | pygmentsLexer: 'swift' 316 | highlightJS: 'swift' 317 | singleLineComment: ['//'] 318 | multiLineComment: ['/*', '*', '*/'] 319 | ignorePrefix: '}' 320 | foldPrefix: '^' 321 | 322 | TypeScript: 323 | nameMatchers: ['.ts'] 324 | pygmentsLexer: 'ts' 325 | multiLineComment: ['/*', '*', '*/'] 326 | singleLineComment: ['//'] 327 | ignorePrefix: '}' 328 | foldPrefix: '^' 329 | 330 | YAML: 331 | nameMatchers: ['.yml', '.yaml'] 332 | pygmentsLexer: 'yaml' 333 | highlightJS: 'AUTO' 334 | singleLineComment: ['#'] 335 | ignorePrefix: '}' 336 | foldPrefix: '^' 337 | -------------------------------------------------------------------------------- /lib/styles/default/behavior.coffee: -------------------------------------------------------------------------------- 1 | tableOfContents = <%= JSON.stringify(tableOfContents) %> 2 | 3 | # # Page Behavior 4 | 5 | # ## Table of Contents 6 | 7 | # Global jQuery references to navigation components we care about. 8 | nav$ = null 9 | toc$ = null 10 | 11 | setTableOfContentsActive = (active) -> 12 | html$ = $('html') 13 | 14 | if active 15 | nav$.addClass 'active' 16 | html$.addClass 'popped' 17 | else 18 | nav$.removeClass 'active' 19 | html$.removeClass 'popped' 20 | 21 | toggleTableOfContents = -> 22 | setTableOfContentsActive not nav$.hasClass 'active' 23 | 24 | # ### Node Navigation 25 | currentNode$ = null 26 | 27 | focusCurrentNode = -> 28 | # We use the first child's offset top rather than toc$.offset().top because there may be borders 29 | # or other stylistic tweaks that further offset the scrollTop. 30 | currentNodeTop = currentNode$.offset().top - toc$.children(':visible').first().offset().top 31 | currentNodeBottom = currentNodeTop + currentNode$.children('.label').height() 32 | 33 | # If the current node is partially or fully above the top of the viewport, scroll it into view. 34 | if currentNodeTop < toc$.scrollTop() 35 | toc$.scrollTop currentNodeTop 36 | 37 | # Similarly, if we're below the bottom of the viewport, scroll up enough to make it visible. 38 | if currentNodeBottom > toc$.scrollTop() + toc$.height() 39 | toc$.scrollTop currentNodeBottom - toc$.height() 40 | 41 | setCurrentNodeExpanded = (expanded) -> 42 | if expanded 43 | currentNode$.addClass 'expanded' 44 | else 45 | if currentNode$.hasClass 'expanded' 46 | currentNode$.removeClass 'expanded' 47 | 48 | # We collapse up to the node's parent if the current node is already collapsed. This allows 49 | # a user to quickly spam left to move up the tree. 50 | else 51 | parents$ = currentNode$.parents('li') 52 | selectNode parents$.first() if parents$.length > 0 53 | 54 | focusCurrentNode() 55 | 56 | selectNode = (newNode$) -> 57 | # Remove first, in case it's the same node 58 | currentNode$.removeClass 'selected' 59 | newNode$.addClass 'selected' 60 | 61 | currentNode$ = newNode$ 62 | focusCurrentNode() 63 | 64 | selectNodeByDocumentPath = (documentPath, headerSlug=null) -> 65 | currentNode$ = fileMap[documentPath] 66 | if headerSlug 67 | for link in currentNode$.find '.outline a' 68 | urlChunks = $(link).attr('href').split '#' 69 | 70 | if urlChunks[1] == headerSlug 71 | currentNode$ = $(link).parents('li').first() 72 | break 73 | 74 | currentNode$.addClass 'selected expanded' 75 | currentNode$.parents('li').addClass 'expanded' 76 | 77 | focusCurrentNode() 78 | 79 | moveCurrentNode = (up) -> 80 | visibleNodes$ = toc$.find 'li:visible:not(.filtered)' 81 | 82 | # Fall back to the first node if anything goes wrong 83 | newIndex = 0 84 | for node, i in visibleNodes$ 85 | if node == currentNode$[0] 86 | newIndex = if up then i - 1 else i + 1 87 | newIndex = 0 if newIndex < 0 88 | newIndex = visibleNodes$.length - 1 if newIndex > visibleNodes$.length - 1 89 | break 90 | 91 | selectNode $(visibleNodes$[newIndex]) 92 | 93 | visitCurrentNode = -> 94 | labelLink$ = currentNode$.children('a.label') 95 | window.location = labelLink$.attr 'href' if labelLink$.length > 0 96 | 97 | 98 | # ## Node Search 99 | 100 | # Only show a filter if it matches this many or fewer nodes 101 | MAX_FILTER_SIZE = 10 102 | 103 | # An array of of [search string, node, label text] triples 104 | searchableNodes = [] 105 | appendSearchNode = (node$) -> 106 | text$ = node$.find('> .label .text') 107 | searchableNodes.push [text$.text().toLowerCase(), node$, text$] 108 | 109 | currentQuery = '' 110 | searchNodes = (queryString) -> 111 | queryString = queryString.toLowerCase().replace(/\s+/, '') 112 | return if queryString == currentQuery 113 | currentQuery = queryString 114 | 115 | return clearFilter() if queryString == '' 116 | 117 | matcher = new RegExp (c.replace /[-[\]{}()*+?.,\\^$|#\s]/, "\\$&" for c in queryString).join '.*' 118 | matched = [] 119 | filtered = [] 120 | 121 | for nodeInfo in searchableNodes 122 | if matcher.test nodeInfo[0] then matched.push nodeInfo else filtered.push nodeInfo 123 | 124 | return clearFilter() if matched.length > MAX_FILTER_SIZE 125 | 126 | nav$.addClass 'searching' 127 | 128 | # Update the DOM 129 | for nodeInfo in filtered 130 | nodeInfo[1].removeClass 'matched-child' 131 | nodeInfo[1].addClass 'filtered' 132 | clearHighlight nodeInfo[2] 133 | 134 | for nodeInfo in matched 135 | nodeInfo[1].removeClass 'filtered matched-child' 136 | nodeInfo[1].addClass 'matched' 137 | 138 | highlightMatch nodeInfo[2], queryString 139 | 140 | # Filter out our immediate parent 141 | $(p).addClass 'matched-child' for p in nodeInfo[1].parents 'li' 142 | 143 | clearFilter = -> 144 | nav$.removeClass 'searching' 145 | currentQuery = '' 146 | 147 | for nodeInfo in searchableNodes 148 | nodeInfo[1].removeClass 'filtered matched-child' 149 | clearHighlight nodeInfo[2] 150 | 151 | highlightMatch = (text$, queryString) -> 152 | nodeText = text$.text() 153 | lowerText = nodeText.toLowerCase() 154 | 155 | markedText = '' 156 | furthestIndex = 0 157 | 158 | for char in queryString 159 | foundIndex = lowerText.indexOf char, furthestIndex 160 | markedText += nodeText[furthestIndex...foundIndex] + "#{nodeText[foundIndex]}" 161 | furthestIndex = foundIndex + 1 162 | 163 | text$.html markedText + nodeText[furthestIndex...] 164 | 165 | clearHighlight = (text$) -> 166 | text$.text text$.text() # Strip all tags 167 | 168 | 169 | # ## DOM Construction 170 | # 171 | # Navigation and the table of contents are entirely managed by us. 172 | fileMap = {} # A map of targetPath -> DOM node 173 | 174 | buildNav = (metaInfo) -> 175 | nav$ = $(""" 176 | 186 | """).appendTo $('body') 187 | toc$ = nav$.find '.toc' 188 | 189 | if metaInfo.githubURL 190 | # Special case the index to go to the project root 191 | if metaInfo.documentPath == 'index' 192 | sourceURL = metaInfo.githubURL 193 | else 194 | sourceURL = "#{metaInfo.githubURL}/blob/master/#{metaInfo.projectPath}" 195 | 196 | nav$.find('.tools').prepend """ 197 |
  • 198 | 199 | View source on GitHub 200 | 201 |
  • 202 | """ 203 | 204 | for node in tableOfContents 205 | toc$.append buildTOCNode node, metaInfo 206 | 207 | nav$ 208 | 209 | buildTOCNode = (node, metaInfo) -> 210 | node$ = $("""
  • """) 211 | 212 | #} just to clarify: we use it in the `clickLabel`-method below, but can 213 | #} reference the first time after initializing it a few more lines below 214 | discloser = null 215 | 216 | switch node.type 217 | when 'file' 218 | #} Single line to avoid extra whitespace 219 | node$.append """#{node.data.title}""" 220 | clickLabel = (evt) -> 221 | if evt.target is discloser 222 | node$.toggleClass 'expanded' 223 | evt.preventDefault() 224 | return false 225 | selectNode node$ 226 | 227 | when 'folder' 228 | node$.append """#{node.data.title}""" 229 | clickLabel = (evt) -> 230 | selectNode node$ 231 | node$.toggleClass 'expanded' 232 | evt.preventDefault() 233 | return false 234 | 235 | if node.children?.length > 0 236 | children$ = $('
      ') 237 | children$.append buildTOCNode c, metaInfo for c in node.children 238 | 239 | node$.append children$ 240 | 241 | label$ = node$.find('> .label') 242 | label$.click clickLabel 243 | 244 | discloser$ = $('').prependTo label$ 245 | discloser$.addClass 'placeholder' unless node.children?.length > 0 246 | discloser = discloser$.get(0) 247 | 248 | # Persist our references to the node 249 | fileMap[node.data.targetPath] = node$ if node.type == 'file' 250 | appendSearchNode node$ 251 | 252 | node$ 253 | 254 | $ -> 255 | metaInfo = 256 | relativeRoot: $('meta[name="groc-relative-root"]').attr('content') 257 | githubURL: $('meta[name="groc-github-url"]').attr('content') 258 | documentPath: $('meta[name="groc-document-path"]').attr('content') 259 | projectPath: $('meta[name="groc-project-path"]').attr('content') 260 | 261 | nav$ = buildNav metaInfo 262 | toc$ = nav$.find '.toc' 263 | search$ = $('#search') 264 | 265 | # Select the current file, and expand up to it 266 | selectNodeByDocumentPath metaInfo.documentPath, window.location.hash.replace '#', '' 267 | 268 | # We use the search box's focus state to toggle the table of contents. This ensures that search 269 | # will always be focused while the toc is up, and that it goes away once the user clicks off. 270 | search$.focus -> setTableOfContentsActive true 271 | 272 | # However, we don't want to hide the table of contents if you are clicking around in the nav. 273 | # 274 | # The blur event doesn't give us the previous event, sadly, so we first trap mousedown events 275 | lastMousedownTimestamp = null 276 | nav$.mousedown (evt) -> 277 | lastMousedownTimestamp = evt.timeStamp unless evt.target == toggle$[0] 278 | 279 | # And we refocus search if we are within a very short duration between the last mousedown in nav$. 280 | search$.blur (evt) -> 281 | if evt.timeStamp - lastMousedownTimestamp < 10 282 | search$.focus() 283 | else 284 | setTableOfContentsActive false 285 | 286 | # Set up the table of contents toggle 287 | toggle$ = nav$.find '.toggle' 288 | toggle$.click (evt) -> 289 | if search$.is ':focus' then search$.blur() else search$.focus() 290 | evt.preventDefault() 291 | 292 | # Prevent text selection if the user taps quickly 293 | toggle$.mousedown (evt) -> 294 | evt.preventDefault() 295 | 296 | # Arrow keys navigate the table of contents whenever it is visible 297 | $('body').keydown (evt) -> 298 | if nav$.hasClass 'active' 299 | switch evt.keyCode 300 | when 13 then visitCurrentNode() # return 301 | when 37 then setCurrentNodeExpanded false # left 302 | when 38 then moveCurrentNode true # up 303 | when 39 then setCurrentNodeExpanded true # right 304 | when 40 then moveCurrentNode false # down 305 | else return 306 | 307 | evt.preventDefault() 308 | 309 | # searching 310 | search$.bind 'keyup search', (evt) -> 311 | searchNodes search$.val() 312 | 313 | search$.keydown (evt) -> 314 | if evt.keyCode == 27 # Esc 315 | if search$.val().trim() == '' 316 | search$.blur() 317 | else 318 | search$.val '' 319 | 320 | # Make folded code blocks toggleable; the marker and the code are clickable. 321 | $('.code.folded').each (index, code) -> 322 | code$ = $(code) 323 | code$.click (evt) -> 324 | code$.toggleClass 'folded' 325 | evt.preventDefault() 326 | return false 327 | -------------------------------------------------------------------------------- /lib/cli.coffee: -------------------------------------------------------------------------------- 1 | # # Command Line Interface 2 | 3 | childProcess = require 'child_process' 4 | fs = require 'fs' 5 | path = require 'path' 6 | 7 | glob = require 'glob' 8 | optimist = require 'optimist' 9 | 10 | CLIHelpers = require './utils/cli_helpers' 11 | Logger = require './utils/logger' 12 | PACKAGE_INFO = require '../package.json' 13 | Project = require './project' 14 | styles = require './styles' 15 | Utils = require './utils' 16 | 17 | 18 | # Readable command line output is just as important as readable documentation! It is the first 19 | # interaction that a developer will have with a tool like this, so we want to leave a good 20 | # impression with nicely formatted and readable command line output. 21 | module.exports = CLI = (inputArgs, callback) -> 22 | # In keeping with our console beautification project, make sure that our output isn't getting 23 | # too comfortable with the user's next shell line. 24 | actualCallback = callback 25 | callback = (args...) -> 26 | console.log '' 27 | 28 | actualCallback args... 29 | 30 | # We use [Optimist](https://github.com/substack/node-optimist) to parse our command line arguments 31 | # in a sane manner, and manage the myriad of options. 32 | opts = optimist inputArgs 33 | 34 | 35 | # ## CLI Overview 36 | 37 | # Readable command line output is just as important as readable documentation! It is the first 38 | # interaction that a developer will have with a tool like this, so we want to leave a good 39 | # impression with nicely formatted and readable output. 40 | opts 41 | .usage(""" 42 | Usage: groc [options] "lib/**/*.coffee" doc/*.md 43 | 44 | groc accepts lists of files and (quoted) glob expressions to match the files you would like to 45 | generate documentation for. Any unnamed options are shorthand for --glob arg. 46 | 47 | You can also specify arguments via a configuration file in the current directory named 48 | .groc.json. It should contain a mapping between option names and their values. For example: 49 | 50 | { "glob": ["lib", "vendor"], out: "documentation", strip: [] } 51 | """) 52 | 53 | 54 | # ## CLI Options 55 | 56 | optionsConfig = 57 | 58 | help: 59 | describe: "You're looking at it." 60 | alias: ['h', '?'] 61 | type: 'boolean' 62 | 63 | glob: 64 | describe: "A file path or globbing expression that matches files to generate documentation for." 65 | default: (opts) -> opts.argv._ 66 | type: 'list' 67 | 68 | except: 69 | describe: "Glob expression of files to exclude. Can be specified multiple times." 70 | alias: 'e' 71 | type: 'list' 72 | 73 | github: 74 | describe: "Generate your docs in the gh-pages branch of your git repository. --out is ignored." 75 | alias: 'gh' 76 | type: 'boolean' 77 | 78 | 'repository-url': 79 | describe: "Supply your GitHub repository URL (if groc fails to guess it)." 80 | type: 'string' 81 | 82 | 'only-render-newer': 83 | describe: "Only render files if the source is newer than the output." 84 | default: true 85 | 86 | out: 87 | describe: "The directory to place generated documentation, relative to the project root." 88 | alias: 'o' 89 | default: './doc' 90 | type: 'string' 91 | 92 | index: 93 | describe: "The file to use as the index of the generated documentation." 94 | alias: 'i' 95 | default: 'README.md' 96 | 97 | 'index-page-title': 98 | describe: "The index's page title in the generated documentation." 99 | default: 'index' 100 | 101 | root: 102 | describe: "The root directory of the project." 103 | alias: 'r' 104 | default: '.' 105 | type: 'path' 106 | 107 | style: 108 | describe: "The style to use when generating documentation." 109 | alias: 's' 110 | default: 'Default' 111 | 112 | highlighter: 113 | describe: "The highlighter to use. Either highlight.js (default) or pygments." 114 | alias: 'hl' 115 | default: 'highlight.js' 116 | 117 | strip: 118 | describe: "A path prefix to strip when generating documentation paths (or --no-strip)." 119 | alias: 't' 120 | 121 | 'empty-lines': 122 | describe: "Allow empty comment lines." 123 | default: true 124 | type: 'boolean' 125 | 126 | 'whitespace-after-token': 127 | describe: "Require whitespace after a comment token for a line to be considered a comment." 128 | default: true 129 | type: 'boolean' 130 | 131 | languages: 132 | describe: "Path to language definition file." 133 | default: "#{__dirname}/languages" 134 | type: 'path' 135 | 136 | silent: 137 | describe: "Output errors only." 138 | 139 | version: 140 | describe: "Shows you the current version of groc (#{PACKAGE_INFO.version})" 141 | alias: 'v' 142 | 143 | verbose: 144 | describe: "Output the inner workings of groc to help diagnose issues." 145 | 146 | 'very-verbose': 147 | describe: "Hey, you asked for it." 148 | 149 | 150 | # ## Argument processing 151 | 152 | # We treat the values within the current project's `.groc.json` as defaults, so that you can 153 | # easily override the persisted configuration when testing and tweaking. 154 | # 155 | # For example, if you have configured your `.groc.json` to include `"github": true`, it is 156 | # extremely helpful to use `groc --no-github` until you are satisfied with the generated output. 157 | projectConfigPath = path.resolve '.groc.json' 158 | try 159 | projectConfig = JSON.parse fs.readFileSync projectConfigPath 160 | catch err 161 | unless err.code == 'ENOENT' || err.code == 'EBADF' 162 | console.log opts.help() 163 | console.log 164 | Logger.error "Failed to load .groc.json: %s", err.message 165 | 166 | return callback err 167 | 168 | # We rely on [CLIHelpers.configureOptimist](utils/cli_helpers.html#configureoptimist) to provide 169 | # the extra options behavior that we require. 170 | CLIHelpers.configureOptimist opts, optionsConfig, projectConfig 171 | #} We have one special case that depends on other defaults... 172 | opts.default 'strip', Utils.guessStripPrefixes opts.argv.glob unless projectConfig?.strip? and opts.argv.glob? 173 | 174 | argv = CLIHelpers.extractArgv opts, optionsConfig 175 | # If we're in tracing mode, the parsed options are extremely helpful. 176 | Logger.trace 'argv: %j', argv if argv['very-verbose'] 177 | 178 | # Version checks short circuit before our pretty printing begins, since it is 179 | # one of those things that you might want to reference from other scripts. 180 | return console.log PACKAGE_INFO.version if argv.version 181 | 182 | # In keeping with our stance on readable output, we don't want it bumping up 183 | # against the shell execution lines and blurring together; use that whitespace 184 | # with great gusto! 185 | console.log '' 186 | 187 | return console.log opts.help() if argv.help 188 | 189 | # ## Project Generation 190 | 191 | # A [Project](project.html) is just a handy way to configure the generation process, and is in 192 | # charge of kicking that off. 193 | project = new Project argv.root, argv.out 194 | 195 | # `--silent`, `--verbose` and `--very-verbose` just impact the logging level of the project. 196 | project.log.minLevel = Logger::LEVELS.ERROR if argv.silent 197 | project.log.minLevel = Logger::LEVELS.DEBUG if argv.verbose 198 | project.log.minLevel = Logger::LEVELS.TRACE if argv['very-verbose'] 199 | 200 | # Set up project-specific options as we get them. 201 | project.options.allowEmptyLines = !!argv['empty-lines'] 202 | project.options.requireWhitespaceAfterToken = !!argv['whitespace-after-token'] 203 | project.options.showdown = argv.showdown 204 | project.options.languages = argv.languages 205 | project.options.highlighter = argv.highlighter 206 | 207 | # We expand the `--glob` expressions into a poor-man's set, so that we can easily remove 208 | # exclusions defined by `--except` before we add the result to the project's file list. 209 | files = {} 210 | for globExpression in argv.glob 211 | files[file] = true for file in glob.sync path.resolve(argv.root, globExpression) 212 | 213 | for globExpression in argv.except 214 | delete files[file] for file in glob.sync path.resolve(argv.root, globExpression) 215 | 216 | # There are several properties that we need to configure on a project before we can go ahead and 217 | # generate its documentation. 218 | project.index = path.resolve(argv.root, argv.index) 219 | project.files = (f for f of files) 220 | project.stripPrefixes = argv.strip 221 | 222 | # `Project#generate` can take some options, such as which style to use. Since we're generating 223 | # differently depending on whether or not github is enabled, let's set those up now: 224 | # If a style was passed in, but it isn't registered, try loading a module. 225 | unless argv.style? and (style = styles[argv.style])? 226 | try 227 | style = require(argv.style) require './styles/base' 228 | catch error 229 | 230 | options = 231 | indexPageTitle: argv['index-page-title'] 232 | onlyRenderNewer: argv['only-render-newer'] 233 | style: style 234 | 235 | # Good to go! 236 | unless argv.github 237 | project.githubURL = argv['repository-url'] 238 | 239 | project.generate options, (error) -> 240 | callback error 241 | 242 | # ## GitHub 243 | else 244 | # We want to be able to annotate generated documentation with the project's GitHub URL. This is 245 | # handy for things like generating links directly to each file's source. 246 | CLIHelpers.guessPrimaryGitHubURL argv['repository-url'], (error, url, remote) -> 247 | console.log "publish_to_github", error, url, remote 248 | 249 | if error 250 | project.log.error error.message 251 | return callback error 252 | 253 | project.githubURL = url 254 | 255 | # We hide the docs inside `.git/groc-tmp` so that we can switch branches without losing the 256 | # generated output. It also keeps us out of the business of finding an OS-sanctioned 257 | # temporary path. 258 | project.outPath = path.resolve path.join '.git', 'groc-tmp' 259 | 260 | # Dealing with generation for github pages is pretty involved, and requires a lot of back 261 | # and forth with git. Rather than descend into callback hell in Node, we farm the logic 262 | # out to a shell script. 263 | 264 | project.generate options, (error) -> 265 | return callback error if error 266 | 267 | project.log.info '' 268 | project.log.info 'Publishing documentation to github...' 269 | 270 | # Roughly, the publishing script: 271 | # 272 | # 1. Switches to the `gh-pages` branch (creating it if necessary) 273 | # 2. Copies the generated docs from `.git/groc-tmp` over any existing files in the branch. 274 | # 3. Creates a commit with _just_ the generated docs; any additional files are removed. 275 | # 4. Cleans up and switches back to the user's original branch. 276 | script = childProcess.spawn path.resolve(__dirname, '..', 'scripts', 'publish-git-pages.sh'), [remote, projectConfig.commitMessage] 277 | 278 | script.stdout.on 'data', (data) -> project.log.info data.toString().trim() 279 | script.stderr.on 'data', (data) -> project.log.error data.toString().trim() 280 | 281 | script.on 'exit', (code) -> 282 | return callback new Error 'Git publish failed' if code != 0 283 | 284 | callback() 285 | -------------------------------------------------------------------------------- /lib/styles/default/style.sass: -------------------------------------------------------------------------------- 1 | @import "compass" 2 | @import "codestyles" 3 | 4 | @import "compass/reset" 5 | 6 | 7 | // ## Configuration 8 | 9 | // ### Sizing 10 | 11 | // * Margin between segments in two-column mode, padding for code/comments in single-column mode. 12 | $common-margin: 1em 13 | // * Additional padding to each column in two-column mode 14 | $column-padding: 1em 15 | // * The `line length` of the commentary column in both modes. 16 | $commentary-width: 29em 17 | // * Width of the tools pulldown in two-column mode. NOT scaled by tools-font. 18 | $tools-width: 20em 19 | // * Height of the main toolbar, scaled by the size of tools-font 20 | $tools-height: 2.1em 21 | 22 | // Smooth-scale the page in viewports that are between ideal widths for two-column and one-column 23 | // modes. Scaling starts at the same width as $tools-width into the code column. 24 | $scale-steps: 136 // Approx 1 per pixel on a desktop (8.5em distance & 16px per em) 25 | $scale-min: 0.85 26 | 27 | // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam cursus lorem at mi hendrerit pretium. Maecenas ultrices dolor dictum orci lobortis fringilla. Integer ut sem at libero scelerisque commodo quis a dui. Nulla pellentesque, purus non ultricies porta, diam est fringilla orci, dignissim mattis leo erat a urna. Donec fermentum orci nec magna vehicula consectetur. Vivamus a dui eget orci malesuada elementum. Cras tempor mi sit amet ligula mollis porta. Nulla eu purus eget dui aliquet iaculis in quis massa. Phasellus sed lectus ac augue tincidunt blandit. Pellentesque et aliquam neque. Donec faucibus ultricies sem, ut mattis ante consectetur sed. In sollicitudin consectetur nisl, nec malesuada diam dictum ut. Duis feugiat, lorem in pulvinar interdum, nibh turpis lobortis erat, dictum hendrerit massa nibh molestie odio. Ut ultricies, dui a lobortis aliquam, massa diam tempus eros, eu ornare risus mi vitae elit. Integer pretium vulputate eros vitae luctus. Mauris in rhoncus nisi. 28 | 29 | // ### Color Scheme 30 | 31 | $commentary-background-color: #f5fbff 32 | $commentary-foreground-color: #4a525a // Thank you, Mr. Gruber 33 | $commentary-shadow-color: #ffffff 34 | 35 | $code-background-color: #4a525a 36 | $code-foreground-color: #cbd1d8 37 | $code-shadow-color: darken($code-background-color, 15%) 38 | 39 | $link-color: #a8614e 40 | $link-hover-foreground-color: #a8614e 41 | $link-hover-background-color: lighten(#a8614e, 43%) 42 | $link-hover-shadow-color: lighten(#a8614e, 60%) 43 | 44 | $tools-background-color: transparentize(#e6e6e6, 0.1) 45 | $tools-foreground-color: #4a525a 46 | $tools-shadow-color: #f0f0f0 47 | 48 | $inline-code-background-color: #fbf8f3 49 | 50 | // ### Typography 51 | 52 | =commentary-font 53 | font-family: "Helvetica Neue", Helvetica, "Droid Sans", sans-serif 54 | font-weight: 300 55 | font-size: 0.9375em 56 | line-height: 1.35 57 | 58 | =code-font 59 | font-family: "Droid Sans Mono", Menlo, Monaco, monospace 60 | font-size: 0.75em 61 | line-height: 1.4 62 | 63 | =header-font 64 | font-family: "HelveticaNeue-UltraLight", "Helvetica Neue", Helvetica, "Droid Sans", sans-serif 65 | font-weight: 100 66 | letter-spacing: 0.0625em 67 | line-height: 1.25 68 | margin-bottom: 0.5em 69 | 70 | =tools-font 71 | +commentary-font 72 | 73 | =header-font-sizes 74 | h1 75 | font-size: 2.5em 76 | h2 77 | font-size: 2em 78 | h3 79 | font-size: 1.6em 80 | h4 81 | font-size: 1.4em 82 | h5 83 | font-size: 1.3em 84 | h6 85 | font-size: 1.2em 86 | 87 | 88 | // ## Two-Column Layout 89 | 90 | // As a general tenet, we try to avoid using margins, as they get us into trouble. Pparticularly, 91 | // when ensuring that our `html`/`body`/`#document` are full height and do not induce scrolling 92 | // unless their content grows past the page. 93 | $commentary-column-width: $commentary-width + ($common-margin + $column-padding) * 2 94 | 95 | html, body 96 | height: 100% 97 | 98 | #document 99 | min-height: 100% 100 | 101 | // Conceptually, the layout focuses the user on the commentary first, so we lay out that column. 102 | body 103 | max-width: $commentary-column-width 104 | 105 | // In two-column mode, the segment is actually shifted over a full column to favor code fragments. 106 | // We may or may not have either .comment or .code sections, so we need to handle all three cases. 107 | .segment 108 | padding: ($common-margin / 2) 0 ($common-margin / 2) $commentary-column-width 109 | white-space: nowrap 110 | 111 | &:first-child 112 | padding-top: $common-margin + $column-padding + $tools-height 113 | 114 | &:last-child 115 | padding-bottom: $common-margin + $column-padding 116 | 117 | .comments, .code 118 | display: inline-block 119 | vertical-align: top 120 | padding: 0 ($common-margin + $column-padding) 121 | 122 | .comments 123 | margin-left: -$commentary-column-width 124 | width: $commentary-width 125 | white-space: normal 126 | 127 | .code 128 | white-space: pre 129 | 130 | // Fit the meta info into the code column 131 | #meta 132 | position: absolute 133 | left: $commentary-column-width 134 | padding: $common-margin / 4 $common-margin 135 | 136 | 137 | // ## Two-Column Scaling 138 | 139 | // Before we swap over to a single column layout, we scale the page down a little bit in an effort 140 | // to squeeze some extra life out of it. 141 | // 142 | // Scaling starts once the toolbar is flush with the main column 143 | $scale-start-width: $commentary-column-width + $tools-width 144 | $scale-stop-width: $scale-start-width * $scale-min 145 | 146 | @for $i from 0 through $scale-steps - 2 // Skip the last step; it's exactly $scale-stop-width 147 | $scale: 1.0 - (1.0 - $scale-min) * ($i / ($scale-steps - 1)) 148 | @media (max-width: #{$scale-start-width * $scale}) 149 | html 150 | font-size: 1em * $scale 151 | 152 | 153 | // ## Single-Column Layout 154 | 155 | // We swap over to the single column layout once we hit the edge of the scaling 156 | $single-column-toggle-width: $scale-stop-width 157 | 158 | // Switch modes at a little wider than a full (padded) column 159 | @media (max-width: #{$single-column-toggle-width}) 160 | 161 | html 162 | font-size: 1em 163 | 164 | body 165 | margin: 0 auto 166 | 167 | .segment 168 | padding: 0 169 | white-space: normal 170 | max-width: $commentary-width 171 | margin: 0 auto 172 | 173 | .comments, .code 174 | display: block 175 | padding: $common-margin 176 | 177 | .comments 178 | margin-left: 0 179 | width: auto 180 | 181 | .code 182 | display: block 183 | overflow-y: hidden 184 | overflow-x: auto 185 | 186 | .wrapper 187 | display: inline-block 188 | 189 | // Fit the meta info into on screen, removing any overflow it creates 190 | #meta 191 | position: static 192 | margin: $common-margin * 2 0 0 0 193 | overflow-y: hidden 194 | overflow-x: auto 195 | 196 | .file-path 197 | display: inline-block 198 | 199 | // ## Navigation 200 | $tools-padding: ($tools-height - 1em) / 2 201 | 202 | nav 203 | position: fixed 204 | top: 0 205 | right: 0 206 | width: $tools-width 207 | 208 | // Nav should be full width in single column mode 209 | @media (max-width: #{$single-column-toggle-width}) 210 | left: 0 211 | width: 100% 212 | 213 | .tools 214 | position: relative 215 | z-index: 100 216 | 217 | li 218 | display: table-cell 219 | vertical-align: middle 220 | text-align: center 221 | white-space: nowrap 222 | height: $tools-height 223 | padding: 0 $tools-padding 224 | 225 | .github 226 | padding: 0 227 | 228 | a 229 | display: block 230 | height: $tools-height 231 | width: $tools-height 232 | text-indent: -9001em 233 | 234 | .search 235 | width: 100% 236 | 237 | input 238 | +box-sizing(border-box) 239 | display: block 240 | width: 100% 241 | 242 | .toc 243 | +box-sizing(border-box) 244 | position: absolute 245 | top: $tools-height 246 | bottom: 0 247 | width: 100% 248 | overflow-x: hidden 249 | overflow-y: auto 250 | 251 | li 252 | position: relative 253 | 254 | .label 255 | display: block 256 | line-height: 2em 257 | padding: 0 $tools-padding 0 $tools-padding 258 | 259 | li li .label 260 | padding-left: $tools-padding * 2 261 | li li li .label 262 | padding-left: $tools-padding * 3 263 | li li li li .label 264 | padding-left: $tools-padding * 4 265 | li li li li li .label 266 | padding-left: $tools-padding * 5 267 | li li li li li li .label 268 | padding-left: $tools-padding * 6 269 | 270 | // ## Behavior 271 | $navigation-shift-duration: 150ms 272 | 273 | // In either mode, we need to expand the nav's height when showing the table of contents (we don't 274 | // want a blank div getting in the way of interaction) 275 | nav 276 | +transition(height 0 $navigation-shift-duration) 277 | 278 | .tools .toggle 279 | +transition(background $navigation-shift-duration) 280 | 281 | &.active 282 | +transition(height 0 0) 283 | height: 100% 284 | 285 | // In two-column mode, we slide the table of contents in from the right 286 | nav .toc 287 | +transition(right $navigation-shift-duration) 288 | right: -100% 289 | 290 | nav.active .toc 291 | right: 0 292 | 293 | // In single column mode, we slide in from the left (and also pop the body) 294 | @media (max-width: #{$single-column-toggle-width}) 295 | nav .toc 296 | +transition(left $navigation-shift-duration) 297 | right: auto 298 | left: -100% 299 | 300 | nav.active .toc 301 | left: 0 302 | 303 | // When we switch to the navigation in single column mode, "pop" the body off by sliding it off 304 | // screen to the right. 305 | @media (max-width: #{$single-column-toggle-width}) 306 | body 307 | +transition(left $navigation-shift-duration) 308 | position: relative 309 | left: 0 310 | 311 | html.popped 312 | overflow: hidden 313 | 314 | body 315 | left: 100% 316 | overflow: hidden 317 | 318 | nav .toc 319 | // By default, all file lists are collapsed. 320 | .children, .outline 321 | display: none 322 | 323 | // We only expand direct descendants for files, but outlines are fully expanded from the outset. 324 | .expanded > .children, .expanded > .outline, .expanded > .outline .children 325 | display: block 326 | 327 | .discloser 328 | +transition-property(-moz-transform -webkit-transform -o-transform transform) // bah. 329 | +transition-duration(200ms) 330 | +rotate(0deg) 331 | display: inline-block 332 | height: 9px 333 | width: 9px 334 | padding: 0.2em 335 | margin: 0.2em 0.2em -0.2em 0.2em 336 | vertical-align: baseline 337 | background: inline-image('disclosure-indicator.png') center center no-repeat 338 | background-size: 9px 9px 339 | 340 | // Every label gets one, but not all need to display them. 341 | .discloser.placeholder, .expanded > .outline .discloser 342 | background: none 343 | 344 | .expanded > .label .discloser 345 | +rotate(90deg) 346 | 347 | // When searching, we only hide labels so that the hierarchy can still be shown to the user 348 | .filtered > .label 349 | display: none 350 | 351 | .matched-child > .label 352 | display: block 353 | 354 | // Make sure that we expand to show any matched nodes 355 | .matched-child > .children, .matched-child > .outline, .matched-child > .outline .children 356 | display: block 357 | 358 | .matched > .children, .matched > .outline, .matched > .outline .children 359 | display: block 360 | 361 | // Disclosure indicators are confusing when we're searching 362 | nav.searching .toc .discloser 363 | display: none 364 | 365 | 366 | // ## Typography 367 | 368 | .comments .wrapper 369 | +commentary-font 370 | 371 | h1, h2, h3, h4, h5, h6 372 | +header-font 373 | +header-font-sizes 374 | 375 | p 376 | margin: 1em 0 377 | 378 | > * 379 | &:first-child 380 | margin-top: 0 381 | 382 | &:last-child 383 | margin-bottom: 0 384 | 385 | ol, ul 386 | padding-left: 1.75em 387 | margin: 1em 0 388 | 389 | ol li 390 | list-style: decimal 391 | 392 | ul li 393 | list-style: disc 394 | 395 | li 396 | margin: 1em 0 397 | 398 | &:first-child 399 | margin-top: 0 400 | &:last-child 401 | margin-bottom: 0 402 | 403 | code 404 | display: inline-block 405 | padding: 0.25em 0.25em 0 0.25em 406 | 407 | pre 408 | display: block 409 | overflow-x: auto 410 | overflow-y: hidden 411 | margin-bottom: 1em 412 | +inline-codestyles 413 | 414 | code 415 | padding: $common-margin 416 | 417 | blockquote 418 | padding: 0 $common-margin 419 | 420 | strong 421 | font-weight: 700 422 | 423 | em 424 | font-style: italic 425 | 426 | // ## Visual Design 427 | 428 | // We want a bit of depth between the columns, so make the code appear inset 429 | $code-column-shadow-width: 1em 430 | $background-gradient: linear-gradient(left, darken($code-background-color, 15%), darken($code-background-color, 5%) 0.3 * $code-column-shadow-width, $code-background-color $code-column-shadow-width) 431 | 432 | html 433 | background: $code-background-color // Degrade gracefully 434 | 435 | // We cheat and use #document to display the shadow 436 | #document 437 | +background($commentary-background-color $background-gradient $commentary-column-width no-repeat) 438 | margin-right: -$code-column-shadow-width 439 | padding-right: $code-column-shadow-width 440 | 441 | @media (max-width: #{$single-column-toggle-width}) 442 | margin-right: 0 443 | padding-right: 0 444 | 445 | #meta > * 446 | +commentary-font 447 | +text-shadow($code-shadow-color 1px 1px 0) 448 | 449 | &, a 450 | color: darken($code-foreground-color, 15%) 451 | 452 | a 453 | text-decoration: none 454 | 455 | // We apply font changes to .wrapper elements so that we do not resize the 'standard' em unit that 456 | // we are relying on for layout. 457 | .comments .wrapper 458 | +commentary-font 459 | +text-shadow($commentary-shadow-color 1px 1px 0) 460 | color: $commentary-foreground-color 461 | 462 | .code 463 | .wrapper 464 | +codestyles 465 | +code-font 466 | +text-shadow($code-shadow-color 1px 1px 0) 467 | color: $code-foreground-color 468 | 469 | @media (max-width: #{$single-column-toggle-width}) 470 | +border-radius(0.4em) 471 | +box-shadow(darken($code-background-color, 15%) 0 0 0.5em 0.2em inset) 472 | background: $code-background-color 473 | 474 | .wrapper 475 | +box-shadow($code-background-color 0 0 0.25em 0.75em) 476 | background: $code-background-color 477 | 478 | @media (max-width: #{$commentary-width}) 479 | +border-radius(0) 480 | 481 | nav 482 | +text-shadow($tools-shadow-color 1px 1px 0) 483 | color: $tools-foreground-color 484 | 485 | .tools, .toc 486 | +tools-font 487 | 488 | .tools 489 | +box-shadow(rgba(0,0,0,0.3) 0 0 0.5em 0.1em) 490 | +background(linear-gradient(top, lighten($tools-background-color, 10%), darken($tools-background-color, 10%))) 491 | +border-bottom-left-radius(0.4em) 492 | border-bottom: 1px solid $tools-foreground-color 493 | border-left: 1px solid $tools-foreground-color 494 | 495 | @media (max-width: #{$scale-start-width}) 496 | +border-bottom-left-radius(0) 497 | 498 | li 499 | border-right: 1px solid $tools-foreground-color 500 | 501 | &:last-child 502 | border-right: none 503 | 504 | .toggle 505 | cursor: pointer 506 | 507 | .github a 508 | +transition(opacity 200ms) 509 | +opacity(0.5) 510 | background: inline-image('github-icon.png') center center no-repeat 511 | background-size: 19.5px 24px 512 | 513 | &:hover 514 | +opacity(0.9) 515 | 516 | &.active .tools 517 | +border-bottom-left-radius(0) 518 | 519 | .toggle 520 | background: darken($tools-background-color, 10%) 521 | position: relative 522 | 523 | .toc 524 | +box-shadow(rgba(0,0,0,0.3) 0 0 0.5em 0.1em) 525 | background: $tools-background-color 526 | border-left: 1px solid $tools-foreground-color 527 | 528 | .label 529 | color: $tools-foreground-color 530 | text-decoration: none 531 | border-top: 1px solid darken($tools-background-color, 15%) 532 | border-bottom: 1px solid darken($tools-background-color, 15%) 533 | margin-top: -1px 534 | 535 | &:hover 536 | background: darken($tools-background-color, 10%) 537 | 538 | .file > .label 539 | font-weight: bold 540 | 541 | .selected > .label 542 | background: $commentary-background-color 543 | 544 | // Search styling 545 | .label em 546 | font-weight: bold 547 | 548 | // This isn't the best of signals... 549 | .file > .label em 550 | color: darken($tools-foreground-color, 25%) 551 | 552 | .matched-child > .label 553 | +opacity(0.65) 554 | +text-shadow(none) 555 | background: darken($tools-background-color, 15%) 556 | 557 | @media (max-width: #{$single-column-toggle-width}) 558 | .tools, .toc 559 | border-left-width: 0 560 | 561 | // Make sure that our tools are opaque in single column mode so that the transitions aren't 562 | // distracting 563 | $opaque-tools-background-color: opacify($tools-background-color, 1.0) 564 | 565 | .tools 566 | +background(linear-gradient(top, lighten($opaque-tools-background-color, 10%), darken($opaque-tools-background-color, 10%))) 567 | 568 | .toc 569 | background: $opaque-tools-background-color 570 | 571 | // ### Commentary Elements 572 | 573 | .comments .wrapper 574 | a 575 | display: inline-block 576 | color: $link-color 577 | text-decoration: none 578 | 579 | &:hover, &:hover * 580 | text-decoration: underline 581 | 582 | code 583 | +code-font 584 | border: 1px solid darken(desaturate($inline-code-background-color, 25%), 10%) 585 | 586 | pre, code 587 | +border-radius(0.4em) 588 | background: $inline-code-background-color 589 | 590 | pre 591 | +box-shadow(darken(desaturate($inline-code-background-color, 15%), 5%) 0 0 0.4em 0.2em) 592 | border: 1px solid darken(desaturate($inline-code-background-color, 15%), 20%) 593 | 594 | code 595 | border-width: 0 596 | background: transparent 597 | 598 | blockquote 599 | $blockquote-border-color: lighten($commentary-foreground-color, 30%) 600 | border-left: 0.15em solid $blockquote-border-color 601 | margin-left: -0.15em 602 | 603 | 604 | // ## Browser-Specific Tweaks 605 | 606 | // Don't allow mobile WebKit clients to mess with the font size 607 | body 608 | -webkit-text-size-adjust: 100% 609 | 610 | // Replicate the WebKit type="search" appearance in other browsers if they don't have their own 611 | // search field styling 612 | input[type="search"] 613 | +border-radius(1em) 614 | +box-shadow(#dddddd 0 1px 1px 0 inset) 615 | border: 1px solid #959595 616 | padding: 0.15em 0.8em 617 | 618 | // ### Doc Tags 619 | .comments.doc-section 620 | //&.doc-section-public, 621 | //&.doc-section-static 622 | // .wrapper 623 | // color: #252519 624 | 625 | .wrapper 626 | color: #252519 627 | 628 | &.doc-section-private, 629 | &.doc-section-protected, 630 | &.doc-section-internal 631 | .wrapper 632 | color: #7f7f7f 633 | 634 | .doc-section-header 635 | font: bold 18px 'helvetica neue', helvetica, sans-serif 636 | 637 | .docs .doc-section-header code 638 | font-size: 18px 639 | 640 | // ## Code folding 641 | 642 | .code .marker, .code .marker.wrapper, .code .wrapper.marker 643 | display: none 644 | 645 | .code.folded 646 | .wrapper 647 | display: none 648 | cursor: default 649 | 650 | .marker 651 | +border-radius(0.2em) 652 | +box-shadow(#2f3539 1px 1px 1px 0) 653 | display: inline-block 654 | border: 1px solid #73787f 655 | padding: 0.2em 0.5em 656 | margin-left: -0.5em 657 | margin-right: -0.5em 658 | background: #58616b 659 | font: 12px 'Droid Sans Mono', Menlo, Monaco, monospace 660 | text-shadow: #2f3539 1px 1px 0px 661 | cursor: pointer 662 | -webkit-touch-callout: none 663 | -webkit-user-select: none 664 | -khtml-user-select: none 665 | -moz-user-select: -moz-none 666 | -ms-user-select: none 667 | user-select: none 668 | 669 | .c1 670 | color: #73787f 671 | font-style: normal 672 | 673 | .marker:hover 674 | background: #5f6872 675 | 676 | .c1 677 | color: #7b8087 678 | 679 | .marker .c1:after 680 | content: " …" 681 | -------------------------------------------------------------------------------- /lib/utils.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Miscellaneous code fragments reside here. 3 | 4 | TODO: should be migrated into `lib/utils`. 5 | ### 6 | 7 | childProcess = require 'child_process' 8 | path = require 'path' 9 | 10 | _ = require 'underscore' 11 | hljs = require 'highlight.js' 12 | marked = require 'marked' 13 | 14 | marked.setOptions 15 | highlight: (code, lang) -> 16 | if lang 17 | try 18 | return hljs.highlight(lang, code, true).value 19 | catch e 20 | return code 21 | return code 22 | 23 | CompatibilityHelpers = require './utils/compatibility_helpers' 24 | LANGUAGES = null 25 | DOC_TAGS = require './doc_tags' 26 | Logger = require './utils/logger' 27 | 28 | 29 | module.exports = Utils = 30 | 31 | # Code from 32 | # via 33 | regexpEscapePattern : /[-[\]{}()*+?.,\\^$|#\s]/g 34 | regexpEscapeReplace : '\\$&' 35 | 36 | # Escape regular expression characters in a String, an Array of Strings or 37 | # any Object having a proper toString-method 38 | regexpEscape: (obj) -> 39 | if _.isArray obj 40 | _.invoke(obj, 'replace', @regexpEscapePattern, @regexpEscapeReplace) 41 | else if _.isString obj 42 | obj.replace(@regexpEscapePattern, @regexpEscapeReplace) 43 | else 44 | @regexpEscape "#{obj}" 45 | 46 | # Detect and return the language that a given file is written in. 47 | # 48 | # The language is also annotated with a name property, matching the language's 49 | # key in LANGUAGES. 50 | getLanguage: (filePath, languageDefinitions = './languages') -> 51 | unless @_languageDetectionCache? 52 | @_languageDetectionCache = [] 53 | 54 | LANGUAGES = require(languageDefinitions) if not LANGUAGES? 55 | 56 | for name, language of LANGUAGES 57 | language.name = name 58 | 59 | for matcher in language.nameMatchers 60 | # If the matcher is a string, we assume that it's a file extension. 61 | # Stick it in a regex: 62 | matcher = ///#{@regexpEscape matcher}$/// if _.isString matcher 63 | 64 | @_languageDetectionCache.push [matcher, language] 65 | 66 | baseName = path.basename filePath 67 | 68 | for pair in @_languageDetectionCache 69 | return pair[1] if baseName.match pair[0] 70 | 71 | # Map a list of file paths to relative target paths by stripping prefixes. 72 | mapFiles: (resolveRoot, files, stripPrefixes) -> 73 | # Ensure that we're dealing with absolute paths across the board. 74 | files = files.map (f) -> path.resolve resolveRoot, f 75 | 76 | # And that the strip prefixes all end with a /, avoids absolute target path. 77 | stripPrefixes = stripPrefixes.map (p) -> 78 | path.join "#{path.resolve resolveRoot, p}#{CompatibilityHelpers.pathSep}" 79 | 80 | # Prefixes are stripped in the order of most specific to least 81 | # (# of directories deep) 82 | prefixes = stripPrefixes.sort (a,b) => @pathDepth(b) - @pathDepth(a) 83 | 84 | result = {} 85 | 86 | for absPath in files 87 | file = absPath 88 | 89 | for stripPath in stripPrefixes 90 | if file[0...stripPath.length] is stripPath 91 | file = file[stripPath.length..] 92 | 93 | # We also strip the extension under the assumption that the consumer of 94 | # this path map is going to substitute in their own. Plus, if they care 95 | # about the extension, they can get it from the keys of the map. 96 | result[absPath] = if not path.extname(file) then file else file[0...-path.extname(file).length] 97 | 98 | result 99 | 100 | # Attempt to guess strip prefixes for a given set of arguments. 101 | guessStripPrefixes: (args) -> 102 | result = [] 103 | for arg in args 104 | # Most globs look something like dir/**/*.ext, so strip up to the leading * 105 | arg = arg.replace /\*.*$/, '' 106 | 107 | result.push arg if arg.slice(-1) == CompatibilityHelpers.pathSep 108 | 109 | # For now, we try to avoid ambiguous situations by guessing the FIRST 110 | # directory given. The assumption is that you don't want merged paths, 111 | # but probably did specify the most important source directory first. 112 | result = _(result).uniq()[...1] 113 | 114 | # How many directories deep is a given path? 115 | pathDepth: (path) -> 116 | path.split(/[\/\\]/).length 117 | 118 | # Split source code into segments (comment + code pairs) 119 | splitSource: (data, language, options={}) -> 120 | lines = data.split /\r?\n/ 121 | 122 | # Always strip shebangs - but don't shift it off the array to 123 | # avoid the perf hit of walking the array to update indices. 124 | lines[0] = '' if lines[0][0..1] is '#!' 125 | 126 | # Special case: If the language is comments-only, we can skip pygments 127 | return [new @Segment [], lines] if language.commentsOnly 128 | 129 | # Special case: If the language is code-only, we can shorten the process 130 | return [new @Segment lines, []] if language.codeOnly 131 | 132 | segments = [] 133 | currSegment = new @Segment 134 | 135 | # Enforced whitespace after the comment token 136 | whitespaceMatch = if options.requireWhitespaceAfterToken then '\\s' else '\\s?' 137 | 138 | if language.singleLineComment? 139 | singleLines = @regexpEscape(language.singleLineComment).join '|' 140 | aSingleLine = /// 141 | ^\s* # Start a line and skip all indention. 142 | (?:#{singleLines}) # Match the single-line start but don't capture this group. 143 | (?: # Also don't capture this group … 144 | #{whitespaceMatch} # … possibly starting with a whitespace, but 145 | (.*) # … capture anything else in this … 146 | )? # … optional group … 147 | $ # … up to the EOL. 148 | /// 149 | 150 | 151 | if language.multiLineComment? 152 | mlc = language.multiLineComment 153 | 154 | unless (mlc.length % 3) is 0 155 | throw new Error('Multi-line block-comment definitions must be a list of 3-tuples') 156 | 157 | blockStarts = _.select mlc, (v, i) -> i % 3 == 0 158 | blockLines = _.select mlc, (v, i) -> i % 3 == 1 159 | blockEnds = _.select mlc, (v, i) -> i % 3 == 2 160 | 161 | # This flag indicates if the end-mark of block-comments (the `blockEnds` 162 | # list above) must correspond to the initial block-mark (the `blockStarts` 163 | # above). If this flag is missing it defaults to `true`. The main idea 164 | # is to embed sample block-comments with syntax A in another block-comment 165 | # with syntax B. This useful in handlebar's mixed syntax or other language 166 | # combinations like html+php, which are supported by `pygmentize`. 167 | strictMultiLineEnd = language.strictMultiLineEnd ? true 168 | 169 | # This map is used to lookup corresponding line- and end-marks. 170 | blockComments = {} 171 | for v, i in blockStarts 172 | blockComments[v] = 173 | linemark: blockLines[i] 174 | endmark : blockEnds[i] 175 | 176 | blockStarts = @regexpEscape(blockStarts).join '|' 177 | blockLines = @regexpEscape(blockLines).join '|' 178 | blockEnds = @regexpEscape(blockEnds).join '|' 179 | 180 | # No need to match for any particular real content in `aBlockStart`, as 181 | # either `aBlockLine`, `aBlockEnd` or the `inBlock` catch-all fallback 182 | # handles the real content, in the implementation below. 183 | aBlockStart = /// 184 | ^(\s*) # Start a line and capture indention, used to reverse indent catch-all fallback lines. 185 | (#{blockStarts}) # Capture the start-mark, to check the if line- and end-marks correspond, … 186 | (#{blockLines})? # … possibly followed by a line, captured to check if its corresponding to the start, 187 | (?:#{whitespaceMatch}|$) # … and finished by whitespace OR the EOL. 188 | /// 189 | 190 | aBlockLine = /// 191 | ^\s* # Start a line and skip all indention. 192 | (#{blockLines}) # Capture the line-mark to check if it corresponds to the start-mark, … 193 | (#{whitespaceMatch}) # … possibly followed by whitespace, 194 | (.*)$ # … and collect all up to the line end. 195 | /// 196 | 197 | aBlockEnd = /// 198 | (#{blockEnds}) # Capture the end-mark to check if it corresponds to the line start, 199 | (.*)?$ # … and collect all up to the line end. 200 | /// 201 | 202 | ### 203 | # A special case used to capture empty block-comment lines, like the one 204 | # below this line … 205 | # 206 | # … and above this line. 207 | ### 208 | aEmptyLine = ///^\s*(?:#{blockLines})$/// 209 | 210 | if language.ignorePrefix? 211 | {ignorePrefix} = language 212 | 213 | if language.foldPrefix? 214 | {foldPrefix} = language 215 | 216 | if (ignorePrefix? or foldPrefix?) and (singleLines? or blockStarts?) 217 | stripMarks = [] 218 | stripMarks.push ignorePrefix if ignorePrefix? 219 | stripMarks.push foldPrefix if foldPrefix? 220 | stripMarks = @regexpEscape(stripMarks).join '|' 221 | 222 | # Strip final space only if one is required, hence yet present. 223 | stripSpace = if options.requireWhitespaceAfterToken then '(?:\\s)?' else '' 224 | 225 | # A dirty lap-dance performed here … 226 | singleStrip = /// 227 | ( # Capture this group: 228 | (?:#{singleLines}) # The comment marker(s) to keep … 229 | #{whitespaceMatch} # … plus whitespace 230 | ) 231 | (?:#{stripMarks}) # The marker(s) to strip from result 232 | #{stripSpace} # … plus an optional whitespace. 233 | /// if singleLines? 234 | 235 | # … and the corresponding gang-bang here. 8-) 236 | blockStrip = /// 237 | ( # Capture this group: 238 | (?:#{blockStarts}) # The comment marker(s) to keep … 239 | (?:#{blockLines})? # … optionally plus one more mark 240 | #{whitespaceMatch} # … plus whitespace 241 | ) 242 | (?:#{stripMarks}) # The marker(s) to strip from result 243 | #{stripSpace} # … plus an optional whitespace. 244 | /// if blockStarts? 245 | 246 | inBlock = false 247 | inFolded = false 248 | inIgnored = false 249 | 250 | # Variables used in temporary assignments have been collected here for 251 | # documentation purposes only. 252 | blockline = null 253 | blockmark = null 254 | linemark = null 255 | space = null 256 | endmark = null 257 | indention = null 258 | comment = null 259 | code = null 260 | 261 | for line in lines 262 | 263 | # Match that line to the language's block-comment syntax, if it exists 264 | if aBlockStart? and not inBlock and (match = line.match aBlockStart)? 265 | inBlock = true 266 | 267 | # Reusing `match` as a placeholder. 268 | [match, indention, blockmark, linemark] = match 269 | 270 | # Strip the block-comments start, preserving any inline stuff. 271 | # We don't touch the `line` itself, as we still need it. 272 | blockline = line.replace aBlockStart, '' 273 | 274 | # If we found a `linemark`, prepend it (back) to the `blockline`, if it 275 | # does not correspond to the initial `blockmark`. 276 | if linemark? and blockComments[blockmark].linemark isnt linemark 277 | blockline = "#{linemark}#{blockline}" 278 | 279 | # Check if this block-comment is collapsible. 280 | if foldPrefix? and blockline.indexOf(foldPrefix) is 0 281 | 282 | # We always start a new segment if the current one is not empty or 283 | # already folded. 284 | if inFolded or currSegment.code.length > 0 285 | segments.push currSegment 286 | currSegment = new @Segment 287 | 288 | ### ^ collapsing block-comments: 289 | # In block-comments only `aBlockStart` may initiate the collapsing. 290 | # This comment utilizes this syntax, by starting the comment with `^`. 291 | ### 292 | inFolded = true 293 | 294 | # Let's strip the “^” character from our original line, for later use. 295 | line = line.replace blockStrip, '$1' 296 | # Also strip it from our `blockline`. 297 | blockline = blockline[foldPrefix.length...] 298 | 299 | # Check if this block-comment stays embedded in the code. 300 | else if ignorePrefix? and blockline.indexOf(ignorePrefix) is 0 301 | ### } embedded block-comments: 302 | # In block-comments only `aBlockStart` may initiate the embedding. 303 | # This comment utilizes this syntax, by starting the comment with `}`. 304 | ### 305 | inIgnored = true 306 | 307 | # Let's strip the “}” character from our original line, for later use. 308 | line = line.replace blockStrip, '$1' 309 | # Also strip it from our `blockline`. 310 | blockline = blockline[ignorePrefix.length...] 311 | 312 | # Block-comments are an important tool to structure code into larger 313 | # segments, therefore we always start a new segment if the current one 314 | # is not empty. 315 | else if currSegment.code.length > 0 316 | segments.push currSegment 317 | currSegment = new @Segment 318 | inFolded = false 319 | 320 | # This flag is triggered above. 321 | if inBlock 322 | 323 | # Catch all lines, unless there is a `blockline` from above. 324 | blockline = line unless blockline? 325 | 326 | # Match a block-comment's end, even when `inFolded or inIgnored` flags 327 | # are true … 328 | if (match = blockline.match aBlockEnd)? 329 | 330 | # Reusing `match` as a placeholder. 331 | [match, endmark, code] = match 332 | 333 | # The `endmark` must correspond to the `blockmark`'s. 334 | if not strictMultiLineEnd or blockComments[blockmark].endmark is endmark 335 | 336 | ### Ensure to leave the block-comment, especially single-lines like this one. ### 337 | inBlock = false 338 | 339 | blockline = blockline.replace aBlockEnd, '' unless (inFolded or inIgnored) 340 | 341 | # Match a block-comment's line, when `inFolded or inIgnored` are false. 342 | if not (inFolded or inIgnored) and (match = blockline.match aBlockLine)? 343 | 344 | # Reusing `match` as a placeholder. 345 | [match, linemark, space, comment] = match 346 | 347 | # If we found a `linemark`, prepend it (back) to the `comment`, 348 | # if it does not correspond to the initial `blockmark`. 349 | if linemark? and blockComments[blockmark].linemark isnt linemark 350 | comment = "#{linemark}#{space ? ''}#{comment}" 351 | 352 | blockline = comment 353 | 354 | if inIgnored 355 | currSegment.code.push line 356 | 357 | # Make sure that the next cycle starts fresh, 358 | # if we are going to leave the block. 359 | inIgnored = false if not inBlock 360 | 361 | else 362 | 363 | if inFolded 364 | 365 | # If the foldMarker is empty assign `blockline` to `foldMarker` … 366 | if currSegment.foldMarker is '' 367 | currSegment.foldMarker = line 368 | 369 | # … and collect the `blockline` as code. 370 | currSegment.code.push line 371 | 372 | else 373 | 374 | # The previous cycle contained code, so lets start a new segment. 375 | if currSegment.code.length > 0 376 | segments.push currSegment 377 | currSegment = new @Segment 378 | 379 | # A special case as described in the initialization of `aEmptyLine`. 380 | if aEmptyLine.test line 381 | currSegment.comments.push "" 382 | 383 | else 384 | ### 385 | Collect all but empty start- and end-block-comment lines, hence 386 | single-line block-comments simultaneous matching `aBlockStart` 387 | and `aBlockEnd` have a false `inBlock` flag at this point, are 388 | included. 389 | ### 390 | if not /^\s*$/.test(blockline) or (inBlock and not aBlockStart.test line) 391 | # Strip leading `indention` from block-comment like the one above 392 | # to align their content with the initial blockmark. 393 | if indention? and indention isnt '' and not aBlockLine.test line 394 | blockline = blockline.replace ///^#{indention}///, '' 395 | 396 | currSegment.comments.push blockline 397 | 398 | # The `code` may occure immediatly after a block-comment end. 399 | if code? 400 | currSegment.code.push code unless inBlock # fool-proof ? 401 | code = null 402 | 403 | # Make sure the next cycle starts fresh. 404 | blockline = null 405 | 406 | # Match that line to the language's single line comment syntax. 407 | # 408 | # However, we treat all comments beginning with } as inline code commentary 409 | # and comments starting with ^ cause that comment and the following code 410 | # block to start folded. 411 | else if aSingleLine? and (match = line.match aSingleLine)? 412 | 413 | # Uses `match` as a placeholder. 414 | [match, comment] = match 415 | 416 | if comment? and comment isnt '' 417 | 418 | # } For example, this comment should be treated as part of our code. 419 | # } Achieved by prefixing the comment's content with “}” 420 | if ignorePrefix? and comment.indexOf(ignorePrefix) is 0 421 | 422 | # } Hint: never start a new segment here, these comments are code ! 423 | # } If we would do so the segments look visually not so appealing in 424 | # } the narrowed single-column-view, and we can not embed a series 425 | # } of comments like these here. 426 | 427 | # Let's strip the “}” character from our documentation 428 | currSegment.code.push line.replace singleStrip, '$1' 429 | 430 | else 431 | 432 | # The previous cycle contained code, so lets start a new segment 433 | # and stop any folding. 434 | if currSegment.code.length > 0 435 | segments.push currSegment 436 | currSegment = new @Segment 437 | inFolded = false 438 | 439 | # It's always a good idea to put a comment before folded content 440 | # like this one here, because folded comments always have their 441 | # own code-segment in their current implementation (see above). 442 | # Without a leading comment, the folded code's segment would just 443 | # follow the above's code segment, which looks visually not so 444 | # appealing in the narrowed single-column-view. 445 | # 446 | # TODO: _Alternative (a)_: Improve folded comments to not start a new segment, like embedded comments from above. _(preferred solution)_ 447 | # TODO: _Alternative (b)_: Improve folded comments visual appearance in single-column view. _(easy solution)_ 448 | # 449 | # ^ … if we start this comment with “^” instead of “}” it and all 450 | # } code up to the next segment's first comment starts folded 451 | if foldPrefix? and comment.indexOf(foldPrefix) is 0 452 | 453 | # } … so folding stops below, as this is a new segment ! 454 | # Let's strip the “^” character from our documentation 455 | currSegment.foldMarker = line.replace singleStrip, '$1' 456 | 457 | # And collect it as code. 458 | currSegment.code.push currSegment.foldMarker 459 | else 460 | currSegment.comments.push comment 461 | 462 | else 463 | if options.allowEmptyLines 464 | currSegment.comments.push '' 465 | 466 | # We surely (should) have raw code at this point. 467 | else 468 | currSegment.code.push line 469 | 470 | segments.push currSegment 471 | 472 | segments 473 | 474 | # Just a convenient prototype for building segments 475 | Segment: class Segment 476 | constructor: (code=[], comments=[], foldMarker='') -> 477 | @code = code 478 | @comments = comments 479 | @foldMarker = foldMarker 480 | 481 | # Annotate an array of segments using [highlight.js](http://highlightjs.org/) 482 | highlightCodeUsingHighlightJS: (segments, language, callback) -> 483 | lang = language.highlightJS or language.pygmentsLexer or '' 484 | doHighlight = do -> 485 | if (lang is 'AUTO') or (lang is '') 486 | (code) -> hljs.highlightAuto(code).value 487 | else 488 | (code) -> hljs.highlight(lang, code, true).value 489 | 490 | for segment in segments 491 | if segment.code?.length 492 | segment.highlightedCode = doHighlight segment.code.join('\n') 493 | else 494 | segment.highlightedCode = "" 495 | 496 | callback() 497 | 498 | # Annotate an array of segments by running their code through [Pygments](http://pygments.org/). 499 | highlightCodeUsingPygments: (segments, language, callback) -> 500 | # Don't bother spawning pygments if we have nothing to highlight 501 | numCodeLines = segments.reduce ( (c,s) -> c + s.code.length ), 0 502 | if numCodeLines == 0 503 | for segment in segments 504 | segment.highlightedCode = '' 505 | 506 | return callback() 507 | 508 | errListener = (error) -> 509 | # This appears to only occur when pygmentize is missing: 510 | Logger.error "Unable to find 'pygmentize' on your PATH. Please install pygments." 511 | Logger.info '' 512 | 513 | # Lack of pygments is a one time setup task, we don't feel bad about killing the process 514 | # off until the user does so. It's a hard requirement. 515 | process.exit 1 516 | 517 | pygmentize = childProcess.spawn 'pygmentize', [ 518 | '-l', language.pygmentsLexer 519 | '-f', 'html' 520 | '-O', 'encoding=utf-8,tabsize=2' 521 | ] 522 | pygmentize.stderr.addListener 'data', (data) -> callback data.toString() 523 | pygmentize.stdin.addListener 'error', errListener 524 | pygmentize.on 'error', errListener 525 | 526 | 527 | # We'll just split the output at the end. pygmentize doesn't stream its output, and a given 528 | # source file is small enough that it shouldn't matter. 529 | result = '' 530 | pygmentize.stdout.addListener 'data', (data) => 531 | result += data.toString() 532 | 533 | # v0.8 changed exit/close event semantics. 534 | match = process.version.match /v(\d+\.\d+)/ 535 | closeEvent = if parseFloat(match[1]) < 0.8 then 'exit' else 'close' 536 | 537 | # We can't include either of the following words ANYWHERE directly adjacent to each other 538 | # Otherwise, our regex (~10 lines below) will split on them, and the number of code blocks 539 | # and comment blocks will not be equal. 540 | seg = 'SEGMENT' 541 | div = 'DIVIDER' 542 | 543 | pygmentize.addListener closeEvent, (args...) => 544 | # pygments spits it out wrapped in `
      ...
      `. We want to 545 | # manage the styling ourselves, so remove that. 546 | result = result.replace('
      ', '').replace('
      ', '') 547 | 548 | # Extract our segments from the pygmentized source. 549 | highlighted = "\n#{result}\n".split ///.*.*/// 550 | 551 | if highlighted.length != segments.length 552 | console.log(result) 553 | 554 | error = new Error CompatibilityHelpers.format 'pygmentize rendered %d of %d segments; expected to be equal', 555 | highlighted.length, segments.length 556 | 557 | error.pygmentsOutput = result 558 | error.failedHighlights = highlighted 559 | return callback error 560 | 561 | # Attach highlighted source to the highlightedCode property of a Segment. 562 | for segment, i in segments 563 | segment.highlightedCode = highlighted[i] 564 | 565 | callback() 566 | 567 | # Rather than spawning pygments for each segment, we stream it all in, separated by 'magic' 568 | # comments so that we can split the highlighted source back into segments. 569 | # 570 | # To further complicate things, pygments doesn't let us cheat with indentation-aware languages: 571 | # We have to match the indentation of the line following the divider comment. 572 | mergedCode = '' 573 | for segment, i in segments 574 | segmentCode = segment.code.join '\n' 575 | 576 | if i > 0 577 | # Double negative: match characters that are spaces but not newlines 578 | indentation = segmentCode.match(/^[^\S\n]+/)?[0] || '' 579 | if language.singleLineComment? 580 | mergedCode += "\n#{indentation}#{language.singleLineComment[0]} #{seg} #{div}\n" 581 | else 582 | mlc = language.multiLineComment 583 | mergedCode += "\n#{indentation}#{mlc[0]} #{seg} #{div} #{mlc[2]}\n" 584 | 585 | mergedCode += segmentCode 586 | 587 | pygmentize.stdin.write mergedCode 588 | pygmentize.stdin.end() 589 | 590 | # default 591 | highlightCode: (segments, language, callback) -> 592 | Logger.warn "Usage of highlightCode is deprecated. Specify highlighter"+ 593 | " instead. (Using highlight.js as default.)" 594 | highlightCodeUsingHighlightJS(segments, language, callback) 595 | 596 | parseDocTags: (segments, project, callback) -> 597 | TAG_REGEX = /(?:^|\n)@(\w+)(?:\s+(.*))?/ 598 | TAG_VALUE_REGEX = /^(?:"(.*)"|'(.*)'|\{(.*)\}|(.*))$/ 599 | 600 | try 601 | for segment, segmentIndex in segments when TAG_REGEX.test segment.comments.join('\n') 602 | tags = [] 603 | currTag = { 604 | name: 'description' 605 | value: '' 606 | } 607 | tags.push currTag 608 | tagSections = {} 609 | 610 | for line in segment.comments when line? 611 | if (match = line.match TAG_REGEX)? 612 | currTag = { 613 | name: match[1] 614 | value: match[2] || '' 615 | } 616 | tags.push currTag 617 | else 618 | currTag.value += "\n#{line}" 619 | 620 | for tag in tags 621 | tag.value = tag.value.replace /^\n|\n$/g, '' 622 | 623 | tagDefinition = DOC_TAGS[tag.name] 624 | 625 | unless tagDefinition? 626 | if tag.value.length == 0 627 | tagDefinition = 'defaultNoValue' 628 | else 629 | tagDefinition = 'defaultHasValue' 630 | 631 | if 'string' == typeof tagDefinition 632 | tagDefinition = DOC_TAGS[tagDefinition] 633 | 634 | tag.definition = tagDefinition 635 | tag.section = tagDefinition.section 636 | 637 | if tagDefinition.valuePrefix? 638 | tag.value = tag.value.replace ///#{tagDefinition.valuePrefix?}\s+///, '' 639 | 640 | if tagDefinition.parseValue? 641 | tag.value = tagDefinition.parseValue tag.value 642 | else if not /\n/.test tag.value 643 | tag.value = tag.value.match(TAG_VALUE_REGEX)[1..].join('') 644 | 645 | tagSections[tag.section] = [] unless tagSections[tag.section]? 646 | tagSections[tag.section].push tag 647 | 648 | segment.tags = tags 649 | segment.tagSections = tagSections 650 | 651 | catch error 652 | return callback error 653 | 654 | callback() 655 | 656 | markdownDocTags: (segments, project, callback) -> 657 | try 658 | for segment, segmentIndex in segments when segment.tags? 659 | 660 | for tag in segment.tags 661 | if tag.definition.markdown? 662 | if 'string' == typeof tag.definition.markdown 663 | tag.markdown = tag.definition.markdown.replace /\{value\}/g, tag.value 664 | else 665 | tag.markdown = tag.definition.markdown(tag.value) 666 | else 667 | if tag.value.length > 0 668 | tag.markdown = "#{tag.name} #{tag.value}" 669 | else 670 | tag.markdown = tag.name 671 | 672 | catch error 673 | return callback error 674 | 675 | callback() 676 | 677 | # Annotate an array of segments by running their comments through 678 | # [marked](https://github.com/chjj/marked). 679 | markdownComments: (segments, project, callback) -> 680 | try 681 | for segment, segmentIndex in segments 682 | markdown = marked segment.comments.join '\n' 683 | headers = [] 684 | 685 | # showdown generates header ids by lowercasing & dropping non-word characters. We'd like 686 | # something a bit more readable. 687 | markdown = @gsub markdown, /([^<]+)<\/h\d>/g, (match) => 688 | header = 689 | level: parseInt match[1] 690 | title: match[2] 691 | slug: @slugifyTitle match[2] 692 | 693 | header.isFileHeader = true if header.level == 1 && segmentIndex == 0 && match.index == 0 694 | 695 | headers.push header 696 | 697 | "#{header.title}" 698 | 699 | # We attach the rendered markdown to the comment 700 | segment.markdownedComments = markdown 701 | # As well as the extracted headers to aid in outline building. 702 | segment.headers = headers 703 | 704 | catch error 705 | return callback error 706 | 707 | callback() 708 | 709 | # Sometimes you just don't want any of them hanging around. 710 | trimBlankLines: (string) -> 711 | string.replace(/^[\r\n]+/, '').replace(/[\r\n]+$/, '') 712 | 713 | # Given a title, convert it into a URL-friendly slug. 714 | slugifyTitle: (string) -> 715 | string.split(/[\s\-\_]+/).map( (s) -> s.replace(/[^\w]/g, '').toLowerCase() ).join '-' 716 | 717 | # replacer is a function that is given the match object, and returns the string to replace with. 718 | gsub: (string, matcher, replacer) -> 719 | throw new Error 'You must pass a global RegExp to gsub!' unless matcher.global? 720 | 721 | result = '' 722 | matcher.lastIndex = 0 723 | furthestIndex = 0 724 | 725 | while (match = matcher.exec string) != null 726 | result += string[furthestIndex...match.index] + replacer match 727 | 728 | furthestIndex = matcher.lastIndex 729 | 730 | result + string[furthestIndex...] 731 | -------------------------------------------------------------------------------- /lib/styles/default/style.css: -------------------------------------------------------------------------------- 1 | html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:normal;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:none}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}html,body{height:100%}#document{min-height:100%}body{max-width:33em}.segment{padding:0.5em 0 0.5em 33em;white-space:nowrap}.segment:first-child{padding-top:4.1em}.segment:last-child{padding-bottom:2em}.segment .comments,.segment .code{display:inline-block;vertical-align:top;padding:0 2em}.segment .comments{margin-left:-33em;width:29em;white-space:normal}.segment .code{white-space:pre}#meta{position:absolute;left:33em;padding:0.25em 1em}@media (max-width: 53em){html{font-size:1em}}@media (max-width: 52.94111em){html{font-size:0.99889em}}@media (max-width: 52.88222em){html{font-size:0.99778em}}@media (max-width: 52.82333em){html{font-size:0.99667em}}@media (max-width: 52.76444em){html{font-size:0.99556em}}@media (max-width: 52.70556em){html{font-size:0.99444em}}@media (max-width: 52.64667em){html{font-size:0.99333em}}@media (max-width: 52.58778em){html{font-size:0.99222em}}@media (max-width: 52.52889em){html{font-size:0.99111em}}@media (max-width: 52.47em){html{font-size:0.99em}}@media (max-width: 52.41111em){html{font-size:0.98889em}}@media (max-width: 52.35222em){html{font-size:0.98778em}}@media (max-width: 52.29333em){html{font-size:0.98667em}}@media (max-width: 52.23444em){html{font-size:0.98556em}}@media (max-width: 52.17556em){html{font-size:0.98444em}}@media (max-width: 52.11667em){html{font-size:0.98333em}}@media (max-width: 52.05778em){html{font-size:0.98222em}}@media (max-width: 51.99889em){html{font-size:0.98111em}}@media (max-width: 51.94em){html{font-size:0.98em}}@media (max-width: 51.88111em){html{font-size:0.97889em}}@media (max-width: 51.82222em){html{font-size:0.97778em}}@media (max-width: 51.76333em){html{font-size:0.97667em}}@media (max-width: 51.70444em){html{font-size:0.97556em}}@media (max-width: 51.64556em){html{font-size:0.97444em}}@media (max-width: 51.58667em){html{font-size:0.97333em}}@media (max-width: 51.52778em){html{font-size:0.97222em}}@media (max-width: 51.46889em){html{font-size:0.97111em}}@media (max-width: 51.41em){html{font-size:0.97em}}@media (max-width: 51.35111em){html{font-size:0.96889em}}@media (max-width: 51.29222em){html{font-size:0.96778em}}@media (max-width: 51.23333em){html{font-size:0.96667em}}@media (max-width: 51.17444em){html{font-size:0.96556em}}@media (max-width: 51.11556em){html{font-size:0.96444em}}@media (max-width: 51.05667em){html{font-size:0.96333em}}@media (max-width: 50.99778em){html{font-size:0.96222em}}@media (max-width: 50.93889em){html{font-size:0.96111em}}@media (max-width: 50.88em){html{font-size:0.96em}}@media (max-width: 50.82111em){html{font-size:0.95889em}}@media (max-width: 50.76222em){html{font-size:0.95778em}}@media (max-width: 50.70333em){html{font-size:0.95667em}}@media (max-width: 50.64444em){html{font-size:0.95556em}}@media (max-width: 50.58556em){html{font-size:0.95444em}}@media (max-width: 50.52667em){html{font-size:0.95333em}}@media (max-width: 50.46778em){html{font-size:0.95222em}}@media (max-width: 50.40889em){html{font-size:0.95111em}}@media (max-width: 50.35em){html{font-size:0.95em}}@media (max-width: 50.29111em){html{font-size:0.94889em}}@media (max-width: 50.23222em){html{font-size:0.94778em}}@media (max-width: 50.17333em){html{font-size:0.94667em}}@media (max-width: 50.11444em){html{font-size:0.94556em}}@media (max-width: 50.05556em){html{font-size:0.94444em}}@media (max-width: 49.99667em){html{font-size:0.94333em}}@media (max-width: 49.93778em){html{font-size:0.94222em}}@media (max-width: 49.87889em){html{font-size:0.94111em}}@media (max-width: 49.82em){html{font-size:0.94em}}@media (max-width: 49.76111em){html{font-size:0.93889em}}@media (max-width: 49.70222em){html{font-size:0.93778em}}@media (max-width: 49.64333em){html{font-size:0.93667em}}@media (max-width: 49.58444em){html{font-size:0.93556em}}@media (max-width: 49.52556em){html{font-size:0.93444em}}@media (max-width: 49.46667em){html{font-size:0.93333em}}@media (max-width: 49.40778em){html{font-size:0.93222em}}@media (max-width: 49.34889em){html{font-size:0.93111em}}@media (max-width: 49.29em){html{font-size:0.93em}}@media (max-width: 49.23111em){html{font-size:0.92889em}}@media (max-width: 49.17222em){html{font-size:0.92778em}}@media (max-width: 49.11333em){html{font-size:0.92667em}}@media (max-width: 49.05444em){html{font-size:0.92556em}}@media (max-width: 48.99556em){html{font-size:0.92444em}}@media (max-width: 48.93667em){html{font-size:0.92333em}}@media (max-width: 48.87778em){html{font-size:0.92222em}}@media (max-width: 48.81889em){html{font-size:0.92111em}}@media (max-width: 48.76em){html{font-size:0.92em}}@media (max-width: 48.70111em){html{font-size:0.91889em}}@media (max-width: 48.64222em){html{font-size:0.91778em}}@media (max-width: 48.58333em){html{font-size:0.91667em}}@media (max-width: 48.52444em){html{font-size:0.91556em}}@media (max-width: 48.46556em){html{font-size:0.91444em}}@media (max-width: 48.40667em){html{font-size:0.91333em}}@media (max-width: 48.34778em){html{font-size:0.91222em}}@media (max-width: 48.28889em){html{font-size:0.91111em}}@media (max-width: 48.23em){html{font-size:0.91em}}@media (max-width: 48.17111em){html{font-size:0.90889em}}@media (max-width: 48.11222em){html{font-size:0.90778em}}@media (max-width: 48.05333em){html{font-size:0.90667em}}@media (max-width: 47.99444em){html{font-size:0.90556em}}@media (max-width: 47.93556em){html{font-size:0.90444em}}@media (max-width: 47.87667em){html{font-size:0.90333em}}@media (max-width: 47.81778em){html{font-size:0.90222em}}@media (max-width: 47.75889em){html{font-size:0.90111em}}@media (max-width: 47.7em){html{font-size:0.9em}}@media (max-width: 47.64111em){html{font-size:0.89889em}}@media (max-width: 47.58222em){html{font-size:0.89778em}}@media (max-width: 47.52333em){html{font-size:0.89667em}}@media (max-width: 47.46444em){html{font-size:0.89556em}}@media (max-width: 47.40556em){html{font-size:0.89444em}}@media (max-width: 47.34667em){html{font-size:0.89333em}}@media (max-width: 47.28778em){html{font-size:0.89222em}}@media (max-width: 47.22889em){html{font-size:0.89111em}}@media (max-width: 47.17em){html{font-size:0.89em}}@media (max-width: 47.11111em){html{font-size:0.88889em}}@media (max-width: 47.05222em){html{font-size:0.88778em}}@media (max-width: 46.99333em){html{font-size:0.88667em}}@media (max-width: 46.93444em){html{font-size:0.88556em}}@media (max-width: 46.87556em){html{font-size:0.88444em}}@media (max-width: 46.81667em){html{font-size:0.88333em}}@media (max-width: 46.75778em){html{font-size:0.88222em}}@media (max-width: 46.69889em){html{font-size:0.88111em}}@media (max-width: 46.64em){html{font-size:0.88em}}@media (max-width: 46.58111em){html{font-size:0.87889em}}@media (max-width: 46.52222em){html{font-size:0.87778em}}@media (max-width: 46.46333em){html{font-size:0.87667em}}@media (max-width: 46.40444em){html{font-size:0.87556em}}@media (max-width: 46.34556em){html{font-size:0.87444em}}@media (max-width: 46.28667em){html{font-size:0.87333em}}@media (max-width: 46.22778em){html{font-size:0.87222em}}@media (max-width: 46.16889em){html{font-size:0.87111em}}@media (max-width: 46.11em){html{font-size:0.87em}}@media (max-width: 46.05111em){html{font-size:0.86889em}}@media (max-width: 45.99222em){html{font-size:0.86778em}}@media (max-width: 45.93333em){html{font-size:0.86667em}}@media (max-width: 45.87444em){html{font-size:0.86556em}}@media (max-width: 45.81556em){html{font-size:0.86444em}}@media (max-width: 45.75667em){html{font-size:0.86333em}}@media (max-width: 45.69778em){html{font-size:0.86222em}}@media (max-width: 45.63889em){html{font-size:0.86111em}}@media (max-width: 45.58em){html{font-size:0.86em}}@media (max-width: 45.52111em){html{font-size:0.85889em}}@media (max-width: 45.46222em){html{font-size:0.85778em}}@media (max-width: 45.40333em){html{font-size:0.85667em}}@media (max-width: 45.34444em){html{font-size:0.85556em}}@media (max-width: 45.28556em){html{font-size:0.85444em}}@media (max-width: 45.22667em){html{font-size:0.85333em}}@media (max-width: 45.16778em){html{font-size:0.85222em}}@media (max-width: 45.10889em){html{font-size:0.85111em}}@media (max-width: 45.05em){html{font-size:1em}body{margin:0 auto}.segment{padding:0;white-space:normal;max-width:29em;margin:0 auto}.segment .comments,.segment .code{display:block;padding:1em}.segment .comments{margin-left:0;width:auto}.segment .code{display:block;overflow-y:hidden;overflow-x:auto}.segment .code .wrapper{display:inline-block}#meta{position:static;margin:2em 0 0 0;overflow-y:hidden;overflow-x:auto}#meta .file-path{display:inline-block}}nav{position:fixed;top:0;right:0;width:20em}@media (max-width: 45.05em){nav{left:0;width:100%}}nav .tools{position:relative;z-index:100}nav .tools li{display:table-cell;vertical-align:middle;text-align:center;white-space:nowrap;height:2.1em;padding:0 0.55em}nav .tools .github{padding:0}nav .tools .github a{display:block;height:2.1em;width:2.1em;text-indent:-9001em}nav .tools .search{width:100%}nav .tools .search input{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;display:block;width:100%}nav .toc{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;position:absolute;top:2.1em;bottom:0;width:100%;overflow-x:hidden;overflow-y:auto}nav .toc li{position:relative}nav .toc .label{display:block;line-height:2em;padding:0 0.55em 0 0.55em}nav .toc li li .label{padding-left:1.1em}nav .toc li li li .label{padding-left:1.65em}nav .toc li li li li .label{padding-left:2.2em}nav .toc li li li li li .label{padding-left:2.75em}nav .toc li li li li li li .label{padding-left:3.3em}nav{-moz-transition:height 150ms 0;-o-transition:height 150ms 0;-webkit-transition:height 150ms 0;transition:height 150ms 0}nav .tools .toggle{-moz-transition:background 150ms;-o-transition:background 150ms;-webkit-transition:background 150ms;transition:background 150ms}nav.active{-moz-transition:height 0;-o-transition:height 0;-webkit-transition:height 0;transition:height 0;height:100%}nav .toc{-moz-transition:right 150ms;-o-transition:right 150ms;-webkit-transition:right 150ms;transition:right 150ms;right:-100%}nav.active .toc{right:0}@media (max-width: 45.05em){nav .toc{-moz-transition:left 150ms;-o-transition:left 150ms;-webkit-transition:left 150ms;transition:left 150ms;right:auto;left:-100%}nav.active .toc{left:0}}@media (max-width: 45.05em){body{-moz-transition:left 150ms;-o-transition:left 150ms;-webkit-transition:left 150ms;transition:left 150ms;position:relative;left:0}html.popped{overflow:hidden}html.popped body{left:100%;overflow:hidden}}nav .toc .children,nav .toc .outline{display:none}nav .toc .expanded>.children,nav .toc .expanded>.outline,nav .toc .expanded>.outline .children{display:block}nav .toc .discloser{-moz-transition-property:-moz-transform,-webkit-transform,-o-transform,-moz-transform;-o-transition-property:-moz-transform,-webkit-transform,-o-transform,-o-transform;-webkit-transition-property:-moz-transform,-webkit-transform,-o-transform,-webkit-transform;transition-property:-moz-transform -webkit-transform -o-transform transform;-moz-transition-duration:200ms;-o-transition-duration:200ms;-webkit-transition-duration:200ms;transition-duration:200ms;-moz-transform:rotate(0deg);-ms-transform:rotate(0deg);-webkit-transform:rotate(0deg);transform:rotate(0deg);display:inline-block;height:9px;width:9px;padding:0.2em;margin:0.2em 0.2em -0.2em 0.2em;vertical-align:baseline;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNS4xIE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowOEFDRENGQzE2NEUxMUUxODdDNUQ2ODM0QzVGRkVBMSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowOEFDRENGRDE2NEUxMUUxODdDNUQ2ODM0QzVGRkVBMSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjA4QUNEQ0ZBMTY0RTExRTE4N0M1RDY4MzRDNUZGRUExIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjA4QUNEQ0ZCMTY0RTExRTE4N0M1RDY4MzRDNUZGRUExIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+nQHMgwAAAM1JREFUeNpi/P//P0NJSYkuAwNDJhDXAPE7BjIAExIbZNA9IC4CYjZKDAIBfiDuBeLzQOxBiUEwoAXE26FYixKDYMAD6rpeqGvJNogBGl5F0PDLpMQgGBAC4mlQF9pTYhAMGADxASBeB8RylBgEA4FAfAOIW4CYhxKDQIAZxmChwJD1QFwGxHfINegaEGcB8UFyA/sd1AA9dEOIddFfIJ4OzdAfcSkiZNAOIC6GegcvwGXQHagBm8jNtB+hBmiTYgi6i+ZCw+EFOWkBIMAA1W4l62UzKWwAAAAASUVORK5CYII=') center center no-repeat;background-size:9px 9px}nav .toc .discloser.placeholder,nav .toc .expanded>.outline .discloser{background:none}nav .toc .expanded>.label .discloser{-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-webkit-transform:rotate(90deg);transform:rotate(90deg)}nav .toc .filtered>.label{display:none}nav .toc .matched-child>.label{display:block}nav .toc .matched-child>.children,nav .toc .matched-child>.outline,nav .toc .matched-child>.outline .children{display:block}nav .toc .matched>.children,nav .toc .matched>.outline,nav .toc .matched>.outline .children{display:block}nav.searching .toc .discloser{display:none}.comments .wrapper{font-family:"Helvetica Neue",Helvetica,"Droid Sans",sans-serif;font-weight:300;font-size:0.9375em;line-height:1.35}.comments .wrapper h1,.comments .wrapper h2,.comments .wrapper h3,.comments .wrapper h4,.comments .wrapper h5,.comments .wrapper h6{font-family:"HelveticaNeue-UltraLight","Helvetica Neue",Helvetica,"Droid Sans",sans-serif;font-weight:100;letter-spacing:0.0625em;line-height:1.25;margin-bottom:0.5em}.comments .wrapper h1{font-size:2.5em}.comments .wrapper h2{font-size:2em}.comments .wrapper h3{font-size:1.6em}.comments .wrapper h4{font-size:1.4em}.comments .wrapper h5{font-size:1.3em}.comments .wrapper h6{font-size:1.2em}.comments .wrapper p{margin:1em 0}.comments .wrapper>*:first-child{margin-top:0}.comments .wrapper>*:last-child{margin-bottom:0}.comments .wrapper ol,.comments .wrapper ul{padding-left:1.75em;margin:1em 0}.comments .wrapper ol li{list-style:decimal}.comments .wrapper ul li{list-style:disc}.comments .wrapper li{margin:1em 0}.comments .wrapper li:first-child{margin-top:0}.comments .wrapper li:last-child{margin-bottom:0}.comments .wrapper code{display:inline-block;padding:0.25em 0.25em 0 0.25em}.comments .wrapper pre{display:block;overflow-x:auto;overflow-y:hidden;margin-bottom:1em}.comments .wrapper pre .hljs-comment,.comments .wrapper pre .hljs-template_comment,.comments .wrapper pre .diff .hljs-header,.comments .wrapper pre .hljs-doctype,.comments .wrapper pre .hljs-pi,.comments .wrapper pre .lisp .hljs-string,.comments .wrapper pre .hljs-javadoc{color:#93a1a1;font-style:italic}.comments .wrapper pre .hljs-keyword,.comments .wrapper pre .hljs-winutils,.comments .wrapper pre .method,.comments .wrapper pre .hljs-addition,.comments .wrapper pre .css .hljs-tag,.comments .wrapper pre .hljs-request,.comments .wrapper pre .hljs-status,.comments .wrapper pre .nginx .hljs-title{color:#859900}.comments .wrapper pre .hljs-number,.comments .wrapper pre .hljs-command,.comments .wrapper pre .hljs-string,.comments .wrapper pre .hljs-tag .hljs-value,.comments .wrapper pre .hljs-rules .hljs-value,.comments .wrapper pre .hljs-phpdoc,.comments .wrapper pre .tex .hljs-formula,.comments .wrapper pre .hljs-regexp,.comments .wrapper pre .hljs-hexcolor{color:#2aa198}.comments .wrapper pre .hljs-title,.comments .wrapper pre .hljs-localvars,.comments .wrapper pre .hljs-chunk,.comments .wrapper pre .hljs-decorator,.comments .wrapper pre .hljs-built_in,.comments .wrapper pre .hljs-identifier,.comments .wrapper pre .vhdl .hljs-literal,.comments .wrapper pre .hljs-id,.comments .wrapper pre .css .hljs-function{color:#268bd2}.comments .wrapper pre .hljs-attribute,.comments .wrapper pre .hljs-variable,.comments .wrapper pre .lisp .hljs-body,.comments .wrapper pre .smalltalk .hljs-number,.comments .wrapper pre .hljs-constant,.comments .wrapper pre .hljs-class .hljs-title,.comments .wrapper pre .hljs-parent,.comments .wrapper pre .haskell .hljs-type{color:#b58900}.comments .wrapper pre .hljs-preprocessor,.comments .wrapper pre .hljs-preprocessor .hljs-keyword,.comments .wrapper pre .hljs-pragma,.comments .wrapper pre .hljs-shebang,.comments .wrapper pre .hljs-symbol,.comments .wrapper pre .hljs-symbol .hljs-string,.comments .wrapper pre .diff .hljs-change,.comments .wrapper pre .hljs-special,.comments .wrapper pre .hljs-attr_selector,.comments .wrapper pre .hljs-important,.comments .wrapper pre .hljs-subst,.comments .wrapper pre .hljs-cdata,.comments .wrapper pre .clojure .hljs-title,.comments .wrapper pre .css .hljs-pseudo{color:#cb4b16}.comments .wrapper pre .hljs-deletion{color:#dc322f}.comments .wrapper pre .tex .hljs-formula{background:#eee8d5}.comments .wrapper pre code{padding:1em}.comments .wrapper blockquote{padding:0 1em}.comments .wrapper strong{font-weight:700}.comments .wrapper em{font-style:italic}html{background:#4a525a}#document{background:#f5fbff url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuMCIgeTE9IjAuNSIgeDI9IjEuMCIgeTI9IjAuNSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzI3MmMzMCIvPjxzdG9wIG9mZnNldD0iMzAlIiBzdG9wLWNvbG9yPSIjM2U0NTRjIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjNGE1MjVhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0idXJsKCNncmFkKSIgLz48L3N2Zz4g') 33em no-repeat;background:#f5fbff -webkit-gradient(linear, 0% 50%, 100% 50%, color-stop(0%, #272c30),color-stop(30%, #3e454c),color-stop(100%, #4a525a)) 33em no-repeat;background:#f5fbff -moz-linear-gradient(left, #272c30,#3e454c 0.3em,#4a525a 1em) 33em no-repeat;background:#f5fbff -webkit-linear-gradient(left, #272c30,#3e454c 0.3em,#4a525a 1em) 33em no-repeat;background:#f5fbff linear-gradient(to right, #272c30,#3e454c 0.3em,#4a525a 1em) 33em no-repeat;margin-right:-1em;padding-right:1em}@media (max-width: 45.05em){#document{margin-right:0;padding-right:0}}#meta>*{font-family:"Helvetica Neue",Helvetica,"Droid Sans",sans-serif;font-weight:300;font-size:0.9375em;line-height:1.35;text-shadow:#272c30 1px 1px 0}#meta>*,#meta>* a{color:#9faab7}#meta>* a{text-decoration:none}.comments .wrapper{font-family:"Helvetica Neue",Helvetica,"Droid Sans",sans-serif;font-weight:300;font-size:0.9375em;line-height:1.35;text-shadow:#fff 1px 1px 0;color:#4a525a}.code .wrapper{font-family:"Droid Sans Mono",Menlo,Monaco,monospace;font-size:0.75em;line-height:1.4;text-shadow:#272c30 1px 1px 0;color:#cbd1d8}.code .wrapper .hljs{display:block;padding:0.5em}.code .wrapper .hljs-comment,.code .wrapper .hljs-template_comment,.code .wrapper .diff .hljs-header,.code .wrapper .hljs-doctype,.code .wrapper .hljs-pi,.code .wrapper .lisp .hljs-string,.code .wrapper .hljs-javadoc{color:#b1bac4;font-style:italic}.code .wrapper .hljs-keyword,.code .wrapper .hljs-winutils,.code .wrapper .method,.code .wrapper .hljs-addition,.code .wrapper .css .hljs-tag,.code .wrapper .hljs-request,.code .wrapper .hljs-status,.code .wrapper .nginx .hljs-title{color:#e0c090}.code .wrapper .hljs-string{color:#e9baba}.code .wrapper .hljs-property{color:#b9d0af}.code .wrapper .hljs-function{color:#abd9cf}.code .wrapper .hljs-class{color:#cee4dd}.code .wrapper .hljs-number,.code .wrapper .hljs-command,.code .wrapper .hljs-tag .hljs-value,.code .wrapper .hljs-rules .hljs-value,.code .wrapper .hljs-phpdoc,.code .wrapper .tex .hljs-formula,.code .wrapper .hljs-regexp,.code .wrapper .hljs-hexcolor{color:#cba8d6}.code .wrapper .hljs-title,.code .wrapper .hljs-localvars,.code .wrapper .hljs-chunk,.code .wrapper .hljs-decorator,.code .wrapper .hljs-built_in,.code .wrapper .hljs-identifier,.code .wrapper .vhdl .hljs-literal,.code .wrapper .hljs-id,.code .wrapper .css .hljs-function{color:#a9c2ba}.code .wrapper .hljs-attribute,.code .wrapper .hljs-variable,.code .wrapper .lisp .hljs-body,.code .wrapper .smalltalk .hljs-number,.code .wrapper .hljs-constant,.code .wrapper .hljs-class .hljs-title,.code .wrapper .hljs-parent,.code .wrapper .haskell .hljs-type{color:#b9d0af}.code .wrapper .hljs-preprocessor,.code .wrapper .hljs-preprocessor .hljs-keyword,.code .wrapper .hljs-pragma,.code .wrapper .hljs-shebang,.code .wrapper .hljs-symbol,.code .wrapper .hljs-symbol .hljs-string,.code .wrapper .diff .hljs-change,.code .wrapper .hljs-special,.code .wrapper .hljs-attr_selector,.code .wrapper .hljs-important,.code .wrapper .hljs-subst,.code .wrapper .hljs-cdata,.code .wrapper .clojure .hljs-title,.code .wrapper .css .hljs-pseudo{color:#cee4dd}.code .wrapper .hljs-deletion{color:#dc322f}.code .wrapper .tex .hljs-formula{background:#e9baba}@media (max-width: 45.05em){.code{-moz-border-radius:0.4em;-webkit-border-radius:0.4em;border-radius:0.4em;-moz-box-shadow:#272c30 0 0 0.5em 0.2em inset;-webkit-box-shadow:#272c30 0 0 0.5em 0.2em inset;box-shadow:#272c30 0 0 0.5em 0.2em inset;background:#4a525a}.code .wrapper{-moz-box-shadow:#4a525a 0 0 0.25em 0.75em;-webkit-box-shadow:#4a525a 0 0 0.25em 0.75em;box-shadow:#4a525a 0 0 0.25em 0.75em;background:#4a525a}}@media (max-width: 29em){.code{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0}}nav{text-shadow:#f0f0f0 1px 1px 0;color:#4a525a}nav .tools,nav .toc{font-family:"Helvetica Neue",Helvetica,"Droid Sans",sans-serif;font-weight:300;font-size:0.9375em;line-height:1.35}nav .tools{-moz-box-shadow:rgba(0,0,0,0.3) 0 0 0.5em 0.1em;-webkit-box-shadow:rgba(0,0,0,0.3) 0 0 0.5em 0.1em;box-shadow:rgba(0,0,0,0.3) 0 0 0.5em 0.1em;background:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIgc3RvcC1vcGFjaXR5PSIwLjkiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNjZGNkY2QiIHN0b3Atb3BhY2l0eT0iMC45Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0idXJsKCNncmFkKSIgLz48L3N2Zz4g');background:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(255,255,255,0.9)),color-stop(100%, rgba(205,205,205,0.9)));background:-moz-linear-gradient(top, rgba(255,255,255,0.9),rgba(205,205,205,0.9));background:-webkit-linear-gradient(top, rgba(255,255,255,0.9),rgba(205,205,205,0.9));background:linear-gradient(to bottom, rgba(255,255,255,0.9),rgba(205,205,205,0.9));-moz-border-radius-bottomleft:0.4em;-webkit-border-bottom-left-radius:0.4em;border-bottom-left-radius:0.4em;border-bottom:1px solid #4a525a;border-left:1px solid #4a525a}@media (max-width: 53em){nav .tools{-moz-border-radius-bottomleft:0;-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0}}nav .tools li{border-right:1px solid #4a525a}nav .tools li:last-child{border-right:none}nav .tools .toggle{cursor:pointer}nav .tools .github a{-moz-transition:opacity 200ms;-o-transition:opacity 200ms;-webkit-transition:opacity 200ms;transition:opacity 200ms;filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=50);opacity:0.5;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACcAAAAwCAYAAACScGMWAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNS4xIE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowOEFDRENGODE2NEUxMUUxODdDNUQ2ODM0QzVGRkVBMSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowOEFDRENGOTE2NEUxMUUxODdDNUQ2ODM0QzVGRkVBMSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjA4QUNEQ0Y2MTY0RTExRTE4N0M1RDY4MzRDNUZGRUExIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjA4QUNEQ0Y3MTY0RTExRTE4N0M1RDY4MzRDNUZGRUExIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+FC/Z5AAACv9JREFUeNrMWXl0VNUdvu/NmzWZJQnZN0hCWIQEIQuErUCBI2qwEAFRBJUeDvUPDwfKVhDUY2mPp8spakVELSq11goIiCBbUVKKUGKQQFiCSBJIQjLJZPbJzOt3J/dOXiYJgcqh3nO+vPfm3Xfvd3/3t94IsiyTH2sTyY+4SfxGpVIpfzdBokdxjQHOADuAj4GmuzRvFjAbmAZkAocEQZjLX/r9/vYbuq0UgigoQKagsxyG74FlgB4gYdAA0UASkAIkACahvSn70Xd/AlrCxm4VRDEBIBScUxg5wiCs7oZcEJDwN7g+AswH3gAOApVALdAIWIEGtpgyYAfwPFtYTU/jbtmy5W/gsZAugnMKbSsJ2oXAn1J62o9AIJCDy/ZgbzbQLVoqkAtM722fa2pqYnHJwfgmXG23MgjV7SjO3bR06BnlogUSe7PW+ntplXQHUlNTHewxqjdyR+8lOehxYNCgQa3hnHoil8a18F60trY2cenSpYU+n48qfXMnvaEQYcIMQ4KmrbAk/Cb3ZGV3gt7GWbx4cSm4xHd1JR2+aPfdIPK/ICIiwlNaWroKfMSgwDq0Moj78feB/1e4cjqdmq1btz6C25921jk5iOLe4i2XdDh+aF/ejhw5ksYEZJE6BAeHSuT83nyaMTLSZzAYfO2K7BPdbo/k9ngkOM8u32i1moBep2/TarVtGrU64PZ6VC0tLVqv1ydyFxLeamtrzbgYgJFSh+Bk2jOpe1ICmf7QtKp5856sHJo7zGoym4Lk3G636HQ4JXurTd3U1KTBtqgcTpcQZTYHIiIjfNExMV6DIaINuuTX6XQBp8sp1t24odv3+d6UN9/cPPTqtWpjOEFYLOeUI3CJwJJULAMZFE5u9cqVx1/esKE8jLebgWc3akCjjIFKbwF46dw0AlLXVnXpUlzx9OJpZyvO9VEShJR9LpfrHdy2SgqDCEB83i7Elj53NozYKeAY3QHAE0ZOz0KQmukuJ+VifUPkMrKyjO9t2bzvwUdmbL5eV2/gBBVE1crAL/OAy4mlJSe5Vq5Zd0pB7O/4OB7XXwJbgC/pYLHRFhJtNhONRk1q6xtIv+RE4vJ4SUOTldTdDKWABcBi+g3wdtH9OfaHJ4zaVpg7NHvH/oPrFKGMW4wghYn/uvJhwvjx1UZLFN+6I/iQ3r/Pnh+iGQcWUVPfaCWNzTZiiowgMA5yo7GJ1N5oIP4OI4llmQzV6QVAdenp8v3J8X1IhF7aplarV0HXaE5IJEniH8md/RwhFZ1iWFoqj3dUop8yQrzFPPvss78Cuf48e7W22IjL7SHXauuUxGgbpTS2goKC5fhu9InyCnLmfFWDiByQv6OOuCu5dmEeU46Iyfj7CqzKwXQp1OLj4wfSrcJEll78q0b5EBMTQ4k++V3N9cE6rVYCuRCP5OTkFm5ECskJFMdxd5X/dLOh3shuG9n1n8pMYuLEiQ10sawmuFU7SQMAfxg3bhxXn1yLyWDx+X0mhVT5O2d4NLADb/GHr0/9J97ltFO9lDJSgruyC3gpNjb20tq1a4+OHj26OaxQGgDMYxarbN8BTxuNxm9nzZr1zZIlS6r4GiWVekhbW0BL9Q0L9s+dO/dqKKfsJvCb4fMuMKuRP/nwvSN4//PHH55CsAXckqkhbGJ4nWYSbMCXmIKkKRNJhQeIATYqvs3PGZD1EX1F55s0adI5xbupXciJ7ddCWBCVopyekuw8ffJr2lkzMLMviYky0/7UeRcDNIMYxbeIKf6IvLy8madOnZqId1KEQU8S4/rA1ZiwODX9Ng9YCcxc/sxjkyWVivo+GRGkurKy8j1G7DW6YGWECMVY2hKTEicjHH1stVpNSYkJztmPlqw/uG/vK/6ATPeC+Nv88JIqUg2X0epwEo/XW4LPltMaFNs+vays7HhSUtLS9OTEpgi9jkRbTMRmd5Ios5G0OuGTA/LkMxcubWtr89MIcRnZyLYnnniC70Ap8JfutjWE9evXF02YMKGCiz0hPu5YWlLC/PuyMzOGZGdKOQP7k76pSSR3YDZJSYyDVE0js7KyVhw88MVQjJkODAR0AzLSyJi8XIJvzMMGZU+M7xP9V5p4snHf3/jqxp+g35+Z1F4GTEFOCsktYCHmANCgKAX779q1a9W7776bd/jw4b6QpFEtSUSr0VyTJPE8vMB3Br2uSRREh0oUbPEpaQlFhQW5dXU3mtUaddzBA4crHE5HItba3+ly9/V4fWbMSQuo7SD2DsanPnQhEMnmfYM66c4Vf3txTFdSDTwakqAYrE2jgAV2u/2tvXv3/mPRokUfZWdnfwhvXhHKBBm4IYXhX8BrwC/wvmDZsmV9mFQXKgzgaeovRUXFr5QcdQcvAKupwFJSUkqqa6q3Kxw0lWIqU/rhly9fTsKWP1hdXR3dW4YL3fumqqrq1xqNhpZ9cewMhiYJNAKdpjqG+a9wy+56VsIkNXLkSOqIZXjx5s/37Z3QnS5SK4yMjDx8J/VBSUlJOb5bATwGUB3rC2j57gQ9BSuyeixwEJLiYdbUucq5uTnXNm3a1B3BXKDtTshZLBbnxYsXV1M31GW82yXH8ArXHbPZbENa/kc8zwPoQcvrwDU+Kd7ZIUUXq57cc+bMKeMlICKCC1sZWsTOnTs/wVyP3i65noqZjezEiNhsNiP83XPovBXYzHKy0EHPzJkzz6elpQXDGIj48/Pz67mWjhgxorqoqOhKKJGwWmkCUADdNXSuBTqrWG8V//d0hXq93spDEEd4Gz58eCMqdoENLtPqPXQyKUl+vK8PS8tMQaMQeDkqk477zkm+dAsjOzxjxozxXq/3rZMnT2Y0NzcbaIWFrXLjKqJKCqZJCHOy4qTIj+ghK47GBLwPhEsITddxL/R+7Npd++CDD86AyJPAMzU1NXGYW0C+5UHkGLhhw4axzL0IyiCvUomBsPM84QefCfd4oi2KlZhgQ2pq6ng83ke3xeFweHo6ykIKJCuykB90GCTdTicQpLq3AyR30oIXQXoRPzLopu9dO52S7uSUkrkXmq67uzMOOAMZRtDj6aeisrqt+UIjqTWasEHbM/eo2HiiQ07G9JtodDpibahTNdXdCPYzRBoFo8XCDr2D48h8clRutH8oROoMkaEVpQ8YzCxE6PEoMERubPHPFJZDvXOwpiB6gyF4/E5/87jcJDE9gxz7bMcDlBx1mOkZGS5UXMFxNHq9v29mf4+I+oIarj9ApCE5w+x8jhNl5emzSmbUBM8aRo3lUaBdCjJzKQrrVUhO23kVcrsP8nk9TJdUxGGzJXz2/pYXbtbWBI/JxkyavKd/Zlbr1StV9PCFDB6aW1dUmN/YLzOr5cK5iuizZ8v7PDD17S+HDLu/8tuy0wP+8NuXh/jdds/8BU9Fet3uXrc2RM7a0NBOjq1ECB0CtBMVoUu2psZxrVbrOKM5emeE2bj7iz27Ilauf/EZR2srVE3tXvP8ui+R55Hla9Z9tfDx2cVVFy6YP9r5acaRQ4deLZk9K9Nlt5fs3r0nc3h+4RRbc+PJAETL5RH0jWHOMKQPk+fM7+7cq+NjSSQum4M01V8nZ//9FfXyxRevXJ02ZtTI6TGxsVf6ZWWv2LP946ms8iofkDOsKeBx/06j1WtPHC99E7XEizSfA7LZ6LSSOzBl7lPuTkqOtn/bO53J3WHLAx4G6igRlvPPZO6FFiy/B2j5V0hLQvYNPcY4AVB/Sa3BEqwTCLnSo3e4S//oiGHFDTXh3cDFsIVMZXH8N4z8bbX/CjAA0UTEH4oMvREAAAAASUVORK5CYII=') center center no-repeat;background-size:19.5px 24px}nav .tools .github a:hover{filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=90);opacity:0.9}nav.active .tools{-moz-border-radius-bottomleft:0;-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0}nav.active .tools .toggle{background:rgba(205,205,205,0.9);position:relative}nav .toc{-moz-box-shadow:rgba(0,0,0,0.3) 0 0 0.5em 0.1em;-webkit-box-shadow:rgba(0,0,0,0.3) 0 0 0.5em 0.1em;box-shadow:rgba(0,0,0,0.3) 0 0 0.5em 0.1em;background:rgba(230,230,230,0.9);border-left:1px solid #4a525a}nav .toc .label{color:#4a525a;text-decoration:none;border-top:1px solid rgba(192,192,192,0.9);border-bottom:1px solid rgba(192,192,192,0.9);margin-top:-1px}nav .toc .label:hover{background:rgba(205,205,205,0.9)}nav .toc .file>.label{font-weight:bold}nav .toc .selected>.label{background:#f5fbff}nav .toc .label em{font-weight:bold}nav .toc .file>.label em{color:#101214}nav .toc .matched-child>.label{filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=65);opacity:0.65;text-shadow:none;background:rgba(192,192,192,0.9)}@media (max-width: 45.05em){nav .tools,nav .toc{border-left-width:0}nav .tools{background:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2NkY2RjZCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');background:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff),color-stop(100%, #cdcdcd));background:-moz-linear-gradient(top, #ffffff,#cdcdcd);background:-webkit-linear-gradient(top, #ffffff,#cdcdcd);background:linear-gradient(to bottom, #ffffff,#cdcdcd)}nav .toc{background:#e6e6e6}}.comments .wrapper a{display:inline-block;color:#a8614e;text-decoration:none}.comments .wrapper a:hover,.comments .wrapper a:hover *{text-decoration:underline}.comments .wrapper code{font-family:"Droid Sans Mono",Menlo,Monaco,monospace;font-size:0.75em;line-height:1.4;border:1px solid #e6e0d5}.comments .wrapper pre,.comments .wrapper code{-moz-border-radius:0.4em;-webkit-border-radius:0.4em;border-radius:0.4em;background:#fbf8f3}.comments .wrapper pre{-moz-box-shadow:#f2ece3 0 0 0.4em 0.2em;-webkit-box-shadow:#f2ece3 0 0 0.4em 0.2em;box-shadow:#f2ece3 0 0 0.4em 0.2em;border:1px solid #d9c9af}.comments .wrapper pre code{border-width:0;background:transparent}.comments .wrapper blockquote{border-left:0.15em solid #959fa8;margin-left:-0.15em}body{-webkit-text-size-adjust:100%}input[type="search"]{-moz-border-radius:1em;-webkit-border-radius:1em;border-radius:1em;-moz-box-shadow:#ddd 0 1px 1px 0 inset;-webkit-box-shadow:#ddd 0 1px 1px 0 inset;box-shadow:#ddd 0 1px 1px 0 inset;border:1px solid #959595;padding:0.15em 0.8em}.comments.doc-section .wrapper{color:#252519}.comments.doc-section.doc-section-private .wrapper,.comments.doc-section.doc-section-protected .wrapper,.comments.doc-section.doc-section-internal .wrapper{color:#7f7f7f}.comments.doc-section .doc-section-header{font:bold 18px "helvetica neue",helvetica,sans-serif}.comments.doc-section .docs .doc-section-header code{font-size:18px}.code .marker,.code .marker.wrapper,.code .wrapper.marker{display:none}.code.folded .wrapper{display:none;cursor:default}.code.folded .marker{-moz-border-radius:0.2em;-webkit-border-radius:0.2em;border-radius:0.2em;-moz-box-shadow:#2f3539 1px 1px 1px 0;-webkit-box-shadow:#2f3539 1px 1px 1px 0;box-shadow:#2f3539 1px 1px 1px 0;display:inline-block;border:1px solid #73787f;padding:0.2em 0.5em;margin-left:-0.5em;margin-right:-0.5em;background:#58616b;font:12px "Droid Sans Mono",Menlo,Monaco,monospace;text-shadow:#2f3539 1px 1px 0px;cursor:pointer;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:-moz-none;-ms-user-select:none;user-select:none}.code.folded .marker .c1{color:#73787f;font-style:normal}.code.folded .marker:hover{background:#5f6872}.code.folded .marker:hover .c1{color:#7b8087}.code.folded .marker .c1:after{content:" …"} 2 | --------------------------------------------------------------------------------