scripts/doctool.js
#!/usr/bin/env node
/// # doctool.js
///
/// Usage: `doctool.js ...jsfiles...`
///
/// Reads each `.js` file and writes a `.md` file in the same directory.
/// The output file consists of the concatenation of the "doc comments"
/// in the input file, which are assumed to contain Markdown content,
/// including any section headings necessary to organize the file.
///
/// A "doc comment" must begin at the start of a line or after
/// whitespace. There are two kinds of doc comments: `/** ... */`
/// (block) comments and `/// ...` (triple-slash) comments.
///
/// If a file begins with the magic string "///!README", the output
/// filename is changed to `README.md`.
///
/// Examples:
///
/// ```
/// /**
/// * This is a block comment. The parser strips the sequence,
/// * [optional whitespace, `*`, optional single space] from
/// * every line that has it.
/// *
/// For lines that don't, no big deal.
///
/// Leading whitespace will be preserved here.
///
/// * We can create a bullet list in here:
/// *
/// * * This is a bullet
/// */
/// ```
///
/// ```
/// /** Single-line block comments are also ok. */
/// ```
///
/// ```
/// /**
/// A block comment whose first line doesn't have a `*` receives
/// no stripping of `*` characters on any line.
///
/// * This is a bullet
///
/// */
/// ```
///
/// ```
/// /// A triple-slash comment starts with `///` followed by an
/// /// optional space (i.e. one space is removed if present).
/// /// Multiple consecutive lines that start with `///` are
/// /// treated together as a single doc comment.
/// /** Separate doc comments get separate paragraphs. */
/// ```
var fs = require('fs');
var path = require('path');
process.argv.slice(2).forEach(function (fileName) {
var text = fs.readFileSync(fileName, "utf8");
var outFileName = fileName.replace(/\.js$/, '') + '.md';
if (text.slice(0, 10) === '///!README') {
outFileName = path.join(path.dirname(fileName), 'README.md');
text = text.slice(10);
}
var docComments = [];
for (;;) {
// This regex breaks down as follows:
//
// 1. Start of line
// 2. Optional whitespace (not newline!)
// 3. `///` (capturing group 1) or `/**` (group 2)
// 4. Looking ahead, NOT `/` or `*`
var nextOpener = /^[ \t]*(?:(\/\/\/)|(\/\*\*))(?![\/\*])/m.exec(text);
if (! nextOpener)
break;
text = text.slice(nextOpener.index + nextOpener[0].length);
if (nextOpener[1]) {
// triple-slash
text = text.replace(/^[ \t]/, ''); // optional space
var comment = text.match(/^[^\n]*/)[0];
text = text.slice(comment.length);
var match;
while ((match = /^\n[ \t]*\/\/\/[ \t]?/.exec(text))) {
// multiple lines in a row become one comment
text = text.slice(match[0].length);
var restOfLine = text.match(/^[^\n]*/)[0];
text = text.slice(restOfLine.length);
comment += '\n' + restOfLine;
}
if (comment.trim())
docComments.push(['///', comment]);
} else if (nextOpener[2]) {
// block comment
var rawComment = text.match(/^[\s\S]*?\*\//);
if ((! rawComment) || (! rawComment[0]))
continue;
rawComment = rawComment[0];
text = text.slice(rawComment.length);
rawComment = rawComment.slice(0, -2); // remove final `*/`
if (rawComment.slice(-1) === ' ')
// make that ' */' for the benefit of single-line blocks
rawComment = rawComment.slice(0, -1);
var lines = rawComment.split('\n');
var stripStars = false;
if (lines[0].trim().length === 0) {
// The comment has a newline after the `/**` (with possible whitespace
// between). This is like most comments, though occasionally people
// may write `/** foo */` on one line. Skip the blank line.
lines.splice(0, 1);
if (! lines.length)
continue;
// Now we determine whether this is block comment with a column of
// asterisks running down the left side, so we can strip them.
stripStars = /^[ \t]*\*/.test(lines[1]);
} else {
// Trim beginning of line after `/**`
lines[0] = lines[0].replace(/^\s*/, '');
}
lines = lines.map(function (s) {
// Strip either up to an asterisk and then an optional space,
// or just an optional space, depending on `stripStars`.
if (stripStars)
return s.replace(/^[ \t]*\* ?/, '');
else
return s;
});
var result = lines.join('\n');
if (result.trim())
docComments.push(['/**', result]);
}
}
if (docComments.length) {
var output = docComments.map(function (x) { return x[1]; }).join('\n\n');
var fileShortName = path.basename(fileName);
output = '*This file is automatically generated from [`' +
fileShortName + '`](' + fileShortName + ').*\n\n' + output;
fs.writeFileSync(outFileName, output, 'utf8');
console.log("Wrote " + docComments.length + " comments to " + outFileName);
}
});