ndoc
Version:
JavaScript API documentor with simple syntax.
398 lines (307 loc) • 9.63 kB
JavaScript
/**
* class NDoc
*
* Handles documentation tree.
**/
;
const path = require('path');
const _ = require('lodash');
const template = require('./ndoc/common').template;
////////////////////////////////////////////////////////////////////////////////
let NDoc = {
renderers: {},
parsers: {}
};
module.exports = NDoc;
////////////////////////////////////////////////////////////////////////////////
// parse all files and prepare a "raw list of nodes
function parse_files(files, options) {
let nodes = {
// root section node
'': {
id: '',
type: 'section',
children: [],
description: '',
short_description: '',
href: '#',
root: true,
file: '',
line: 0
}
};
files.forEach(file => {
let fn = NDoc.parsers[path.extname(file)];
if (!fn) return;
/* eslint-disable no-console */
console.info('Parsing file: ' + file);
let file_nodes = fn(file, options);
// TODO: fail on name clash here as well -- as we might get name clash
// from different parsers, or even differnret files
_.extend(nodes, file_nodes);
});
return nodes;
}
function build_tree(nodes, options) {
let tree, parted, sections, children;
//
// preprocess nodes
//
_.each(nodes, node => {
if (!node.href && !node.root && options.linkFormat) {
node.href = template(options.linkFormat, {
package: options.package,
file: node.file.replace(/\\/g, '/'),
line: node.line
});
}
});
//
// for each element with undefined section try to guess the section
// E.g. for ".Ajax.Updater" we try to find "SECTION.Ajax" element.
// If found, rename ".Ajax.Updater" to "SECTION.Ajax.Updater"
//
// prepare nodes of sections
// N.B. starting with 1 we skip "" section
parted = _.keys(nodes).sort().slice(1).map(id =>
({ id, parted: id.split(/[.#@]/), node: nodes[id] }));
_.each(parted, data => {
let found;
// leave only ids without defined section
if (data.parted[0] !== '') return;
found = _.find(parted, other =>
!!other.parted[0] && other.parted[1] === data.parted[1]);
if (found) {
delete nodes[data.id];
data.node.id = found.parted[0] + data.id;
data.parted[0] = found.parted[0];
nodes[data.node.id] = data.node;
}
});
// sort elements in case-insensitive manner
tree = {};
sections = _.keys(nodes).sort((a, b) => {
a = a.toLowerCase();
b = b.toLowerCase();
return a === b ? 0 : a < b ? -1 : 1;
});
sections.forEach(id => { tree[id] = nodes[id]; });
// rebuild the tree from the end to beginning.
// N.B. since the nodes we iterate over is sorted, we can determine precisely
// the parent of any element.
_.each(sections.slice(0).reverse(), id => {
let idx, parent;
// parent name is this element's name without portion after
// the last '.' for class member, last '#' for instance member,
// or first '@' for events
// first check for event, because event name can contain '.', '#' and '@'
idx = id.indexOf('@');
if (idx === -1) {
idx = Math.max(id.lastIndexOf('.'), id.lastIndexOf('#'));
}
// get parent element
parent = tree[id.substring(0, idx)];
// no '.' or '#' or '@' found or no parent? -- top level section. skip it
if (idx === -1 || !parent) return;
// parent element found. move this element to parent's children nodes,
// maintaing order
parent.children.unshift(tree[id]);
delete tree[id];
});
// cleanup nodes, reassign right ids after we resolved
// to which sections every element belongs
_.each(nodes, (node, id) => {
delete nodes[id];
// compose new id
node.id = id.replace(/^[^.]*\./, '');
// First check for event, because event name can contain '.' and '#'
let idx = node.id.indexOf('@'); // get position of @event start
// Otherwise get property/method delimiter position
if (idx === -1) {
idx = Math.max(node.id.lastIndexOf('.'), node.id.lastIndexOf('#'));
}
if (idx === -1) {
node.name = node.id;
} else {
node.name = node.id.substring(idx + 1);
node.name_prefix = node.id.substring(0, idx + 1);
}
// sections have lowercased ids, to not clash with other elements
if (node.type === 'section') {
node.id = node.id.toLowerCase();
}
// prototype members have different paths
node.path = node.id.replace(/#/g, '.prototype.');
// events have different paths as well, but only first '@' separates event name
node.path = node.path.replace(/@/, '.event.');
delete node.section;
// prune sections from nodes
if (node.type !== 'section') {
nodes[node.id] = node;
}
});
// assign aliases, subclasses, constructors
// correct method types (class or entity)
_.each(nodes, node /*, id*/ => {
// aliases
if (node.alias_of && nodes[node.alias_of]) {
nodes[node.alias_of].aliases.push(node.id);
}
// classes hierarchy
if (node.type === 'class') {
//if (d.superclass) console.log('SUPER', id, d.superclass)
if (node.superclass && nodes[node.superclass]) {
nodes[node.superclass].subclasses.push(node.id);
}
return;
}
if (node.type === 'constructor') {
node.id = 'new ' + node.id.replace(/\.new$/, '');
return;
}
// methods and properties
if (node.type === 'method' || node.type === 'property') {
// FIXME: shouldn't it be assigned by parser?
if (node.id.match(/^\$/)) {
node.type = 'utility';
return;
}
// first check for event, because event name can contain '.' and '#'
if (node.id.indexOf('@') >= 0) {
node.type = 'event';
return;
}
if (node.id.indexOf('#') >= 0) {
node.type = 'instance ' + node.type;
return;
}
if (node.id.indexOf('.') >= 0) {
node.type = 'class ' + node.type;
return;
}
}
});
// tree is hash of sections.
// convert sections to uniform children array of tree top level
children = [];
_.each(tree, (node, id) => {
if (id === '') children = children.concat(node.children);
else children.push(node);
delete tree[id];
});
tree.children = children;
return { list: nodes, tree };
}
////////////////////////////////////////////////////////////////////////////////
/**
* NDoc.parse(files, options) -> AST
* - files (Array): Files to be parsed
* - options (Object): Parser options
*
* Execute `name` parser against `files` with given options.
*
*
* ##### Options
*
* - **linkFormat**: Format for link to source file. This can have variables:
* - `{file}`: Current file
* - `{line}`: Current line
* - `{package.*}`: Any package.json variable
**/
NDoc.parse = function parse(files, options) {
return build_tree(parse_files(files, options), options);
};
/**
* NDoc.render(name, ast, options) -> Void
* - name (String): Renderer name
* - ast (Object): Parsed AST (should consist of `list` and `tree`)
* - options (Object): Renderer options
*
* Execute `name` renderer for `ast` with given options.
**/
NDoc.render = function render(name, ast, options) {
if (!NDoc.renderers[name]) throw new Error(`Unknown renderer: ${name}`);
NDoc.renderers[name](ast, options);
};
/**
* NDoc.cli -> cli
**/
NDoc.cli = require('./ndoc/cli');
NDoc.cli.ndoc = NDoc;
/**
* NDoc.VERSION -> String
*
* NDoc version.
**/
NDoc.VERSION = require('./ndoc/version');
/**
* NDoc.use(plugin) -> Void
* - plugin (Function): Infection `plugin(NDocClass)`
*
* Runs given `plugin` against NDoc base class.
*
*
* ##### Examples
*
* ```js
* NDoc.use(require('my-renderer'));
* ```
**/
NDoc.use = function use(plugin) { plugin(this); };
/**
* NDoc.registerRenderer(name, func) -> Void
* - name (String): Name of the renderer, e.g. `'html'`
* - func (Function): Renderer function `func(ast, options)`
*
* Registers given function as `name` renderer.
**/
NDoc.registerRenderer = function (name, func) { NDoc.renderers[name] = func; };
/**
* NDoc.registerParser(extension, func) -> Void
* - extension (String): Extension suitable for the parser, e.g. `'js'`
* - func (Function): Parser function `func(source, options)`
*
* Registers given function as `name` renderer.
**/
NDoc.registerParser = function (extension, func) {
extension = path.extname('name.' + extension);
Object.defineProperty(NDoc.parsers, extension, {
get() { return func; },
configurable: true
});
};
/**
* NDoc.extensionAlias(alias, extension) -> Void
* - alias (String): Extension as for the parser, e.g. `'cc'`
* - extension (String): Extension as for the parser, e.g. `'js'`
*
* Registers `alias` of the `extension` parser.
*
*
* ##### Example
*
* Parse all `*.cc` files with parser registered for `*.js`
*
* ```js
* ndoc.extensionAlias('cc', 'js');
* ```
*
*
* ##### See Also
*
* - [[NDoc.registerParser]]
**/
NDoc.extensionAlias = function (alias, extension) {
alias = path.extname('name.' + alias);
extension = path.extname('name.' + extension);
Object.defineProperty(NDoc.parsers, alias, {
get() { return NDoc.parsers[extension]; },
configurable: true
});
};
//
// require base plugins
//
NDoc.use(require(path.join(__dirname, 'ndoc/plugins/parsers/javascript')));
NDoc.use(require(path.join(__dirname, 'ndoc/plugins/renderers/html')));
NDoc.use(require(path.join(__dirname, 'ndoc/plugins/renderers/json')));