mirror of
https://github.com/josdejong/mathjs.git
synced 2025-12-08 19:46:04 +00:00
622 lines
17 KiB
JavaScript
622 lines
17 KiB
JavaScript
/**
|
|
* 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');
|
|
var glob = require('glob');
|
|
var mkdirp = require('mkdirp');
|
|
var gutil = require('gulp-util');
|
|
|
|
// special cases for function syntax
|
|
var SYNTAX = {
|
|
cbrt: 'math.cbrt(x [, allRoots])',
|
|
createUnit: 'math.createUnit(units)',
|
|
gcd: 'math.gcd(a, b)',
|
|
log: 'math.log(x [, base])',
|
|
lcm: 'math.lcm(a, b)',
|
|
norm: 'math.norm(x [, p])',
|
|
round: 'math.round(x [, n])',
|
|
complex: 'math.complex(re, im)',
|
|
matrix: 'math.matrix(x)',
|
|
sparse: 'math.sparse(x)',
|
|
unit: 'math.unit(x)',
|
|
eval: 'math.eval(expr [, scope])',
|
|
parse: 'math.parse(expr [, scope])',
|
|
concat: 'math.concat(a, b, c, ... [, dim])',
|
|
ones: 'math.ones(m, n, p, ...)',
|
|
range: 'math.range(start, end [, step])',
|
|
resize: 'math.resize(x, size [, defaultValue])',
|
|
subset: 'math.subset(x, index [, replacement])',
|
|
splitUnit: 'math.splitUnit(unit, parts)',
|
|
zeros: 'math.zeros(m, n, p, ...)',
|
|
permutations: 'math.permutations(n [, k])',
|
|
random: 'math.random([min, max])',
|
|
randomInt: 'math.randomInt([min, max])',
|
|
format: 'math.format(value [, precision])',
|
|
'import': 'math.import(object, override)',
|
|
print: 'math.print(template, values [, precision])'
|
|
};
|
|
|
|
var IGNORE_FUNCTIONS = {
|
|
distribution: true
|
|
};
|
|
|
|
var IGNORE_WARNINGS = {
|
|
seeAlso: ['help', 'intersect', 'clone', 'typeof', 'chain', 'import', 'config', 'typed'],
|
|
parameters: ['parser'],
|
|
returns: ['forEach', 'import']
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
// get block comment from code
|
|
var match = /\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\//.exec(code);
|
|
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
// get text content inside block comment
|
|
var comment = match[0].replace('/**', '')
|
|
.replace('*/', '')
|
|
.replace(/\n\s*\* ?/g, '\n')
|
|
.replace(/\r/g, '');
|
|
|
|
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;
|
|
}
|
|
|
|
// returns true if current line is a header like 'Syntax:'
|
|
function isHeader() {
|
|
return /^(Name|Syntax|Description|Examples|See also)/i.test(line);
|
|
}
|
|
|
|
// returns true if the current line starts with an annotation like @param
|
|
function isAnnotation() {
|
|
return /^@/.test(line);
|
|
}
|
|
|
|
function skipEmptyLines () {
|
|
while (exists() && empty()) next();
|
|
}
|
|
|
|
function stripLeadingSpaces(lines) {
|
|
var spaces = null;
|
|
lines.forEach(function (line) {
|
|
var match = /^ +/.exec(line);
|
|
var s = match && match[0] && match[0].length;
|
|
if (s > 0 && (spaces == null || s < spaces)) {
|
|
spaces = s;
|
|
}
|
|
});
|
|
|
|
if (spaces) {
|
|
lines.forEach(function (line, index) {
|
|
lines[index] = line.substring(spaces);
|
|
})
|
|
}
|
|
}
|
|
|
|
function parseDescription () {
|
|
var description = '';
|
|
|
|
while (exists() && !isHeader() && !isAnnotation()) {
|
|
description += line + '\n';
|
|
|
|
next();
|
|
}
|
|
|
|
// remove trailing returns
|
|
while (description.charAt(description.length - 1) == '\n') {
|
|
description = description.substring(0, description.length - 1);
|
|
}
|
|
|
|
doc.description = description;
|
|
}
|
|
|
|
function parseSyntax() {
|
|
if (/^syntax/i.test(line)) {
|
|
next();
|
|
skipEmptyLines();
|
|
|
|
while (exists() && !empty()) {
|
|
doc.syntax.push(line);
|
|
next();
|
|
}
|
|
stripLeadingSpaces(doc.syntax);
|
|
|
|
skipEmptyLines();
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function parseWhere() {
|
|
if (/^where/i.test(line)) {
|
|
next();
|
|
skipEmptyLines();
|
|
|
|
while (exists() && !empty()) {
|
|
doc.where.push(line);
|
|
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);
|
|
next();
|
|
}
|
|
stripLeadingSpaces(doc.examples);
|
|
|
|
if (doc.examples.length > 0 && 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 trim (text) {
|
|
return text.trim();
|
|
}
|
|
|
|
// replace characters like '<' with HTML entities like '<'
|
|
function escapeTags (text) {
|
|
return text.replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
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(trim).map(escapeTags)
|
|
};
|
|
doc.parameters.push(annotation);
|
|
|
|
// TODO: this is an ugly hack to extract the default value
|
|
var index = annotation.description.indexOf(']');
|
|
var defaultValue = null;
|
|
if (index != -1) {
|
|
defaultValue = annotation.description.substring(1, index).trim();
|
|
annotation.description = annotation.description.substring(index + 1).trim();
|
|
}
|
|
|
|
// multi line description
|
|
while (exists() && !empty() && /^\s{6}/.test(line)) {
|
|
annotation.description += ' ' + line.trim();
|
|
next();
|
|
}
|
|
|
|
if (defaultValue !== null) {
|
|
annotation.description += ' Default value: ' + defaultValue + '.';
|
|
}
|
|
}
|
|
} while (match);
|
|
|
|
return count > 0;
|
|
}
|
|
|
|
function parseReturns() {
|
|
var match = /\s*@returns?\s*\{(.*)}\s*(.*)?$/.exec(line);
|
|
if (match) {
|
|
next();
|
|
|
|
doc.returns = {
|
|
description: match[2] || '',
|
|
types: match[1].split('|').map(trim).map(escapeTags)
|
|
};
|
|
|
|
// 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: [],
|
|
where: [],
|
|
examples: [],
|
|
seeAlso: [],
|
|
parameters: [],
|
|
returns: null
|
|
};
|
|
|
|
next();
|
|
skipEmptyLines();
|
|
parseDescription();
|
|
|
|
do {
|
|
skipEmptyLines();
|
|
|
|
var handled = parseSyntax() ||
|
|
parseWhere() ||
|
|
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 = [];
|
|
|
|
function ignore(field) {
|
|
return IGNORE_WARNINGS[field].indexOf(doc.name) !== -1
|
|
}
|
|
|
|
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 {
|
|
if (!ignore('parameters')) {
|
|
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 {
|
|
if (!ignore('returns')) {
|
|
issues.push('function "' + doc.name + '": returns missing');
|
|
}
|
|
}
|
|
|
|
if (!doc.seeAlso || doc.seeAlso.length == 0) {
|
|
if (!ignore('seeAlso')) {
|
|
issues.push('function "' + doc.name + '": seeAlso missing');
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
/**
|
|
* Generate markdown
|
|
* @param {Object} doc A JSON object generated with generateDoc()
|
|
* @param {Object} functions All functions, used to generate correct links
|
|
* under seeAlso
|
|
* @returns {string} markdown Markdown contents
|
|
*/
|
|
function generateMarkdown (doc, functions) {
|
|
var text = '';
|
|
|
|
// TODO: should escape HTML characters in text
|
|
|
|
text += '<!-- Note: This file is automatically generated from source code comments. Changes made in this file will be overridden. -->\n\n';
|
|
|
|
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';
|
|
}
|
|
|
|
if (doc.where && doc.where.length) {
|
|
text += '### Where\n\n' + doc.where.join('\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';
|
|
|
|
if (doc.returns) {
|
|
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) {
|
|
return '[' + name + '](' + name + '.md)';
|
|
}).join(',\n') +
|
|
'\n';
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
/**
|
|
* Iterate over all source files and generate markdown documents for each of them
|
|
* @param {String} inputPath Path to /lib/
|
|
* @param {String} outputPath Path to /docs/reference/functions
|
|
* @param {String} outputRoot Path to /docs/reference
|
|
*/
|
|
function iteratePath (inputPath, outputPath, outputRoot) {
|
|
if (!fs.existsSync(outputPath)) {
|
|
mkdirp.sync(outputPath);
|
|
}
|
|
|
|
glob(inputPath + '**/*.js', null, function (err, files) {
|
|
// generate path information for each of the files
|
|
var functions = {}; // TODO: change to array
|
|
|
|
files.forEach(function (fullPath) {
|
|
var path = fullPath.split('/');
|
|
var name = path.pop().replace(/.js$/, '');
|
|
var functionIndex = path.indexOf('function');
|
|
var category;
|
|
|
|
// Note: determining whether a file is a function and what it's category
|
|
// is is a bit tricky and quite specific to the structure of the code,
|
|
// we reckon with some edge cases here.
|
|
if (path.indexOf('docs') === -1 && functionIndex !== -1) {
|
|
if (path.indexOf('expression') !== -1) {
|
|
category = 'expression';
|
|
}
|
|
else if (/^.\/lib\/type\/[a-zA-Z0-9_]*\/function/.test(fullPath)) {
|
|
category = 'construction';
|
|
}
|
|
else if (/^.\/lib\/core\/function/.test(fullPath)) {
|
|
category = 'core';
|
|
}
|
|
else {
|
|
category = path[functionIndex + 1];
|
|
}
|
|
}
|
|
else if (path.join('/') === './lib/type') {
|
|
// for boolean.js, number.js, string.js
|
|
category = 'construction';
|
|
}
|
|
|
|
if (IGNORE_FUNCTIONS[name]) {
|
|
category = null;
|
|
}
|
|
|
|
if (category) {
|
|
functions[name] = {
|
|
name: name,
|
|
category: category,
|
|
fullPath: fullPath,
|
|
relativePath: fullPath.substring(inputPath.length)
|
|
};
|
|
}
|
|
});
|
|
|
|
// 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 = String(fs.readFileSync(fn.fullPath));
|
|
|
|
var isFunction = code.indexOf('exports.name') !== -1
|
|
&& code.indexOf('exports.factory') !== -1
|
|
&& code.indexOf('exports.path') === -1;
|
|
var doc = isFunction && generateDoc(name, code);
|
|
|
|
if (isFunction && doc) {
|
|
fn.doc = doc;
|
|
issues = issues.concat(validateDoc(doc));
|
|
var markdown = generateMarkdown(doc, functions);
|
|
fs.writeFileSync(outputPath + '/' + fn.name + '.md', markdown);
|
|
}
|
|
else {
|
|
//gutil.log('Ignoring', fn.fullPath);
|
|
delete functions[name];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to generate a markdown list entry for a function.
|
|
* Used to generate both alphabetical and categorical index pages.
|
|
* @param {string} name Function name
|
|
* @returns {string} Returns a markdown list entry
|
|
*/
|
|
function functionEntry (name) {
|
|
var fn = functions[name];
|
|
var syntax = SYNTAX[name] || fn.doc && fn.doc.syntax && fn.doc.syntax[0] || name;
|
|
syntax = syntax
|
|
//.replace(/^math\./, '')
|
|
.replace(/\s+\/\/.*$/, '')
|
|
.replace(/;$/, '');
|
|
if (syntax.length < 40) {
|
|
syntax = syntax.replace(/ /g, ' ');
|
|
}
|
|
|
|
var description = '';
|
|
if (fn.doc.description) {
|
|
description = fn.doc.description.replace(/\n/g, ' ').split('.')[0] + '.';
|
|
}
|
|
|
|
return '[' + syntax + '](functions/' + name + '.md) | ' + description;
|
|
}
|
|
|
|
/**
|
|
* Change the first letter of the given string to upper case
|
|
* @param {string} text
|
|
*/
|
|
function toCapital (text) {
|
|
return text[0].toUpperCase() + text.slice(1);
|
|
}
|
|
|
|
var order = ['core', 'construction', 'expression']; // and then the rest
|
|
function categoryIndex (entry) {
|
|
var index = order.indexOf(entry);
|
|
return index === -1 ? Infinity : index;
|
|
}
|
|
function compareAsc (a, b) {
|
|
return a > b ? 1 : (a < b ? -1 : 0);
|
|
}
|
|
function compareCategory (a, b) {
|
|
var indexA = categoryIndex(a);
|
|
var indexB = categoryIndex(b);
|
|
return (indexA > indexB) ? 1 : (indexA < indexB ? -1 : compareAsc(a, b));
|
|
}
|
|
|
|
// generate categorical page with all functions
|
|
var categories = {};
|
|
Object.keys(functions).forEach(function (name) {
|
|
var fn = functions[name];
|
|
var category = categories[fn.category];
|
|
if (!category) {
|
|
categories[fn.category] = {};
|
|
}
|
|
categories[fn.category][name] = fn;
|
|
});
|
|
var categorical = '# Function reference\n\n';
|
|
categorical += Object.keys(categories).sort(compareCategory).map(function (category) {
|
|
var functions = categories[category];
|
|
|
|
return '## ' + toCapital(category) + ' functions\n\n' +
|
|
'Function | Description\n' +
|
|
'---- | -----------\n' +
|
|
Object.keys(functions).sort().map(functionEntry).join('\n') + '\n';
|
|
}).join('\n');
|
|
categorical += '\n\n\n<!-- Note: This file is automatically generated from source code comments. Changes made in this file will be overridden. -->\n';
|
|
|
|
fs.writeFileSync(outputRoot + 'functions.md', categorical);
|
|
|
|
|
|
// 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; |