1 // Copyright Joyent, Inc. and other Node contributors.
3 // Permission is hereby granted, free of charge, to any person obtaining a
4 // copy of this software and associated documentation files (the
5 // "Software"), to deal in the Software without restriction, including
6 // without limitation the rights to use, copy, modify, merge, publish,
7 // distribute, sublicense, and/or sell copies of the Software, and to permit
8 // persons to whom the Software is furnished to do so, subject to the
9 // following conditions:
11 // The above copyright notice and this permission notice shall be included
12 // in all copies or substantial portions of the Software.
14 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15 // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17 // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18 // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20 // USE OR OTHER DEALINGS IN THE SOFTWARE.
22 module.exports = doJSON;
24 // Take the lexed input, and return a JSON-encoded object
25 // A module looks like this: https://gist.github.com/1777387
27 var marked = require('marked');
29 function doJSON(input, filename, cb) {
30 var root = {source: filename};
35 var lexed = marked.lexer(input);
36 lexed.forEach(function (tok) {
40 // <!-- type = module -->
41 // This is for cases where the markdown semantic structure is lacking.
42 if (type === 'paragraph' || type === 'html') {
43 var metaExpr = /<!--([^=]+)=([^\-]+)-->\n*/g;
44 text = text.replace(metaExpr, function(_0, k, v) {
45 current[k.trim()] = v.trim();
52 if (type === 'heading' &&
53 !text.trim().match(/^example/i)) {
54 if (tok.depth - depth > 1) {
55 return cb(new Error('Inappropriate heading level\n'+
56 JSON.stringify(tok)));
59 // Sometimes we have two headings with a single
60 // blob of description. Treat as a clone.
62 state === 'AFTERHEADING' &&
63 depth === tok.depth) {
65 current = newSection(tok);
66 current.clone = clone;
67 // don't keep it around on the stack.
70 // if the level is greater than the current depth,
71 // then it's a child, so we should just leave the stack
73 // However, if it's a sibling or higher, then it implies
74 // the closure of the other sections that came before.
75 // root is always considered the level=0 section,
76 // and the lowest heading is 1, so this should always
77 // result in having a valid parent node.
80 finishSection(stack.pop(), stack[stack.length - 1]);
83 current = newSection(tok);
88 state = 'AFTERHEADING';
92 // Immediately after a heading, we can expect the following
94 // { type: 'code', text: 'Stability: ...' },
96 // a list: starting with list_start, ending with list_end,
97 // maybe containing other nested lists in each item.
99 // If one of these isnt' found, then anything that comes between
100 // here and the next heading should be parsed as the desc.
102 if (state === 'AFTERHEADING') {
103 if (type === 'code' &&
104 (stability = text.match(/^Stability: ([0-5])(?:\s*-\s*)?(.*)$/))) {
105 current.stability = parseInt(stability[1], 10);
106 current.stabilityText = stability[2].trim();
108 } else if (type === 'list_start' && !tok.ordered) {
109 state = 'AFTERHEADING_LIST';
110 current.list = current.list || [];
111 current.list.push(tok);
112 current.list.level = 1;
114 current.desc = current.desc || [];
115 if (!Array.isArray(current.desc)) {
116 current.shortDesc = current.desc;
119 current.desc.push(tok);
125 if (state === 'AFTERHEADING_LIST') {
126 current.list.push(tok);
127 if (type === 'list_start') {
128 current.list.level++;
129 } else if (type === 'list_end') {
130 current.list.level--;
132 if (current.list.level === 0) {
133 state = 'AFTERHEADING';
134 processList(current);
139 current.desc = current.desc || [];
140 current.desc.push(tok);
144 // finish any sections left open
145 while (root !== (current = stack.pop())) {
146 finishSection(current, stack[stack.length - 1]);
149 return cb(null, root)
153 // go from something like this:
154 // [ { type: 'list_item_start' },
156 // text: '`settings` Object, Optional' },
157 // { type: 'list_start', ordered: false },
158 // { type: 'list_item_start' },
160 // text: 'exec: String, file path to worker file. Default: `__filename`' },
161 // { type: 'list_item_end' },
162 // { type: 'list_item_start' },
164 // text: 'args: Array, string arguments passed to worker.' },
166 // text: 'Default: `process.argv.slice(2)`' },
167 // { type: 'list_item_end' },
168 // { type: 'list_item_start' },
170 // text: 'silent: Boolean, whether or not to send output to parent\'s stdio.' },
171 // { type: 'text', text: 'Default: `false`' },
172 // { type: 'space' },
173 // { type: 'list_item_end' },
174 // { type: 'list_end' },
175 // { type: 'list_item_end' },
176 // { type: 'list_end' } ]
177 // to something like:
178 // [ { name: 'settings',
184 // desc: 'file path to worker file',
185 // default: '__filename' },
188 // default: 'process.argv.slice(2)',
189 // desc: 'string arguments passed to worker.' },
192 // desc: 'whether or not to send output to parent\'s stdio.',
193 // default: 'false' } ] } ]
195 function processList(section) {
196 var list = section.list;
201 // for now, *just* build the heirarchical list
202 list.forEach(function(tok) {
204 if (type === 'space') return;
205 if (type === 'list_item_start') {
211 current.options = current.options || [];
214 current.options.push(n);
218 } else if (type === 'list_item_end') {
220 throw new Error('invalid list - end without current item\n' +
221 JSON.stringify(tok) + '\n' +
222 JSON.stringify(list));
224 current = stack.pop();
225 } else if (type === 'text') {
227 throw new Error('invalid list - text without current item\n' +
228 JSON.stringify(tok) + '\n' +
229 JSON.stringify(list));
231 current.textRaw = current.textRaw || '';
232 current.textRaw += tok.text + ' ';
236 // shove the name in there for properties, since they are always
237 // just going to be the value etc.
238 if (section.type === 'property' && values[0]) {
239 values[0].textRaw = '`' + section.name + '` ' + values[0].textRaw;
242 // now pull the actual values out of the text bits.
243 values.forEach(parseListItem);
245 // Now figure out what this list actually means.
246 // depending on the section type, the list could be different things.
248 switch (section.type) {
252 // each item is an argument, unless the name is 'return',
253 // in which case it's the return value.
254 section.signatures = section.signatures || [];
256 section.signatures.push(sig);
257 sig.params = values.filter(function(v) {
258 if (v.name === 'return') {
264 parseSignature(section.textRaw, sig);
268 // there should be only one item, which is the value.
269 // copy the data up to the section.
270 var value = values[0] || {};
272 section.typeof = value.type;
274 Object.keys(value).forEach(function(k) {
275 section[k] = value[k];
280 // event: each item is an argument.
281 section.params = values;
285 // section.listParsed = values;
290 // textRaw = "someobject.someMethod(a[, b=100][, c])"
291 function parseSignature(text, sig) {
292 var params = text.match(paramExpr);
295 // the [ is irrelevant. ] indicates optionalness.
296 params = params.replace(/\[/g, '');
297 params = params.split(/,/)
298 params.forEach(function(p, i, _) {
301 var param = sig.params[i];
302 var optional = false;
305 if (p.charAt(p.length - 1) === ']') {
307 p = p.substr(0, p.length - 1);
310 var eq = p.indexOf('=');
312 def = p.substr(eq + 1);
316 param = sig.params[i] = { name: p };
318 // at this point, the name should match.
319 if (p !== param.name) {
320 console.error('Warning: invalid param "%s"', p);
321 console.error(' > ' + JSON.stringify(param));
322 console.error(' > ' + text);
324 if (optional) param.optional = true;
325 if (def !== undefined) param.default = def;
330 function parseListItem(item) {
331 if (item.options) item.options.forEach(parseListItem);
332 if (!item.textRaw) return;
334 // the goal here is to find the name, type, default, and optional.
335 // anything left over is 'desc'
336 var text = item.textRaw.trim();
337 // text = text.replace(/^(Argument|Param)s?\s*:?\s*/i, '');
339 text = text.replace(/^, /, '').trim();
340 var retExpr = /^returns?\s*:?\s*/i;
341 var ret = text.match(retExpr);
343 item.name = 'return';
344 text = text.replace(retExpr, '');
346 var nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/;
347 var name = text.match(nameExpr);
350 text = text.replace(nameExpr, '');
355 var defaultExpr = /\(default\s*[:=]?\s*['"`]?([^, '"`]*)['"`]?\)/i;
356 var def = text.match(defaultExpr);
358 item.default = def[1];
359 text = text.replace(defaultExpr, '');
363 var typeExpr = /^\{([^\}]+)\}/;
364 var type = text.match(typeExpr);
367 text = text.replace(typeExpr, '');
371 var optExpr = /^Optional\.|(?:, )?Optional$/;
372 var optional = text.match(optExpr);
374 item.optional = true;
375 text = text.replace(optExpr, '');
378 text = text.replace(/^\s*-\s*/, '');
380 if (text) item.desc = text;
384 function finishSection(section, parent) {
385 if (!section || !parent) {
386 throw new Error('Invalid finishSection call\n'+
387 JSON.stringify(section) + '\n' +
388 JSON.stringify(parent));
392 section.type = 'module';
393 if (parent && (parent.type === 'misc')) {
394 section.type = 'misc';
396 section.displayName = section.name;
397 section.name = section.name.toLowerCase()
398 .trim().replace(/\s+/g, '_');
401 if (section.desc && Array.isArray(section.desc)) {
402 section.desc.links = section.desc.links || [];
403 section.desc = marked.parser(section.desc);
406 if (!section.list) section.list = [];
407 processList(section);
409 // classes sometimes have various 'ctor' children
410 // which are actually just descriptions of a constructor
412 // Merge them into the parent.
413 if (section.type === 'class' && section.ctors) {
414 section.signatures = section.signatures || [];
415 var sigs = section.signatures;
416 section.ctors.forEach(function(ctor) {
417 ctor.signatures = ctor.signatures || [{}];
418 ctor.signatures.forEach(function(sig) {
419 sig.desc = ctor.desc;
421 sigs.push.apply(sigs, ctor.signatures);
423 delete section.ctors;
426 // properties are a bit special.
427 // their "type" is the type of object, not "property"
428 if (section.properties) {
429 section.properties.forEach(function (p) {
430 if (p.typeof) p.type = p.typeof;
438 var clone = section.clone;
439 delete section.clone;
441 deepCopy(section, clone);
442 finishSection(clone, parent);
446 if (section.type.slice(-1) === 's') {
447 plur = section.type + 'es';
448 } else if (section.type.slice(-1) === 'y') {
449 plur = section.type.replace(/y$/, 'ies');
451 plur = section.type + 's';
454 // if the parent's type is 'misc', then it's just a random
455 // collection of stuff, like the "globals" section.
456 // Make the children top-level items.
457 if (section.type === 'misc') {
458 Object.keys(section).forEach(function(k) {
467 if (parent.type === 'misc') {
470 if (Array.isArray(k) && parent[k]) {
471 parent[k] = parent[k].concat(section[k]);
472 } else if (!parent[k]) {
473 parent[k] = section[k];
475 // parent already has, and it's not an array.
482 parent[plur] = parent[plur] || [];
483 parent[plur].push(section);
487 // Not a general purpose deep copy.
488 // But sufficient for these basic things.
489 function deepCopy(src, dest) {
490 Object.keys(src).filter(function(k) {
491 return !dest.hasOwnProperty(k);
492 }).forEach(function(k) {
493 dest[k] = deepCopy_(src[k]);
497 function deepCopy_(src) {
498 if (!src) return src;
499 if (Array.isArray(src)) {
500 var c = new Array(src.length);
501 src.forEach(function(v, i) {
506 if (typeof src === 'object') {
508 Object.keys(src).forEach(function(k) {
509 c[k] = deepCopy_(src[k]);
517 // these parse out the contents of an H# tag
518 var eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i;
519 var classExpr = /^Class:\s*([^ ]+).*?$/i;
520 var propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i;
521 var braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i;
523 /^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i;
525 /^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i;
526 var newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/;
527 var paramExpr = /\((.*)\);?$/;
529 function newSection(tok) {
531 // infer the type from the text.
532 var text = section.textRaw = tok.text;
533 if (text.match(eventExpr)) {
534 section.type = 'event';
535 section.name = text.replace(eventExpr, '$1');
536 } else if (text.match(classExpr)) {
537 section.type = 'class';
538 section.name = text.replace(classExpr, '$1');
539 } else if (text.match(braceExpr)) {
540 section.type = 'property';
541 section.name = text.replace(braceExpr, '$1');
542 } else if (text.match(propExpr)) {
543 section.type = 'property';
544 section.name = text.replace(propExpr, '$1');
545 } else if (text.match(classMethExpr)) {
546 section.type = 'classMethod';
547 section.name = text.replace(classMethExpr, '$1');
548 } else if (text.match(methExpr)) {
549 section.type = 'method';
550 section.name = text.replace(methExpr, '$1');
551 } else if (text.match(newExpr)) {
552 section.type = 'ctor';
553 section.name = text.replace(newExpr, '$1');