From b87f989c4305e4d562001d5a910eecd3cf0e2a80 Mon Sep 17 00:00:00 2001 From: josdejong Date: Mon, 17 Feb 2014 22:54:09 +0100 Subject: [PATCH] Created a simple docgenerator to generate references. It's too boring to do manually --- gulpfile.js | 9 +- package.json | 3 +- tools/docgenerator.js | 377 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 387 insertions(+), 2 deletions(-) create mode 100644 tools/docgenerator.js diff --git a/gulpfile.js b/gulpfile.js index 4af5d7839..aa481079f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2,7 +2,8 @@ var fs = require('fs'), gulp = require('gulp'), gutil = require('gulp-util'), webpack = require('webpack'), - uglify = require('uglify-js'); + uglify = require('uglify-js'), + docgenerator = require('./tools/docgenerator'); var ENTRY = './index.js', HEADER = './lib/header.js', @@ -10,6 +11,8 @@ var ENTRY = './index.js', FILE_MIN = 'math.min.js', FILE_MAP = 'math.map', DIST = './dist', + DOCS_SRC = './lib/function/', + DOCS_DEST = './dist/reference/', MATH_JS = DIST + '/' + FILE, MATH_MIN_JS = DIST + '/' + FILE_MIN, MATH_MAP_JS = DIST + '/' + FILE_MAP; @@ -76,6 +79,10 @@ gulp.task('minify', ['bundle'], function () { gutil.log('Mapped ' + MATH_MAP_JS); }); +gulp.task('docs', function () { + docgenerator.iteratePath(DOCS_SRC, DOCS_DEST); +}); + // The default task (called when you run `gulp`) gulp.task('default', ['bundle', 'minify']); diff --git a/package.json b/package.json index e1547a4e6..7efea4543 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "underscore": "latest", "seed-random": "latest", "gulp": "*", - "gulp-util": "latest" + "gulp-util": "latest", + "glob": "latest" }, "main": "./index", "scripts": { diff --git a/tools/docgenerator.js b/tools/docgenerator.js new file mode 100644 index 000000000..44d1a3271 --- /dev/null +++ b/tools/docgenerator.js @@ -0,0 +1,377 @@ +/** + * This is a little tool to generate reference documentation of all math.js + * functions under ./lib/functions. This is NO generic solution. + * + * The tool can parse documentation information from the block comment in the + * functions code, and generate a markdown file with the documentation. + */ +var fs = require('fs'), + glob = require('glob'), + gutil = require('gulp-util'); + +/** + * Extract JSON documentation from the comments in a file with JavaScript code + * @param {String} name Function name + * @param {String} code javascript code containing a block comment + * describing a math.js function + * @return {Object} doc json document + */ +function generateDoc(name, code) { + var match = /\/\*\*(.|\n)*\*\//.exec(code); + var comment = match && match[0]; + + // remove block comment + comment = comment.replace('/**', '') + .replace('*/', '') + .replace(/\n\s*\* ?/g, '\n'); + + var lines = comment.split('\n'), + line = ''; + + // get next line + function next () { + line = lines.shift(); + } + + // returns true if current line is empty + function empty() { + return !line || !line.trim(); + } + + // returns true if there still is a current line + function exists() { + return line !== undefined; + } + + function skipEmptyLines () { + while (exists() && empty()) next(); + } + + function parseDescription () { + var description = ''; + while (exists() && !empty()) { + description += (description ? ' ' : '') + line; + next(); + } + + doc.description = description; + } + + function parseSyntax() { + if (/^syntax/i.test(line)) { + next(); + skipEmptyLines(); + + while (exists() && !empty()) { + doc.syntax.push(line.trim()); + next(); + } + + skipEmptyLines(); + + return true; + } + return false; + } + + function parseExamples() { + if (/^example/i.test(line)) { + next(); + skipEmptyLines(); + + while (exists() && (empty() || line.charAt(0) == ' ')) { + doc.examples.push(line.trim()); + next(); + } + + if (doc.examples[doc.examples.length - 1].trim() == '') doc.examples.pop(); + + skipEmptyLines(); + + return true; + } + return false; + } + + function parseSeeAlso() { + if (/^see also/i.test(line)) { + next(); + skipEmptyLines(); + + while (exists() && !empty()) { + var names = line.split(','); + doc.seeAlso = doc.seeAlso.concat(names.map(function (name) { + return name.trim(); + })); + next(); + } + + skipEmptyLines(); + + return true; + } + return false; + } + + function parseParameters() { + var count = 0; + do { + var match = /\s*@param\s*{(.*)}\s*\[?(\w*)\]?\s*(.*)?$/.exec(line); + if (match) { + next(); + + count++; + var annotation = { + name: match[2] || '', + description: (match[3] || '').trim(), + types: match[1].split('|').map(function (t) { + return t.trim(); + }) + }; + doc.parameters.push(annotation); + + // multi line description + while (exists() && !empty() && /^\s{6}/.test(line)) { + annotation.description += ' ' + line.trim(); + next(); + } + } + } while (match); + + return count > 0; + } + + function parseReturns() { + var match = /\s*@return\s*{(.*)}\s*(.*)?$/.exec(line); + if (match) { + next(); + + doc.returns = { + description: match[2] || '', + types: match[1].split('|').map(function (t) { + return t.trim(); + }) + }; + + // multi line description + while (exists() && !empty() && /^\s{6}/.test(line)) { + doc.returns.description += ' ' + line.trim(); + next(); + } + + return true; + } + return false; + } + + // initialize doc + var doc = { + name: name, + description: '', + syntax: [], + examples: [], + seeAlso: [], + parameters: [], + returns: {} + }; + + next(); + skipEmptyLines(); + parseDescription(); + + do { + skipEmptyLines(); + + var handled = parseSyntax() || + parseExamples() || + parseSeeAlso() || + parseParameters() || + parseReturns(); + + if (!handled) { + // skip this line, no one knows what to do with it + next(); + } + } while (exists()); + + return doc; +} + +/** + * Validate whether all required fields are available in given doc + * @param {Object} doc + * @return {String[]} issues + */ +function validateDoc (doc) { + var issues = []; + + if (!doc.name) { + issues.push('name missing in document'); + } + + if (!doc.description) { + issues.push('function "' + doc.name + '": description missing'); + } + + if (!doc.syntax || doc.syntax.length == 0) { + issues.push('function "' + doc.name + '": syntax missing'); + } + + if (!doc.examples || doc.examples.length == 0) { + issues.push('function "' + doc.name + '": examples missing'); + } + + if (doc.parameters && doc.parameters.length) { + doc.parameters.forEach(function (param, index) { + if (!param.name || !param.name.trim()) { + issues.push('function "' + doc.name + '": name missing of parameter ' + index + ''); + } + if (!param.description || !param.description.trim()) { + issues.push('function "' + doc.name + '": description missing for parameter ' + (param.name || index)); + } + if (!param.types || !param.types.length) { + issues.push('function "' + doc.name + '": types missing for parameter ' + (param.name || index)); + } + }); + } + else { + issues.push('function "' + doc.name + '": parameters missing'); + } + + if (doc.returns) { + if (!doc.returns.description || !doc.returns.description.trim()) { + issues.push('function "' + doc.name + '": description missing of returns'); + } + if (!doc.returns.types || !doc.returns.types.length) { + issues.push('function "' + doc.name + '": types missing of returns'); + } + } + else { + issues.push('function "' + doc.name + '": returns missing'); + } + + if (!doc.seeAlso || doc.seeAlso.length == 0) { + issues.push('function "' + doc.name + '": seeAlso missing'); + } + + return issues; +} + +/** + * Generate markdown + * @param {Object} doc A JSON object generated with generateDoc() + * @returns {string} markdown Markdown contents + */ +function generateMarkdown (doc) { + var text = ''; + + text += '# Function ' + doc.name + '\n\n'; + + text += doc.description + '\n\n\n'; + + if (doc.syntax && doc.syntax.length) { + text += '## Syntax\n\n' + + '```js\n' + + doc.syntax.join('\n') + + '\n```\n\n'; + } + + text += '### Parameters\n\n' + + 'Parameter | Type | Description\n' + + '--------- | ---- | -----------\n' + + doc.parameters.map(function (p) { + return '`' + p.name + '` | ' + + (p.types ? p.types.join(' | ') : '') + ' | ' + + p.description + }).join('\n') + + '\n\n'; + + text += '### Returns\n\n' + + 'Type | Description\n' + + '---- | -----------\n' + + (doc.returns.types ? doc.returns.types.join(' | ') : '') + ' | ' + doc.returns.description + + '\n\n\n'; + + if (doc.examples && doc.examples.length) { + text += '## Examples\n\n' + + '```js\n' + + doc.examples.join('\n') + + '\n```\n\n\n'; + } + + if (doc.seeAlso && doc.seeAlso.length) { + text += '## See also\n\n' + + doc.seeAlso.map(function (name) { + // TODO: make the see also links working + return name; + //return '[' + name + '](' + name + '.md)'; + }).join(', ') + + '\n'; + } + + text += '\n'; + + return text; +} + +/** + * Iterate over all source files and generate markdown documents for each of them + * @param {String} inputPath + * @param {String} outputPath + */ +function iteratePath (inputPath, outputPath) { + if (!fs.existsSync(outputPath)) { + fs.mkdirSync(outputPath); + } + + glob(inputPath + '**/*.js', null, function (err, files) { + // generate path information for each of the files + var functions = {}; + files.forEach(function (fullPath) { + var name = fullPath.match(/\/(\w*)\.js/)[1], + relativePath = fullPath.substring(inputPath.length); + + functions[name] = { + name: name, + category: relativePath.match(/^(.*)\//)[1], + fullPath: fullPath, + relativePath: relativePath + }; + }); + + // loop over all files, generate a doc for each of them + var issues = []; + for (var name in functions) { + if (functions.hasOwnProperty(name)) { + var fn = functions[name]; + + var code = fs.readFileSync(fn.fullPath); + var doc = generateDoc(name, code); + + issues = issues.concat(validateDoc(doc)); + + var markdown = generateMarkdown(doc); + + var path = outputPath + fn.category; + if (!fs.existsSync(path)) { + fs.mkdirSync(path); + } + fs.writeFileSync(path + '/' + fn.name + '.md', markdown); + } + } + + // output all issues + if (issues.length) { + issues.forEach(function (issue) { + gutil.log('Warning: ' + issue); + }); + gutil.log(issues.length + ' warnings'); + } + }); +} + +// exports +exports.iteratePath = iteratePath; +exports.generateDoc = generateDoc; +exports.validateDoc = validateDoc; +exports.generateMarkdown = generateMarkdown; \ No newline at end of file