123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562 |
- /**
- * @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;
- };
|