UNPKG

14.2 kBJavaScriptView Raw
1var _ = require('lodash');
2var fs = require('fs');
3var path = require('path');
4var util = require('util');
5
6var findFiles = require('./utils/find_files');
7
8var ParameterError = require('./errors/parameter_error');
9var ParserError = require('./errors/parser_error');
10
11var app = {};
12
13function Parser(_app) {
14 var self = this;
15
16 // global variables
17 app = _app;
18
19 // class variables
20 self.languages = {};
21 self.parsers = {};
22 self.parsedFileElements = [];
23 self.parsedFiles = [];
24 self.countDeprecated = {};
25
26 // load languages
27 var languages = Object.keys(app.languages);
28 languages.forEach(function(language) {
29 var filename = app.languages[language];
30 app.log.debug('load parser language: ' + language + ', ' + filename);
31 self.addLanguage(language, require(filename));
32 });
33
34 // load parser
35 var parsers = Object.keys(app.parsers);
36 parsers.forEach(function(parser) {
37 var filename = app.parsers[parser];
38 app.log.debug('load parser: ' + parser + ', ' + filename);
39 self.addParser(parser, require(filename));
40 });
41}
42
43/**
44 * Inherit
45 */
46util.inherits(Parser, Object);
47
48/**
49 * Exports
50 */
51module.exports = Parser;
52
53/**
54 * Add a Language
55 */
56Parser.prototype.addLanguage = function(name, language) {
57 this.languages[name] = language;
58};
59
60/**
61 * Add a Parser
62 */
63Parser.prototype.addParser = function(name, parser) {
64 this.parsers[name] = parser;
65};
66
67/**
68 * Parse files in specified folder
69 *
70 * @param {Object} options The options used to parse and filder the files.
71 * @param {Object[]} parsedFiles List of parsed files.
72 * @param {String[]} parsedFilenames List of parsed files, with full path.
73 */
74Parser.prototype.parseFiles = function(options, parsedFiles, parsedFilenames) {
75 var self = this;
76
77 findFiles.setPath(options.src);
78 findFiles.setExcludeFilters(options.excludeFilters);
79 findFiles.setIncludeFilters(options.includeFilters);
80 var files = findFiles.search();
81
82 // Parser
83 for (var i = 0; i < files.length; i += 1) {
84 var filename = options.src + files[i];
85 var parsedFile = self.parseFile(filename);
86 if (parsedFile) {
87 app.log.verbose('parse file: ' + filename);
88 parsedFiles.push(parsedFile);
89 parsedFilenames.push(filename);
90 }
91 }
92};
93
94/**
95 * Execute Fileparsing
96 */
97Parser.prototype.parseFile = function(filename) {
98 var self = this;
99
100 app.log.debug('inspect file: ' + filename);
101
102 self.filename = filename;
103 self.extension = path.extname(filename).toLowerCase();
104 self.src = fs.readFileSync(filename, 'utf8').toString();
105 app.log.debug('size: ' + self.src.length);
106
107 // unify line-breaks
108 self.src = self.src.replace(/\r\n/g, '\n');
109
110 self.blocks = [];
111 self.indexApiBlocks = [];
112
113 // determine blocks
114 self.blocks = self._findBlocks();
115 if (self.blocks.length === 0)
116 return;
117
118 app.log.debug('count blocks: ' + self.blocks.length);
119
120 // determine elements in blocks
121 self.elements = self.blocks.map(function(block, i) {
122 var elements = self._findElements(block);
123 app.log.debug('count elements in block ' + i + ': ' + elements.length);
124 return elements;
125 });
126 if (self.elements.length === 0)
127 return;
128
129 // determine list of blocks with API elements
130 self.indexApiBlocks = self._findBlockWithApiGetIndex(self.elements);
131 if (self.indexApiBlocks.length === 0)
132 return;
133
134 return self._parseBlockElements(self.indexApiBlocks, self.elements, filename);
135};
136
137/**
138 * Parse API Elements with Plugins
139 *
140 * @param indexApiBlocks
141 * @param detectedElements
142 * @returns {Array}
143 */
144Parser.prototype._parseBlockElements = function(indexApiBlocks, detectedElements, filename) {
145 var self = this;
146 var parsedBlocks = [];
147
148 for (var i = 0; i < indexApiBlocks.length; i += 1) {
149 var blockIndex = indexApiBlocks[i];
150 var elements = detectedElements[blockIndex];
151 var blockData = {
152 global: {},
153 local : {}
154 };
155 var countAllowedMultiple = 0;
156
157 for (var j = 0; j < elements.length; j += 1) {
158 var element = elements[j];
159 var elementParser = self.parsers[element.name];
160
161 if ( ! elementParser) {
162 app.log.warn('parser plugin \'' + element.name + '\' not found.');
163 break;
164 }
165
166 app.log.debug('found @' + element.sourceName + ', in block: ' + i);
167
168 // Deprecation warning
169 if (elementParser.deprecated) {
170 self.countDeprecated[element.sourceName] = self.countDeprecated[element.sourceName] ? self.countDeprecated[element.sourceName] + 1 : 1;
171
172 var message = '@' + element.sourceName + ' is deprecated';
173 if (elementParser.alternative)
174 message = '@' + element.sourceName + ' is deprecated, please use ' + elementParser.alternative;
175
176 if (self.countDeprecated[element.sourceName] === 1)
177 // show deprecated message only 1 time as warning
178 app.log.warn(message);
179 else
180 // show deprecated message more than 1 time as verbose message
181 app.log.verbose(message);
182
183 app.log.verbose('in file: ' + filename + ', block: ' + blockIndex);
184 }
185
186 var values;
187 var preventGlobal;
188 var allowMultiple;
189 var pathTo;
190 var attachMethod;
191 try {
192 // parse element and retrieve values
193 values = elementParser.parse(element.content, element.source);
194
195 // HINT: pathTo MUST be read after elementParser.parse, because of dynamic paths
196 // Add all other options after parse too, in case of a custom plugin need to modify params.
197
198 // check if it is allowed to add to global namespace
199 preventGlobal = elementParser.preventGlobal === true;
200
201 // allow multiple inserts into pathTo
202 allowMultiple = elementParser.allowMultiple === true;
203
204
205 // path to an array, where the values should be attached
206 pathTo = '';
207 if (elementParser.path) {
208 if (typeof elementParser.path === 'string')
209 pathTo = elementParser.path;
210 else
211 pathTo = elementParser.path(); // for dynamic paths
212 }
213
214 if ( ! pathTo)
215 throw new ParserError('pathTo is not defined in the parser file.', '', '', element.sourceName);
216
217 // method how the values should be attached (insert or push)
218 attachMethod = elementParser.method || 'push';
219
220 if (attachMethod !== 'insert' && attachMethod !== 'push')
221 throw new ParserError('Only push or insert are allowed parser method values.', '', '', element.sourceName);
222
223 // Markdown
224 // TODO: put this into converters
225 if ( values &&
226 app.markdown &&
227 elementParser.markdownFields &&
228 elementParser.markdownFields.length > 0
229 ) {
230 for (var markdownIndex = 0; markdownIndex < elementParser.markdownFields.length; markdownIndex += 1) {
231 var markdownField = elementParser.markdownFields[markdownIndex];
232 if (values[markdownField] && app.markdown) {
233 values[markdownField] = app.markdown(values[markdownField]);
234 // remove line breaks
235 values[markdownField] = values[markdownField].replace(/(\r\n|\n|\r)/g, ' ');
236 }
237 }
238 }
239 } catch(e) {
240 if (e instanceof ParameterError) {
241 var extra = [];
242 if (e.definition)
243 extra.push({ 'Definition': e.definition });
244 if (e.example)
245 extra.push({ 'Example': e.example });
246 throw new ParserError(e.message,
247 self.filename, (blockIndex + 1), element.sourceName, element.source, extra);
248 }
249 throw new ParserError('Undefined error.',
250 self.filename, (blockIndex + 1), element.sourceName, element.source);
251 }
252
253 if ( ! values)
254 throw new ParserError('Empty parser result.',
255 self.filename, (blockIndex + 1), element.sourceName, element.source);
256
257 if (preventGlobal) {
258 // Check if count global namespace entries > count allowed
259 // (e.g. @successTitle is global, but should co-exist with @apiErrorStructure)
260 if (Object.keys(blockData.global).length > countAllowedMultiple)
261 throw new ParserError('Only one definition or usage is allowed in the same block.',
262 self.filename, (blockIndex + 1), element.sourceName, element.source);
263 }
264
265 // only one global allowed per block
266 if (pathTo === 'global' || pathTo.substr(0, 7) === 'global.') {
267 if (allowMultiple) {
268 countAllowedMultiple += 1;
269 } else {
270 if (Object.keys(blockData.global).length > 0)
271 throw new ParserError('Only one definition is allowed in the same block.',
272 self.filename, (blockIndex + 1), element.sourceName, element.source);
273
274 if (preventGlobal === true)
275 throw new ParserError('Only one definition or usage is allowed in the same block.',
276 self.filename, (blockIndex + 1), element.sourceName, element.source);
277 }
278 }
279
280 if ( ! blockData[pathTo])
281 self._createObjectPath(blockData, pathTo, attachMethod);
282
283 var blockDataPath = self._pathToObject(pathTo, blockData);
284
285 // insert Fieldvalues in Path-Array
286 if (attachMethod === 'push')
287 blockDataPath.push(values);
288 else
289 _.extend(blockDataPath, values);
290
291 // insert Fieldvalues in Mainpath
292 if (elementParser.extendRoot === true)
293 _.extend(blockData, values);
294
295 blockData.index = blockIndex + 1;
296 }
297 parsedBlocks.push(blockData);
298 }
299
300 return parsedBlocks;
301};
302
303/**
304 * Create a not existing Path in an Object
305 *
306 * @param src
307 * @param path
308 * @param {String} attachMethod Create last element as object or array: 'insert', 'push'
309 * @returns {Object}
310 */
311Parser.prototype._createObjectPath = function(src, path, attachMethod) {
312 if ( ! path)
313 return src;
314 var pathParts = path.split('.');
315 var current = src;
316 for (var i = 0; i < pathParts.length; i += 1) {
317 var part = pathParts[i];
318 if ( ! current[part]) {
319 if (i === (pathParts.length - 1) && attachMethod === 'push' )
320 current[part] = [];
321 else
322 current[part] = {};
323 }
324 current = current[part];
325 }
326 return current;
327};
328
329
330/**
331 * Return Path to Object
332 */
333Parser.prototype._pathToObject = function(path, src) {
334 if ( ! path)
335 return src;
336 var pathParts = path.split('.');
337 var current = src;
338 for (var i = 0; i < pathParts.length; i += 1) {
339 var part = pathParts[i];
340 current = current[part];
341 }
342 return current;
343};
344
345/**
346 * Determine Blocks
347 */
348Parser.prototype._findBlocks = function() {
349 var self = this;
350 var blocks = [];
351 var src = self.src;
352
353 // Replace Linebreak with Unicode
354 src = src.replace(/\n/g, '\uffff');
355
356 var regexForFile = this.languages[self.extension] || this.languages['default'];
357 var matches = regexForFile.docBlocksRegExp.exec(src);
358 while (matches) {
359 var block = matches[2] || matches[1];
360
361 // Reverse Unicode Linebreaks
362 block = block.replace(/\uffff/g, '\n');
363
364 block = block.replace(regexForFile.inlineRegExp, '');
365 blocks.push(block);
366
367 // Find next
368 matches = regexForFile.docBlocksRegExp.exec(src);
369 }
370 return blocks;
371};
372
373/**
374 * Return block indexes with active API-elements
375 *
376 * An @apiIgnore ignores the block.
377 * Other, non @api elements, will be ignored.
378 */
379Parser.prototype._findBlockWithApiGetIndex = function(blocks) {
380 var foundIndexes = [];
381 for (var i = 0; i < blocks.length; i += 1) {
382 var found = false;
383 for (var j = 0; j < blocks[i].length; j += 1) {
384 if (blocks[i][j].name.substr(0, 9) === 'apiignore') {
385 app.log.debug('apiIgnore found in block: ' + i);
386 found = false;
387 break;
388 }
389
390 if (blocks[i][j].name.substr(0, 3) === 'api')
391 found = true;
392 }
393 if (found) {
394 foundIndexes.push(i);
395 app.log.debug('api found in block: ' + i);
396 }
397 }
398 return foundIndexes;
399};
400
401/**
402 * Get Elements of Blocks
403 */
404Parser.prototype._findElements = function(block) {
405 var elements = [];
406
407 // Replace Linebreak with Unicode
408 block = block.replace(/\n/g, '\uffff');
409
410 // Elements start with @
411 var elementsRegExp = /(@(\w*)\s?(.+?)(?=\uffff[\s\*]*@|$))/gm;
412 var matches = elementsRegExp.exec(block);
413 while (matches) {
414 var element = {
415 source : matches[1],
416 name : matches[2].toLowerCase(),
417 sourceName: matches[2],
418 content : matches[3]
419 };
420
421 // reverse Unicode Linebreaks
422 element.content = element.content.replace(/\uffff/g, '\n');
423 element.source = element.source.replace(/\uffff/g, '\n');
424
425 elements.push(element);
426
427 // next Match
428 matches = elementsRegExp.exec(block);
429 }
430 return elements;
431};