diff --git a/README.md b/README.md index ec4bc530d..9b6f57567 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ lets you run `documentation` as a [Gulp](http://gulpjs.com/) build task. * [Node API](docs/NODE_API.md): documentation.js's self-generated documentation * [FAQ](docs/FAQ.md) -* [Theming HTML](docs/THEME_HTML.md): tips for theming documentation output in HTML +* [Theming](docs/THEMING.md): tips for theming documentation output in HTML * [See also](docs/SEE_ALSO.md): a list of projects similar to documentation.js ## User Guide diff --git a/docs/THEME_HTML.md b/docs/THEME_HTML.md deleted file mode 100644 index c7b0cc93e..000000000 --- a/docs/THEME_HTML.md +++ /dev/null @@ -1,20 +0,0 @@ -# HTML Themes - -HTML themes for documentation consist of three parts - -* `index.hbs`, the main template that defines the document structure -* `section.hbs`, a partial used to render each chunk of documentation -* `assets/*`, any assets, including CSS & JS - -# Helpers - -* `{{format_params}}`: format function parameters, including the types - included within. -* `{{permalink}}`: in the context of a documentation chunk, - return the chunk's permalink -* `{{autolink TEXT}}`: given a chunk of text that may be a reference to a - method, property, or other namespaced item, link the text to the item -* `{{md TEXT}}`: render Markdown-formatted text, parsing additional - JSDoc inline tag syntax and linking links when necessary -* `{{format_type}}`: format a type definition, like `{string}` within a - param definition. diff --git a/docs/THEMING.md b/docs/THEMING.md index a8088c54d..82bbd47a3 100644 --- a/docs/THEMING.md +++ b/docs/THEMING.md @@ -1,27 +1,24 @@ -This assumes you have node.js installed: +Documentation.js supports customizable themes for HTML output. A theme is a Node.js +module that exports a single function with the following signature: -First install dev-documentation: - -``` -npm install -g dev-documentation@2.1.0-alpha1 ``` +/** + * @function + * @param {Array} comments - an array of comments to be output + * @param {Object} options - theme options + * @param {ThemeCallback} callback - see below + */ -Get a JSDoc-documented project to test against: - +/** + * @callback ThemeCallback + * @param {?Error} error + * @param {?Array} output + */ ``` -git clone git@github.com:mapbox/mapbox-gl-js.git -``` - -Get a checkout of the default style -``` -git@github.com:documentationjs/documentation-theme-default.git -``` - -Now start theming - -``` -dev-documentation mapbox-gl-js/js/mapbox-gl.js -t ./documentation-theme-default/ -``` +The theme function should call the callback with either an error, if one occurs, +or an array of [vinyl](https://github.com/gulpjs/vinyl) `File` objects. -If you have [LiveReload](https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei) installed, you can enable it and it'll automatically refresh the page when you edit the theme. +The theme is free to implement HTML generation however it chooses. See +[the default theme](https://github.com/documentationjs/documentation-theme-default/) +for some ideas. diff --git a/lib/format_type.js b/lib/format_type.js deleted file mode 100644 index b701dfaf9..000000000 --- a/lib/format_type.js +++ /dev/null @@ -1,134 +0,0 @@ -var Syntax = require('doctrine').Syntax, - globalsDocs = require('globals-docs'), - u = require('unist-builder'); - -function t(text) { - return u('text', text); -} - -function link(text) { - var docs = globalsDocs.getDoc(text); - if (docs) { - return u('link', { href: docs }, [u('text', text)]); - } - return u('text', text); -} - -function commaList(getNamedLink, items, start, end, sep) { - var res = []; - if (start) { - res.push(t(start)); - } - for (var i = 0, iz = items.length; i < iz; ++i) { - res = res.concat(formatType(items[i], getNamedLink)); - if (i + 1 !== iz) { - res.push(t(sep || ', ')); - } - } - if (end) { - res.push(t(end)); - } - return res; -} - -function decorate(formatted, str, prefix) { - if (prefix) { - return [t(str)].concat(formatted); - } - return formatted.concat(t(str)); -} - -/** - * Helper used to format JSDoc-style type definitions into HTML or Markdown. - * - * @name formatType - * @param {Object} node type object in doctrine style - * @param {function(text): text} getNamedLink a function that tries - * to find a URL to point a named link to - * @returns {string} string - * @example - * var x = { type: 'NameExpression', name: 'String' }; - * // in template - * // {{ type x }} - * // generates String - */ -function formatType(node, getNamedLink) { - var result = []; - - if (!node) { - return []; - } - - switch (node.type) { - case Syntax.NullableLiteral: - return [t('?')]; - case Syntax.AllLiteral: - return [t('Any')]; - case Syntax.NullLiteral: - return [t('null')]; - case Syntax.VoidLiteral: - return [t('void')]; - case Syntax.UndefinedLiteral: - return [link('undefined')]; - case Syntax.NameExpression: - return [link(node.name)]; - case Syntax.ParameterType: - return [t(node.name + ': ')].concat(formatType(node.expression, getNamedLink)); - - case Syntax.TypeApplication: - return formatType(node.expression, getNamedLink) - .concat(commaList(getNamedLink, node.applications, '.<', '>')); - case Syntax.UnionType: - return commaList(getNamedLink, node.elements, '(', ')', '|'); - case Syntax.ArrayType: - return commaList(getNamedLink, node.elements, '[', ']'); - case Syntax.RecordType: - return commaList(getNamedLink, node.fields, '{', '}'); - - case Syntax.FieldType: - if (node.value) { - return [t(node.key + ': ')].concat(formatType(node.value, getNamedLink)); - } - return [t(node.key)]; - - case Syntax.FunctionType: - result = [t('function (')]; - - if (node['this']) { - if (node['new']) { - result.push(t('new: ')); - } else { - result.push(t('this: ')); - } - - result = result.concat(formatType(node['this'], getNamedLink)); - - if (node.params.length !== 0) { - result.push(t(', ')); - } - } - - result = result.concat(commaList(getNamedLink, node.params, '', ')')); - - if (node.result) { - result = result.concat([t(': ')].concat(formatType(node.result, getNamedLink))); - } - return result; - - case Syntax.RestType: - // note that here we diverge from doctrine itself, which - // lets the expression be omitted. - return decorate(formatType(node.expression, getNamedLink), '...', true); - case Syntax.OptionalType: - return decorate(formatType(node.expression, getNamedLink), '='); - case Syntax.NonNullableType: - return decorate(formatType(node.expression, getNamedLink), '!', node.prefix); - case Syntax.NullableType: - return decorate(formatType(node.expression, getNamedLink), '?', node.prefix); - - default: - throw new Error('Unknown type ' + node.type); - } -} - -module.exports = formatType; diff --git a/lib/get_template.js b/lib/get_template.js deleted file mode 100644 index 77c6cbf52..000000000 --- a/lib/get_template.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var path = require('path'); - -/** - * Get a Handlebars template file out of a theme and compile it into - * a template function - * - * @param {Object} Handlebars handlebars instance - * @param {string} themeModule base directory of themey - * @param {string} name template name - * @returns {Function} template function - */ -function getTemplate(Handlebars, themeModule, name) { - try { - return Handlebars - .compile(fs.readFileSync(path.join(themeModule, name), 'utf8')); - } catch (e) { - throw new Error('Template file ' + name + ' missing'); - } -} - -module.exports = getTemplate; diff --git a/lib/html_helpers.js b/lib/html_helpers.js deleted file mode 100644 index 939ce40de..000000000 --- a/lib/html_helpers.js +++ /dev/null @@ -1,151 +0,0 @@ -'use strict'; - -var getDoc = require('globals-docs').getDoc, - formatType = require('./format_type'), - mdast = require('mdast'), - html = require('mdast-html'), - inlineLex = require('jsdoc-inline-lex'); - -/** - * Format link & tutorial tags with simple code inline tags. - * - * @param {Array} paths potential linkable namepaths - * @param {string} text input - typically a description - * @returns {string} markdown-friendly output - * @private - * @example - * formatInlineTags('{@link Foo}'); // "[Foo](#foo)" - */ -function formatInlineTags(paths, text) { - var output = ''; - var tokens = inlineLex(text); - - for (var i = 0; i < tokens.length; i++) { - if (tokens[i].type === 'text') { - output += tokens[i].capture[0]; - } else if (tokens[i].type === 'link') { - var described = tokens[i].capture[1].match(/([^\s|\|]*)(\s|\|)(.*)/); - if (described) { - // 1 is the match, 3 is description - output += autolink(paths, described[1], described[3]); - } else { - output += autolink(paths, tokens[i].capture[1]); - } - } else if (tokens[i].type === 'prefixLink') { - output += autolink(paths, tokens[i].capture[1], tokens[i].capture[2]); - } - } - - return output; -} - -/** - * Format a parameter name. This is used in formatParameters - * and just needs to be careful about differentiating optional - * parameters - * - * @param {Object} param a param as a type spec - * @returns {string} formatted parameter representation. - */ -function formatParameter(param) { - return (param.type && param.type.type === 'OptionalType') ? - '[' + param.name + ']' : param.name; -} - -/** - * Format the parameters of a function into a quickly-readable - * summary that resembles how you would call the function - * initially. - * - * @returns {string} formatted parameters - */ -function formatParameters() { - if (!this.params) { - return ''; - } - return '(' + this.params.map(function (param) { - return formatParameter(param); - }).join(', ') + ')'; -} - -/** - * Link text to this page or to a central resource. - * @param {Array} paths list of valid namespace paths that are linkable - * @param {string} text inner text of the link - * @returns {string?} potentially a url - */ -function getNamedLink(paths, text) { - if (paths.indexOf(text) !== -1) { - return '#' + text; - } else if (getDoc(text)) { - return getDoc(text); - } -} - -/** - * Link text to this page or to a central resource. - * @param {Array} paths list of valid namespace paths that are linkable - * @param {string} text inner text of the link - * @param {string} description link text override - * @returns {string} potentially linked HTML - */ -function autolink(paths, text, description) { - var url = getNamedLink(paths, text); - if (url) { - return '' + (description || text) + ''; - } - return text; -} - -/** - * Given a Handlebars instance, register helpers - * - * @param {Object} Handlebars template instance - * @param {Array} paths list of valid namespace paths that are linkable - * @returns {undefined} invokes side effects on Handlebars - */ -function htmlHelpers(Handlebars, paths) { - - Handlebars.registerHelper('permalink', function () { - return this.path.join('.'); - }); - - Handlebars.registerHelper('autolink', autolink.bind(autolink, paths)); - - Handlebars.registerHelper('format_params', formatParameters); - - var htmlOptions = { - entities: false - }; - - /** - * This helper is exposed in templates as `md` and is useful for showing - * Markdown-formatted text as proper HTML. - * - * @name formatMarkdown - * @param {string} string - * @returns {string} string - * @example - * var x = '## foo'; - * // in template - * // {{ md x }} - * // generates

foo

- */ - Handlebars.registerHelper('md', function formatMarkdown(string) { - return new Handlebars.SafeString(mdast().use(html, htmlOptions) - .process(formatInlineTags(paths, string))); - }); - - Handlebars.registerHelper('format_type', function (type) { - if (!type) { - return ''; - } - return new Handlebars.SafeString(mdast().use(html, htmlOptions) - .stringify({ - type: 'root', - children: formatType(type, paths) - })); - }); -} - -module.exports = htmlHelpers; diff --git a/lib/output/html.js b/lib/output/html.js index c2a24dd8c..5d5f2bfe8 100644 --- a/lib/output/html.js +++ b/lib/output/html.js @@ -1,13 +1,7 @@ 'use strict'; -var File = require('vinyl'), - vfs = require('vinyl-fs'), - concat = require('concat-stream'), - Handlebars = require('handlebars'), - walk = require('../walk'), - getTemplate = require('../get_template'), - resolveTheme = require('../resolve_theme'), - helpers = require('../html_helpers'), +var walk = require('../walk'), + path = require('path'), hljs = require('highlight.js'); /** @@ -49,7 +43,7 @@ function highlight(comment, options) { * * @param {Array} comments parsed comments * @param {Object} options Options that can customize the output - * @param {string} [options.theme] Name of a module used for an HTML theme. + * @param {string} [options.theme='documentation-theme-default'] Name of a module used for an HTML theme. * @param {Function} callback called with array of results as vinyl-fs objects * @returns {undefined} calls callback * @name html @@ -57,30 +51,9 @@ function highlight(comment, options) { module.exports = function makeHTML(comments, options, callback) { options = options || {}; comments = walk(comments, highlight, options); - - var themeModule = resolveTheme(options.theme); - var pageTemplate = getTemplate(Handlebars, themeModule, 'index.hbs'); - - Handlebars.registerPartial('section', - getTemplate(Handlebars, themeModule, 'section.hbs')); - - var paths = comments.map(function (comment) { - return comment.path.join('.'); - }).filter(function (path) { - return path; - }); - - helpers(Handlebars, paths); - - // push assets into the pipeline as well. - vfs.src([themeModule + '/assets/**'], { base: themeModule }) - .pipe(concat(function (files) { - callback(null, files.concat(new File({ - path: 'index.html', - contents: new Buffer(pageTemplate({ - docs: comments, - options: options - }), 'utf8') - }))); - })); + var theme = require('documentation-theme-default'); + if (options.theme) { + theme = require(path.resolve(process.cwd(), options.theme)); + } + theme(comments, options, callback); }; diff --git a/lib/output/markdown_ast.js b/lib/output/markdown_ast.js index fc7b60a67..3087dd02b 100644 --- a/lib/output/markdown_ast.js +++ b/lib/output/markdown_ast.js @@ -1,6 +1,6 @@ var mdast = require('mdast'), u = require('unist-builder'), - formatType = require('../format_type'), + formatType = require('documentation-theme-utils').formatType, formatInlineTags = require('../format_inline_tags'), hljs = require('highlight.js'); diff --git a/lib/resolve_theme.js b/lib/resolve_theme.js deleted file mode 100644 index 2de547fda..000000000 --- a/lib/resolve_theme.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -var path = require('path'), - resolve = require('resolve'); - -/** - * Given the name of a theme as a module, return the directory it - * resides in, or throw an error if it is not found - * @param {string} [theme='documentation-theme-default'] the module name - * @throws {Error} if theme is not found - * @returns {string} directory - */ -function resolveTheme(theme) { - var basedir = theme ? process.cwd() : __dirname; - - theme = theme || 'documentation-theme-default'; - - try { - return path.dirname(resolve.sync(theme, { basedir: basedir })); - } catch (e) { - throw new Error('Theme ' + theme + ' not found'); - } -} - -module.exports = resolveTheme; diff --git a/package.json b/package.json index e56ce4ed0..928509bdc 100644 --- a/package.json +++ b/package.json @@ -15,18 +15,16 @@ "concat-stream": "^1.5.0", "debounce": "^1.0.0", "doctrine": "^0.7.1", - "documentation-theme-default": "^1.0.2", + "documentation-theme-default": "2.0.0", + "documentation-theme-utils": "^1.0.1", "events": "^1.1.0", "extend": "^3.0.0", "get-comments": "^1.0.1", "github-url-from-git": "^1.4.0", - "globals-docs": "^2.1.0", - "handlebars": "^3.0.0", "highlight.js": "^8.4.0", "js-yaml": "^3.3.1", "jsdoc-inline-lex": "^1.0.1", "mdast": "^2.0.0", - "mdast-html": "^1.2.1", "mdast-toc": "^1.1.0", "micromatch": "^2.1.6", "mime": "^1.3.4", @@ -42,7 +40,6 @@ "vfile": "^1.1.2", "vfile-reporter": "^1.4.1", "vfile-sort": "^1.0.0", - "vinyl": "^0.5.0", "vinyl-fs": "^1.0.0", "yargs": "^3.31.0" }, diff --git a/test/bin.js b/test/bin.js index 62e4b171a..c4273df9b 100644 --- a/test/bin.js +++ b/test/bin.js @@ -207,6 +207,20 @@ test('write to html', function (t) { }, false); }, options); +test('write to html with custom theme', function (t) { + + var dstDir = path.join(os.tmpdir(), (Date.now() + Math.random()).toString()); + fs.mkdirSync(dstDir); + + documentation(['build -t fixture/custom_theme --shallow fixture/internal.input.js -f html -o ' + dstDir], {}, + function (err, data) { + t.error(err); + t.equal(data, ''); + t.ok(fs.readFileSync(path.join(dstDir, 'index.html'), 'utf8'), 'Hello world'); + t.end(); + }, false); +}, options); + test('write to html, highlightAuto', function (t) { var fixture = 'fixture/auto_lang_hljs/multilanguage.input.js', diff --git a/test/fixture/custom_theme/index.js b/test/fixture/custom_theme/index.js new file mode 100644 index 000000000..db1e3a385 --- /dev/null +++ b/test/fixture/custom_theme/index.js @@ -0,0 +1,13 @@ +var File = require('vinyl'); + +/** + * This is a theme only used by documentation to test custom theme + * support. + */ +module.exports = function(comments, options, callback) { + return callback(null, [new File({ + base: '/', + path: '/index.html', + contents: new Buffer('Hello world') + })]); +}; diff --git a/test/lib/format_type.js b/test/lib/format_type.js deleted file mode 100644 index 1d48eeef3..000000000 --- a/test/lib/format_type.js +++ /dev/null @@ -1,46 +0,0 @@ -/*eslint max-len: 0 */ -'use strict'; - -var formatType = require('../../lib/format_type'), - mdast = require('mdast'), - parseParamType = require('doctrine/lib/typed').parseParamType, - test = require('tap').test; - -test('formatType', function (t) { - [ - ['Foo', 'Foo'], - ['null', 'null'], - ['null', 'null'], - ['*', 'Any'], - ['Array|undefined', '([Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)|[undefined](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined))'], - ['Array', '[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array).<[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)>'], - ['number!', '[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)!'], - ['function(string, boolean)', 'function ([string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), [boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean))'], - ['function(string, boolean): number', 'function ([string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), [boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)): [number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)'], - ['function()', 'function ()'], - ['function(this:something, string)', 'function (this: something, [string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String))'], - ['function(new:something)', 'function (new: something)'], - ['{myNum: number, myObject}', '{myNum: [number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number), myObject}'], - ['[string,]', '[[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)]'], - ['number?', '[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?'], - ['?number', '?[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)'], - ['?', '?'], - ['void', 'void'], - ['function(a:b)', 'function (a: b)'], - ['function(a):void', 'function (a): void'], - ['number=', '[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)='], - ['...number', '...[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)'], - ['undefined', '[undefined](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined)'] - ].forEach(function (example) { - t.deepEqual(mdast().stringify({ - type: 'paragraph', - children: formatType(parseParamType(example[0])) - }), example[1], example[0]); - }); - - t.throws(function () { - formatType({}); - }); - - t.end(); -}); diff --git a/test/lib/get_template.js b/test/lib/get_template.js deleted file mode 100644 index da13e62cd..000000000 --- a/test/lib/get_template.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -var getTemplate = require('../../lib/get_template.js'), - Handlebars = require('handlebars'), - test = require('tap').test; - -test('getTemplate', function (t) { - - t.throws(function () { - getTemplate(Handlebars, 'DOES_NOT_EXIST', 'foo'); - }, 'Template file foo missing'); - - t.end(); -}); diff --git a/test/lib/resolve_theme.js b/test/lib/resolve_theme.js deleted file mode 100644 index 8cc223d77..000000000 --- a/test/lib/resolve_theme.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -var test = require('tap').test, - resolveTheme = require('../../lib/resolve_theme'); - -test('resolveTheme', function (t) { - - t.throws(function () { - resolveTheme('INVALID-THEME'); - }); - - t.ok(resolveTheme('documentation-theme-default'), 'finds default'); - - t.end(); -});