Edit File: doclet.js
/** * @module jsdoc/doclet */ const _ = require('underscore'); const jsdoc = { env: require('jsdoc/env'), name: require('jsdoc/name'), src: { astnode: require('jsdoc/src/astnode'), Syntax: require('jsdoc/src/syntax').Syntax }, tag: { Tag: require('jsdoc/tag').Tag, dictionary: require('jsdoc/tag/dictionary') }, util: { doop: require('jsdoc/util/doop') } }; const path = require('jsdoc/path'); const Syntax = jsdoc.src.Syntax; const util = require('util'); function applyTag(doclet, {title, value}) { if (title === 'name') { doclet.name = value; } if (title === 'kind') { doclet.kind = value; } if (title === 'description') { doclet.description = value; } } function fakeMeta(node) { return { type: node ? node.type : null, node: node }; } // use the meta info about the source code to guess what the doclet kind should be // TODO: set this elsewhere (maybe jsdoc/src/astnode.getInfo) function codeToKind(code) { const isFunction = jsdoc.src.astnode.isFunction; let kind = 'member'; const node = code.node; if ( isFunction(code.type) && code.type !== Syntax.MethodDefinition ) { kind = 'function'; } else if (code.type === Syntax.MethodDefinition) { if (code.node.kind === 'constructor') { kind = 'class'; } else if (code.node.kind !== 'get' && code.node.kind !== 'set') { kind = 'function'; } } else if (code.type === Syntax.ClassDeclaration || code.type === Syntax.ClassExpression) { kind = 'class'; } else if (code.type === Syntax.ExportAllDeclaration) { // this value will often be an Identifier for a variable, which isn't very useful kind = codeToKind(fakeMeta(node.source)); } else if (code.type === Syntax.ExportDefaultDeclaration || code.type === Syntax.ExportNamedDeclaration) { kind = codeToKind(fakeMeta(node.declaration)); } else if (code.type === Syntax.ExportSpecifier) { // this value will often be an Identifier for a variable, which isn't very useful kind = codeToKind(fakeMeta(node.local)); } else if ( code.node && code.node.parent && isFunction(code.node.parent) ) { kind = 'param'; } return kind; } function unwrap(docletSrc) { if (!docletSrc) { return ''; } // note: keep trailing whitespace for @examples // extra opening/closing stars are ignored // left margin is considered a star and a space // use the /m flag on regex to avoid having to guess what this platform's newline is docletSrc = // remove opening slash+stars docletSrc.replace(/^\/\*\*+/, '') // replace closing star slash with end-marker .replace(/\**\*\/$/, '\\Z') // remove left margin like: spaces+star or spaces+end-marker .replace(/^\s*(\* ?|\\Z)/gm, '') // remove end-marker .replace(/\s*\\Z$/g, ''); return docletSrc; } /** * Convert the raw source of the doclet comment into an array of pseudo-Tag objects. * @private */ function toTags(docletSrc) { let parsedTag; const tagData = []; let tagText; let tagTitle; // split out the basic tags, keep surrounding whitespace // like: @tagTitle tagBody docletSrc // replace splitter ats with an arbitrary sequence .replace(/^(\s*)@(\S)/gm, '$1\\@$2') // then split on that arbitrary sequence .split('\\@') .forEach($ => { if ($) { parsedTag = $.match(/^(\S+)(?:\s+(\S[\s\S]*))?/); if (parsedTag) { tagTitle = parsedTag[1]; tagText = parsedTag[2]; if (tagTitle) { tagData.push({ title: tagTitle, text: tagText }); } } } }); return tagData; } function fixDescription(docletSrc, {code}) { let isClass; if (!/^\s*@/.test(docletSrc) && docletSrc.replace(/\s/g, '').length) { isClass = code && (code.type === Syntax.ClassDeclaration || code.type === Syntax.ClassExpression); docletSrc = `${isClass ? '@classdesc' : '@description'} ${docletSrc}`; } return docletSrc; } /** * Replace the existing tag dictionary with a new tag dictionary. * * Used for testing only. * * @private * @param {module:jsdoc/tag/dictionary.Dictionary} dict - The new tag dictionary. */ exports._replaceDictionary = function _replaceDictionary(dict) { jsdoc.tag.dictionary = dict; require('jsdoc/tag')._replaceDictionary(dict); require('jsdoc/util/templateHelper')._replaceDictionary(dict); }; function removeGlobal(longname) { const globalRegexp = new RegExp(`^${jsdoc.name.LONGNAMES.GLOBAL}\\.?`); return longname.replace(globalRegexp, ''); } /** * Get the full path to the source file that is associated with a doclet. * * @private * @param {module:jsdoc/doclet.Doclet} The doclet to check for a filepath. * @return {string} The path to the doclet's source file, or an empty string if the path is not * available. */ function getFilepath(doclet) { if (!doclet || !doclet.meta || !doclet.meta.filename) { return ''; } return path.join(doclet.meta.path || '', doclet.meta.filename); } function dooper(source, target, properties) { properties.forEach(property => { switch (typeof source[property]) { case 'function': // do nothing break; case 'object': target[property] = jsdoc.util.doop(source[property]); break; default: target[property] = source[property]; } }); } /** * Copy all but a list of excluded properties from one of two doclets onto a target doclet. Prefers * the primary doclet over the secondary doclet. * * @private * @param {module:jsdoc/doclet.Doclet} primary - The primary doclet. * @param {module:jsdoc/doclet.Doclet} secondary - The secondary doclet. * @param {module:jsdoc/doclet.Doclet} target - The doclet to which properties will be copied. * @param {Array.<string>} exclude - The names of properties to exclude from copying. */ function copyMostProperties(primary, secondary, target, exclude) { const primaryProperties = _.difference(Object.getOwnPropertyNames(primary), exclude); const secondaryProperties = _.difference(Object.getOwnPropertyNames(secondary), exclude.concat(primaryProperties)); dooper(primary, target, primaryProperties); dooper(secondary, target, secondaryProperties); } /** * Copy specific properties from one of two doclets onto a target doclet, as long as the property * has a non-falsy value and a length greater than 0. Prefers the primary doclet over the secondary * doclet. * * @private * @param {module:jsdoc/doclet.Doclet} primary - The primary doclet. * @param {module:jsdoc/doclet.Doclet} secondary - The secondary doclet. * @param {module:jsdoc/doclet.Doclet} target - The doclet to which properties will be copied. * @param {Array.<string>} include - The names of properties to copy. */ function copySpecificProperties(primary, secondary, target, include) { include.forEach(property => { if ({}.hasOwnProperty.call(primary, property) && primary[property] && primary[property].length) { target[property] = jsdoc.util.doop(primary[property]); } else if ({}.hasOwnProperty.call(secondary, property) && secondary[property] && secondary[property].length) { target[property] = jsdoc.util.doop(secondary[property]); } }); } /** * Represents a single JSDoc comment. * * @alias module:jsdoc/doclet.Doclet */ class Doclet { /** * Create a doclet. * * @param {string} docletSrc - The raw source code of the jsdoc comment. * @param {object=} meta - Properties describing the code related to this comment. */ constructor(docletSrc, meta = {}) { let newTags = []; /** The original text of the comment from the source code. */ this.comment = docletSrc; this.setMeta(meta); docletSrc = unwrap(docletSrc); docletSrc = fixDescription(docletSrc, meta); newTags = toTags.call(this, docletSrc); for (let i = 0, l = newTags.length; i < l; i++) { this.addTag(newTags[i].title, newTags[i].text); } this.postProcess(); } /** Called once after all tags have been added. */ postProcess() { let i; let l; if (!this.preserveName) { jsdoc.name.resolve(this); } if (this.name && !this.longname) { this.setLongname(this.name); } if (this.memberof === '') { delete this.memberof; } if (!this.kind && this.meta && this.meta.code) { this.addTag( 'kind', codeToKind(this.meta.code) ); } if (this.variation && this.longname && !/\)$/.test(this.longname) ) { this.longname += `(${this.variation})`; } // add in any missing param names if (this.params && this.meta && this.meta.code && this.meta.code.paramnames) { for (i = 0, l = this.params.length; i < l; i++) { if (!this.params[i].name) { this.params[i].name = this.meta.code.paramnames[i] || ''; } } } } /** * Add a tag to the doclet. * * @param {string} title - The title of the tag being added. * @param {string} [text] - The text of the tag being added. */ addTag(title, text) { const tagDef = jsdoc.tag.dictionary.lookUp(title); const newTag = new jsdoc.tag.Tag(title, text, this.meta); if (tagDef && tagDef.onTagged) { tagDef.onTagged(this, newTag); } if (!tagDef) { this.tags = this.tags || []; this.tags.push(newTag); } applyTag(this, newTag); } /** * Set the doclet's `memberof` property. * * @param {string} sid - The longname of the doclet's parent symbol. */ setMemberof(sid) { /** * The longname of the symbol that contains this one, if any. * @type {string} */ this.memberof = removeGlobal(sid) .replace(/\.prototype/g, jsdoc.name.SCOPE.PUNC.INSTANCE); } /** * Set the doclet's `longname` property. * * @param {string} name - The longname for the doclet. */ setLongname(name) { /** * The fully resolved symbol name. * @type {string} */ this.longname = removeGlobal(name); if (jsdoc.tag.dictionary.isNamespace(this.kind)) { this.longname = jsdoc.name.applyNamespace(this.longname, this.kind); } } /** * Set the doclet's `scope` property. Must correspond to a scope name that is defined in * {@link module:jsdoc/name.SCOPE.NAMES}. * * @param {module:jsdoc/name.SCOPE.NAMES} scope - The scope for the doclet relative to the * symbol's parent. * @throws {Error} If the scope name is not recognized. */ setScope(scope) { let errorMessage; let filepath; const scopeNames = _.values(jsdoc.name.SCOPE.NAMES); if (!scopeNames.includes(scope)) { filepath = getFilepath(this); errorMessage = util.format('The scope name "%s" is not recognized. Use one of the ' + 'following values: %j', scope, scopeNames); if (filepath) { errorMessage += util.format(' (Source file: %s)', filepath); } throw new Error(errorMessage); } this.scope = scope; } /** * Add a symbol to this doclet's `borrowed` array. * * @param {string} source - The longname of the symbol that is the source. * @param {string} target - The name the symbol is being assigned to. */ borrow(source, target) { const about = { from: source }; if (target) { about.as = target; } if (!this.borrowed) { /** * A list of symbols that are borrowed by this one, if any. * @type {Array.<string>} */ this.borrowed = []; } this.borrowed.push(about); } mix(source) { /** * A list of symbols that are mixed into this one, if any. * @type Array.<string> */ this.mixes = this.mixes || []; this.mixes.push(source); } /** * Add a symbol to the doclet's `augments` array. * * @param {string} base - The longname of the base symbol. */ augment(base) { /** * A list of symbols that are augmented by this one, if any. * @type Array.<string> */ this.augments = this.augments || []; this.augments.push(base); } /** * Set the `meta` property of this doclet. * * @param {object} meta */ setMeta(meta) { let pathname; /** * Information about the source code associated with this doclet. * @namespace */ this.meta = this.meta || {}; if (meta.range) { /** * The positions of the first and last characters of the code associated with this doclet. * @type Array.<number> */ this.meta.range = meta.range.slice(0); } if (meta.lineno) { /** * The name of the file containing the code associated with this doclet. * @type string */ this.meta.filename = path.basename(meta.filename); /** * The line number of the code associated with this doclet. * @type number */ this.meta.lineno = meta.lineno; /** * The column number of the code associated with this doclet. * @type number */ this.meta.columnno = meta.columnno; pathname = path.dirname(meta.filename); if (pathname && pathname !== '.') { this.meta.path = pathname; } } /** * Information about the code symbol. * @namespace */ this.meta.code = this.meta.code || {}; if (meta.id) { this.meta.code.id = meta.id; } if (meta.code) { if (meta.code.name) { /** * The name of the symbol in the source code. * @type {string} */ this.meta.code.name = meta.code.name; } if (meta.code.type) { /** * The type of the symbol in the source code. * @type {string} */ this.meta.code.type = meta.code.type; } if (meta.code.node) { Object.defineProperty(this.meta.code, 'node', { value: meta.code.node, enumerable: false }); } if (meta.code.funcscope) { this.meta.code.funcscope = meta.code.funcscope; } if (typeof meta.code.value !== 'undefined') { /** * The value of the symbol in the source code. * @type {*} */ this.meta.code.value = meta.code.value; } if (meta.code.paramnames) { this.meta.code.paramnames = meta.code.paramnames.slice(0); } } } } exports.Doclet = Doclet; /** * Combine two doclets into a new doclet. * * @param {module:jsdoc/doclet.Doclet} primary - The doclet whose properties will be used. * @param {module:jsdoc/doclet.Doclet} secondary - The doclet to use as a fallback for properties * that the primary doclet does not have. * @returns {module:jsdoc/doclet.Doclet} A new doclet that combines the primary and secondary * doclets. */ exports.combine = (primary, secondary) => { const copyMostPropertiesExclude = [ 'params', 'properties', 'undocumented' ]; const copySpecificPropertiesInclude = [ 'params', 'properties' ]; const target = new Doclet(''); // First, copy most properties to the target doclet. copyMostProperties(primary, secondary, target, copyMostPropertiesExclude); // Then copy a few specific properties to the target doclet, as long as they're not falsy and // have a length greater than 0. copySpecificProperties(primary, secondary, target, copySpecificPropertiesInclude); return target; };
Back to File Manager