From 1c68d98b21ddd7e736a8b73629e946f523856a01 Mon Sep 17 00:00:00 2001 From: Nils Knappmeier Date: Fri, 24 Mar 2017 11:44:22 +0100 Subject: [PATCH] More documentation (especially API-docs of the helpers) --- .thought/partials/usage.md.hbs | 15 +- .thought/templates/docs/helpers.md.hbs | 6 + README.md | 32 +- docs/helpers.md | 458 ++++++++++++++++++ handlebars/helpers.js | 626 ++++++++++++++----------- package.json | 1 + 6 files changed, 852 insertions(+), 286 deletions(-) create mode 100644 .thought/templates/docs/helpers.md.hbs create mode 100644 docs/helpers.md diff --git a/.thought/partials/usage.md.hbs b/.thought/partials/usage.md.hbs index d4197e7..84a967a 100644 --- a/.thought/partials/usage.md.hbs +++ b/.thought/partials/usage.md.hbs @@ -87,6 +87,13 @@ You can now create a directory `.thought` in the your project root, that has the If you create a file `.thought/partials/contributing.md.hbs`, it will replace the default `partials/contributing.md.hbs` file. Same for templates. +#### Handlebars-Helpers + +Thought comes with a default set of Handlebars-helpers, that can be called +from within templates and partials. +Have a look at the [API docs of the builtin-helpers](docs/helpers.md). All helpers that are +[built-into Handlebars](http://handlebarsjs.com/builtin_helpers.html) are also available, of course. + ### Customizing the preprocessor Thought uses `preprocessor.js` function to extend the `package.json` before passing it to the handlebars @@ -96,7 +103,6 @@ your custom function. Some partials and template rely on the data created there. ### Customizing helpers -Thought comes with a default set of Handlebars-helpers that are called from within the template. If you want to provide your own helpers (for example to perform some project-specific computations) you can create a file named `.thought/helpers.js` in your project. This file should export an object of helper-functions like @@ -124,8 +130,11 @@ function. ### Authoring modules -Have a look at {{npm 'customize-engine-handlebars'}} for an example on how to write a module. For -a real example have a look at {{npm 'thought-plugin-jsdoc'}}. +Thought uses {{npm 'customize-engine-handlebars'}} under the hood, so the documentation +of this module is a good starting-point, if you want to know how to create override-configurations. +Have a look at for an example on how to write a module. + +For a real example of a thought-plugin have a look at {{npm 'thought-plugin-jsdoc'}}. ### Using modules diff --git a/.thought/templates/docs/helpers.md.hbs b/.thought/templates/docs/helpers.md.hbs new file mode 100644 index 0000000..05d367d --- /dev/null +++ b/.thought/templates/docs/helpers.md.hbs @@ -0,0 +1,6 @@ +## Helpers API + +The following helpers are built-in for use in templates and partials + +{{jsdoc 'handlebars/helpers.js'}} + diff --git a/README.md b/README.md index 6188a1f..b47dc52 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,13 @@ You can now create a directory `.thought` in the your project root, that has the If you create a file `.thought/partials/contributing.md.hbs`, it will replace the default `partials/contributing.md.hbs` file. Same for templates. +#### Handlebars-Helpers + +Thought comes with a default set of Handlebars-helpers, that can be called +from within templates and partials. +Have a look at the [API docs of the builtin-helpers](docs/helpers.md). All helpers that are +[built-into Handlebars](http://handlebarsjs.com/builtin_helpers.html) are also available, of course. + ### Customizing the preprocessor Thought uses `preprocessor.js` function to extend the `package.json` before passing it to the handlebars @@ -202,7 +209,6 @@ your custom function. Some partials and template rely on the data created there. ### Customizing helpers -Thought comes with a default set of Handlebars-helpers that are called from within the template. If you want to provide your own helpers (for example to perform some project-specific computations) you can create a file named `.thought/helpers.js` in your project. This file should export an object of helper-functions like @@ -230,8 +236,11 @@ function. ### Authoring modules -Have a look at [customize-engine-handlebars](https://npmjs.com/package/customize-engine-handlebars) for an example on how to write a module. For -a real example have a look at [thought-plugin-jsdoc](https://npmjs.com/package/thought-plugin-jsdoc). +Thought uses [customize-engine-handlebars](https://npmjs.com/package/customize-engine-handlebars) under the hood, so the documentation +of this module is a good starting-point, if you want to know how to create override-configurations. +Have a look at for an example on how to write a module. + +For a real example of a thought-plugin have a look at [thought-plugin-jsdoc](https://npmjs.com/package/thought-plugin-jsdoc). ### Using modules @@ -279,6 +288,23 @@ projects... +# API reference + + + +## thought(options) +Execute Thought in the current directory + +**Kind**: global function +**Api**: public + +| Param | Type | Description | +| --- | --- | --- | +| options | object | | +| [options.cwd] | string | the working directory to use as project root | +| [options.addToGit] | boolean | add created files to git | + + ## License diff --git a/docs/helpers.md b/docs/helpers.md new file mode 100644 index 0000000..eaa41ee --- /dev/null +++ b/docs/helpers.md @@ -0,0 +1,458 @@ +## Helpers API + +The following helpers are built-in for use in templates and partials + +## Functions + +
+
json(obj)string
+

Display an object as indented JSON-String.

+

This is mainly for testing purposes when adapting templates

+
+
include(filename, language)string
+

Include a file into a markdown code-block

+
+
includeRaw(filename)
+

Directly include a file without markdown fences.

+
+
example(filename)
+

Includes an example file into the template, replacing require() calls to the current +module by require('module-name') (only single-quotes are replaced)

+

If your file is examples/example.js, you would do

+
var fn = require('../')
+
+

to load your module. That way, you get an executable script. +The helper will when include

+
var fn = require('module-name')
+
+

in your docs, which is what a user of the module will do.

+
+
exists(filename)boolean
+

Return true if a file exists

+
+
exec(command, options)string
+

Execute a command and include the output in a fenced code-block.

+
+
dirTree(globPattern, [baseDir], options)string
+

Return a drawing of a directory tree (using archy)

+
+
renderTree(object, options)string
+

Render an object hierarchy.

+

The expected input is of the form

+
{
+  prop1: 'value',
+  prop2: 'value',
+  ...,
+  children: [
+    {
+       prop1: 'value',
+       propt2: 'value',
+       ...,
+       children: ...
+    }
+  ]
+}
+

The tree is transformed and rendered using archy

+
+
withPackageOf(filePath, options)
+

Set special variable for accessing information from the context of a file (possibly in a dependency)

+

This block helper executes the block in the current context but sets special variables:

+
    +
  • @url: The github-url of the given file in the current package version is stored into
  • +
  • @packageThe package.json of the file's module is stored into
  • +
+
+
npm(packageName)
+

Create a link to the npm-page of a package

+
+
htmlId(value)string
+

Convert a name into a valid id (like github does with header names)

+

This helper creates valid html-ids from header name. It attempts to +follow the same rules that github uses to convert header names (h1, h2) into the +hash-part of the URL referencing this header.

+
htmlId('abc') === 'abc'
+htmlId('abc cde') === 'abc-cde' // Replace spaces by '-'
+htmlId('a$b&c%d') === 'abcd'  // Remove all characters execpt alpahnumericals and minus
+htmlId('mäxchen' === 'mäxchen' // Do not remove german umlauts
+htmlId('ハッピークリスマス') === 'ハッピークリスマス' // Do not remove japanase word characters
+htmlId('ABCDE') === 'abcde'   // Convert to lowercase
+
+
+
hasCoveralls()boolean
+

Check, if coveralls.io is configured in this package

+

Check the .travis.yml and the appveyor.yml files for the string 'coveralls' +and return true if any of them exists and contains the string. +We expect coveralls to be configured then.

+
+
hasGreenkeeper(options)
+

Check, if Greenkeeper is enabled for this repository

+

This is done by analyzing the greenkeeper.io-badge

+
+
transformTree(object, fn)
+

Transfrom an object hierarchy into archy's format

+

Transform a tree-structure of the form

+
{
+  prop1: 'value',
+  prop2: 'value',
+  ...,
+  children: [
+    {
+       prop1: 'value',
+       propt2: 'value',
+       ...,
+       children: ...
+    }
+  ]
+}
+

Into an archy-compatible format, by passing each node to a block-helper function. +The result of the function should be a string which is then used as label for the node.

+
+
treeFromPathComponents(files, label)object
+

Transform an array of path components into an archy-compatible tree structure.

+
[ [ 'abc', 'cde', 'efg' ], [ 'abc','cde','abc'], ['abc','zyx'] ]
+

becomes

+
{
+  label: 'abc',
+  nodes: [
+        {
+          label: 'cde',
+          nodes: [
+            'efg',
+            'abc'
+          ]
+        },
+        'zyx'
+  ]
+}
+

Nodes with a single subnode are collapsed and the resulting node gets the label node/subnode.

+
+
github(filePath)string
+

Resolve the display-URL of a file on github.

+

This works for files in the current package and in dependencies, as long as the repository-url is +set correctly in package.json

+
+
githubRepo()string
+

Returns the current repository group and name (e.g. nknapp/thought for this project)

+
+
+ + + +## json(obj) ⇒ string +Display an object as indented JSON-String. + +This is mainly for testing purposes when adapting templates + +**Kind**: global function +**Returns**: string - JSON.stringify() +**Api**: public + +| Param | Type | Description | +| --- | --- | --- | +| obj | object | the object | + + + +## include(filename, language) ⇒ string +Include a file into a markdown code-block + +**Kind**: global function +**Api**: public + +| Param | Description | +| --- | --- | +| filename | | +| language | the programming language used for the code-block | + + + +## includeRaw(filename) +Directly include a file without markdown fences. + +**Kind**: global function + +| Param | +| --- | +| filename | + + + +## example(filename) +Includes an example file into the template, replacing `require()` calls to the current +module by `require('module-name')` (only single-quotes are replaced) + +If your file is `examples/example.js`, you would do +```js +var fn = require('../') +``` +to load your module. That way, you get an executable script. +The helper will when include +```js +var fn = require('module-name') +``` +in your docs, which is what a user of the module will do. + +**Kind**: global function +**Api**: public + +| Param | Type | Description | +| --- | --- | --- | +| filename | string | the name of the example file | +| options.hash | object | | +| options.hash.snippet | boolean | If set to true, only the lines between <snip> and </snip> will be included | + + + +## exists(filename) ⇒ boolean +Return true if a file exists + +**Kind**: global function +**Returns**: boolean - true, if the file or diectory exists +**Api**: public + +| Param | Type | Description | +| --- | --- | --- | +| filename | string | the path to the file | + + + +## exec(command, options) ⇒ string +Execute a command and include the output in a fenced code-block. + +**Kind**: global function +**Returns**: string - the output of `execSync`, enclosed in fences. +**Api**: public + +| Param | Type | Description | +| --- | --- | --- | +| command | string | the command, passed to `child-process#execSync()` | +| options | object | optional arguments and Handlebars internal args. | +| options.hash.lang | string | the language tag that should be attached to the fence (like `js` or `bash`). If this is set to `raw`, the output is included as-is, without fences. | +| options.hash.cwd | string | the current working directory of the example process | + + + +## dirTree(globPattern, [baseDir], options) ⇒ string +Return a drawing of a directory tree (using [archy](https://www.npmjs.com/package/archy)) + +**Kind**: global function +**Returns**: string - a display of the directory tree of the selected files and directories. +**Api**: public + +| Param | Type | Description | +| --- | --- | --- | +| globPattern | string | a pattern describing all files and directories to include into the tree-view. | +| [baseDir] | string | the base directory from which the `globPattern` is applied. | +| options | object | passsed in by Handlebars | + + + +## renderTree(object, options) ⇒ string +Render an object hierarchy. + +The expected input is of the form + +``` +{ + prop1: 'value', + prop2: 'value', + ..., + children: [ + { + prop1: 'value', + propt2: 'value', + ..., + children: ... + } + ] +} +``` + +The tree is transformed and rendered using [archy](https://www.npmjs.com/package/archy) + +**Kind**: global function +**Api**: public + +| Param | Type | Description | +| --- | --- | --- | +| object | | | +| options | | | +| options.fn | function | computes the label for a node based on the node itself | + + + +## withPackageOf(filePath, options) +Set special variable for accessing information from the context of a file (possibly in a dependency) + +This block helper executes the block in the current context but sets special variables: + +* `@url`: The github-url of the given file in the current package version is stored into +* `@package`The `package.json` of the file's module is stored into + +**Kind**: global function +**Api**: public + +| Param | Type | Description | +| --- | --- | --- | +| filePath | string | file that is used to find that package.json | +| options | object | options passed in by Handlebars | + + + +## npm(packageName) +Create a link to the npm-page of a package + +**Kind**: global function +**Api**: public + +| Param | Type | Description | +| --- | --- | --- | +| packageName | string | the name of the package | + + + +## htmlId(value) ⇒ string +Convert a name into a valid id (like github does with header names) + +This helper creates valid html-ids from header name. It attempts to +follow the same rules that github uses to convert header names (h1, h2) into the +hash-part of the URL referencing this header. + +```js +htmlId('abc') === 'abc' +htmlId('abc cde') === 'abc-cde' // Replace spaces by '-' +htmlId('a$b&c%d') === 'abcd' // Remove all characters execpt alpahnumericals and minus +htmlId('mäxchen' === 'mäxchen' // Do not remove german umlauts +htmlId('ハッピークリスマス') === 'ハッピークリスマス' // Do not remove japanase word characters +htmlId('ABCDE') === 'abcde' // Convert to lowercase +``` + +**Kind**: global function +**Returns**: string - a string value +**Api**: public + +| Param | Type | Description | +| --- | --- | --- | +| value | string | the input value of the URL (e.g. the header name) | + + + +## hasCoveralls() ⇒ boolean +Check, if [coveralls.io](https://coveralls.io) is configured in this package + +Check the .travis.yml and the appveyor.yml files for the string 'coveralls' +and return true if any of them exists and contains the string. +We expect coveralls to be configured then. + +**Kind**: global function +**Returns**: boolean - true, if coveralls is configured +**Api**: public + + +## hasGreenkeeper(options) +Check, if [Greenkeeper](https://greenkeeper.io) is enabled for this repository + +This is done by analyzing the greenkeeper.io-[badge](https://badges.greenkeeper.io/nknapp/thought.svg) + +**Kind**: global function +**Api**: public + +| Param | Type | Description | +| --- | --- | --- | +| options | object | options passed in by Handlebars | + + + +## transformTree(object, fn) +Transfrom an object hierarchy into `archy`'s format + +Transform a tree-structure of the form +``` +{ + prop1: 'value', + prop2: 'value', + ..., + children: [ + { + prop1: 'value', + propt2: 'value', + ..., + children: ... + } + ] +} +``` +Into an [archy](https://www.npmjs.com/package/archy)-compatible format, by passing each node to a block-helper function. +The result of the function should be a string which is then used as label for the node. + +**Kind**: global function +**Api**: private + +| Param | Type | Description | +| --- | --- | --- | +| object | object | the tree data | +| fn | | the block-helper function (options.fn) of Handlebars (http://handlebarsjs.com/block_helpers.html) | + + + +## treeFromPathComponents(files, label) ⇒ object +Transform an array of path components into an [archy](https://www.npmjs.com/package/archy)-compatible tree structure. + +``` +[ [ 'abc', 'cde', 'efg' ], [ 'abc','cde','abc'], ['abc','zyx'] ] +``` + +becomes + +``` +{ + label: 'abc', + nodes: [ + { + label: 'cde', + nodes: [ + 'efg', + 'abc' + ] + }, + 'zyx' + ] +} +``` + +Nodes with a single subnode are collapsed and the resulting node gets the label `node/subnode`. + +**Kind**: global function +**Returns**: object - a tree structure as needed by [archy](https://www.npmjs.com/package/archy) +**Api**: private + +| Param | Type | Description | +| --- | --- | --- | +| files | Array.<Array.<string>> | an array of filenames, split by `path.sep` | +| label | string | the label for the current tree node | + + + +## github(filePath) ⇒ string +Resolve the display-URL of a file on github. + +This works for files in the current package and in dependencies, as long as the repository-url is +set correctly in package.json + +**Kind**: global function +**Returns**: string - the URL +**Api**: public + +| Param | Type | Description | +| --- | --- | --- | +| filePath | string | the path to the file | + + + +## githubRepo() ⇒ string +Returns the current repository group and name (e.g. `nknapp/thought` for this project) + +**Kind**: global function +**Api**: public + + diff --git a/handlebars/helpers.js b/handlebars/helpers.js index 4aeb1a5..5016180 100644 --- a/handlebars/helpers.js +++ b/handlebars/helpers.js @@ -1,296 +1,343 @@ -var path = require('path') -var cp = require('child_process') -var _ = require('lodash') -var debug = require('debug')('thought:helpers') -var Promise = require('bluebird') -var glob = Promise.promisify(require('glob')) -var findPackage = require('find-package') -var Handlebars = require('handlebars') -var qfs = require('m-io/fs') -var util = require('util') -var Q = require('q') +const path = require('path') +const cp = require('child_process') +const _ = require('lodash') +const debug = require('debug')('thought:helpers') +const Promise = require('bluebird') +const glob = Promise.promisify(require('glob')) +const findPackage = require('find-package') +const Handlebars = require('handlebars') +const qfs = require('m-io/fs') +const util = require('util') +const Q = require('q') module.exports = { + json, + include, + includeRaw, + example, + exists, + exec, + dirTree, + renderTree, + withPackageOf, + github, + githubRepo, + npm, + htmlId, + hasCoveralls, + hasGreenkeeper +} - /** - * Display an object as indented JSON-String. - * This is mainly for testing purposes when adapting templates - * @param {object} obj the object - * @returns {string} JSON.stringify() - */ - json: function (obj) { - return '```json\n' + JSON.stringify(obj, null, 2) + '\n```\n' - }, +/** + * Display an object as indented JSON-String. + * + * This is mainly for testing purposes when adapting templates + * @param {object} obj the object + * @returns {string} JSON.stringify() + * @api public + */ +function json (obj) { + return '```json\n' + JSON.stringify(obj, null, 2) + '\n```\n' +} - /** - * Include a file into a markdown code-block - * @param filename - * @param language the programming language used for the code-block - * @returns {string} - */ - include: function (filename, language) { - return qfs.read(filename).then(function (contents) { - return '```' + - (_.isString(language) ? language : path.extname(filename).substr(1)) + - '\n' + - contents + - '\n```\n' - }) - }, +/** + * Include a file into a markdown code-block + * + * @param filename + * @param language the programming language used for the code-block + * @returns {string} + * @api public + */ +function include (filename, language) { + return qfs.read(filename).then(function (contents) { + return '```' + + (_.isString(language) ? language : path.extname(filename).substr(1)) + + '\n' + + contents + + '\n```\n' + }) +} - /** - * Directly include a file. - * @param filename - */ - includeRaw: function (filename) { - return qfs.read(filename) - }, +/** + * Directly include a file without markdown fences. + * + * @param filename + */ +function includeRaw (filename) { + return qfs.read(filename) +} - /** - * Includes an example file into the template, replacing `require()` calls to the current - * module by `require('module-name')` (only single-quotes are replaced) - * If your file is `examples/example.js`, you would do - * ```js - * var fn = require('../') - * ``` - * to load your module. That way, you get an executable script. - * The helper will when include - * ```js - * var fn = require('module-name') - * ``` - * in your docs, which is what a user of the module will do. - * - * @param {string} filename the name of the example file - * @param {object} options.hash - * @param {boolean} options.hash.snippet If set to true, only the lines between - * <snip> and </snip> will be included - * @api public - */ - example: function (filename, options) { - return qfs.read(filename) - .then(function (contents) { - // Relative path to the current module (e.g. "../"). This path must be replaced - // by the module name in the - var modulePath = path.relative(path.dirname(filename), '.') - debug('example modulepath', modulePath) - var requireModuleRegex = new RegExp(regex`require\('${modulePath}/?(.*?)'\)`, 'g') - if (options && options.hash && options.hash.snippet) { - contents = contents.match(/------.*\n([\S\s]*?)\n.*---<\/snip>---/)[1] - } +/** + * Includes an example file into the template, replacing `require()` calls to the current + * module by `require('module-name')` (only single-quotes are replaced) + * + * If your file is `examples/example.js`, you would do + * ```js + * var fn = require('../') + * ``` + * to load your module. That way, you get an executable script. + * The helper will when include + * ```js + * var fn = require('module-name') + * ``` + * in your docs, which is what a user of the module will do. + * + * @param {string} filename the name of the example file + * @param {object} options.hash + * @param {boolean} options.hash.snippet If set to true, only the lines between + * <snip> and </snip> will be included + * @api public + */ +function example (filename, options) { + return qfs.read(filename) + .then(function (contents) { + // Relative path to the current module (e.g. "../"). This path must be replaced + // by the module name in the + const modulePath = path.relative(path.dirname(filename), '.') + debug('example modulepath', modulePath) + const requireModuleRegex = new RegExp(regex`require\('${modulePath}/?(.*?)'\)`, 'g') + if (options && options.hash && options.hash.snippet) { + contents = contents.match(/------.*\n([\S\s]*?)\n.*---<\/snip>---/)[1] + } - return util.format('```%s\n%s\n```', - path.extname(filename).substr(1), - contents.trim().replace(requireModuleRegex, function (match, suffix) { - return `require('${require(process.cwd() + '/package').name}${suffix ? '/' + suffix : ''}')` - }) - ) - }) - }, + return util.format('```%s\n%s\n```', + path.extname(filename).substr(1), + contents.trim().replace(requireModuleRegex, function (match, suffix) { + return `require('${require(process.cwd() + '/package').name}${suffix ? '/' + suffix : ''}')` + }) + ) + }) +} - /** - * Return true if a file exists - * @param {string} filename the path to the file - * @return {boolean} true, if the file or diectory exists - * @api public - */ - exists: function (filename) { - return qfs.exists(filename) - }, +/** + * Return true if a file exists + * + * @param {string} filename the path to the file + * @return {boolean} true, if the file or diectory exists + * @api public + */ +function exists (filename) { + return qfs.exists(filename) +} - /** - * Execute a command and include the output in a fenced code-block. - * - * @param {string} command the command, passed to `child-process#execSync()` - * @param {object} options optional arguments and Handlebars internal args. - * @param {string} options.hash.lang the language tag that should be attached to the fence - * (like `js` or `bash`). If this is set to `raw`, the output is included as-is, without fences. - * @param {string} options.hash.cwd the current working directory of the example process - * @returns {string} the output of `execSync`, enclosed in fences. - * @api public - */ - exec: function (command, options) { - var start - var end - var lang = options.hash && options.hash.lang - switch (lang) { - case 'raw': - start = end = '' - break - case 'inline': - start = end = '`' - break - default: - var fenceLanguage = _.isString(lang) ? lang : '' - start = '```' + fenceLanguage + '\n' - end = '\n```' - } - var output = cp.execSync(command, { - encoding: 'utf8', - cwd: options.hash && options.hash.cwd - }) - return start + output.trim() + end - }, +/** + * Execute a command and include the output in a fenced code-block. + * + * @param {string} command the command, passed to `child-process#execSync()` + * @param {object} options optional arguments and Handlebars internal args. + * @param {string} options.hash.lang the language tag that should be attached to the fence + * (like `js` or `bash`). If this is set to `raw`, the output is included as-is, without fences. + * @param {string} options.hash.cwd the current working directory of the example process + * @returns {string} the output of `execSync`, enclosed in fences. + * @api public + */ +function exec (command, options) { + let start + let end + const lang = options.hash && options.hash.lang + switch (lang) { + case 'raw': + start = end = '' + break + case 'inline': + start = end = '`' + break + default: + const fenceLanguage = _.isString(lang) ? lang : '' + start = '```' + fenceLanguage + '\n' + end = '\n```' + } + const output = cp.execSync(command, { + encoding: 'utf8', + cwd: options.hash && options.hash.cwd + }) + return start + output.trim() + end +} - /** - * Return a drawing of a directory tree (using [archy](https://www.npmjs.com/package/archy)) - * @param {string} globPattern a pattern describing all files and directories to include into the tree-view. - * @param {string=} baseDir the base directory from which the `globPattern` is applied. - * @returns {string} a display of the directory tree of the selected files and directories. - * @api public - */ - dirTree: function (baseDir, globPattern, options) { - // Is basedir is not a string, it is probably the handlebars "options" object - if (!_.isString(globPattern)) { - globPattern = '**' - } +/** + * Return a drawing of a directory tree (using [archy](https://www.npmjs.com/package/archy)) + * + * @param {string} globPattern a pattern describing all files and directories to include into the tree-view. + * @param {string=} baseDir the base directory from which the `globPattern` is applied. + * @param {object} options passsed in by Handlebars + * @returns {string} a display of the directory tree of the selected files and directories. + * @api public + */ +function dirTree (baseDir, globPattern, options) { + // Is basedir is not a string, it is probably the handlebars "options" object + if (!_.isString(globPattern)) { + globPattern = '**' + } - const label = options && options.data && options.data.label - return glob(globPattern, {cwd: baseDir, mark: true}) - .then((files) => { - debug('dirTree glob result', files) - if (files.length === 0) { - throw new Error('Cannot find a single file for \'' + globPattern + '\' in \'' + baseDir + '\'') - } - files.sort() - // Split paths into components - const pathComponents = files.map((file) => { - // a/b/c or a/b/dir/ - return file.split(path.sep) - // a, b, c or a, b, dir, '' - .map((component, index, all) => component + (index < all.length - 1 ? '/' : '')) - // a/, b/, c or a/, b/, dir/, '' - .filter(component => component) // Filter empty parts - }) - const treeObject = treeFromPathComponents(pathComponents, label) - const tree = require('archy')(treeObject) - return '
\n' + tree.trim() + '\n
' + const label = options && options.data && options.data.label + return glob(globPattern, {cwd: baseDir, mark: true}) + .then((files) => { + debug('dirTree glob result', files) + if (files.length === 0) { + throw new Error('Cannot find a single file for \'' + globPattern + '\' in \'' + baseDir + '\'') + } + files.sort() + // Split paths into components + const pathComponents = files.map((file) => { + // a/b/c or a/b/dir/ + return file.split(path.sep) + // a, b, c or a, b, dir, '' + .map((component, index, all) => component + (index < all.length - 1 ? '/' : '')) + // a/, b/, c or a/, b/, dir/, '' + .filter(component => component) // Filter empty parts }) - }, - - /** - * Render an object hierarchy of the form - * - * ``` - * { - * prop1: 'value', - * prop2: 'value', - * ..., - * children: [ - * { - * prop1: 'value', - * propt2: 'value', - * ..., - * children: ... - * } - * ] - * } - * ``` - * @param object - * @param options - * @param {function} options.fn computes the label for a node based on the node itself - * @returns {string} - */ - renderTree: function (object, options) { - var tree = require('archy')(transformTree(object, options.fn)) - return '
\n' + tree + '
' - }, + const treeObject = treeFromPathComponents(pathComponents, label) + const tree = require('archy')(treeObject) + return '
\n' + tree.trim() + '\n
' + }) +} - /** - * Block helper that executes the block in the current context but sets to special variables: - * - * * The github-url of the given file in the current package version is stored into `@url` - * * The `package.json` of the file's module is stored into `@package` - * @param filePath file that is used to find that package.json - * @param options block-helper options - */ - withPackageOf: function (filePath, options) { - const data = Handlebars.createFrame(options.data) - data.url = githubUrl(filePath) - data.package = findPackage(path.resolve(filePath), false) - return options.fn(this, {data: data}) - }, +/** + * Render an object hierarchy. + * + * The expected input is of the form + * + * ``` + * { + * prop1: 'value', + * prop2: 'value', + * ..., + * children: [ + * { + * prop1: 'value', + * propt2: 'value', + * ..., + * children: ... + * } + * ] + * } + * ``` + * + * The tree is transformed and rendered using [archy](https://www.npmjs.com/package/archy) + * + * @param object + * @param options + * @param {function} options.fn computes the label for a node based on the node itself + * @returns {string} + * @api public + */ +function renderTree (object, options) { + const tree = require('archy')(transformTree(object, options.fn)) + return '
\n' + tree + '
' +} - github: function (filePath) { - return githubUrl(filePath) - }, +/** + * Set special variable for accessing information from the context of a file (possibly in a dependency) + * + * This block helper executes the block in the current context but sets special variables: + * + * * `@url`: The github-url of the given file in the current package version is stored into + * * `@package`The `package.json` of the file's module is stored into + * + * @param {string} filePath file that is used to find that package.json + * @param {object} options options passed in by Handlebars + * @api public + */ +function withPackageOf (filePath, options) { + const data = Handlebars.createFrame(options.data) + data.url = github(filePath) + data.package = findPackage(path.resolve(filePath), false) + return options.fn(this, {data: data}) +} - /** - * Returns the path to the github repository (below github.com) based on the $.repository.url. - * @param options - * @returns {string=} the repository path within github.com (or null) - */ - githubRepo: githubRepo, +/** + * Create a link to the npm-page of a package + * + * @param {string} packageName the name of the package + * @api public + */ +function npm (packageName) { + return '[' + packageName + '](https://npmjs.com/package/' + packageName + ')' +} - /** - * Create a link to the npm-package of a package - * @param packageName the name of the package - */ - npm: function (packageName) { - return '[' + packageName + '](https://npmjs.com/package/' + packageName + ')' - }, +/** + * Convert a name into a valid id (like github does with header names) + * + * This helper creates valid html-ids from header name. It attempts to + * follow the same rules that github uses to convert header names (h1, h2) into the + * hash-part of the URL referencing this header. + * + * ```js + * htmlId('abc') === 'abc' + * htmlId('abc cde') === 'abc-cde' // Replace spaces by '-' + * htmlId('a$b&c%d') === 'abcd' // Remove all characters execpt alpahnumericals and minus + * htmlId('mäxchen' === 'mäxchen' // Do not remove german umlauts + * htmlId('ハッピークリスマス') === 'ハッピークリスマス' // Do not remove japanase word characters + * htmlId('ABCDE') === 'abcde' // Convert to lowercase + * ``` + * + * @param {string} value the input value of the URL (e.g. the header name) + * @return {string} a string value + * + * @api public + */ +function htmlId (value) { + // see http://stackoverflow.com/questions/19001140/amend-regular-expression-to-allow-german-umlauts-french-accents-and-other-valid + // and https://github.com/mathiasbynens/unicode-data/blob/17a920119b8015c6f7fe922ee7ab9fe29bef4394/4.0.1/blocks/Katakana-regex.js + // There are probably a lot of characters missing. Please make a PR, if you want to include more ranges in the valid characters. + // Make sure to add a test-case + const validChars = /[^A-Za-z0-9-_\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u30A0-\u30FF]/g + return value + .replace(/ /g, '-') + .replace(validChars, '') + .toLowerCase() +} - /** - * This helper creates valid html-ids from header name. It attempts to - * follow the same rules that github uses to convert header names (h1, h2) into the - * hash-part of the URL referencing this header. - * - * ```js - * htmlId('abc') === 'abc' - * htmlId('abc cde') === 'abc-cde' // Replace spaces by '-' - * htmlId('a$b&c%d') === 'abcd' // Remove all characters execpt alpahnumericals and minus - * htmlId('mäxchen' === 'mäxchen' // Do not remove german umlauts - * htmlId('ハッピークリスマス') === 'ハッピークリスマス' // Do not remove japanase word characters - * htmlId('ABCDE') === 'abcde' // Convert to lowercase - * ``` - */ - 'htmlId': function (value) { - // see http://stackoverflow.com/questions/19001140/amend-regular-expression-to-allow-german-umlauts-french-accents-and-other-valid - // and https://github.com/mathiasbynens/unicode-data/blob/17a920119b8015c6f7fe922ee7ab9fe29bef4394/4.0.1/blocks/Katakana-regex.js - // There are probably a lot of characters missing. Please make a PR, if you want to include more ranges in the valid characters. - // Make sure to add a test-case - var validChars = /[^A-Za-z0-9-_\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u30A0-\u30FF]/g - return value - .replace(/ /g, '-') - .replace(validChars, '') - .toLowerCase() - }, +/** + * Check, if [coveralls.io](https://coveralls.io) is configured in this package + * + * Check the .travis.yml and the appveyor.yml files for the string 'coveralls' + * and return true if any of them exists and contains the string. + * We expect coveralls to be configured then. + * + * @return {boolean} true, if coveralls is configured + * + * @api public + */ +function hasCoveralls () { + const travis = qfs.read('.travis.yml') + const appveyor = qfs.read('appveyor.yml') + return Q.allSettled([travis, appveyor]).then(function (files) { + let i + for (i = 0; i < files.length; i++) { + if (files[i].state === 'fulfilled' && files[i].value.indexOf('coveralls') >= 0) { + return true + } + } + return false + }) +} - /** - * Check the .travis.yml and the appveyor.yml files for the string 'coveralls' - * and return true if any of them exists and contains the string. - * We expect coveralls to be configured then - */ - 'hasCoveralls': function hasCoveralls () { - var travis = qfs.read('.travis.yml') - var appveyor = qfs.read('appveyor.yml') - return Q.allSettled([travis, appveyor]).then(function (files) { - var i - for (i = 0; i < files.length; i++) { - if (files[i].state === 'fulfilled' && files[i].value.indexOf('coveralls') >= 0) { - return true - } +/** + * Check, if [Greenkeeper](https://greenkeeper.io) is enabled for this repository + * + * This is done by analyzing the greenkeeper.io-[badge](https://badges.greenkeeper.io/nknapp/thought.svg) + * + * @param {object} options options passed in by Handlebars + * @api public + */ +function hasGreenkeeper (options) { + const slug = githubRepo(options) + return require('request-promise')(`https://badges.greenkeeper.io/${slug}.svg`) + .then(function (response) { + return require('cheerio')(response).find('text').last().text() !== 'not found' + }) + .catch(function (err) { + if (err.statusCode === 404) { + return false } - return false + throw err }) - }, - - 'hasGreenkeeper': function hasGreenkeeper (options) { - var slug = githubRepo(options) - return require('request-promise')(`https://badges.greenkeeper.io/${slug}.svg`) - .then(function (response) { - return require('cheerio')(response).find('text').last().text() !== 'not found' - }) - .catch(function (err) { - if (err.statusCode === 404) { - return false - } - throw err - }) - } - } /** + * Transfrom an object hierarchy into `archy`'s format + * * Transform a tree-structure of the form * ``` * { @@ -312,9 +359,11 @@ module.exports = { * * @param {object} object the tree data * @param fn the block-helper function (options.fn) of Handlebars (http://handlebarsjs.com/block_helpers.html) + * + * @api private */ function transformTree (object, fn) { - var label = fn(object).trim() + const label = fn(object).trim() if (object.children) { return { label: label, @@ -357,18 +406,19 @@ function transformTree (object, fn) { * @param {string[][]} files an array of filenames, split by `path.sep` * @param {string} label the label for the current tree node * @returns {object} a tree structure as needed by [archy](https://www.npmjs.com/package/archy) + * @api private */ function treeFromPathComponents (files, label) { debug('treeFromPathComponents', files, label) if (files.length === 0) { return label } - var result = { + const result = { label: label, nodes: _(files) .groupBy('0') .map(function (group, key) { - var values = group + const values = group .map(function (item) { return item.slice(1) }) @@ -390,22 +440,38 @@ function treeFromPathComponents (files, label) { } } -function githubUrl (filePath) { +/** + * Resolve the display-URL of a file on github. + * + * This works for files in the current package and in dependencies, as long as the repository-url is + * set correctly in package.json + * + * @param {string} filePath the path to the file + * @returns {string} the URL + * @api public + */ +function github (filePath) { // Build url to correct version and file in github - var packageJson = findPackage(path.resolve(filePath), true) - var url = packageJson && packageJson.repository && packageJson.repository.url + const packageJson = findPackage(path.resolve(filePath), true) + const url = packageJson && packageJson.repository && packageJson.repository.url if (url && url.match(/github.com/)) { - var version = packageJson.version + const version = packageJson.version // path within the package - var relativePath = path.relative(path.dirname(packageJson.paths.absolute), filePath) + const relativePath = path.relative(path.dirname(packageJson.paths.absolute), filePath) return url.replace(/^git\+/, '').replace(/\.git$/, '') + '/blob/v' + version + '/' + relativePath } } +/** + * Returns the current repository group and name (e.g. `nknapp/thought` for this project) + * + * @returns {string} + * @api public + */ function githubRepo (options) { try { - var url = options.data.root.package.repository.url - var match = url.match(/.*?(:\/\/|@)github\.com[/:](.*?)(#.*?)?$/) + const url = options.data.root.package.repository.url + const match = url.match(/.*?(:\/\/|@)github\.com[/:](.*?)(#.*?)?$/) if (match) { return match[2].replace(/\.git$/, '') } else { diff --git a/package.json b/package.json index 5cd06b0..4198f33 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "mocha": "^3.2.0", "nock": "^9.0.9", "recursive-copy": "^2.0.5", + "thought-plugin-jsdoc": "^1.0.0", "thoughtful-release": "^0.3.0" }, "standard": {