123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569 |
- /*
- * apidoc
- * https://apidocjs.com
- *
- * Authors:
- * Peter Rottmann <rottmann@inveris.de>
- * Nicolas CARPi @ Deltablot
- * Copyright (c) 2013 inveris OHG
- * Licensed under the MIT license.
- */
- const { isObject, get, set, extend } = require('lodash');
- const fs = require('fs');
- const path = require('path');
- const util = require('util');
- const iconv = require('iconv-lite');
- const findFiles = require('./utils/find_files');
- const ParameterError = require('./errors/parameter_error');
- const ParserError = require('./errors/parser_error');
- let app = {};
- let filterTag = null; // define the tag to filter by
- function Parser (_app) {
- const self = this;
- // global variables
- app = _app;
- // class variables
- self.languages = {};
- self.parsers = {};
- self.parsedFileElements = [];
- self.parsedFiles = [];
- self.countDeprecated = {};
- // load languages
- const languages = Object.keys(app.languages);
- languages.forEach(function (language) {
- if (isObject(app.languages[language])) {
- app.log.debug('inject parser language: ' + language);
- self.addLanguage(language, app.languages[language]);
- } else {
- const filename = app.languages[language];
- app.log.debug('load parser language: ' + language + ', ' + filename);
- self.addLanguage(language, require(filename));
- }
- });
- // load parser
- const parsers = Object.keys(app.parsers);
- parsers.forEach(function (parser) {
- if (isObject(app.parsers[parser])) {
- app.log.debug('inject parser: ' + parser);
- self.addParser(parser, app.parsers[parser]);
- } else {
- const filename = app.parsers[parser];
- app.log.debug('load parser: ' + parser + ', ' + filename);
- self.addParser(parser, require(filename));
- }
- });
- // check app.options.filterBy and define the tag to filter by
- if (app.options.filterBy) {
- const tag = app.options.filterBy.split('=')[0];
- filterTag = tag.indexOf('api') !== -1 ? tag : null;
- }
- }
- /**
- * Inherit
- */
- util.inherits(Parser, Object);
- /**
- * Exports
- */
- module.exports = Parser;
- /**
- * Add a Language
- */
- Parser.prototype.addLanguage = function (name, language) {
- this.languages[name] = language;
- };
- /**
- * Add a Parser
- */
- Parser.prototype.addParser = function (name, parser) {
- this.parsers[name] = parser;
- };
- /**
- * Parse files in specified folder
- *
- * @param {Object} options The options used to parse and filder the files.
- * @param {Object[]} parsedFiles List of parsed files.
- * @param {String[]} parsedFilenames List of parsed files, with full path.
- */
- Parser.prototype.parseFiles = function (options, parsedFiles, parsedFilenames) {
- const self = this;
- findFiles.setPath(options.src);
- findFiles.setExcludeFilters(options.excludeFilters);
- findFiles.setIncludeFilters(options.includeFilters);
- const files = findFiles.search();
- // Parser
- for (let i = 0; i < files.length; i += 1) {
- const filename = options.src + files[i];
- const parsedFile = self.parseFile(filename, options.encoding);
- if (parsedFile) {
- app.log.verbose('parse file: ' + filename);
- parsedFiles.push(parsedFile);
- // only push the filename without full path to prevent information disclosure
- parsedFilenames.push(files[i]);
- }
- }
- };
- /**
- * Execute Fileparsing
- */
- Parser.prototype.parseFile = function (filename, encoding) {
- const self = this;
- if (typeof encoding === 'undefined') { encoding = 'utf8'; }
- app.log.debug('inspect file: ' + filename);
- self.filename = filename;
- self.extension = path.extname(filename).toLowerCase();
- // TODO: Not sure if this is correct. Without skipDecodeWarning we got string errors
- // https://github.com/apidoc/apidoc-core/pull/25
- const fileContent = fs.readFileSync(filename, { encoding: 'binary' });
- return self.parseSource(fileContent, encoding, filename);
- };
- /**
- * Execute Sourceparsing
- */
- Parser.prototype.parseSource = function (fileContent, encoding, filename) {
- const self = this;
- iconv.skipDecodeWarning = true;
- self.src = iconv.decode(fileContent, encoding);
- app.log.debug('size: ' + self.src.length);
- // unify line-breaks
- self.src = self.src.replace(/\r\n/g, '\n');
- self.blocks = [];
- self.indexApiBlocks = [];
- // determine blocks
- self.blocks = self._findBlocks();
- if (self.blocks.length === 0) { return; }
- app.log.debug('count blocks: ' + self.blocks.length);
- // determine elements in blocks
- self.elements = self.blocks.map(function (block, i) {
- const elements = self.findElements(block, filename);
- app.log.debug('count elements in block ' + i + ': ' + elements.length);
- return elements;
- });
- if (self.elements.length === 0) { return; }
- // determine list of blocks with API elements
- self.indexApiBlocks = self._findBlockWithApiGetIndex(self.elements);
- if (self.indexApiBlocks.length === 0) { return; }
- const parsedBlocks = self._parseBlockElements(self.indexApiBlocks, self.elements, filename);
- _sanityChecks(parsedBlocks, app.log, filename);
- return parsedBlocks;
- };
- /**
- * Parse API Elements with Plugins
- *
- * @param indexApiBlocks
- * @param detectedElements
- * @returns {Array}
- */
- Parser.prototype._parseBlockElements = function (indexApiBlocks, detectedElements, filename) {
- const self = this;
- const parsedBlocks = [];
- for (let i = 0; i < indexApiBlocks.length; i += 1) {
- const blockIndex = indexApiBlocks[i];
- const elements = detectedElements[blockIndex];
- const blockData = {
- global: {},
- local: {},
- };
- let countAllowedMultiple = 0;
- for (let j = 0; j < elements.length; j += 1) {
- const element = elements[j];
- const elementParser = self.parsers[element.name];
- if (!elementParser) {
- app.log.warn(`parser plugin '${element.name}' not found in block: '${blockIndex}' in file: '${filename}'`);
- } else if (!element.sourceName.endsWith('Example') && element.source.match(/[^\s:]\[[^\]]/)) {
- app.log.warn(`The use of square brackets for object properties is deprecated. Please use dot notation instead: "${element.source}"`);
- } else {
- app.log.debug('found @' + element.sourceName + ' in block: ' + blockIndex);
- // Deprecation warning
- if (elementParser.deprecated) {
- self.countDeprecated[element.sourceName] = self.countDeprecated[element.sourceName] ? self.countDeprecated[element.sourceName] + 1 : 1;
- let message = '@' + element.sourceName + ' is deprecated';
- if (elementParser.alternative) { message = '@' + element.sourceName + ' is deprecated, please use ' + elementParser.alternative; }
- if (self.countDeprecated[element.sourceName] === 1) {
- // show deprecated message only 1 time as warning
- app.log.warn(message);
- } else {
- // show deprecated message more than 1 time as verbose message
- app.log.verbose(message);
- }
- app.log.verbose('in file: ' + filename + ', block: ' + blockIndex);
- }
- let values = '';
- let preventGlobal = false;
- let allowMultiple = false;
- let pathTo = '';
- let attachMethod = '';
- try {
- // parse element and retrieve values
- values = elementParser.parse(element.content, element.source);
- // HINT: pathTo MUST be read after elementParser.parse, because of dynamic paths
- // Add all other options after parse too, in case of a custom plugin need to modify params.
- // check if it is allowed to add to global namespace
- preventGlobal = elementParser.preventGlobal === true;
- // allow multiple inserts into pathTo
- allowMultiple = true;
- // path to an array, where the values should be attached
- pathTo = '';
- if (elementParser.path) {
- if (typeof elementParser.path === 'string') { pathTo = elementParser.path; } else { pathTo = elementParser.path(); } // for dynamic paths
- }
- if (!pathTo) { throw new ParserError('pathTo is not defined in the parser file.', '', '', element.sourceName); }
- // method how the values should be attached (insert or push)
- attachMethod = elementParser.method || 'push';
- if (attachMethod !== 'insert' && attachMethod !== 'push') { throw new ParserError('Only push or insert are allowed parser method values.', '', '', element.sourceName); }
- // TODO: put this into "converters"
- if (values) {
- // Markdown.
- if (app.markdownParser &&
- elementParser.markdownFields &&
- elementParser.markdownFields.length > 0
- ) {
- for (let markdownIndex = 0; markdownIndex < elementParser.markdownFields.length; markdownIndex += 1) {
- const field = elementParser.markdownFields[markdownIndex];
- let value = get(values, field);
- if (value) {
- value = app.markdownParser.render(value);
- // remove line breaks, but not within <pre> tags
- value = value.replace(/(?:^|<\/pre>)[^]*?(?:<pre>|$)/g, m => {
- return m.replace(/(\r\n|\n|\r)/g, ' ');
- });
- value = value.trim();
- set(values, field, value);
- // TODO: Little hacky, not sure to handle this here or in template
- if (elementParser.markdownRemovePTags &&
- elementParser.markdownRemovePTags.length > 0 &&
- elementParser.markdownRemovePTags.indexOf(field) !== -1
- ) {
- // Remove p-Tags
- value = value.replace(/(<p>|<\/p>)/g, '');
- set(values, field, value);
- }
- }
- }
- }
- }
- } catch (e) {
- if (e instanceof ParameterError) {
- const extra = [];
- if (e.definition) { extra.push({ Definition: e.definition }); }
- if (e.example) { extra.push({ Example: e.example }); }
- throw new ParserError(e.message,
- self.filename, blockIndex + 1, element.sourceName, element.source, extra);
- }
- throw new ParserError('Undefined error.',
- self.filename, blockIndex + 1, element.sourceName, element.source);
- }
- if (!values) {
- throw new ParserError('Empty parser result.',
- self.filename, blockIndex + 1, element.sourceName, element.source);
- }
- if (preventGlobal) {
- // Check if count global namespace entries > count allowed
- // (e.g. @successTitle is global, but should co-exist with @apiErrorStructure)
- if (Object.keys(blockData.global).length > countAllowedMultiple) {
- throw new ParserError('Only one definition or usage is allowed in the same block.',
- self.filename, blockIndex + 1, element.sourceName, element.source);
- }
- }
- // only one global allowed per block
- if (pathTo === 'global' || pathTo.substr(0, 7) === 'global.') {
- if (allowMultiple) {
- countAllowedMultiple += 1;
- } else {
- if (Object.keys(blockData.global).length > 0) {
- throw new ParserError('Only one definition is allowed in the same block.',
- self.filename, blockIndex + 1, element.sourceName, element.source);
- }
- if (preventGlobal) {
- throw new ParserError('Only one definition or usage is allowed in the same block.',
- self.filename, blockIndex + 1, element.sourceName, element.source);
- }
- }
- }
- if (!blockData[pathTo]) { self._createObjectPath(blockData, pathTo, attachMethod); }
- const blockDataPath = self._pathToObject(pathTo, blockData);
- // insert Fieldvalues in Path-Array
- if (attachMethod === 'push') { blockDataPath.push(values); } else { extend(blockDataPath, values); }
- // insert Fieldvalues in Mainpath
- if (elementParser.extendRoot === true) { extend(blockData, values); }
- blockData.index = blockIndex + 1;
- }
- }
- if (blockData.index && blockData.index > 0) { parsedBlocks.push(blockData); }
- }
- return parsedBlocks;
- };
- /**
- * Create a not existing Path in an Object
- *
- * @param src
- * @param path
- * @param {String} attachMethod Create last element as object or array: 'insert', 'push'
- * @returns {Object}
- */
- Parser.prototype._createObjectPath = function (src, path, attachMethod) {
- if (!path) { return src; }
- const pathParts = path.split('.');
- let current = src;
- for (let i = 0; i < pathParts.length; i += 1) {
- const part = pathParts[i];
- if (!current[part]) {
- if (i === pathParts.length - 1 && attachMethod === 'push') { current[part] = []; } else { current[part] = {}; }
- }
- current = current[part];
- }
- return current;
- };
- /**
- * Return Path to Object
- */
- Parser.prototype._pathToObject = function (path, src) {
- if (!path) { return src; }
- const pathParts = path.split('.');
- let current = src;
- for (let i = 0; i < pathParts.length; i += 1) {
- const part = pathParts[i];
- current = current[part];
- }
- return current;
- };
- /**
- * Determine Blocks
- */
- Parser.prototype._findBlocks = function () {
- const self = this;
- const blocks = [];
- let src = self.src;
- // Replace Linebreak with Unicode
- src = src.replace(/\n/g, '\uffff');
- const regexForFile = this.languages[self.extension] || this.languages.default;
- let matches = regexForFile.docBlocksRegExp.exec(src);
- while (matches) {
- let block = matches[2] || matches[1];
- // Reverse Unicode Linebreaks
- block = block.replace(/\uffff/g, '\n');
- block = block.replace(regexForFile.inlineRegExp, '');
- blocks.push(block);
- // Find next
- matches = regexForFile.docBlocksRegExp.exec(src);
- }
- return blocks;
- };
- /**
- * Return block indexes with active API-elements
- *
- * An @apiIgnore ignores the block.
- * Other, non @api elements, will be ignored.
- */
- Parser.prototype._findBlockWithApiGetIndex = function (blocks) {
- const foundIndexes = [];
- // get value to filter by
- const valueTofilter = filterTag ? app.options.filterBy.split('=')[1] : null;
- for (let i = 0; i < blocks.length; i += 1) {
- let found = false;
- let isToFilterBy = false;
- let isDefine = false;
- for (let j = 0; j < blocks[i].length; j += 1) {
- // check apiIgnore
- if (blocks[i][j].name.substr(0, 9) === 'apiignore') {
- app.log.debug('apiIgnore found in block: ' + i);
- found = false;
- break;
- }
- // check app.options.apiprivate and apiPrivate
- if (!app.options.apiprivate && blocks[i][j].name.substr(0, 10) === 'apiprivate') {
- app.log.debug('private flag is set to false and apiPrivate found in block: ' + i);
- found = false;
- break;
- }
- // check if the user want to filter by some specific tag
- if (filterTag) {
- // we need to add all apidefine
- if (blocks[i][j].name.substr(0, 9) === 'apidefine') {
- isDefine = true;
- }
- if (blocks[i][j].name.substr(0, filterTag.length) === filterTag && blocks[i][j].content === valueTofilter) {
- isToFilterBy = true;
- }
- }
- if (blocks[i][j].name.substr(0, 3) === 'api') { found = true; }
- }
- // add block if it's apidefine or the tag is equal to the value defined in options
- if (filterTag) {
- found = found && (isToFilterBy || isDefine);
- }
- if (found) {
- foundIndexes.push(i);
- app.log.debug('api found in block: ' + i);
- }
- }
- return foundIndexes;
- };
- /**
- * Get Elements of Blocks
- */
- Parser.prototype.findElements = function (block, filename) {
- const elements = [];
- // Replace Linebreak with Unicode
- block = block.replace(/\n/g, '\uffff');
- // Elements start with @api
- const elementsRegExp = /(@(api\w*)\s?(.*?)(?=\uffff[\s*]*@api|$))/gm;
- let matches = elementsRegExp.exec(block);
- while (matches) {
- const element = {
- source: matches[1],
- name: matches[2].toLowerCase(),
- sourceName: matches[2],
- content: matches[3],
- };
- // reverse Unicode Linebreaks
- element.content = element.content.replace(/\uffff/g, '\n');
- element.source = element.source.replace(/\uffff/g, '\n');
- app.hook('parser-find-element-' + element.name, element, block, filename);
- elements.push(element);
- app.hook('parser-find-elements', elements, element, block, filename);
- // next Match
- matches = elementsRegExp.exec(block);
- }
- return elements;
- };
- /**
- * Emit warnings for inconsistent API doc elements
- */
- function _sanityChecks (parsedBlocks, log, filename) {
- const definedBlocksByName = {};
- for (const block of parsedBlocks) {
- if (block.global.define && block.global.define.name) {
- definedBlocksByName[block.global.define.name] = block;
- }
- }
- for (const block of parsedBlocks) {
- const paramFields = _paramFieldsFromBlock(block);
- let paramFieldsDefinedOutside = [];
- if (block.local.use) {
- for (const define of block.local.use) {
- const definedBlock = definedBlocksByName[define.name];
- if (definedBlock) {
- paramFieldsDefinedOutside = paramFieldsDefinedOutside.concat(_paramFieldsFromBlock(definedBlock));
- }
- }
- }
- const urlParams = [];
- if (block.local.url) {
- // The dummy URL base is only used for parses of relative URLs.
- const url = new URL(block.local.url, 'https://dummy.base');
- // For API parameters in the URL parts delimited by `/` (e.g. `/:foo/:bar`).
- for (const pathnamePart of url.pathname.split('/')) {
- if (pathnamePart.charAt(0) === ':') {
- urlParams.push(pathnamePart.slice(1));
- }
- }
- }
- for (const urlParam of urlParams) {
- if (!paramFields.some(pf => pf.field === urlParam) && !paramFieldsDefinedOutside.some(pf => pf.field === urlParam)) {
- log.warn(`URL contains a parameter ':${urlParam}' that is not documented as @apiParam in @api '${block.local.title}' in file: '${filename}'`);
- }
- }
- if (!block.global.define) {
- for (const paramField of paramFields) {
- // Emit the warning only if the field is mandatory.
- if (!paramField.optional && !urlParams.some(up => up === paramField.field)) {
- log.warn(`@apiParam '${paramField.field}' was defined but does not appear in URL of @api '${block.local.title}' in file: '${filename}'`);
- }
- }
- }
- }
- }
- function _paramFieldsFromBlock (block) {
- let paramFields = [];
- if (block.local.parameter && block.local.parameter.fields) {
- // Loop all fields regardless of the field group. The default field group is `Parameter` but it could be provided by the developer.
- for (const key in block.local.parameter.fields) {
- paramFields = paramFields.concat(block.local.parameter.fields[key]);
- }
- }
- return paramFields;
- }
|