From 453c68da19058965dc18c7eff421ae5575b9834d Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 8 Aug 2012 20:56:56 +0200 Subject: [PATCH] Add doc generator. --- doc/template.html | 23 ++ tools/doc/LICENSE | 18 ++ tools/doc/README.md | 76 ++++++ tools/doc/generate.js | 120 +++++++++ tools/doc/html.js | 174 +++++++++++++ tools/doc/json.js | 557 +++++++++++++++++++++++++++++++++++++++++ tools/doc/package.json | 15 ++ 7 files changed, 983 insertions(+) create mode 100644 doc/template.html create mode 100644 tools/doc/LICENSE create mode 100644 tools/doc/README.md create mode 100644 tools/doc/generate.js create mode 100644 tools/doc/html.js create mode 100644 tools/doc/json.js create mode 100644 tools/doc/package.json diff --git a/doc/template.html b/doc/template.html new file mode 100644 index 000000000..2eb939872 --- /dev/null +++ b/doc/template.html @@ -0,0 +1,23 @@ + + + + + __SECTION__ Etherpad-Lite Manual & Documentation + + + + + +
+

Table of Contents

+ __TOC__ +
+ +
+ __CONTENT__ +
+ + + diff --git a/tools/doc/LICENSE b/tools/doc/LICENSE new file mode 100644 index 000000000..e3d4e695a --- /dev/null +++ b/tools/doc/LICENSE @@ -0,0 +1,18 @@ +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/tools/doc/README.md b/tools/doc/README.md new file mode 100644 index 000000000..743bfaae1 --- /dev/null +++ b/tools/doc/README.md @@ -0,0 +1,76 @@ +Here's how the node docs work. + +Each type of heading has a description block. + + + ## module + + Stability: 3 - Stable + + description and examples. + + ### module.property + + * Type + + description of the property. + + ### module.someFunction(x, y, [z=100]) + + * `x` {String} the description of the string + * `y` {Boolean} Should I stay or should I go? + * `z` {Number} How many zebras to bring. + + A description of the function. + + ### Event: 'blerg' + + * Argument: SomeClass object. + + Modules don't usually raise events on themselves. `cluster` is the + only exception. + + ## Class: SomeClass + + description of the class. + + ### Class Method: SomeClass.classMethod(anArg) + + * `anArg` {Object} Just an argument + * `field` {String} anArg can have this field. + * `field2` {Boolean} Another field. Default: `false`. + * Return: {Boolean} `true` if it worked. + + Description of the method for humans. + + ### someClass.nextSibling() + + * Return: {SomeClass object | null} The next someClass in line. + + ### someClass.someProperty + + * String + + The indication of what someProperty is. + + ### Event: 'grelb' + + * `isBlerg` {Boolean} + + This event is emitted on instances of SomeClass, not on the module itself. + + +* Modules have (description, Properties, Functions, Classes, Examples) +* Properties have (type, description) +* Functions have (list of arguments, description) +* Classes have (description, Properties, Methods, Events) +* Events have (list of arguments, description) +* Methods have (list of arguments, description) +* Properties have (type, description) + +# CLI usage + +Run the following from the etherpad-lite root directory: +```sh +$ node tools/doc/generate doc/all.md --format=html --template=doc/template.html > out.htm +``` \ No newline at end of file diff --git a/tools/doc/generate.js b/tools/doc/generate.js new file mode 100644 index 000000000..8d52e101e --- /dev/null +++ b/tools/doc/generate.js @@ -0,0 +1,120 @@ +#!/usr/bin/env node +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +var marked = require('marked'); +var fs = require('fs'); +var path = require('path'); + +// parse the args. +// Don't use nopt or whatever for this. It's simple enough. + +var args = process.argv.slice(2); +var format = 'json'; +var template = null; +var inputFile = null; + +args.forEach(function (arg) { + if (!arg.match(/^\-\-/)) { + inputFile = arg; + } else if (arg.match(/^\-\-format=/)) { + format = arg.replace(/^\-\-format=/, ''); + } else if (arg.match(/^\-\-template=/)) { + template = arg.replace(/^\-\-template=/, ''); + } +}) + + +if (!inputFile) { + throw new Error('No input file specified'); +} + + +console.error('Input file = %s', inputFile); +fs.readFile(inputFile, 'utf8', function(er, input) { + if (er) throw er; + // process the input for @include lines + processIncludes(inputFile, input, next); +}); + + +var includeExpr = /^@include\s+([A-Za-z0-9-_\/]+)(?:\.)?([a-zA-Z]*)$/gmi; +var includeData = {}; +function processIncludes(inputFile, input, cb) { + var includes = input.match(includeExpr); + if (includes === null) return cb(null, input); + var errState = null; + console.error(includes); + var incCount = includes.length; + if (incCount === 0) cb(null, input); + + includes.forEach(function(include) { + var fname = include.replace(/^@include\s+/, ''); + if (!fname.match(/\.md$/)) fname += '.md'; + + if (includeData.hasOwnProperty(fname)) { + input = input.split(include).join(includeData[fname]); + incCount--; + if (incCount === 0) { + return cb(null, input); + } + } + + var fullFname = path.resolve(path.dirname(inputFile), fname); + fs.readFile(fullFname, 'utf8', function(er, inc) { + if (errState) return; + if (er) return cb(errState = er); + processIncludes(fullFname, inc, function(er, inc) { + if (errState) return; + if (er) return cb(errState = er); + incCount--; + includeData[fname] = inc; + input = input.split(include).join(includeData[fname]); + if (incCount === 0) { + return cb(null, input); + } + }); + }); + }); +} + + +function next(er, input) { + if (er) throw er; + switch (format) { + case 'json': + require('./json.js')(input, inputFile, function(er, obj) { + console.log(JSON.stringify(obj, null, 2)); + if (er) throw er; + }); + break; + + case 'html': + require('./html.js')(input, inputFile, template, function(er, html) { + if (er) throw er; + console.log(html); + }); + break; + + default: + throw new Error('Invalid format: ' + format); + } +} diff --git a/tools/doc/html.js b/tools/doc/html.js new file mode 100644 index 000000000..c52fff70a --- /dev/null +++ b/tools/doc/html.js @@ -0,0 +1,174 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +var fs = require('fs'); +var marked = require('marked'); +var path = require('path'); + +module.exports = toHTML; + +function toHTML(input, filename, template, cb) { + var lexed = marked.lexer(input); + fs.readFile(template, 'utf8', function(er, template) { + if (er) return cb(er); + render(lexed, filename, template, cb); + }); +} + +function render(lexed, filename, template, cb) { + // get the section + var section = getSection(lexed); + + filename = path.basename(filename, '.md'); + + lexed = parseLists(lexed); + + // generate the table of contents. + // this mutates the lexed contents in-place. + buildToc(lexed, filename, function(er, toc) { + if (er) return cb(er); + + template = template.replace(/__FILENAME__/g, filename); + template = template.replace(/__SECTION__/g, section); + template = template.replace(/__VERSION__/g, process.version); + template = template.replace(/__TOC__/g, toc); + + // content has to be the last thing we do with + // the lexed tokens, because it's destructive. + content = marked.parser(lexed); + template = template.replace(/__CONTENT__/g, content); + + cb(null, template); + }); +} + + +// just update the list item text in-place. +// lists that come right after a heading are what we're after. +function parseLists(input) { + var state = null; + var depth = 0; + var output = []; + output.links = input.links; + input.forEach(function(tok) { + if (state === null) { + if (tok.type === 'heading') { + state = 'AFTERHEADING'; + } + output.push(tok); + return; + } + if (state === 'AFTERHEADING') { + if (tok.type === 'list_start') { + state = 'LIST'; + if (depth === 0) { + output.push({ type:'html', text: '
' }); + } + depth++; + output.push(tok); + return; + } + state = null; + output.push(tok); + return; + } + if (state === 'LIST') { + if (tok.type === 'list_start') { + depth++; + output.push(tok); + return; + } + if (tok.type === 'list_end') { + depth--; + if (depth === 0) { + state = null; + output.push({ type:'html', text: '
' }); + } + output.push(tok); + return; + } + if (tok.text) { + tok.text = parseListItem(tok.text); + } + } + output.push(tok); + }); + + return output; +} + + +function parseListItem(text) { + text = text.replace(/\{([^\}]+)\}/, '$1'); + //XXX maybe put more stuff here? + return text; +} + + +// section is just the first heading +function getSection(lexed) { + var section = ''; + for (var i = 0, l = lexed.length; i < l; i++) { + var tok = lexed[i]; + if (tok.type === 'heading') return tok.text; + } + return ''; +} + + +function buildToc(lexed, filename, cb) { + var indent = 0; + var toc = []; + var depth = 0; + lexed.forEach(function(tok) { + if (tok.type !== 'heading') return; + if (tok.depth - depth > 1) { + return cb(new Error('Inappropriate heading level\n' + + JSON.stringify(tok))); + } + + depth = tok.depth; + var id = getId(filename + '_' + tok.text.trim()); + toc.push(new Array((depth - 1) * 2 + 1).join(' ') + + '* ' + + tok.text + ''); + tok.text += '#'; + }); + + toc = marked.parse(toc.join('\n')); + cb(null, toc); +} + +var idCounters = {}; +function getId(text) { + text = text.toLowerCase(); + text = text.replace(/[^a-z0-9]+/g, '_'); + text = text.replace(/^_+|_+$/, ''); + text = text.replace(/^([^a-z])/, '_$1'); + if (idCounters.hasOwnProperty(text)) { + text += '_' + (++idCounters[text]); + } else { + idCounters[text] = 0; + } + return text; +} + diff --git a/tools/doc/json.js b/tools/doc/json.js new file mode 100644 index 000000000..2459bc26a --- /dev/null +++ b/tools/doc/json.js @@ -0,0 +1,557 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +module.exports = doJSON; + +// Take the lexed input, and return a JSON-encoded object +// A module looks like this: https://gist.github.com/1777387 + +var marked = require('marked'); + +function doJSON(input, filename, cb) { + var root = {source: filename}; + var stack = [root]; + var depth = 0; + var current = root; + var state = null; + var lexed = marked.lexer(input); + lexed.forEach(function (tok) { + var type = tok.type; + var text = tok.text; + + // + // This is for cases where the markdown semantic structure is lacking. + if (type === 'paragraph' || type === 'html') { + var metaExpr = /\n*/g; + text = text.replace(metaExpr, function(_0, k, v) { + current[k.trim()] = v.trim(); + return ''; + }); + text = text.trim(); + if (!text) return; + } + + if (type === 'heading' && + !text.trim().match(/^example/i)) { + if (tok.depth - depth > 1) { + return cb(new Error('Inappropriate heading level\n'+ + JSON.stringify(tok))); + } + + // Sometimes we have two headings with a single + // blob of description. Treat as a clone. + if (current && + state === 'AFTERHEADING' && + depth === tok.depth) { + var clone = current; + current = newSection(tok); + current.clone = clone; + // don't keep it around on the stack. + stack.pop(); + } else { + // if the level is greater than the current depth, + // then it's a child, so we should just leave the stack + // as it is. + // However, if it's a sibling or higher, then it implies + // the closure of the other sections that came before. + // root is always considered the level=0 section, + // and the lowest heading is 1, so this should always + // result in having a valid parent node. + var d = tok.depth; + while (d <= depth) { + finishSection(stack.pop(), stack[stack.length - 1]); + d++; + } + current = newSection(tok); + } + + depth = tok.depth; + stack.push(current); + state = 'AFTERHEADING'; + return; + } // heading + + // Immediately after a heading, we can expect the following + // + // { type: 'code', text: 'Stability: ...' }, + // + // a list: starting with list_start, ending with list_end, + // maybe containing other nested lists in each item. + // + // If one of these isnt' found, then anything that comes between + // here and the next heading should be parsed as the desc. + var stability + if (state === 'AFTERHEADING') { + if (type === 'code' && + (stability = text.match(/^Stability: ([0-5])(?:\s*-\s*)?(.*)$/))) { + current.stability = parseInt(stability[1], 10); + current.stabilityText = stability[2].trim(); + return; + } else if (type === 'list_start' && !tok.ordered) { + state = 'AFTERHEADING_LIST'; + current.list = current.list || []; + current.list.push(tok); + current.list.level = 1; + } else { + current.desc = current.desc || []; + if (!Array.isArray(current.desc)) { + current.shortDesc = current.desc; + current.desc = []; + } + current.desc.push(tok); + state = 'DESC'; + } + return; + } + + if (state === 'AFTERHEADING_LIST') { + current.list.push(tok); + if (type === 'list_start') { + current.list.level++; + } else if (type === 'list_end') { + current.list.level--; + } + if (current.list.level === 0) { + state = 'AFTERHEADING'; + processList(current); + } + return; + } + + current.desc = current.desc || []; + current.desc.push(tok); + + }); + + // finish any sections left open + while (root !== (current = stack.pop())) { + finishSection(current, stack[stack.length - 1]); + } + + return cb(null, root) +} + + +// go from something like this: +// [ { type: 'list_item_start' }, +// { type: 'text', +// text: '`settings` Object, Optional' }, +// { type: 'list_start', ordered: false }, +// { type: 'list_item_start' }, +// { type: 'text', +// text: 'exec: String, file path to worker file. Default: `__filename`' }, +// { type: 'list_item_end' }, +// { type: 'list_item_start' }, +// { type: 'text', +// text: 'args: Array, string arguments passed to worker.' }, +// { type: 'text', +// text: 'Default: `process.argv.slice(2)`' }, +// { type: 'list_item_end' }, +// { type: 'list_item_start' }, +// { type: 'text', +// text: 'silent: Boolean, whether or not to send output to parent\'s stdio.' }, +// { type: 'text', text: 'Default: `false`' }, +// { type: 'space' }, +// { type: 'list_item_end' }, +// { type: 'list_end' }, +// { type: 'list_item_end' }, +// { type: 'list_end' } ] +// to something like: +// [ { name: 'settings', +// type: 'object', +// optional: true, +// settings: +// [ { name: 'exec', +// type: 'string', +// desc: 'file path to worker file', +// default: '__filename' }, +// { name: 'args', +// type: 'array', +// default: 'process.argv.slice(2)', +// desc: 'string arguments passed to worker.' }, +// { name: 'silent', +// type: 'boolean', +// desc: 'whether or not to send output to parent\'s stdio.', +// default: 'false' } ] } ] + +function processList(section) { + var list = section.list; + var values = []; + var current; + var stack = []; + + // for now, *just* build the heirarchical list + list.forEach(function(tok) { + var type = tok.type; + if (type === 'space') return; + if (type === 'list_item_start') { + if (!current) { + var n = {}; + values.push(n); + current = n; + } else { + current.options = current.options || []; + stack.push(current); + var n = {}; + current.options.push(n); + current = n; + } + return; + } else if (type === 'list_item_end') { + if (!current) { + throw new Error('invalid list - end without current item\n' + + JSON.stringify(tok) + '\n' + + JSON.stringify(list)); + } + current = stack.pop(); + } else if (type === 'text') { + if (!current) { + throw new Error('invalid list - text without current item\n' + + JSON.stringify(tok) + '\n' + + JSON.stringify(list)); + } + current.textRaw = current.textRaw || ''; + current.textRaw += tok.text + ' '; + } + }); + + // shove the name in there for properties, since they are always + // just going to be the value etc. + if (section.type === 'property' && values[0]) { + values[0].textRaw = '`' + section.name + '` ' + values[0].textRaw; + } + + // now pull the actual values out of the text bits. + values.forEach(parseListItem); + + // Now figure out what this list actually means. + // depending on the section type, the list could be different things. + + switch (section.type) { + case 'ctor': + case 'classMethod': + case 'method': + // each item is an argument, unless the name is 'return', + // in which case it's the return value. + section.signatures = section.signatures || []; + var sig = {} + section.signatures.push(sig); + sig.params = values.filter(function(v) { + if (v.name === 'return') { + sig.return = v; + return false; + } + return true; + }); + parseSignature(section.textRaw, sig); + break; + + case 'property': + // there should be only one item, which is the value. + // copy the data up to the section. + var value = values[0] || {}; + delete value.name; + section.typeof = value.type; + delete value.type; + Object.keys(value).forEach(function(k) { + section[k] = value[k]; + }); + break; + + case 'event': + // event: each item is an argument. + section.params = values; + break; + } + + // section.listParsed = values; + delete section.list; +} + + +// textRaw = "someobject.someMethod(a, [b=100], [c])" +function parseSignature(text, sig) { + var params = text.match(paramExpr); + if (!params) return; + params = params[1]; + // the ] is irrelevant. [ indicates optionalness. + params = params.replace(/\]/g, ''); + params = params.split(/,/) + params.forEach(function(p, i, _) { + p = p.trim(); + if (!p) return; + var param = sig.params[i]; + var optional = false; + var def; + // [foo] -> optional + if (p.charAt(0) === '[') { + optional = true; + p = p.substr(1); + } + var eq = p.indexOf('='); + if (eq !== -1) { + def = p.substr(eq + 1); + p = p.substr(0, eq); + } + if (!param) { + param = sig.params[i] = { name: p }; + } + // at this point, the name should match. + if (p !== param.name) { + console.error('Warning: invalid param "%s"', p); + console.error(' > ' + JSON.stringify(param)); + console.error(' > ' + text); + } + if (optional) param.optional = true; + if (def !== undefined) param.default = def; + }); +} + + +function parseListItem(item) { + if (item.options) item.options.forEach(parseListItem); + if (!item.textRaw) return; + + // the goal here is to find the name, type, default, and optional. + // anything left over is 'desc' + var text = item.textRaw.trim(); + // text = text.replace(/^(Argument|Param)s?\s*:?\s*/i, ''); + + text = text.replace(/^, /, '').trim(); + var retExpr = /^returns?\s*:?\s*/i; + var ret = text.match(retExpr); + if (ret) { + item.name = 'return'; + text = text.replace(retExpr, ''); + } else { + var nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/; + var name = text.match(nameExpr); + if (name) { + item.name = name[1]; + text = text.replace(nameExpr, ''); + } + } + + text = text.trim(); + var defaultExpr = /\(default\s*[:=]?\s*['"`]?([^, '"`]*)['"`]?\)/i; + var def = text.match(defaultExpr); + if (def) { + item.default = def[1]; + text = text.replace(defaultExpr, ''); + } + + text = text.trim(); + var typeExpr = /^\{([^\}]+)\}/; + var type = text.match(typeExpr); + if (type) { + item.type = type[1]; + text = text.replace(typeExpr, ''); + } + + text = text.trim(); + var optExpr = /^Optional\.|(?:, )?Optional$/; + var optional = text.match(optExpr); + if (optional) { + item.optional = true; + text = text.replace(optExpr, ''); + } + + text = text.replace(/^\s*-\s*/, ''); + text = text.trim(); + if (text) item.desc = text; +} + + +function finishSection(section, parent) { + if (!section || !parent) { + throw new Error('Invalid finishSection call\n'+ + JSON.stringify(section) + '\n' + + JSON.stringify(parent)); + } + + if (!section.type) { + section.type = 'module'; + if (parent && (parent.type === 'misc')) { + section.type = 'misc'; + } + section.displayName = section.name; + section.name = section.name.toLowerCase() + .trim().replace(/\s+/g, '_'); + } + + if (section.desc && Array.isArray(section.desc)) { + section.desc.links = section.desc.links || []; + section.desc = marked.parser(section.desc); + } + + if (!section.list) section.list = []; + processList(section); + + // classes sometimes have various 'ctor' children + // which are actually just descriptions of a constructor + // class signature. + // Merge them into the parent. + if (section.type === 'class' && section.ctors) { + section.signatures = section.signatures || []; + var sigs = section.signatures; + section.ctors.forEach(function(ctor) { + ctor.signatures = ctor.signatures || [{}]; + ctor.signatures.forEach(function(sig) { + sig.desc = ctor.desc; + }); + sigs.push.apply(sigs, ctor.signatures); + }); + delete section.ctors; + } + + // properties are a bit special. + // their "type" is the type of object, not "property" + if (section.properties) { + section.properties.forEach(function (p) { + if (p.typeof) p.type = p.typeof; + else delete p.type; + delete p.typeof; + }); + } + + // handle clones + if (section.clone) { + var clone = section.clone; + delete section.clone; + delete clone.clone; + deepCopy(section, clone); + finishSection(clone, parent); + } + + var plur; + if (section.type.slice(-1) === 's') { + plur = section.type + 'es'; + } else if (section.type.slice(-1) === 'y') { + plur = section.type.replace(/y$/, 'ies'); + } else { + plur = section.type + 's'; + } + + // if the parent's type is 'misc', then it's just a random + // collection of stuff, like the "globals" section. + // Make the children top-level items. + if (section.type === 'misc') { + Object.keys(section).forEach(function(k) { + switch (k) { + case 'textRaw': + case 'name': + case 'type': + case 'desc': + case 'miscs': + return; + default: + if (parent.type === 'misc') { + return; + } + if (Array.isArray(k) && parent[k]) { + parent[k] = parent[k].concat(section[k]); + } else if (!parent[k]) { + parent[k] = section[k]; + } else { + // parent already has, and it's not an array. + return; + } + } + }); + } + + parent[plur] = parent[plur] || []; + parent[plur].push(section); +} + + +// Not a general purpose deep copy. +// But sufficient for these basic things. +function deepCopy(src, dest) { + Object.keys(src).filter(function(k) { + return !dest.hasOwnProperty(k); + }).forEach(function(k) { + dest[k] = deepCopy_(src[k]); + }); +} + +function deepCopy_(src) { + if (!src) return src; + if (Array.isArray(src)) { + var c = new Array(src.length); + src.forEach(function(v, i) { + c[i] = deepCopy_(v); + }); + return c; + } + if (typeof src === 'object') { + var c = {}; + Object.keys(src).forEach(function(k) { + c[k] = deepCopy_(src[k]); + }); + return c; + } + return src; +} + + +// these parse out the contents of an H# tag +var eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i; +var classExpr = /^Class:\s*([^ ]+).*?$/i; +var propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i; +var braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i; +var classMethExpr = + /^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i; +var methExpr = + /^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i; +var newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/; +var paramExpr = /\((.*)\);?$/; + +function newSection(tok) { + var section = {}; + // infer the type from the text. + var text = section.textRaw = tok.text; + if (text.match(eventExpr)) { + section.type = 'event'; + section.name = text.replace(eventExpr, '$1'); + } else if (text.match(classExpr)) { + section.type = 'class'; + section.name = text.replace(classExpr, '$1'); + } else if (text.match(braceExpr)) { + section.type = 'property'; + section.name = text.replace(braceExpr, '$1'); + } else if (text.match(propExpr)) { + section.type = 'property'; + section.name = text.replace(propExpr, '$1'); + } else if (text.match(classMethExpr)) { + section.type = 'classMethod'; + section.name = text.replace(classMethExpr, '$1'); + } else if (text.match(methExpr)) { + section.type = 'method'; + section.name = text.replace(methExpr, '$1'); + } else if (text.match(newExpr)) { + section.type = 'ctor'; + section.name = text.replace(newExpr, '$1'); + } else { + section.name = text; + } + return section; +} diff --git a/tools/doc/package.json b/tools/doc/package.json new file mode 100644 index 000000000..d87c9345b --- /dev/null +++ b/tools/doc/package.json @@ -0,0 +1,15 @@ +{ + "author": "Isaac Z. Schlueter (http://blog.izs.me/)", + "name": "node-doc-generator", + "description": "Internal tool for generating Node.js API docs", + "version": "0.0.0", + "engines": { + "node": ">=0.6.10" + }, + "dependencies": { + "marked": "~0.1.9" + }, + "devDependencies": {}, + "optionalDependencies": {}, + "bin": "./generate.js" +}