9fdc3bc1b4a02e49e85a5f9871e47d894c2038bc
[platform/upstream/nodejs.git] / tools / doc / json.js
1 // Copyright Joyent, Inc. and other Node contributors.
2 //
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:
10 //
11 // The above copyright notice and this permission notice shall be included
12 // in all copies or substantial portions of the Software.
13 //
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.
21
22 module.exports = doJSON;
23
24 // Take the lexed input, and return a JSON-encoded object
25 // A module looks like this: https://gist.github.com/1777387
26
27 var marked = require('marked');
28
29 function doJSON(input, filename, cb) {
30   var root = {source: filename};
31   var stack = [root];
32   var depth = 0;
33   var current = root;
34   var state = null;
35   var lexed = marked.lexer(input);
36   lexed.forEach(function (tok) {
37     var type = tok.type;
38     var text = tok.text;
39
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();
46         return '';
47       });
48       text = text.trim();
49       if (!text) return;
50     }
51
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)));
57       }
58
59       // Sometimes we have two headings with a single
60       // blob of description.  Treat as a clone.
61       if (current &&
62           state === 'AFTERHEADING' &&
63           depth === tok.depth) {
64         var clone = current;
65         current = newSection(tok);
66         current.clone = clone;
67         // don't keep it around on the stack.
68         stack.pop();
69       } else {
70         // if the level is greater than the current depth,
71         // then it's a child, so we should just leave the stack
72         // as it is.
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.
78         var d = tok.depth;
79         while (d <= depth) {
80           finishSection(stack.pop(), stack[stack.length - 1]);
81           d++;
82         }
83         current = newSection(tok);
84       }
85
86       depth = tok.depth;
87       stack.push(current);
88       state = 'AFTERHEADING';
89       return;
90     } // heading
91
92     // Immediately after a heading, we can expect the following
93     //
94     // { type: 'code', text: 'Stability: ...' },
95     //
96     // a list: starting with list_start, ending with list_end,
97     // maybe containing other nested lists in each item.
98     //
99     // If one of these isnt' found, then anything that comes between
100     // here and the next heading should be parsed as the desc.
101     var stability
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();
107         return;
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;
113       } else {
114         current.desc = current.desc || [];
115         if (!Array.isArray(current.desc)) {
116           current.shortDesc = current.desc;
117           current.desc = [];
118         }
119         current.desc.push(tok);
120         state = 'DESC';
121       }
122       return;
123     }
124
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--;
131       }
132       if (current.list.level === 0) {
133         state = 'AFTERHEADING';
134         processList(current);
135       }
136       return;
137     }
138
139     current.desc = current.desc || [];
140     current.desc.push(tok);
141
142   });
143
144   // finish any sections left open
145   while (root !== (current = stack.pop())) {
146     finishSection(current, stack[stack.length - 1]);
147   }
148
149   return cb(null, root)
150 }
151
152
153 // go from something like this:
154 // [ { type: 'list_item_start' },
155 //   { type: 'text',
156 //     text: '`settings` Object, Optional' },
157 //   { type: 'list_start', ordered: false },
158 //   { type: 'list_item_start' },
159 //   { type: 'text',
160 //     text: 'exec: String, file path to worker file.  Default: `__filename`' },
161 //   { type: 'list_item_end' },
162 //   { type: 'list_item_start' },
163 //   { type: 'text',
164 //     text: 'args: Array, string arguments passed to worker.' },
165 //   { type: 'text',
166 //     text: 'Default: `process.argv.slice(2)`' },
167 //   { type: 'list_item_end' },
168 //   { type: 'list_item_start' },
169 //   { type: 'text',
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',
179 //     type: 'object',
180 //     optional: true,
181 //     settings:
182 //      [ { name: 'exec',
183 //          type: 'string',
184 //          desc: 'file path to worker file',
185 //          default: '__filename' },
186 //        { name: 'args',
187 //          type: 'array',
188 //          default: 'process.argv.slice(2)',
189 //          desc: 'string arguments passed to worker.' },
190 //        { name: 'silent',
191 //          type: 'boolean',
192 //          desc: 'whether or not to send output to parent\'s stdio.',
193 //          default: 'false' } ] } ]
194
195 function processList(section) {
196   var list = section.list;
197   var values = [];
198   var current;
199   var stack = [];
200
201   // for now, *just* build the heirarchical list
202   list.forEach(function(tok) {
203     var type = tok.type;
204     if (type === 'space') return;
205     if (type === 'list_item_start') {
206       if (!current) {
207         var n = {};
208         values.push(n);
209         current = n;
210       } else {
211         current.options = current.options || [];
212         stack.push(current);
213         var n = {};
214         current.options.push(n);
215         current = n;
216       }
217       return;
218     } else if (type === 'list_item_end') {
219       if (!current) {
220         throw new Error('invalid list - end without current item\n' +
221                         JSON.stringify(tok) + '\n' +
222                         JSON.stringify(list));
223       }
224       current = stack.pop();
225     } else if (type === 'text') {
226       if (!current) {
227         throw new Error('invalid list - text without current item\n' +
228                         JSON.stringify(tok) + '\n' +
229                         JSON.stringify(list));
230       }
231       current.textRaw = current.textRaw || '';
232       current.textRaw += tok.text + ' ';
233     }
234   });
235
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;
240   }
241
242   // now pull the actual values out of the text bits.
243   values.forEach(parseListItem);
244
245   // Now figure out what this list actually means.
246   // depending on the section type, the list could be different things.
247
248   switch (section.type) {
249     case 'ctor':
250     case 'classMethod':
251     case 'method':
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 || [];
255       var sig = {}
256       section.signatures.push(sig);
257       sig.params = values.filter(function(v) {
258         if (v.name === 'return') {
259           sig.return = v;
260           return false;
261         }
262         return true;
263       });
264       parseSignature(section.textRaw, sig);
265       break;
266
267     case 'property':
268       // there should be only one item, which is the value.
269       // copy the data up to the section.
270       var value = values[0] || {};
271       delete value.name;
272       section.typeof = value.type;
273       delete value.type;
274       Object.keys(value).forEach(function(k) {
275         section[k] = value[k];
276       });
277       break;
278
279     case 'event':
280       // event: each item is an argument.
281       section.params = values;
282       break;
283   }
284
285   // section.listParsed = values;
286   delete section.list;
287 }
288
289
290 // textRaw = "someobject.someMethod(a[, b=100][, c])"
291 function parseSignature(text, sig) {
292   var params = text.match(paramExpr);
293   if (!params) return;
294   params = params[1];
295   // the [ is irrelevant. ] indicates optionalness.
296   params = params.replace(/\[/g, '');
297   params = params.split(/,/)
298   params.forEach(function(p, i, _) {
299     p = p.trim();
300     if (!p) return;
301     var param = sig.params[i];
302     var optional = false;
303     var def;
304     // [foo] -> optional
305     if (p.charAt(p.length - 1) === ']') {
306       optional = true;
307       p = p.substr(0, p.length - 1);
308       p = p.trim();
309     }
310     var eq = p.indexOf('=');
311     if (eq !== -1) {
312       def = p.substr(eq + 1);
313       p = p.substr(0, eq);
314     }
315     if (!param) {
316       param = sig.params[i] = { name: p };
317     }
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);
323     }
324     if (optional) param.optional = true;
325     if (def !== undefined) param.default = def;
326   });
327 }
328
329
330 function parseListItem(item) {
331   if (item.options) item.options.forEach(parseListItem);
332   if (!item.textRaw) return;
333
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, '');
338
339   text = text.replace(/^, /, '').trim();
340   var retExpr = /^returns?\s*:?\s*/i;
341   var ret = text.match(retExpr);
342   if (ret) {
343     item.name = 'return';
344     text = text.replace(retExpr, '');
345   } else {
346     var nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/;
347     var name = text.match(nameExpr);
348     if (name) {
349       item.name = name[1];
350       text = text.replace(nameExpr, '');
351     }
352   }
353
354   text = text.trim();
355   var defaultExpr = /\(default\s*[:=]?\s*['"`]?([^, '"`]*)['"`]?\)/i;
356   var def = text.match(defaultExpr);
357   if (def) {
358     item.default = def[1];
359     text = text.replace(defaultExpr, '');
360   }
361
362   text = text.trim();
363   var typeExpr = /^\{([^\}]+)\}/;
364   var type = text.match(typeExpr);
365   if (type) {
366     item.type = type[1];
367     text = text.replace(typeExpr, '');
368   }
369
370   text = text.trim();
371   var optExpr = /^Optional\.|(?:, )?Optional$/;
372   var optional = text.match(optExpr);
373   if (optional) {
374     item.optional = true;
375     text = text.replace(optExpr, '');
376   }
377
378   text = text.replace(/^\s*-\s*/, '');
379   text = text.trim();
380   if (text) item.desc = text;
381 }
382
383
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));
389   }
390
391   if (!section.type) {
392     section.type = 'module';
393     if (parent && (parent.type === 'misc')) {
394       section.type = 'misc';
395     }
396     section.displayName = section.name;
397     section.name = section.name.toLowerCase()
398       .trim().replace(/\s+/g, '_');
399   }
400
401   if (section.desc && Array.isArray(section.desc)) {
402     section.desc.links = section.desc.links || [];
403     section.desc = marked.parser(section.desc);
404   }
405
406   if (!section.list) section.list = [];
407   processList(section);
408
409   // classes sometimes have various 'ctor' children
410   // which are actually just descriptions of a constructor
411   // class signature.
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;
420       });
421       sigs.push.apply(sigs, ctor.signatures);
422     });
423     delete section.ctors;
424   }
425
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;
431       else delete p.type;
432       delete p.typeof;
433     });
434   }
435
436   // handle clones
437   if (section.clone) {
438     var clone = section.clone;
439     delete section.clone;
440     delete clone.clone;
441     deepCopy(section, clone);
442     finishSection(clone, parent);
443   }
444
445   var plur;
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');
450   } else {
451     plur = section.type + 's';
452   }
453
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) {
459       switch (k) {
460         case 'textRaw':
461         case 'name':
462         case 'type':
463         case 'desc':
464         case 'miscs':
465           return;
466         default:
467           if (parent.type === 'misc') {
468             return;
469           }
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];
474           } else {
475             // parent already has, and it's not an array.
476             return;
477           }
478       }
479     });
480   }
481
482   parent[plur] = parent[plur] || [];
483   parent[plur].push(section);
484 }
485
486
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]);
494   });
495 }
496
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) {
502       c[i] = deepCopy_(v);
503     });
504     return c;
505   }
506   if (typeof src === 'object') {
507     var c = {};
508     Object.keys(src).forEach(function(k) {
509       c[k] = deepCopy_(src[k]);
510     });
511     return c;
512   }
513   return src;
514 }
515
516
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;
522 var classMethExpr =
523   /^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i;
524 var methExpr =
525   /^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i;
526 var newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/;
527 var paramExpr = /\((.*)\);?$/;
528
529 function newSection(tok) {
530   var section = {};
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');
554   } else {
555     section.name = text;
556   }
557   return section;
558 }