parser.js 17 KB

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