1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | const _ = require('underscore');
|
7 | const fs = require('graceful-fs');
|
8 | const path = require('path');
|
9 | const utils = require('./utils');
|
10 | const debug = require('debug')('firedoc:ast');
|
11 | const ParserContext = require('./ast/context').ParserContext;
|
12 | const CWD = process.cwd();
|
13 | const REGEX_LINES = /\r\n|\n/;
|
14 | const REGEX_START_COMMENT = {
|
15 | js: /^\s*\/\*\*/,
|
16 | coffee: /^\s*###\*/
|
17 | };
|
18 | const REGEX_END_COMMENT = {
|
19 | js: /\*\/\s*$/,
|
20 | coffee: /###\s*$/
|
21 | };
|
22 | const REGEX_LINE_HEAD_CHAR = {
|
23 | js: /^\s*\*/,
|
24 | coffee: /^\s*[#\*]/
|
25 | };
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 | const IGNORE_TAGLIST = ['media'];
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 | const CORRECTIONS = require('./ast/corrections');
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 | const SHORT_TAGS = require('./ast/short-tags');
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 | const TAGLIST = require('./ast/tags');
|
63 | const DIGESTERS = require('./ast/digesters');
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 | var AST = {
|
76 |
|
77 | |
78 |
|
79 |
|
80 |
|
81 | project: {},
|
82 |
|
83 | |
84 |
|
85 |
|
86 | files: {},
|
87 |
|
88 | |
89 |
|
90 |
|
91 | codes: {},
|
92 |
|
93 | |
94 |
|
95 |
|
96 | modules: {},
|
97 |
|
98 | |
99 |
|
100 |
|
101 | classes: {},
|
102 |
|
103 | |
104 |
|
105 |
|
106 | members: [],
|
107 |
|
108 | |
109 |
|
110 |
|
111 | inheritedMembers: [],
|
112 |
|
113 | |
114 |
|
115 |
|
116 | namespacesMap: {},
|
117 |
|
118 | |
119 |
|
120 |
|
121 | commentsMap: {},
|
122 |
|
123 | |
124 |
|
125 |
|
126 | syntaxType: 'js',
|
127 |
|
128 | |
129 |
|
130 |
|
131 | context: null,
|
132 |
|
133 | |
134 |
|
135 |
|
136 | warnings: [],
|
137 |
|
138 | |
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
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 |
|
162 |
|
163 |
|
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 |
|
186 |
|
187 |
|
188 |
|
189 |
|
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 |
|
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);
|
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 |
|
228 | if (cursor === 0 && part.substr(0, 1) !== '@') {
|
229 | if (part) {
|
230 | tag = '@description';
|
231 | val = part;
|
232 | } else {
|
233 | skip = true;
|
234 | }
|
235 | } else {
|
236 | tag = part;
|
237 |
|
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 |
|
256 |
|
257 |
|
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 |
|
288 | _.each(block, this.ontag, this);
|
289 |
|
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 |
|
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 |
|
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 |
|
339 |
|
340 |
|
341 |
|
342 |
|
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 |
|
356 |
|
357 | name = CORRECTIONS[name];
|
358 | item.tag = name;
|
359 | } else {
|
360 |
|
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 |
|
382 |
|
383 | block.digesters.push({
|
384 | fn: digest,
|
385 | name: name,
|
386 | value: value
|
387 | });
|
388 | }
|
389 | }
|
390 | },
|
391 |
|
392 | |
393 |
|
394 |
|
395 |
|
396 |
|
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
402 |
|
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 |
|
434 |
|
435 |
|
436 |
|
437 |
|
438 |
|
439 |
|
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 |
|
450 | exports.AST = AST;
|
451 | exports.DIGESTERS = DIGESTERS;
|