Add doc generator.
parent
db4ae500a1
commit
453c68da19
|
@ -0,0 +1,23 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>__SECTION__ Etherpad-Lite Manual & Documentation</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body class="apidoc" id="api-section-__FILENAME__">
|
||||
<header id="header">
|
||||
<h1>Etherpad-Lite Manual & Documentation</h1>
|
||||
</header>
|
||||
|
||||
<div id="toc">
|
||||
<h2>Table of Contents</h2>
|
||||
__TOC__
|
||||
</div>
|
||||
|
||||
<div id="apicontent">
|
||||
__CONTENT__
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -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.
|
|
@ -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
|
||||
```
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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: '<div class="signature">' });
|
||||
}
|
||||
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: '</div>' });
|
||||
}
|
||||
output.push(tok);
|
||||
return;
|
||||
}
|
||||
if (tok.text) {
|
||||
tok.text = parseListItem(tok.text);
|
||||
}
|
||||
}
|
||||
output.push(tok);
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
function parseListItem(text) {
|
||||
text = text.replace(/\{([^\}]+)\}/, '<span class="type">$1</span>');
|
||||
//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(' ') +
|
||||
'* <a href="#' + id + '">' +
|
||||
tok.text + '</a>');
|
||||
tok.text += '<span><a class="mark" href="#' + id + '" ' +
|
||||
'id="' + id + '">#</a></span>';
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
// <!-- type = module -->
|
||||
// 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;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"author": "Isaac Z. Schlueter <i@izs.me> (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"
|
||||
}
|
Loading…
Reference in New Issue