-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathwhatsupdoc.js
More file actions
586 lines (556 loc) · 16.6 KB
/
whatsupdoc.js
File metadata and controls
586 lines (556 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
#!/usr/bin/env node
/**
* An API for parsing inline JavaScript documentation.
* @author Kris Kowal (http://askawizard.blogspot.com)
* <kris@cixar.com>
* @license MIT License
* @module
*/
// TODO {@link support}
var PARSE = require("./parse");
var Author = require("./author").Author;
var expand = require("./expand").expand;
/**
* Creates a documentation tree for a module.
*
* @param {String} text the text of a JavaScript program
* @param {String} id optional module identifier
* @returns {{name, doc, children}} a tree of code point
* documentation descriptors of the form `{name, type,
* params, returns, throws, doc, errors, children, fileName,
* lineNo}`
*/
exports.parseModule = function (text, id) {
text = expand(text);
var comments = exports.comments(text);
var markup = exports.guessMarkup(comments);
var docs = exports.docs(comments);
var nodes = exports.parseDocs(docs, markup);
var tree = exports.tree(nodes, id);
return tree;
};
/**
* takes the text of a JavaScript file and returns an array
* of each of the block comments, the prefix on the initial
* line (for measuring indentation), and the subsequent code
* up to the next comment.
*
* It's a good idea to expand the tabs in the program text
* before running them through this function. The `util`
* module in the `narwhal-lib` package provides an `expand`
* function that serves this purpose.
*
* @param {String} text the text of a JavaScript program.
* @param {String} fileName optional
* @param {Number} lineNo line number starting with 1,
* optional
* @returns {Array * {comment, code, prefix, fileName,
* lineNo}}
*/
// XXX known issue: does not find quoted or slashed sections
exports.comments = function (text, fileName, lineNo) {
lineNo = lineNo || 1;
var nodes = [];
do {
var nextBlock = text.indexOf("/*");
var nextInline = text.indexOf("//");
if (nextBlock < 0 && nextInline < 0) {
// neither
if (nodes.length)
nodes[nodes.length - 1].code += text;
text = "";
} else if (
nextBlock < 0 ||
(nextInline >= 0 && nextInline < nextBlock)
) {
// inline
if (nodes.length)
nodes[nodes.length - 1].code += text.slice(0, nextInline);
text.slice(0, nextInline).replace(/\n/g, function () {
lineNo++;
});
text = text.slice(nextInline);
var nextLf = text.indexOf("\n");
if (nextLf < 0) {
text = "";
} else {
if (nodes.length)
nodes[nodes.length - 1].code += text.slice(0, nextLf + 1);
text = text.slice(nextLf + 1);
lineNo++;
}
} else {
// block
var prefix = text.slice(0, nextBlock);
prefix.replace(/\n/g, function () {
lineNo++;
return "\n";
});
if (nodes.length)
nodes[nodes.length - 1].code += prefix
var prevLf = prefix.lastIndexOf("\n");
var prefixLength;
if (prevLf < 0) {
prefix = text.slice(0, nextBlock);
text = text.slice(nextBlock);
} else {
prefix = prefix.slice(prevLf + 1);
text = text.slice(nextBlock);
}
var nextBlockEnd = text.indexOf("*/");
var comment;
if (nextBlockEnd < 0) {
comment = text.slice(2);
text = "";
} else {
comment = text.slice(2, nextBlockEnd);
text = text.slice(nextBlockEnd + 2);
}
nodes.push({
"comment": comment,
"prefix": prefix,
"code": "",
"fileName": fileName,
"lineNo": lineNo
});
comment.replace(/\n/g, function () {
lineNo++;
});
}
} while (text.length);
return nodes;
};
/**
* Extracts documentation nodes from comment nodes.
*
* Takes the output of the `comments` method and returns an
* array of objects with `doc`, `code`, and `level`
* properties, consumable by `parseDocs`.
*
* * `doc` is a string containing each line of
* the original `comment` with the leading cruft on each
* line trimmed off.
* * `code` is the subsequent code up
* to the next comment.
* * `level` is the number of extra stars on the beginning
* of the comment. These can be used to provide an
* indication of hierarchical depth.
*
* @param {Array * {comment, prefix, code, fileName,
* lineNo}} nodes nodes in the format produced by `comments`
* @returns {Array * {doc, code, name, level, fileName,
* lineNo}} nodes
*/
exports.docs = function (nodes) {
var docs = [];
nodes.forEach(function (node) {
var comment = node.comment;
var lines = comment.split(/\r?\n/);
var firstLine = lines.shift();
if (!/^\*+/.test(firstLine))
return;
var spacePrefix = spaces(node.prefix.length + 4);
var starPrefix = spaces(node.prefix.length) + " \\* ?";
var expression = new RegExp(
"^(" +
spacePrefix + "|" +
starPrefix +
")" + "(.*)$"
);
lines = lines.map(function (line) {
if (!line.trim().length)
return "";
if (!expression.test(line))
return line; // XXX throw useful error
return expression.exec(line)[2];
});
while (
lines.length &&
!lines[lines.length - 1].trim().length
)
lines.pop();
var match = /^(\*+) ?(.*)$/.exec(firstLine)
var firstLine = match[2];
if (firstLine.trim())
lines.unshift(firstLine);
docs.push({
"level": match[1].length - 1,
"doc": lines.join("\n"),
"code": node.code,
"fileName": node.fileName,
"lineNo": node.lineNo
});
});
return docs;
};
/**
* Scans an array of comment descriptors as provided by the
* `comments` method and returns a markup language module
* name. The default is `undefined`.
*/
exports.guessMarkup = function (comments) {
var markup;
comments.forEach(function (node) {
var match = /^markup\s+(\S+)\s*$/.exec(node.comment);
if (match)
markup = match[1];
});
return markup;
};
/**
* Takes an array of nodes produced by `docs` and returns an
* array of nodes consumable by `tree`. Uses `parseDoc` to
* extract `@` meta-data like `param` and `returns`. Uses
* `guessName` to guess the name of the object. `@name`
* overrides the guess.
*
* @param {Array * {level, doc, code, fileName, lineNo}} nodes
* @param {String} markup the name of a markup module, a
* module that exports a `to` method for whatever format the
* documentation must be rendered in, for example `toHtml`.
* The `to` method may accept a second argument, a function
* for resolving custom references if they are supported by
* the language.
* @returns {Array} with `name`, `level`, `markup`, and the
* properties added by {@link parseDoc}.
*/
exports.parseDocs = function (docs, markup) {
var n = 0;
return docs.map(function (doc) {
var node = {
"name": exports.guessName(doc.code) || String(n++),
"level": doc.level,
"markup": markup,
"fileName": doc.fileName,
"lineNo": doc.lineNo
};
exports.parseDoc(
doc.doc,
node,
exports.tagParsers,
markup
);
return node;
});
};
/**
* takes a linear list of documentation nodes from a single
* module, such as those returned by `parseDocs`, and
* returns the root of a tree of nodes, where the `level`
* numbers are translated into `children` arrays.
*
* @param {Array * {level, ...}} nodes
* @param {String} id optional module identifier
* @returns {{"type": "module", "id": id, "children":
* Array * ...}}
*/
exports.tree = function (nodes, id) {
var root = {};
if (nodes.length && nodes[0].module) {
root = nodes.shift();
delete root.level;
}
root.type = 'module';
root.name = root.id = id;
root.children = [];
var stack = [root.children];
var level = 0;
nodes.forEach(function (node) {
while (node.level > stack.length - 1) {
var top = stack[stack.length - 1];
var last;
if (!top.length)
last = top;
else
last = top[top.length - 1];
if (!last.children)
last.children = [];
stack.push(last.children);
}
while (node.level < stack.length - 1) {
stack.pop();
}
delete node.level;
stack[stack.length - 1].push(node);
});
return root;
};
/**
* @param {String} code a block of code to search for an
* applicable name.
* @returns {String | Undefined} a name or undefined if no
* name can be found.
*/
var guessRe = /(\w+)\s*=|["']([^'"]+)["']\s*:/;
exports.guessName = function (code) {
var match = guessRe.exec(code);
if (match)
return match[1] || match[2];
};
/**
* Accepts a documentation string and a documentation node,
* strips out and applies the `@` metadata blocks to the
* documentation node.
*
* @param {String} text a documentation string
* @param {Object} node a documentation node, as provided by
* `parseDocs`
* @param {Object * [name String, handler Function(text,
* node)]} tagParsers an optional mapping of `@` tag names
* to corresponding handlers. Defaults to `tagParsers` as
* exported by the same module.
* @returns node
*/
exports.parseDoc = function (text, node, tagParsers) {
node = node || {};
tagParsers = tagParsers || exports.tagParsers;
var docs = [];
node.params = [];
node.errors = [];
node['throws'] = [];
node.see = [];
var blocks = text.replace(/\n@/g, "\n\n\n@").split(/\n\n+/g);
blocks.forEach(function (block) {
if (/^@/.test(block)) {
var blocks = block.slice(1).split(/\n@/);
blocks.forEach(function (block) {
var match = /^(\S+)(?:\s+([\s\S]*))?/.exec(block);
var tag = match[1];
var text = (match[2] || "").split(/\n/g).join(" ");
if (!tagParsers[tag]) {
node.errors.push("Did not recognize " +
JSON.stringify(tag) + " tag.");
} else {
tagParsers[tag](text, node);
}
});
} else {
docs.push(block);
}
});
node.doc = docs.join("\n\n").trim();
if (/^{/.test(node.doc)) {
var match = PARSE.parseCurly(node.doc, node);
node.jsType = match[0];
node.doc = match[1].trim();
}
return node;
};
/**
* an `Object` mapping tag names to parser functions, in the
* form accepted by the `tagParsers` argument of `parseDoc`.
* The parser functions accept the tag text and the node
* they are augmenting, in the form of one of the objects
* returned by `parseDoc`.
*/
var tagParsers = exports.tagParsers = {};
/*** parses an `@param` tag, pushing an `Object` of the
* form `{name, type, description}` onto the `params`
* `Array` of the given `node`.
*
* @param {String} text
* @param {{errors Array}} node
*/
tagParsers.param = function (text, node) {
if (/^{/.test(text)) {
var match = PARSE.parseCurly(text, node);
var type = match[0];
text = match[1].trim();
}
var match = /^(\w+)(?:\s+([\S\s]*))?$/.exec(text);
if (!match) {
node.errors.push("Could not recognize `@param` " + JSON.stringify(text));
return;
}
var name = match[1];
text = match[2];
// TODO parse the text into an object
node.params.push({
"name": name,
"type": type,
"doc": text
});
};
/*** parses an `@params` tag for variadic parameters, pushing an
* `Object` of the form `{name, type, description, variadic: true}`
* onto the `params` `Array` of the given `node`.
*
* @param {String} text
* @param {{errors Array}} node
*/
tagParsers.params = function (text, node) {
if (/^{/.test(text)) {
var match = PARSE.parseCurly(text, node);
var type = match[0];
text = match[1].trim();
}
var match = /^(\w+)(?:\s+([\S\s]*))?$/.exec(text);
if (!match) {
node.errors.push("Could not recognize `@param` " + JSON.stringify(text));
return;
}
var name = match[1];
text = match[2];
// TODO parse the text into an object
node.params.push({
"name": name,
"type": type,
"variadic": true,
"doc": text
});
};
/*** parses an `@returns` tag, setting the `returns`
* property of the given `node` to an `Object` of the form,
* `{name, type, description}`.
*
* @param {String} text
* @param {{errors Array}} node
*/
tagParsers.returns = function (text, node) {
// TODO parse the text into an object
if (/^{/.test(text)) {
var match = PARSE.parseCurly(text, node);
var type = match[0];
text = match[1].trim();
}
// TODO parse the text into an object
node.returns = {
"type": type,
"doc": text
};
};
/***
* @see returns
* @name return
* @param {String} text
* @param {{errors Array}} node
*/
tagParsers["return"] = tagParsers.returns;
/***
* Specifies or overrides the documentation parser's guess
* for the name of the object being documented.
*
* @param {String} text
* @param {{errors Array}} node
*/
tagParsers.name = function (text, node) {
node.name = text.trim();
};
/***
* Notes the author. Uses the `Author` type from the
* `packages` module of the `narwhal-lib` package to
* normalize a string representation of an author of the
* form:
*
* Author Name (http://example.com) <author@example.com>
*
* Where each component is optional and gets composed into
* an `Object` with `name`, `url` and `email` properties.
*
* @param {String} text
* @param {{errors Array}} node
*/
tagParsers.author = function (text, node) {
node.author = new Author(text);
};
/***
* Notes a contributor. More than one can be credited.
*
* @see author
*
* @param {String} text
* @param {{errors Array}} node
*/
tagParsers.contributor = function (text, node) {
node.contributors.push(new Author(text));
};
/***
* Tags a node as a constructor function.
*
* @param {String} text
* @param {{errors Array}} node
*/
tagParsers.constructor = function (text, node) {
if (text.trim().length)
node.errors.push("`@constructor` tag had superfluous text");
node.constructor = true;
};
/***
* Tags a documentation node as deprecated.
*
* @param {String} text
* @param {{errors Array}} node
*/
tagParsers.deprecated = function (text, node) {
if (text.trim().length)
node.errors.push("`@deprecated` tag had superfluous text");
node.deprecated = true;
};
/***
* @param {String} text
* @param {{errors Array}} node
*/
tagParsers.module = function (text, node) {
if (text.trim().length)
node.errors.push("`@module` tag had superfluous text");
node.module = true;
};
/***
* @see module
* @param {String} text
* @param {{errors Array}} node
*/
tagParsers.fileoverview = tagParsers.module;
/***
*/
tagParsers['throws'] = function (text, node) {
if (/^{/.test(text)) {
var match = PARSE.parseCurly(text, node);
var type = match[0];
text = match[1].trim();
}
node['throws'].push({
"type": type,
"doc": text
});
};
/*
TODO
@exception Synonym for @throws
@argument
@requires
@private Signifies that a method is private
@return Documents a return value
@see Documents an association to another object
@this Specifies the type of the object to which the keyword "this" refers within a function.
@throws Documents an exception thrown by a method
@version Provides the version number of a library
@fileoverview
@module
@type the return type of a function
@extends
@private
@final
@member
@ignore
@base
@addon
@classmethod
@classproperty
@readonly
@configurable
@get
@set
*/
var spaces = function (n) {
return Array(n + 1).join(" ");
};
if (require.main == module) {
// node side only, not in browser. use parens to prevent
// static dependency.
var FS = (require)("fs");
var fileName = process.argv[2] || module.path || module.filename
var text = FS.readFileSync(fileName);
var tree = exports.parseModule(text, module.id);
console.log(JSON.stringify(tree, null, 4));
}