UNPKG

11.7 kBJavaScriptView Raw
1
2/**
3 * @module firedoc
4 */
5
6const _ = require('underscore');
7const fs = require('graceful-fs');
8const path = require('path');
9const utils = require('./utils');
10const debug = require('debug')('firedoc:ast');
11const ParserContext = require('./ast/context').ParserContext;
12const CWD = process.cwd();
13const REGEX_LINES = /\r\n|\n/;
14const REGEX_START_COMMENT = {
15 js: /^\s*\/\*\*/,
16 coffee: /^\s*###\*/
17};
18const REGEX_END_COMMENT = {
19 js: /\*\/\s*$/,
20 coffee: /###\s*$/
21};
22const REGEX_LINE_HEAD_CHAR = {
23 js: /^\s*\*/,
24 coffee: /^\s*[#\*]/
25};
26
27/**
28 * A list of ignored tags. These tags should be ignored because there is
29 * likely to be used for purposes other than JSDoc tags in JavaScript comments.
30 * @property IGNORE_TAGLIST
31 * @type Array
32 * @final
33 */
34const IGNORE_TAGLIST = ['media'];
35
36/**
37 * Common errors will get scrubbed instead of being ignored.
38 * @property CORRECTIONS
39 * @type Object
40 * @final
41 */
42const CORRECTIONS = require('./ast/corrections');
43
44/**
45 * Short tags
46 * @property SHORT_TAGS
47 * @type Object
48 * @final
49 */
50const SHORT_TAGS = require('./ast/short-tags');
51
52/**
53 * A list of known tags. This populates a member variable
54 * during initialization, and will be updated if additional
55 * digesters are added.
56 *
57 * @property TAGLIST
58 * @type Array
59 * @final
60 * @for DocParser
61 */
62const TAGLIST = require('./ast/tags');
63const DIGESTERS = require('./ast/digesters');
64
65/**
66 * !#en
67 * The AST(Abstract syntax tree) of the comment
68 * !#zh
69 * 注释的抽象语法树
70 *
71 * {{#crossLink "AST"}}{{/crossLink}}
72 *
73 * @class AST
74 */
75var AST = {
76
77 /**
78 * !#en the project !#zh 项目
79 * @property {Object} project
80 */
81 project: {},
82
83 /**
84 * @property {Object} files - The files
85 */
86 files: {},
87
88 /**
89 * @property {Object} codes - The source codes
90 */
91 codes: {},
92
93 /**
94 * @property {Object} modules - The modules
95 */
96 modules: {},
97
98 /**
99 * @property {Object} classes - The classes
100 */
101 classes: {},
102
103 /**
104 * @property {Array} members - The members
105 */
106 members: [],
107
108 /**
109 * @property {Array} inheritedMembers - The inherited members
110 */
111 inheritedMembers: [],
112
113 /**
114 * @property {Object} namespacesMap - The namespaces map object
115 */
116 namespacesMap: {},
117
118 /**
119 * @property {Object} commentsMap - The comments map object
120 */
121 commentsMap: {},
122
123 /**
124 * @property {String} syntaxType - The syntax type
125 */
126 syntaxType: 'js',
127
128 /**
129 * @property {Context} context - The context object
130 */
131 context: null,
132
133 /**
134 * @proerty {Array} warnings - The parser warnings
135 */
136 warnings: [],
137
138 /**
139 * !#en
140 * Create an AST object
141 * !#zh
142 * 创建一个抽象语法树对象
143 *
144 * @method create
145 * @param {Object} files - The files
146 * @param {Object} dirs - The directorys
147 * @param {String} [syntaxType] - The syntax type: `coffee` or `js`
148 * @return {AST} the created AST object
149 */
150 create: function (files, dirs, syntaxType) {
151 var instance = AST;
152 instance.syntaxType = syntaxType || instance.syntaxType;
153 instance.context = ParserContext;
154 instance.context.ast = instance;
155 instance.extract(files, dirs);
156 instance.transform();
157 return instance;
158 },
159
160 /**
161 * Reset the AST instance
162 *
163 * @method reset
164 */
165 reset: function () {
166 AST.project = {};
167 AST.files = {};
168 AST.codes = {};
169 AST.modules = {};
170 AST.classes = {};
171 AST.members = [];
172 AST.inheritedMembers = [];
173 AST.commentsMap = {};
174 AST.syntaxType = 'js';
175 AST.warnings = [];
176 if (AST.context && AST.context.reset) {
177 AST.context.reset();
178 AST.context = null;
179 }
180 return AST;
181 },
182
183 /**
184 *
185 * @method oncomment
186 * @param {String} comment
187 * @param {String} filename
188 * @param {String} linenum
189 * @return {Object}
190 */
191 oncomment: function (comment, filename, linenum) {
192 var lines = comment.split(REGEX_LINES);
193 const len = lines.length;
194 const lineHeadCharRegex = REGEX_LINE_HEAD_CHAR[this.syntaxType];
195 const hasLineHeadChar = lines[0] && lineHeadCharRegex.test(lines[0]);
196 // match all tags except ignored ones
197 const r = new RegExp('(?:^|\\n)\\s*((?!@' + IGNORE_TAGLIST.join(')(?!@') + ')@\\w*)');
198
199 var results = [
200 {
201 'tag': 'file',
202 'value': filename
203 },
204 {
205 'tag': 'line',
206 'value': linenum
207 }
208 ];
209
210 if (hasLineHeadChar) {
211 lines = _.map(lines, function (line) {
212 return line.trim().replace(lineHeadCharRegex, '');
213 });
214 }
215
216 const unidented = utils.unindent(lines.join('\n'));
217 const parts = unidented.split(r); // with capturing parentheses, tags are also included in the result array
218
219 var cursor = 0;
220 for (; cursor < parts.length; cursor++) {
221 var skip;
222 var val = '';
223 var part = parts[cursor];
224 if (part === '') continue;
225
226 skip = false;
227 // the first token may be the description, otherwise it should be a tag
228 if (cursor === 0 && part.substr(0, 1) !== '@') { //description ahead
229 if (part) {
230 tag = '@description';
231 val = part;
232 } else {
233 skip = true;
234 }
235 } else {
236 tag = part;
237 // lookahead for the tag value
238 var peek = parts[cursor + 1];
239 if (peek) {
240 val = peek;
241 cursor += 1;
242 }
243 }
244 if (!skip && tag) {
245 results.push({
246 tag: tag.substr(1).toLowerCase(),
247 value: val || ''
248 });
249 }
250 }
251 return results;
252 },
253
254 /**
255 * Processes all the tags in a single comment block
256 * @method onblock
257 * @param {Array} an array of the tag/text pairs
258 */
259 onblock: function (block) {
260 var raw = _.reduce(block, onreduce, {});
261 raw.line = Number(raw.line);
262
263 this.context.block = {
264 'self': block,
265 'target': {
266 'file': raw.file,
267 'line': raw.line,
268 '_raw': raw
269 },
270 'host': null,
271 'digesters': []
272 };
273
274 function onreduce (map, item) {
275 var key = utils.safetrim(item.tag);
276 var val = utils.safetrim(item.value);
277 if (!map[key]) {
278 map[key] = val;
279 } else if (!_.isArray(map[key])) {
280 map[key] = [map[key], val];
281 } else {
282 map[key].push(val);
283 }
284 return map;
285 }
286
287 // handle tags and push digesters to context.block.digesters
288 _.each(block, this.ontag, this);
289 // run digiesters
290 _.each(this.context.block.digesters, ondigester, this);
291
292 function ondigester (ctx) {
293 var ret = ctx.fn.call(this, ctx.name, ctx.value,
294 this.context.block.target, this.context.block.self);
295 this.context.block.host = this.context.block.host || ret;
296 }
297
298 // post process
299 if (this.context.block.host) {
300 this.context.block.host = _.extend(
301 this.context.block.host, this.context.block.target);
302 } else {
303 var target = this.context.block.target;
304 target.clazz = this.context.clazz;
305 target.module = this.context.module;
306 target.isGlobal = (this.context.clazz === '');
307 target.submodule = this.context.submodule;
308
309 // set namespace
310 var ns = utils.getNamespace(target);
311 if (ns) {
312 this.namespacesMap[ns] = target;
313 target.namespace = ns;
314
315 var parent = this.classes[target.clazz] || this.modules[target.module];
316 Object.defineProperty(target, 'parent', {
317 enumerable: true,
318 get: function () {
319 return parent;
320 }
321 });
322
323 target.process = target.process || parent.process;
324 }
325
326 if (target.itemtype) {
327 this.members.push(target);
328 } else if (target.isTypeDef) {
329 var parent = this.classes[this.context.clazz] ||
330 this.modules[this.context.module];
331 if (!parent) return;
332 parent.types[target.name] = target;
333 }
334 }
335 },
336
337 /**
338 * Process tag
339 * @method ontag
340 * @param {Object} item
341 * @param {String} item.tag
342 * @param {Object} item.value
343 */
344 ontag: function (item) {
345 var name = utils.safetrim(item.tag);
346 var value = utils.safetrim(item.value);
347 var file = this.context.block.target.file;
348
349 if (SHORT_TAGS[name] && value === '') {
350 value = 1;
351 }
352
353 if (TAGLIST.indexOf(name) === -1) {
354 if (_.has(CORRECTIONS, name)) {
355 // correction part
356 // TODO(Yorkie): log the correction
357 name = CORRECTIONS[name];
358 item.tag = name;
359 } else {
360 // TODO(Yorkie): report the unknown correction
361 }
362 }
363
364 if (_.has(DIGESTERS, name) === -1) {
365 this.context.block.target[name] = value;
366 } else {
367 var digest = DIGESTERS[name];
368 if (_.isString(digest)) {
369 digest = DIGESTERS[digest];
370 }
371 var block = this.context.block;
372 var target = block.target;
373 if (target._raw.description)
374 target.description = target._raw.description;
375 if (target._raw.type)
376 target.type = utils.fixType(target._raw.type);
377 if (target._raw.extends)
378 target.extends = utils.fixType(target._raw.extends);
379
380 if (_.isFunction(digest)) {
381 // here we only push and run later
382 // because CORRECTION perhaps doesn't apply the later tags.
383 block.digesters.push({
384 fn: digest,
385 name: name,
386 value: value
387 });
388 }
389 }
390 },
391
392 /**
393 * Accepts a map of filenames to file content. Returns
394 * a map of filenames to an array of API comment block
395 * text. This expects the comment to start with / **
396 * on its own line, and end with * / on its own
397 * line. Override this function to provide an
398 * alternative comment parser.
399 *
400 * @method extract
401 * @param {Object} files
402 * @param {Object} dirs
403 */
404 extract: function (files, dirs) {
405 _.each(files, function (code, filename) {
406 filename = path.relative(CWD, filename);
407 this.codes[filename] = code;
408 const lines = code.split(REGEX_LINES);
409 const len = lines.length;
410 var comment;
411 var cursor = 0;
412 for (; cursor < len; cursor++) {
413 var line = lines[cursor];
414 if (REGEX_START_COMMENT[this.syntaxType].test(line)) {
415 var comments = [];
416 var linenum = cursor + 1;
417 while (cursor < len &&
418 (!REGEX_END_COMMENT[this.syntaxType].test(line))) {
419 comments.push(line);
420 cursor += 1;
421 line = lines[cursor];
422 }
423 comments.shift();
424 comment = comments.join('\n');
425 this.commentsMap[filename] = this.commentsMap[filename] || [];
426 this.commentsMap[filename].push(this.oncomment(comment, filename, linenum));
427 }
428 }
429 }, this);
430 },
431
432 /**
433 * Transforms a map of filenames to arrays of comment blocks into a
434 * JSON structure that represents the entire processed API doc info
435 * and relationships between elements for the entire project.
436 *
437 * @method transform
438 * @param {Object} commentmap The hash of files and parsed comment blocks
439 * @return {Object} The transformed data for the project
440 */
441 transform: function () {
442 _.each(this.commentsMap, function (blocks, filename) {
443 this.context.file = filename;
444 _.each(blocks, this.onblock, this);
445 }, this);
446 }
447
448};
449
450exports.AST = AST;
451exports.DIGESTERS = DIGESTERS;