diff --git a/.jshintignore b/.jshintignore index bef11ae..291aa98 100644 --- a/.jshintignore +++ b/.jshintignore @@ -4,6 +4,6 @@ node_modules/ tmp/ tests/ skins/ +lib/ndoc/plugins/parsers/ndoc/ lib/ndoc/plugins/renderers/html/ -lib/ndoc/parsers/javascript.js .cache/ diff --git a/Makefile b/Makefile index 8e2fe31..7af1c8e 100644 --- a/Makefile +++ b/Makefile @@ -57,14 +57,14 @@ publish: npm publish https://github.com/${GITHUB_PROJ}/tarball/${NPM_VERSION} -lib: lib/ndoc/parsers/javascript.js -lib/ndoc/parsers/javascript.js: +lib: lib/ndoc/plugins/parsers/ndoc/parser.js +lib/ndoc/plugins/parsers/ndoc/parser.js: @if test ! `which jison` ; then \ echo "You need 'jison' installed in order to compile parsers." >&2 ; \ echo " $ make dev-deps" >&2 ; \ exit 128 ; \ fi - jison src/js-parser.y && mv js-parser.js lib/ndoc/parsers/javascript.js + jison src/js-parser.y && mv js-parser.js lib/ndoc/plugins/parsers/ndoc/parser.js compile-parsers: diff --git a/bin/ndoc.js b/bin/ndoc.js index 9a1637a..de5caa8 100755 --- a/bin/ndoc.js +++ b/bin/ndoc.js @@ -164,14 +164,19 @@ walk_many(opts.paths, extensionPattern, function (filename, stat, cb) { } // build tree - ndoc = new NDoc(files, { - // given package URL, file name and line in the file, format link to source file. - // do so only if `packageUrl` is set or `linkFormat` is set - formatLink: (opts.linkFormat || opts.package.url) && function (file, line) { - // '\' -> '/' for windows - return interpolate(opts.linkFormat, file.replace(/\\/g, '/'), line); - } - }); + try { + ndoc = new NDoc(files, _.extend({ + // given package URL, file name and line in the file, format link to source file. + // do so only if `packageUrl` is set or `linkFormat` is set + formatLink: (opts.linkFormat || opts.package.url) && function (file, line) { + // '\' -> '/' for windows + return interpolate(opts.linkFormat, file.replace(/\\/g, '/'), line); + } + }, opts)); + } catch (err) { + console.error('FATAL:', err.stack || err.message || err); + process.exit(1); + } // output tree ndoc.render(opts.render, opts, function (err) { diff --git a/lib/ndoc.js b/lib/ndoc.js index eda41ae..61483c3 100644 --- a/lib/ndoc.js +++ b/lib/ndoc.js @@ -19,8 +19,8 @@ var _ = require('underscore'); // internal var extend = require('./ndoc/common').extend; -var parser = require('./ndoc/parsers/javascript'); var renderers = require('./ndoc/renderers'); +var parsers = require('./ndoc/parsers'); //////////////////////////////////////////////////////////////////////////////// @@ -40,262 +40,12 @@ var renderers = require('./ndoc/renderers'); * Called with `(file, line)` arguments. **/ var NDoc = module.exports = function NDoc(files, options) { - var list, tree, parted, sections; - // TODO: cleanup NDoc constructor + this.options = extend({}, options); + this.list = {}; // flat representation of ast + this.tree = {}; // ast + this.files = files; - // options - this.options = extend({}, options); - - // documentation tree consists of sections, which are populated with documents - list = { - '': { - id: '', - type: 'section', - children: [], - description: '', - short_description: '' - } - }; - - // parse specified source files - files.forEach(function (file) { - console.log('Compiling file', file); - - try { - var text, nodes; - - text = Fs.readFileSync(file, 'utf8'); - - // TODO: consider amending failing document inplace. - // Say, if it doesn't parse, insert a fake '*' line at failing `line` and retry - - nodes = parser.parse(text); - - // do pre-distribute early work - _.each(nodes, function (node, id) { - var clone; - - // assign hierarchy helpers - node.aliases = []; - node.children = []; - - if ('class' === node.type) { - node.subclasses = []; - } - - // compose links to source files - if (options.formatLink) { - node.href = options.formatLink(file, node.line); - } - - // collect sections - if ('section' === node.type) { - list[node.id] = node; - return; - } - - // elements with undefined section get '' section, - // and will be resolved later, when we'll have full - // element list - list[(node.section || '') + '.' + node.id] = node; - - // bound methods produce two methods with the same description but different signatures - // E.g. Element.foo(@element, a, b) becomes - // Element.foo(element, a, b) and Element#foo(a, b) - if ('method' === node.type && node.bound) { - clone = extend(node); - clone.id = node.id.replace(/(.+)\.(.+)/, '$1#$2'); - - // link to methods - node.bound = clone.id; - clone.bound = node.id; - - // insert bound method clone - list[(node.section || '') + '.' + clone.id] = clone; - } - }); - } catch (err) { - console.error('FATAL:', file, err.stack || err.message || err); - process.exit(1); - } - }); - - // TODO: section.related_to should mark related element as belonging to the section - //_.each(list, function (node, id) { - // var ref_id = '.' + node.related_to, ref; - // if ('section' === node.type && node.related_to && list[ref_id]) { - // ref = list[ref_id]; - // ref.id = node.id + '.' + node.related_to; - // delete list[ref_id]; - // list[ref.id] = ref; - // } - //}); - - - // - // 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 list of sections - // N.B. starting with 1 we skip "" section - parted = _.keys(list).sort().slice(1).map(function (id) { - return {id: id, parted: id.split(/[.#@]/), node: list[id]}; - }); - - _.each(parted, function (data) { - var found; - - // leave only ids without defined section - if ('' !== data.parted[0]) { - return; - } - - found = _.find(parted, function (other) { - return !!other.parted[0] && other.parted[1] === data.parted[1]; - }); - - if (found) { - delete list[data.id]; - - data.node.id = found.parted[0] + data.id; - data.parted[0] = found.parted[0]; - - list[data.node.id] = data.node; - } - }); - - // sort elements in case-insensitive manner - tree = {}; - - sections = _.keys(list).sort(function (a, b) { - a = a.toLowerCase(); - b = b.toLowerCase(); - return a === b ? 0 : a < b ? -1 : 1; - }); - - sections.forEach(function (id) { - tree[id] = list[id]; - }); - - // rebuild the tree from the end to beginning. - // N.B. since the list we iterate over is sorted, we can determine precisely - // the parent of any element. - _.each(sections.slice(0).reverse(), function (id) { - var // parent name is this element's name without portion after - // the last '.' for class member, '#' for instance member, - // or '@' for events - idx = Math.max(id.lastIndexOf('.'), id.lastIndexOf('#'), id.lastIndexOf('@')), - // get parent element - parent = tree[id.substring(0, idx)]; - - // no '.' or '#' found? this is top level section. just skip it - // no parent? skip it as well - if (-1 === idx || !parent) { - return; - } - - // parent element found. move this element to parent's children list, - // maintaing order - parent.children.unshift(tree[id]); - delete tree[id]; - }); - - // cleanup list, reassign right ids after we resolved - // to which sections every element belongs - _.each(list, function (node, id) { - delete list[id]; - - // compose new id - node.id = id.replace(/^[^.]*\./, ''); - node.name = node.id.replace(/^.*[.#@]/, ''); - - // sections have lowercased ids, to not clash with other elements - if ('section' === node.type) { - node.id = node.id.toLowerCase(); - } - - // prototype members have different paths - // events have different paths as well - node.path = node.id.replace(/#/g, '.prototype.').replace(/@/g, '.event.'); - delete node.section; - - // prune sections from list - if ('section' !== node.type) { - list[node.id] = node; - } - }); - - // assign aliases, subclasses, constructors - // correct method types (class or entity) - _.each(list, function (node, id) { - // aliases - if (node.alias_of && list[node.alias_of]) { - list[node.alias_of].aliases.push(node.id); - } - - // classes hierarchy - if ('class' === node.type) { - //if (d.superclass) console.log('SUPER', id, d.superclass) - if (node.superclass && list[node.superclass]) { - list[node.superclass].subclasses.push(node.id); - } - - return; - } - - if ('constructor' === node.type) { - node.id = 'new ' + node.id.replace(/\.new$/, ''); - return; - } - - // methods and properties - if ('method' === node.type || 'prototype' === node.type) { - // FIXME: shouldn't it be assigned by parser? - - if (node.id.match(/^\$/)) { - node.type = 'utility'; - return; - } - - if (node.id.indexOf('#') >= 0) { - node.type = 'instance ' + node.type; - return; - } - - if (node.id.indexOf('.') >= 0) { - node.type = 'class ' + node.type; - return; - } - - if (node.id.indexOf('@') >= 0) { - node.type = 'event'; - return; - } - } - }); - - // tree is hash of sections. - // convert sections to uniform children array of tree top level - var children = []; - - _.each(tree, function (node, id) { - if (id === '') { - children = children.concat(node.children); - } else { - children.push(node); - } - - delete tree[id]; - }); - - tree.children = children; - - // store tree and flat list - this.list = list; - this.tree = tree; + parsers[options.parser](this, options); }; @@ -359,10 +109,23 @@ NDoc.registerRenderer = function (name, func) { }; +/** + * NDoc.registerParser(name, func) -> Void + * - name (String): Name of the parser, e.g. `'ndoc'` + * - func (Function): Parser function `func(ndocInstance, options, callback)` + * + * Registers given function as `name` renderer. + **/ +NDoc.registerParser = function (name, func) { + parsers[name] = func; +}; + + // // require base plugins // +NDoc.use(require(__dirname + '/ndoc/plugins/parsers/ndoc')); NDoc.use(require(__dirname + '/ndoc/plugins/renderers/html')); NDoc.use(require(__dirname + '/ndoc/plugins/renderers/json')); diff --git a/lib/ndoc/cli.js b/lib/ndoc/cli.js index 7690f96..669965b 100644 --- a/lib/ndoc/cli.js +++ b/lib/ndoc/cli.js @@ -3,6 +3,7 @@ // internal var renderers = require('./renderers'); +var parsers = require('./parsers'); // 3rd-party @@ -101,6 +102,15 @@ cli.addArgument(['-r', '--render'], { }); +cli.addArgument(['--parser'], { + help: 'Documentation parser', + choices: function () { return _.keys(parsers).join(','); }, + metavar: 'PARSER', + action: 'store+lazyChoices', + defaultValue: 'ndoc' +}); + + cli.addArgument(['-l', '--link-format'], { dest: 'linkFormat', help: 'Format for link to source file', diff --git a/lib/ndoc/parsers.js b/lib/ndoc/parsers.js new file mode 100644 index 0000000..53c307b --- /dev/null +++ b/lib/ndoc/parsers.js @@ -0,0 +1,5 @@ +'use strict'; + +// Simple "shared" parsers object. +// Used to avoid circular dependencies (between cli and ndoc). +module.exports = {}; diff --git a/lib/ndoc/plugins.js b/lib/ndoc/plugins.js index 4afca36..88de8f5 100644 --- a/lib/ndoc/plugins.js +++ b/lib/ndoc/plugins.js @@ -10,3 +10,10 @@ * * Built-in renderer plugins. **/ + + +/** section: Plugins + * Parsers + * + * Built-in parser plugins. + **/ diff --git a/lib/ndoc/plugins/parsers/ndoc.js b/lib/ndoc/plugins/parsers/ndoc.js new file mode 100644 index 0000000..d1fa2b9 --- /dev/null +++ b/lib/ndoc/plugins/parsers/ndoc.js @@ -0,0 +1,292 @@ +/** internal, section: Plugins + * Parsers.ndoc(NDoc) -> Void + * + * Registers NDoc parser as `ndoc`. + * + * + * ##### Example + * + * doc.parse('ndoc', options, function (err) { + * // ... + * }); + **/ + + +'use strict'; + + +// stdlib +var fs = require('fs'); + + +// 3rd-party +var _ = require('underscore'); + + +// internal +var interpolate = require('../../common').interpolate; +var extend = require('../../common').extend; +var ndoc_parser = require('./ndoc/parser'); + + +//////////////////////////////////////////////////////////////////////////////// + + +var parser_func = function (ndoc, options) { + var list, tree, parted, sections, children; + + // documentation tree consists of sections, which are populated with documents + list = { + // root section node + '': { + id: '', + type: 'section', + children: [], + description: '', + short_description: '' + } + }; + + // parse specified source files + ndoc.files.forEach(function (file) { + console.log('Compiling file', file); + + var text, nodes; + + text = fs.readFileSync(file, 'utf8'); + + // TODO: consider amending failing document inplace. + // Say, if it doesn't parse, insert a fake '*' line at failing `line` and retry + + nodes = ndoc_parser.parse(text); + + // do pre-distribute early work + _.each(nodes, function (node, id) { + var clone; + + // assign hierarchy helpers + node.aliases = []; + node.children = []; + + if ('class' === node.type) { + node.subclasses = []; + } + + // compose links to source files + if (options.formatLink) { + node.href = options.formatLink(file, node.line); + } + + // collect sections + if ('section' === node.type) { + list[node.id] = node; + return; + } + + // elements with undefined section get '' section, + // and will be resolved later, when we'll have full + // element list + list[(node.section || '') + '.' + node.id] = node; + + // bound methods produce two methods with the same description but different signatures + // E.g. Element.foo(@element, a, b) becomes + // Element.foo(element, a, b) and Element#foo(a, b) + if ('method' === node.type && node.bound) { + clone = extend(node); + clone.id = node.id.replace(/(.+)\.(.+)/, '$1#$2'); + + // link to methods + node.bound = clone.id; + clone.bound = node.id; + + // insert bound method clone + list[(node.section || '') + '.' + clone.id] = clone; + } + }); + }); + + // TODO: section.related_to should mark related element as belonging to the section + //_.each(list, function (node, id) { + // var ref_id = '.' + node.related_to, ref; + // if ('section' === node.type && node.related_to && list[ref_id]) { + // ref = list[ref_id]; + // ref.id = node.id + '.' + node.related_to; + // delete list[ref_id]; + // list[ref.id] = ref; + // } + //}); + + + // + // 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 list of sections + // N.B. starting with 1 we skip "" section + parted = _.keys(list).sort().slice(1).map(function (id) { + return {id: id, parted: id.split(/[.#@]/), node: list[id]}; + }); + + _.each(parted, function (data) { + var found; + + // leave only ids without defined section + if ('' !== data.parted[0]) { + return; + } + + found = _.find(parted, function (other) { + return !!other.parted[0] && other.parted[1] === data.parted[1]; + }); + + if (found) { + delete list[data.id]; + + data.node.id = found.parted[0] + data.id; + data.parted[0] = found.parted[0]; + + list[data.node.id] = data.node; + } + }); + + // sort elements in case-insensitive manner + tree = {}; + + sections = _.keys(list).sort(function (a, b) { + a = a.toLowerCase(); + b = b.toLowerCase(); + return a === b ? 0 : a < b ? -1 : 1; + }); + + sections.forEach(function (id) { + tree[id] = list[id]; + }); + + // rebuild the tree from the end to beginning. + // N.B. since the list we iterate over is sorted, we can determine precisely + // the parent of any element. + _.each(sections.slice(0).reverse(), function (id) { + var // parent name is this element's name without portion after + // the last '.' for class member, '#' for instance member, + // or '@' for events + idx = Math.max(id.lastIndexOf('.'), id.lastIndexOf('#'), id.lastIndexOf('@')), + // get parent element + parent = tree[id.substring(0, idx)]; + + // no '.' or '#' found? this is top level section. just skip it + // no parent? skip it as well + if (-1 === idx || !parent) { + return; + } + + // parent element found. move this element to parent's children list, + // maintaing order + parent.children.unshift(tree[id]); + delete tree[id]; + }); + + // cleanup list, reassign right ids after we resolved + // to which sections every element belongs + _.each(list, function (node, id) { + delete list[id]; + + // compose new id + node.id = id.replace(/^[^.]*\./, ''); + node.name = node.id.replace(/^.*[.#@]/, ''); + + // sections have lowercased ids, to not clash with other elements + if ('section' === node.type) { + node.id = node.id.toLowerCase(); + } + + // prototype members have different paths + // events have different paths as well + node.path = node.id.replace(/#/g, '.prototype.').replace(/@/g, '.event.'); + delete node.section; + + // prune sections from list + if ('section' !== node.type) { + list[node.id] = node; + } + }); + + // assign aliases, subclasses, constructors + // correct method types (class or entity) + _.each(list, function (node, id) { + // aliases + if (node.alias_of && list[node.alias_of]) { + list[node.alias_of].aliases.push(node.id); + } + + // classes hierarchy + if ('class' === node.type) { + //if (d.superclass) console.log('SUPER', id, d.superclass) + if (node.superclass && list[node.superclass]) { + list[node.superclass].subclasses.push(node.id); + } + + return; + } + + if ('constructor' === node.type) { + node.id = 'new ' + node.id.replace(/\.new$/, ''); + return; + } + + // methods and properties + if ('method' === node.type || 'prototype' === node.type) { + // FIXME: shouldn't it be assigned by parser? + + if (node.id.match(/^\$/)) { + node.type = 'utility'; + return; + } + + if (node.id.indexOf('#') >= 0) { + node.type = 'instance ' + node.type; + return; + } + + if (node.id.indexOf('.') >= 0) { + node.type = 'class ' + node.type; + return; + } + + if (node.id.indexOf('@') >= 0) { + node.type = 'event'; + return; + } + } + }); + + // tree is hash of sections. + // convert sections to uniform children array of tree top level + children = []; + + _.each(tree, function (node, id) { + if (id === '') { + children = children.concat(node.children); + } else { + children.push(node); + } + + delete tree[id]; + }); + + tree.children = children; + + // store tree and flat list + ndoc.list = list; + ndoc.tree = tree; +}; + + +//////////////////////////////////////////////////////////////////////////////// + + +module.exports = function (NDoc) { + NDoc.registerParser('ndoc', parser_func); +}; diff --git a/lib/ndoc/parsers/javascript.js b/lib/ndoc/plugins/parsers/ndoc/parser.js similarity index 100% rename from lib/ndoc/parsers/javascript.js rename to lib/ndoc/plugins/parsers/ndoc/parser.js