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