1 | var _ = require('lodash');
|
2 | var fs = require('fs');
|
3 | var path = require('path');
|
4 | var util = require('util');
|
5 | var iconv = require('iconv-lite');
|
6 |
|
7 | var findFiles = require('./utils/find_files');
|
8 |
|
9 | var ParameterError = require('./errors/parameter_error');
|
10 | var ParserError = require('./errors/parser_error');
|
11 |
|
12 | var app = {};
|
13 |
|
14 | function Parser(_app) {
|
15 | var self = this;
|
16 |
|
17 |
|
18 | app = _app;
|
19 |
|
20 |
|
21 | self.languages = {};
|
22 | self.parsers = {};
|
23 | self.parsedFileElements = [];
|
24 | self.parsedFiles = [];
|
25 | self.countDeprecated = {};
|
26 |
|
27 |
|
28 | var languages = Object.keys(app.languages);
|
29 | languages.forEach(function(language) {
|
30 | if (_.isObject( app.languages[language] )) {
|
31 | app.log.debug('inject parser language: ' + language);
|
32 | self.addLanguage(language, app.languages[language] );
|
33 | } else {
|
34 | var filename = app.languages[language];
|
35 | app.log.debug('load parser language: ' + language + ', ' + filename);
|
36 | self.addLanguage(language, require(filename));
|
37 | }
|
38 | });
|
39 |
|
40 |
|
41 | var parsers = Object.keys(app.parsers);
|
42 | parsers.forEach(function(parser) {
|
43 | if (_.isObject( app.parsers[parser] )) {
|
44 | app.log.debug('inject parser: ' + parser);
|
45 | self.addParser(parser, app.parsers[parser] );
|
46 | } else {
|
47 | var filename = app.parsers[parser];
|
48 | app.log.debug('load parser: ' + parser + ', ' + filename);
|
49 | self.addParser(parser, require(filename));
|
50 | }
|
51 | });
|
52 | }
|
53 |
|
54 |
|
55 |
|
56 |
|
57 | util.inherits(Parser, Object);
|
58 |
|
59 |
|
60 |
|
61 |
|
62 | module.exports = Parser;
|
63 |
|
64 |
|
65 |
|
66 |
|
67 | Parser.prototype.addLanguage = function(name, language) {
|
68 | this.languages[name] = language;
|
69 | };
|
70 |
|
71 |
|
72 |
|
73 |
|
74 | Parser.prototype.addParser = function(name, parser) {
|
75 | this.parsers[name] = parser;
|
76 | };
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 | Parser.prototype.parseFiles = function(options, parsedFiles, parsedFilenames) {
|
86 | var self = this;
|
87 |
|
88 | findFiles.setPath(options.src);
|
89 | findFiles.setExcludeFilters(options.excludeFilters);
|
90 | findFiles.setIncludeFilters(options.includeFilters);
|
91 | var files = findFiles.search();
|
92 |
|
93 |
|
94 | for (var i = 0; i < files.length; i += 1) {
|
95 | var filename = options.src + files[i];
|
96 | var parsedFile = self.parseFile(filename, options.encoding);
|
97 | if (parsedFile) {
|
98 | app.log.verbose('parse file: ' + filename);
|
99 | parsedFiles.push(parsedFile);
|
100 | parsedFilenames.push(filename);
|
101 | }
|
102 | }
|
103 | };
|
104 |
|
105 |
|
106 |
|
107 |
|
108 | Parser.prototype.parseFile = function(filename, encoding) {
|
109 | var self = this;
|
110 |
|
111 | if (typeof(encoding) === 'undefined')
|
112 | encoding = 'utf8';
|
113 |
|
114 | app.log.debug('inspect file: ' + filename);
|
115 |
|
116 | self.filename = filename;
|
117 | self.extension = path.extname(filename).toLowerCase();
|
118 |
|
119 |
|
120 | var fileContent = fs.readFileSync(filename, { encoding: 'binary' });
|
121 | iconv.skipDecodeWarning = true;
|
122 | self.src = iconv.decode(fileContent, encoding);
|
123 | app.log.debug('size: ' + self.src.length);
|
124 |
|
125 |
|
126 | self.src = self.src.replace(/\r\n/g, '\n');
|
127 |
|
128 | self.blocks = [];
|
129 | self.indexApiBlocks = [];
|
130 |
|
131 |
|
132 | self.blocks = self._findBlocks();
|
133 | if (self.blocks.length === 0)
|
134 | return;
|
135 |
|
136 | app.log.debug('count blocks: ' + self.blocks.length);
|
137 |
|
138 |
|
139 | self.elements = self.blocks.map(function(block, i) {
|
140 | var elements = self.findElements(block, filename);
|
141 | app.log.debug('count elements in block ' + i + ': ' + elements.length);
|
142 | return elements;
|
143 | });
|
144 | if (self.elements.length === 0)
|
145 | return;
|
146 |
|
147 |
|
148 | self.indexApiBlocks = self._findBlockWithApiGetIndex(self.elements);
|
149 | if (self.indexApiBlocks.length === 0)
|
150 | return;
|
151 |
|
152 | return self._parseBlockElements(self.indexApiBlocks, self.elements, filename);
|
153 | };
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 | Parser.prototype._parseBlockElements = function(indexApiBlocks, detectedElements, filename) {
|
163 | var self = this;
|
164 | var parsedBlocks = [];
|
165 |
|
166 | for (var i = 0; i < indexApiBlocks.length; i += 1) {
|
167 | var blockIndex = indexApiBlocks[i];
|
168 | var elements = detectedElements[blockIndex];
|
169 | var blockData = {
|
170 | global: {},
|
171 | local : {}
|
172 | };
|
173 | var countAllowedMultiple = 0;
|
174 |
|
175 | for (var j = 0; j < elements.length; j += 1) {
|
176 | var element = elements[j];
|
177 | var elementParser = self.parsers[element.name];
|
178 |
|
179 | if ( ! elementParser) {
|
180 | app.log.warn('parser plugin \'' + element.name + '\' not found in block: ' + blockIndex);
|
181 | } else {
|
182 | app.log.debug('found @' + element.sourceName + ' in block: ' + blockIndex);
|
183 |
|
184 |
|
185 | if (elementParser.deprecated) {
|
186 | self.countDeprecated[element.sourceName] = self.countDeprecated[element.sourceName] ? self.countDeprecated[element.sourceName] + 1 : 1;
|
187 |
|
188 | var message = '@' + element.sourceName + ' is deprecated';
|
189 | if (elementParser.alternative)
|
190 | message = '@' + element.sourceName + ' is deprecated, please use ' + elementParser.alternative;
|
191 |
|
192 | if (self.countDeprecated[element.sourceName] === 1)
|
193 |
|
194 | app.log.warn(message);
|
195 | else
|
196 |
|
197 | app.log.verbose(message);
|
198 |
|
199 | app.log.verbose('in file: ' + filename + ', block: ' + blockIndex);
|
200 | }
|
201 |
|
202 | var values;
|
203 | var preventGlobal;
|
204 | var allowMultiple;
|
205 | var pathTo;
|
206 | var attachMethod;
|
207 | try {
|
208 |
|
209 | values = elementParser.parse(element.content, element.source);
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 | preventGlobal = elementParser.preventGlobal === true;
|
216 |
|
217 |
|
218 | allowMultiple = elementParser.allowMultiple === true;
|
219 |
|
220 |
|
221 |
|
222 | pathTo = '';
|
223 | if (elementParser.path) {
|
224 | if (typeof elementParser.path === 'string')
|
225 | pathTo = elementParser.path;
|
226 | else
|
227 | pathTo = elementParser.path();
|
228 | }
|
229 |
|
230 | if ( ! pathTo)
|
231 | throw new ParserError('pathTo is not defined in the parser file.', '', '', element.sourceName);
|
232 |
|
233 |
|
234 | attachMethod = elementParser.method || 'push';
|
235 |
|
236 | if (attachMethod !== 'insert' && attachMethod !== 'push')
|
237 | throw new ParserError('Only push or insert are allowed parser method values.', '', '', element.sourceName);
|
238 |
|
239 |
|
240 | if (values) {
|
241 |
|
242 | if ( app.markdownParser &&
|
243 | elementParser.markdownFields &&
|
244 | elementParser.markdownFields.length > 0
|
245 | ) {
|
246 | for (var markdownIndex = 0; markdownIndex < elementParser.markdownFields.length; markdownIndex += 1) {
|
247 | var field = elementParser.markdownFields[markdownIndex];
|
248 | var value = _.get(values, field);
|
249 | if (value) {
|
250 | value = app.markdownParser.render(value);
|
251 |
|
252 | value = value.replace(/(\r\n|\n|\r)/g, ' ');
|
253 |
|
254 | value = value.trim();
|
255 | _.set(values, field, value);
|
256 |
|
257 |
|
258 | if ( elementParser.markdownRemovePTags &&
|
259 | elementParser.markdownRemovePTags.length > 0 &&
|
260 | elementParser.markdownRemovePTags.indexOf(field) !== -1
|
261 | ) {
|
262 |
|
263 | value = value.replace(/(<p>|<\/p>)/g, '');
|
264 | _.set(values, field, value);
|
265 | }
|
266 | }
|
267 | }
|
268 | }
|
269 | }
|
270 | } catch(e) {
|
271 | if (e instanceof ParameterError) {
|
272 | var extra = [];
|
273 | if (e.definition)
|
274 | extra.push({ 'Definition': e.definition });
|
275 | if (e.example)
|
276 | extra.push({ 'Example': e.example });
|
277 | throw new ParserError(e.message,
|
278 | self.filename, (blockIndex + 1), element.sourceName, element.source, extra);
|
279 | }
|
280 | throw new ParserError('Undefined error.',
|
281 | self.filename, (blockIndex + 1), element.sourceName, element.source);
|
282 | }
|
283 |
|
284 | if ( ! values)
|
285 | throw new ParserError('Empty parser result.',
|
286 | self.filename, (blockIndex + 1), element.sourceName, element.source);
|
287 |
|
288 | if (preventGlobal) {
|
289 |
|
290 |
|
291 | if (Object.keys(blockData.global).length > countAllowedMultiple)
|
292 | throw new ParserError('Only one definition or usage is allowed in the same block.',
|
293 | self.filename, (blockIndex + 1), element.sourceName, element.source);
|
294 | }
|
295 |
|
296 |
|
297 | if (pathTo === 'global' || pathTo.substr(0, 7) === 'global.') {
|
298 | if (allowMultiple) {
|
299 | countAllowedMultiple += 1;
|
300 | } else {
|
301 | if (Object.keys(blockData.global).length > 0)
|
302 | throw new ParserError('Only one definition is allowed in the same block.',
|
303 | self.filename, (blockIndex + 1), element.sourceName, element.source);
|
304 |
|
305 | if (preventGlobal === true)
|
306 | throw new ParserError('Only one definition or usage is allowed in the same block.',
|
307 | self.filename, (blockIndex + 1), element.sourceName, element.source);
|
308 | }
|
309 | }
|
310 |
|
311 | if ( ! blockData[pathTo])
|
312 | self._createObjectPath(blockData, pathTo, attachMethod);
|
313 |
|
314 | var blockDataPath = self._pathToObject(pathTo, blockData);
|
315 |
|
316 |
|
317 | if (attachMethod === 'push')
|
318 | blockDataPath.push(values);
|
319 | else
|
320 | _.extend(blockDataPath, values);
|
321 |
|
322 |
|
323 | if (elementParser.extendRoot === true)
|
324 | _.extend(blockData, values);
|
325 |
|
326 | blockData.index = blockIndex + 1;
|
327 | }
|
328 | }
|
329 | if (blockData.index && blockData.index > 0)
|
330 | parsedBlocks.push(blockData);
|
331 | }
|
332 | return parsedBlocks;
|
333 | };
|
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 |
|
343 | Parser.prototype._createObjectPath = function(src, path, attachMethod) {
|
344 | if ( ! path)
|
345 | return src;
|
346 | var pathParts = path.split('.');
|
347 | var current = src;
|
348 | for (var i = 0; i < pathParts.length; i += 1) {
|
349 | var part = pathParts[i];
|
350 | if ( ! current[part]) {
|
351 | if (i === (pathParts.length - 1) && attachMethod === 'push' )
|
352 | current[part] = [];
|
353 | else
|
354 | current[part] = {};
|
355 | }
|
356 | current = current[part];
|
357 | }
|
358 | return current;
|
359 | };
|
360 |
|
361 |
|
362 |
|
363 |
|
364 |
|
365 | Parser.prototype._pathToObject = function(path, src) {
|
366 | if ( ! path)
|
367 | return src;
|
368 | var pathParts = path.split('.');
|
369 | var current = src;
|
370 | for (var i = 0; i < pathParts.length; i += 1) {
|
371 | var part = pathParts[i];
|
372 | current = current[part];
|
373 | }
|
374 | return current;
|
375 | };
|
376 |
|
377 |
|
378 |
|
379 |
|
380 | Parser.prototype._findBlocks = function() {
|
381 | var self = this;
|
382 | var blocks = [];
|
383 | var src = self.src;
|
384 |
|
385 |
|
386 | src = src.replace(/\n/g, '\uffff');
|
387 |
|
388 | var regexForFile = this.languages[self.extension] || this.languages['default'];
|
389 | var matches = regexForFile.docBlocksRegExp.exec(src);
|
390 | while (matches) {
|
391 | var block = matches[2] || matches[1];
|
392 |
|
393 |
|
394 | block = block.replace(/\uffff/g, '\n');
|
395 |
|
396 | block = block.replace(regexForFile.inlineRegExp, '');
|
397 | blocks.push(block);
|
398 |
|
399 |
|
400 | matches = regexForFile.docBlocksRegExp.exec(src);
|
401 | }
|
402 | return blocks;
|
403 | };
|
404 |
|
405 |
|
406 |
|
407 |
|
408 |
|
409 |
|
410 |
|
411 | Parser.prototype._findBlockWithApiGetIndex = function(blocks) {
|
412 | var foundIndexes = [];
|
413 | for (var i = 0; i < blocks.length; i += 1) {
|
414 | var found = false;
|
415 | for (var j = 0; j < blocks[i].length; j += 1) {
|
416 |
|
417 | if (blocks[i][j].name.substr(0, 9) === 'apiignore') {
|
418 | app.log.debug('apiIgnore found in block: ' + i);
|
419 | found = false;
|
420 | break;
|
421 | }
|
422 |
|
423 |
|
424 | if (!app.options.apiprivate && blocks[i][j].name.substr(0, 10) === 'apiprivate') {
|
425 | app.log.debug('private flag is set to false and apiPrivate found in block: ' + i);
|
426 | found = false;
|
427 | break;
|
428 | }
|
429 |
|
430 | if (blocks[i][j].name.substr(0, 3) === 'api')
|
431 | found = true;
|
432 | }
|
433 | if (found) {
|
434 | foundIndexes.push(i);
|
435 | app.log.debug('api found in block: ' + i);
|
436 | }
|
437 | }
|
438 | return foundIndexes;
|
439 | };
|
440 |
|
441 |
|
442 |
|
443 |
|
444 | Parser.prototype.findElements = function(block, filename) {
|
445 | var elements = [];
|
446 |
|
447 |
|
448 | block = block.replace(/\n/g, '\uffff');
|
449 |
|
450 |
|
451 | var elementsRegExp = /(@(\w*)\s?(.+?)(?=\uffff[\s\*]*@|$))/gm;
|
452 | var matches = elementsRegExp.exec(block);
|
453 | while (matches) {
|
454 | var element = {
|
455 | source : matches[1],
|
456 | name : matches[2].toLowerCase(),
|
457 | sourceName: matches[2],
|
458 | content : matches[3]
|
459 | };
|
460 |
|
461 |
|
462 | element.content = element.content.replace(/\uffff/g, '\n');
|
463 | element.source = element.source.replace(/\uffff/g, '\n');
|
464 |
|
465 | app.hook('parser-find-element-' + element.name, element, block, filename);
|
466 |
|
467 | elements.push(element);
|
468 |
|
469 | app.hook('parser-find-elements', elements, element, block, filename);
|
470 |
|
471 |
|
472 | matches = elementsRegExp.exec(block);
|
473 | }
|
474 | return elements;
|
475 | };
|