UNPKG

16.9 kBJavaScriptView Raw
1var _ = require('lodash');
2var fs = require('fs');
3var path = require('path');
4var util = require('util');
5var iconv = require('iconv-lite');
6
7var findFiles = require('./utils/find_files');
8
9var ParameterError = require('./errors/parameter_error');
10var ParserError = require('./errors/parser_error');
11
12var app = {};
13
14function Parser(_app) {
15 var self = this;
16
17 // global variables
18 app = _app;
19
20 // class variables
21 self.languages = {};
22 self.parsers = {};
23 self.parsedFileElements = [];
24 self.parsedFiles = [];
25 self.countDeprecated = {};
26
27 // load languages
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 // load parser
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 * Inherit
56 */
57util.inherits(Parser, Object);
58
59/**
60 * Exports
61 */
62module.exports = Parser;
63
64/**
65 * Add a Language
66 */
67Parser.prototype.addLanguage = function(name, language) {
68 this.languages[name] = language;
69};
70
71/**
72 * Add a Parser
73 */
74Parser.prototype.addParser = function(name, parser) {
75 this.parsers[name] = parser;
76};
77
78/**
79 * Parse files in specified folder
80 *
81 * @param {Object} options The options used to parse and filder the files.
82 * @param {Object[]} parsedFiles List of parsed files.
83 * @param {String[]} parsedFilenames List of parsed files, with full path.
84 */
85Parser.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 // Parser
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 * Execute Fileparsing
107 */
108Parser.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 // TODO: Not sure if this is correct. Without skipDecodeWarning we got string errors
119 // https://github.com/apidoc/apidoc-core/pull/25
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 // unify line-breaks
126 self.src = self.src.replace(/\r\n/g, '\n');
127
128 self.blocks = [];
129 self.indexApiBlocks = [];
130
131 // determine blocks
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 // determine elements in blocks
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 // determine list of blocks with API elements
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 * Parse API Elements with Plugins
157 *
158 * @param indexApiBlocks
159 * @param detectedElements
160 * @returns {Array}
161 */
162Parser.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 // Deprecation warning
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 // show deprecated message only 1 time as warning
194 app.log.warn(message);
195 else
196 // show deprecated message more than 1 time as verbose message
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 // parse element and retrieve values
209 values = elementParser.parse(element.content, element.source);
210
211 // HINT: pathTo MUST be read after elementParser.parse, because of dynamic paths
212 // Add all other options after parse too, in case of a custom plugin need to modify params.
213
214 // check if it is allowed to add to global namespace
215 preventGlobal = elementParser.preventGlobal === true;
216
217 // allow multiple inserts into pathTo
218 allowMultiple = elementParser.allowMultiple === true;
219
220
221 // path to an array, where the values should be attached
222 pathTo = '';
223 if (elementParser.path) {
224 if (typeof elementParser.path === 'string')
225 pathTo = elementParser.path;
226 else
227 pathTo = elementParser.path(); // for dynamic paths
228 }
229
230 if ( ! pathTo)
231 throw new ParserError('pathTo is not defined in the parser file.', '', '', element.sourceName);
232
233 // method how the values should be attached (insert or push)
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 // TODO: put this into "converters"
240 if (values) {
241 // Markdown.
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 // remove line breaks
252 value = value.replace(/(\r\n|\n|\r)/g, ' ');
253
254 value = value.trim();
255 _.set(values, field, value);
256
257 // TODO: Little hacky, not sure to handle this here or in template
258 if ( elementParser.markdownRemovePTags &&
259 elementParser.markdownRemovePTags.length > 0 &&
260 elementParser.markdownRemovePTags.indexOf(field) !== -1
261 ) {
262 // Remove p-Tags
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 // Check if count global namespace entries > count allowed
290 // (e.g. @successTitle is global, but should co-exist with @apiErrorStructure)
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 // only one global allowed per block
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 // insert Fieldvalues in Path-Array
317 if (attachMethod === 'push')
318 blockDataPath.push(values);
319 else
320 _.extend(blockDataPath, values);
321
322 // insert Fieldvalues in Mainpath
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 * Create a not existing Path in an Object
337 *
338 * @param src
339 * @param path
340 * @param {String} attachMethod Create last element as object or array: 'insert', 'push'
341 * @returns {Object}
342 */
343Parser.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 * Return Path to Object
364 */
365Parser.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 * Determine Blocks
379 */
380Parser.prototype._findBlocks = function() {
381 var self = this;
382 var blocks = [];
383 var src = self.src;
384
385 // Replace Linebreak with Unicode
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 // Reverse Unicode Linebreaks
394 block = block.replace(/\uffff/g, '\n');
395
396 block = block.replace(regexForFile.inlineRegExp, '');
397 blocks.push(block);
398
399 // Find next
400 matches = regexForFile.docBlocksRegExp.exec(src);
401 }
402 return blocks;
403};
404
405/**
406 * Return block indexes with active API-elements
407 *
408 * An @apiIgnore ignores the block.
409 * Other, non @api elements, will be ignored.
410 */
411Parser.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 // check apiIgnore
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 // check app.options.apiprivate and apiPrivate
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 * Get Elements of Blocks
443 */
444Parser.prototype.findElements = function(block, filename) {
445 var elements = [];
446
447 // Replace Linebreak with Unicode
448 block = block.replace(/\n/g, '\uffff');
449
450 // Elements start with @
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 // reverse Unicode Linebreaks
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 // next Match
472 matches = elementsRegExp.exec(block);
473 }
474 return elements;
475};