diff --git a/bin/documentation.js b/bin/documentation.js index 73246c2d6..46034d9b6 100755 --- a/bin/documentation.js +++ b/bin/documentation.js @@ -15,7 +15,7 @@ commands[parsedArgs.command](documentation, parsedArgs); function parseArgs() { // reset() needs to be called at parse time because the yargs module uses an // internal global variable to hold option state - var argv = yargs + var argv = addCommands(yargs, true) .usage('Usage: $0 [options]') .version(function () { return require('../package').version; @@ -108,10 +108,14 @@ function parseArgs() { }; } -function addCommands(parser) { +function addCommands(parser, descriptionOnly) { parser = parser.demand(1); for (var cmd in commands) { - parser = parser.command(cmd, commands[cmd].description, commands[cmd].parseArgs); + if (descriptionOnly) { + parser = parser.command(cmd, commands[cmd].description); + } else { + parser = parser.command(cmd, commands[cmd].description, commands[cmd].parseArgs); + } } return parser.help('help'); } diff --git a/index.js b/index.js index 0dc256d77..d873ecf12 100644 --- a/index.js +++ b/index.js @@ -183,5 +183,6 @@ module.exports.expandInputs = expandInputs; module.exports.formats = { html: require('./lib/output/html'), md: require('./lib/output/markdown'), + mdast: require('./lib/output/markdown_ast'), json: require('./lib/output/json') }; diff --git a/lib/commands/index.js b/lib/commands/index.js index edda87ed1..3bce6d3ae 100644 --- a/lib/commands/index.js +++ b/lib/commands/index.js @@ -12,5 +12,6 @@ module.exports = { 'build': require('./build'), 'serve': require('./serve'), - 'lint': require('./lint') + 'lint': require('./lint'), + 'readme': require('./readme') }; diff --git a/lib/commands/readme.js b/lib/commands/readme.js new file mode 100644 index 000000000..ed29bcbfc --- /dev/null +++ b/lib/commands/readme.js @@ -0,0 +1,92 @@ +'use strict'; + +var fs = require('fs'); +var mdast = require('mdast'); +var inject = require('mdast-util-inject'); +var chalk = require('chalk'); +var disparity = require('disparity'); +var build = require('./build'); + +module.exports = readme; +module.exports.description = 'inject documentation into your README.md'; +module.exports.parseArgs = function (yargs) { + yargs.usage('Usage: $0 readme [--readme-file=README.md] --section "API"' + + ' [--compare-only] [other documentationjs options]') + .option('readme-file', { + describe: 'The markdown file into which to inject documentation', + default: 'README.md' + }) + .option('section', { + alias: 's', + describe: 'The section heading after which to inject generated documentation', + required: true + }) + .option('diff-only', { + alias: 'd', + describe: 'Instead of updating the given README with the generated documentation,' + + ' just check if its contents match, exiting nonzero if not.', + default: false + }) + .option('quiet', { + alias: 'q', + describe: 'Quiet mode: do not print messages or README diff to stdout.', + default: false + }) + .help('help') + .example('$0 readme index.js -s "API Docs" --github'); +}; + +function readme(documentation, parsedArgs) { + var readmeOptions = parsedArgs.commandOptions; + readmeOptions.format = 'mdast'; + /* eslint no-console: 0 */ + var log = readmeOptions.q ? function () {} + : console.log.bind(console, '[documentation-readme] '); + var readmeFile = readmeOptions['readme-file']; + + build(documentation, parsedArgs, onAst); + + function onAst(err, docsAst) { + if (err) { + throw err; + } + var readmeContent = fs.readFileSync(readmeFile, 'utf8'); + mdast.use(plugin, { + section: readmeOptions.section, + toInject: docsAst + }).process(readmeContent, onInjected.bind(null, readmeContent)); + } + + function onInjected(readmeContent, err, file, content) { + if (err) { + throw err; + } + + var diffOutput = disparity.unified(readmeContent, content, { + paths: [readmeFile, readmeFile] + }); + if (!diffOutput.length) { + log(readmeFile + ' is up to date.'); + process.exit(0); + } + + if (readmeOptions.d) { + log(chalk.bold(readmeFile + ' needs the following updates:'), '\n' + diffOutput); + process.exit(1); + } else { + log(chalk.bold('Updating ' + readmeFile), '\n' + diffOutput); + } + + fs.writeFileSync(readmeFile, content); + } +} + +// wrap the inject utility as an mdast plugin +function plugin(mdast, options) { + return function transform(targetAst, file, next) { + if (!inject(options.section, targetAst, options.toInject)) { + return next(new Error('Heading ' + options.section + ' not found.')); + } + next(); + }; +} diff --git a/package.json b/package.json index ab05f4ad7..86b927d37 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,11 @@ "ast-types": "^0.8.12", "babel-core": "^5.0.0", "babelify": "^6.3.0", + "chalk": "^1.1.1", "chokidar": "^1.2.0", "concat-stream": "^1.5.0", "debounce": "^1.0.0", + "disparity": "^2.0.0", "doctrine": "^0.7.1", "documentation-theme-default": "2.1.1", "documentation-theme-utils": "^1.0.1", @@ -26,6 +28,7 @@ "jsdoc-inline-lex": "^1.0.1", "mdast": "^2.0.0", "mdast-toc": "^1.1.0", + "mdast-util-inject": "^1.1.0", "micromatch": "^2.1.6", "mime": "^1.3.4", "module-deps": "^4.0.2", @@ -46,10 +49,12 @@ "devDependencies": { "chdir": "0.0.0", "eslint": "^1.5.1", + "fs-extra": "^0.26.2", "glob": "^6.0.1", "lodash": "^3.10.1", "mock-fs": "^3.5.0", - "tap": "^2.2.0" + "tap": "^2.2.0", + "tmp": "0.0.28" }, "keywords": [ "documentation", diff --git a/test/bin-readme.js b/test/bin-readme.js new file mode 100644 index 000000000..94c33a595 --- /dev/null +++ b/test/bin-readme.js @@ -0,0 +1,101 @@ +var test = require('tap').test, + path = require('path'), + os = require('os'), + exec = require('child_process').exec, + tmp = require('tmp'), + fs = require('fs-extra'); + +function documentation(args, options, callback, parseJSON) { + if (!callback) { + callback = options; + options = {}; + } + + if (!options.cwd) { + options.cwd = __dirname; + } + + options.maxBuffer = 1024 * 1024; + + args.unshift(path.join(__dirname, '../bin/documentation.js')); + + exec(args.join(' '), options, callback); +} + +test('readme command', function (group) { + var fixtures = path.join(__dirname, 'fixture/readme'); + var sourceFile = path.join(fixtures, 'index.js'); + + tmp.dir({unsafeCleanup: true}, function (err, d) { + group.error(err); + fs.copySync(path.join(fixtures, 'README.input.md'), path.join(d, 'README.md')); + fs.copySync(path.join(fixtures, 'index.js'), path.join(d, 'index.js')); + + // run tests after setting up temp dir + + group.test('--diff-only: changes needed', function (t) { + t.error(err); + var before = fs.readFileSync(path.join(d, 'README.md'), 'utf-8'); + documentation(['readme index.js --diff-only -s API'], {cwd: d}, function (err, stdout, stderr) { + var after = fs.readFileSync(path.join(d, 'README.md'), 'utf-8'); + t.ok(err); + t.notEqual(err.code, 0, 'exit nonzero'); + t.same(after, before, 'readme unchanged'); + t.end(); + }); + }); + + var expectedFile = path.join(fixtures, 'README.output.md'); + var expected = fs.readFileSync(expectedFile, 'utf-8'); + + group.test('updates README.md', function (t) { + documentation(['readme index.js -s API'], {cwd: d}, function (err, stdout) { + t.error(err); + var actual = fs.readFileSync(path.join(d, 'README.md'), 'utf-8'); + t.same(actual, expected, 'generated readme output'); + t.end(); + }); + }); + + group.test('--readme-file', function (t) { + fs.copySync(path.join(fixtures, 'README.input.md'), path.join(d, 'other.md')); + documentation(['readme index.js -s API --readme-file other.md'], {cwd: d}, function (err, stdout) { + t.error(err); + var actual = fs.readFileSync(path.join(d, 'other.md'), 'utf-8'); + t.same(actual, expected, 'generated readme output'); + t.end(); + }); + }); + + group.test('--diff-only: changes NOT needed', function (t) { + t.error(err); + fs.copySync(path.join(fixtures, 'README.output.md'), path.join(d, 'uptodate.md')); + documentation(['readme index.js --diff-only -s API --readme-file uptodate.md'], + {cwd: d}, function (err, stdout, stderr) { + t.error(err); + t.match(stdout, 'is up to date.'); + t.end(); + }); + }); + + group.test('requires -s option', function (t) { + documentation(['readme index.js'], {cwd: d}, function (err, stdout, stderr) { + t.ok(err); + t.ok(err.code !== 0, 'exit nonzero'); + t.match(stderr, 'Missing required argument: s'); + t.end(); + }); + }); + + group.test('errors if specified readme section is missing', function (t) { + documentation(['readme index.js -s DUMMY'], {cwd: d}, function (err, stdout, stderr) { + t.ok(err); + t.ok(err.code !== 0, 'exit nonzero'); + t.end(); + }); + }); + + group.end(); + }); +}); + diff --git a/test/bin.js b/test/bin.js index 5f7f09fcb..96a20a86a 100644 --- a/test/bin.js +++ b/test/bin.js @@ -4,7 +4,8 @@ var test = require('tap').test, path = require('path'), os = require('os'), exec = require('child_process').exec, - fs = require('fs'); + tmp = require('tmp'), + fs = require('fs-extra'); function documentation(args, options, callback, parseJSON) { if (!callback) { diff --git a/test/fixture/readme/README.input.md b/test/fixture/readme/README.input.md new file mode 100644 index 000000000..826a3973f --- /dev/null +++ b/test/fixture/readme/README.input.md @@ -0,0 +1,5 @@ +# A title + +# API + +# Another section diff --git a/test/fixture/readme/README.output.md b/test/fixture/readme/README.output.md new file mode 100644 index 000000000..5c256723e --- /dev/null +++ b/test/fixture/readme/README.output.md @@ -0,0 +1,23 @@ +# A title + +# API + +## bar + +A second function with docs + +**Parameters** + +- `b` + +## foo + +A function with documentation. + +**Parameters** + +- `a` {string} blah + +Returns **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** answer + +# Another section diff --git a/test/fixture/readme/index.js b/test/fixture/readme/index.js new file mode 100644 index 000000000..9b193faee --- /dev/null +++ b/test/fixture/readme/index.js @@ -0,0 +1,16 @@ + +/** + * A function with documentation. + * @param a {string} blah + * @return {number} answer + */ +function foo(a) { + +} + +/** + * A second function with docs + */ +function bar(b) { + +}