mirror of
https://github.com/jsdoc/jsdoc.git
synced 2025-12-08 19:46:11 +00:00
528 lines
14 KiB
JavaScript
528 lines
14 KiB
JavaScript
/**
|
|
* Methods for manipulating symbol names in JSDoc.
|
|
*
|
|
* @alias @jsdoc/core.name
|
|
*/
|
|
const _ = require('lodash');
|
|
const escape = require('escape-string-regexp');
|
|
|
|
const hasOwnProp = Object.prototype.hasOwnProperty;
|
|
|
|
/**
|
|
* Longnames that have a special meaning in JSDoc.
|
|
*
|
|
* @enum {string}
|
|
* @static
|
|
* @memberof module:jsdoc/name
|
|
*/
|
|
exports.LONGNAMES = {
|
|
/** Longname used for doclets that do not have a longname, such as anonymous functions. */
|
|
ANONYMOUS: '<anonymous>',
|
|
/** Longname that represents global scope. */
|
|
GLOBAL: '<global>'
|
|
};
|
|
|
|
// Module namespace prefix.
|
|
exports.MODULE_NAMESPACE = 'module:';
|
|
|
|
/**
|
|
* Names and punctuation marks that identify doclet scopes.
|
|
*
|
|
* @enum {string}
|
|
* @static
|
|
* @memberof module:jsdoc/name
|
|
*/
|
|
const SCOPE = exports.SCOPE = {
|
|
NAMES: {
|
|
GLOBAL: 'global',
|
|
INNER: 'inner',
|
|
INSTANCE: 'instance',
|
|
STATIC: 'static'
|
|
},
|
|
PUNC: {
|
|
INNER: '~',
|
|
INSTANCE: '#',
|
|
STATIC: '.'
|
|
}
|
|
};
|
|
|
|
// Keys must be lowercase.
|
|
const SCOPE_TO_PUNC = exports.SCOPE_TO_PUNC = {
|
|
inner: SCOPE.PUNC.INNER,
|
|
instance: SCOPE.PUNC.INSTANCE,
|
|
static: SCOPE.PUNC.STATIC
|
|
};
|
|
|
|
exports.PUNC_TO_SCOPE = _.invert(SCOPE_TO_PUNC);
|
|
|
|
const SCOPE_PUNC = _.values(SCOPE.PUNC);
|
|
const SCOPE_PUNC_STRING = `[${SCOPE_PUNC.join()}]`;
|
|
const REGEXP_LEADING_SCOPE = new RegExp(`^(${SCOPE_PUNC_STRING})`);
|
|
const REGEXP_TRAILING_SCOPE = new RegExp(`(${SCOPE_PUNC_STRING})$`);
|
|
|
|
const DESCRIPTION = '(?:(?:[ \\t]*\\-\\s*|\\s+)(\\S[\\s\\S]*))?$';
|
|
const REGEXP_DESCRIPTION = new RegExp(DESCRIPTION);
|
|
const REGEXP_NAME_DESCRIPTION = new RegExp(`^(\\[[^\\]]+\\]|\\S+)${DESCRIPTION}`);
|
|
|
|
/**
|
|
* Check whether a name appears to represent a complete longname that is a member of the specified
|
|
* parent.
|
|
*
|
|
* @example
|
|
* exports.nameIsLongname('foo.bar', 'foo'); // true
|
|
* exports.nameIsLongname('foo.bar', 'baz'); // false
|
|
* exports.nameIsLongname('bar', 'foo'); // false
|
|
* @param {string} name - The name to check.
|
|
* @param {string} memberof - The parent of the name.
|
|
* @returns {boolean} `true` if the name represents a complete longname that is a member of the
|
|
* parent; otherwise, `false`.
|
|
*/
|
|
exports.nameIsLongname = (name, memberof) => {
|
|
const regexp = new RegExp(`^${escape(memberof)}${SCOPE_PUNC_STRING}`);
|
|
|
|
return regexp.test(name);
|
|
};
|
|
|
|
/**
|
|
* For names that identify a property of a prototype, replace the `prototype` portion of the name
|
|
* with `#`, which indicates instance-level scope. For example, `Foo.prototype.bar` becomes
|
|
* `Foo#bar`.
|
|
*
|
|
* @param {string} name - The name in which to change `prototype` to `#`.
|
|
* @returns {string} The updated name.
|
|
*/
|
|
const prototypeToPunc = exports.prototypeToPunc = name => {
|
|
// Don't mangle symbols named `prototype`.
|
|
if (name === 'prototype') {
|
|
return name;
|
|
}
|
|
|
|
return name.replace(/(?:^|\.)prototype\.?/g, SCOPE.PUNC.INSTANCE);
|
|
};
|
|
|
|
/**
|
|
* Check whether a name begins with a character that identifies a scope.
|
|
*
|
|
* @param {string} name - The name to check.
|
|
* @returns {boolean} `true` if the name begins with a scope character; otherwise, `false`.
|
|
*/
|
|
exports.hasLeadingScope = name => REGEXP_LEADING_SCOPE.test(name);
|
|
|
|
/**
|
|
* Check whether a name ends with a character that identifies a scope.
|
|
*
|
|
* @param {string} name - The name to check.
|
|
* @returns {boolean} `true` if the name ends with a scope character; otherwise, `false`.
|
|
*/
|
|
exports.hasTrailingScope = name => REGEXP_TRAILING_SCOPE.test(name);
|
|
|
|
/**
|
|
* Get a symbol's basename, which is the first part of its full name before any punctuation (other
|
|
* than an underscore). For example, all of the following names have the basename `Foo`:
|
|
*
|
|
* + `Foo`
|
|
* + `Foo.bar`
|
|
* + `Foo.prototype.bar`
|
|
* + `Foo#bar`
|
|
*
|
|
* @param {?string} [name] - The symbol's full name.
|
|
* @returns {?string} The symbol's basename.
|
|
*/
|
|
exports.getBasename = name => {
|
|
if (!name) {
|
|
return null;
|
|
}
|
|
|
|
return name.replace(/^([$a-z_][$a-z_0-9]*).*?$/i, '$1');
|
|
};
|
|
|
|
// TODO: docs
|
|
exports.stripNamespace = longname => longname.replace(/^[a-zA-Z]+:/, '');
|
|
|
|
// TODO: docs
|
|
function slice(longname, sliceChars, forcedMemberof) {
|
|
let i;
|
|
let memberof = '';
|
|
let name = '';
|
|
let parts;
|
|
let partsRegExp;
|
|
let scopePunc = '';
|
|
let token;
|
|
const tokens = [];
|
|
let variation;
|
|
|
|
sliceChars = sliceChars || SCOPE_PUNC;
|
|
|
|
// Quoted strings in a longname are atomic, so we convert them to tokens:
|
|
// foo["bar"] => foo.@{1}@
|
|
// Foo.prototype["bar"] => Foo#@{1}
|
|
longname = longname.replace(/(prototype|#)?(\[?["'].+?["']\]?)/g, ($, p1, p2) => {
|
|
let punc = '';
|
|
|
|
// Is there a leading bracket?
|
|
if ( /^\[/.test(p2) ) {
|
|
// Is it a static or instance member?
|
|
punc = p1 ? SCOPE.PUNC.INSTANCE : SCOPE.PUNC.STATIC;
|
|
p2 = p2.replace(/^\[/g, '')
|
|
.replace(/\]$/g, '');
|
|
}
|
|
|
|
token = `@{${tokens.length}}@`;
|
|
tokens.push(p2);
|
|
|
|
return punc + token;
|
|
});
|
|
|
|
longname = prototypeToPunc(longname);
|
|
|
|
if (typeof forcedMemberof !== 'undefined') {
|
|
partsRegExp = new RegExp(`^(.*?)([${sliceChars.join()}]?)$`);
|
|
name = longname.substr(forcedMemberof.length);
|
|
parts = forcedMemberof.match(partsRegExp);
|
|
|
|
if (parts[1]) {
|
|
memberof = parts[1] || forcedMemberof;
|
|
}
|
|
if (parts[2]) {
|
|
scopePunc = parts[2];
|
|
}
|
|
}
|
|
else if (longname) {
|
|
parts = longname.match(new RegExp(`^(:?(.+)([${sliceChars.join()}]))?(.+?)$`)) || [];
|
|
name = parts.pop() || '';
|
|
scopePunc = parts.pop() || '';
|
|
memberof = parts.pop() || '';
|
|
}
|
|
|
|
// Like `@name foo.bar(2)`.
|
|
if (/(.+)\(([^)]+)\)$/.test(name)) {
|
|
name = RegExp.$1;
|
|
variation = RegExp.$2;
|
|
}
|
|
|
|
// Restore quoted strings.
|
|
i = tokens.length;
|
|
while (i--) {
|
|
longname = longname.replace(`@{${i}}@`, tokens[i]);
|
|
memberof = memberof.replace(`@{${i}}@`, tokens[i]);
|
|
scopePunc = scopePunc.replace(`@{${i}}@`, tokens[i]);
|
|
name = name.replace(`@{${i}}@`, tokens[i]);
|
|
}
|
|
|
|
return {
|
|
longname: longname,
|
|
memberof: memberof,
|
|
scope: scopePunc,
|
|
name: name,
|
|
variation: variation
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Given a longname like `a.b#c(2)`, split it into the following parts:
|
|
*
|
|
* + `longname`
|
|
* + `memberof`
|
|
* + `scope`
|
|
* + `name`
|
|
* + `variation`
|
|
*
|
|
* @param {string} longname - The longname to divide into parts.
|
|
* @param {string} forcedMemberof
|
|
* @returns {object} Representing the properties of the given name.
|
|
*/
|
|
exports.toParts = (longname, forcedMemberof) => slice(
|
|
longname, null, forcedMemberof
|
|
);
|
|
|
|
// TODO: docs
|
|
/**
|
|
* @param {string} longname The full longname of the symbol.
|
|
* @param {string} ns The namespace to be applied.
|
|
* @returns {string} The longname with the namespace applied.
|
|
*/
|
|
exports.applyNamespace = (longname, ns) => {
|
|
const nameParts = slice(longname);
|
|
const name = nameParts.name;
|
|
|
|
longname = nameParts.longname;
|
|
|
|
if (!/^[a-zA-Z]+?:.+$/i.test(name)) {
|
|
longname = longname.replace(new RegExp(`${escape(name)}$`), `${ns}:${name}`);
|
|
}
|
|
|
|
return longname;
|
|
};
|
|
|
|
/**
|
|
* Check whether a parent longname is an ancestor of a child longname.
|
|
*
|
|
* @param {string} parent - The parent longname.
|
|
* @param {string} child - The child longname.
|
|
* @return {boolean} `true` if the parent is an ancestor of the child; otherwise, `false`.
|
|
*/
|
|
exports.hasAncestor = (parent, child) => {
|
|
let hasAncestor = false;
|
|
let memberof = child;
|
|
|
|
if (!parent || !child) {
|
|
return hasAncestor;
|
|
}
|
|
|
|
// Fast path for obvious non-ancestors.
|
|
if (child.indexOf(parent) !== 0) {
|
|
return hasAncestor;
|
|
}
|
|
|
|
do {
|
|
memberof = slice(memberof).memberof;
|
|
|
|
if (memberof === parent) {
|
|
hasAncestor = true;
|
|
}
|
|
} while (!hasAncestor && memberof);
|
|
|
|
return hasAncestor;
|
|
};
|
|
|
|
// TODO: docs
|
|
const fromParts = exports.fromParts = ({memberof, scope, name, variation}) => [
|
|
(memberof || ''),
|
|
(scope || ''),
|
|
(name || ''),
|
|
(variation ? `(${variation})` : '')
|
|
].join('');
|
|
|
|
// TODO: docs
|
|
exports.stripVariation = name => {
|
|
const parts = slice(name);
|
|
|
|
parts.variation = '';
|
|
|
|
return fromParts(parts);
|
|
};
|
|
|
|
function splitLongname(longname, options) {
|
|
const chunks = [];
|
|
let currentNameInfo;
|
|
const nameInfo = {};
|
|
let previousName = longname;
|
|
const splitters = SCOPE_PUNC.concat('/');
|
|
|
|
options = _.defaults(options || {}, {
|
|
includeVariation: true
|
|
});
|
|
|
|
do {
|
|
if (!options.includeVariation) {
|
|
previousName = exports.stripVariation(previousName);
|
|
}
|
|
currentNameInfo = nameInfo[previousName] = slice(previousName, splitters);
|
|
previousName = currentNameInfo.memberof;
|
|
chunks.push(currentNameInfo.scope + currentNameInfo.name);
|
|
} while (previousName);
|
|
|
|
return {
|
|
chunks: chunks.reverse(),
|
|
nameInfo: nameInfo
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert an array of doclet longnames into a tree structure, optionally attaching doclets to the
|
|
* tree.
|
|
*
|
|
* Each level of the tree is an object with the following properties:
|
|
*
|
|
* + `longname {string}`: The longname.
|
|
* + `memberof {string?}`: The memberof.
|
|
* + `scope {string?}`: The longname's scope, represented as a punctuation mark (for example, `#`
|
|
* for instance and `.` for static).
|
|
* + `name {string}`: The short name.
|
|
* + `doclet {Object?}`: The doclet associated with the longname, or `null` if the doclet was not
|
|
* provided.
|
|
* + `children {Object?}`: The children of the current longname. Not present if there are no
|
|
* children.
|
|
*
|
|
* For example, suppose you have the following array of doclet longnames:
|
|
*
|
|
* ```js
|
|
* [
|
|
* "module:a",
|
|
* "module:a/b",
|
|
* "myNamespace",
|
|
* "myNamespace.Foo",
|
|
* "myNamespace.Foo#bar"
|
|
* ]
|
|
* ```
|
|
*
|
|
* This method converts these longnames to the following tree:
|
|
*
|
|
* ```js
|
|
* {
|
|
* "module:a": {
|
|
* "longname": "module:a",
|
|
* "memberof": "",
|
|
* "scope": "",
|
|
* "name": "module:a",
|
|
* "doclet": null,
|
|
* "children": {
|
|
* "/b": {
|
|
* "longname": "module:a/b",
|
|
* "memberof": "module:a",
|
|
* "scope": "/",
|
|
* "name": "b",
|
|
* "doclet": null
|
|
* }
|
|
* }
|
|
* },
|
|
* "myNamespace": {
|
|
* "longname": "myNamespace",
|
|
* "memberof": "",
|
|
* "scope": "",
|
|
* "name": "myNamespace",
|
|
* "doclet": null,
|
|
* "children": {
|
|
* ".Foo": {
|
|
* "longname": "myNamespace.Foo",
|
|
* "memberof": "myNamespace",
|
|
* "scope": ".",
|
|
* "name": "Foo",
|
|
* "doclet": null,
|
|
* "children": {
|
|
* "#bar": {
|
|
* "longname": "myNamespace.Foo#bar",
|
|
* "memberof": "myNamespace.Foo",
|
|
* "scope": "#",
|
|
* "name": "bar",
|
|
* "doclet": null
|
|
* }
|
|
* }
|
|
* }
|
|
* }
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* @param {Array<string>} longnames - The longnames to convert into a tree.
|
|
* @param {Object<string, module:jsdoc/doclet.Doclet>} doclets - The doclets to attach to a tree.
|
|
* Each property should be the longname of a doclet, and each value should be the doclet for that
|
|
* longname.
|
|
* @return {Object} A tree with information about each longname in the format shown above.
|
|
*/
|
|
exports.longnamesToTree = (longnames, doclets) => {
|
|
const splitOptions = { includeVariation: false };
|
|
const tree = {};
|
|
|
|
longnames.forEach(longname => {
|
|
let currentLongname = '';
|
|
let currentParent = tree;
|
|
let nameInfo;
|
|
let processed;
|
|
|
|
// Don't try to add empty longnames to the tree.
|
|
if (!longname) {
|
|
return;
|
|
}
|
|
|
|
processed = splitLongname(longname, splitOptions);
|
|
nameInfo = processed.nameInfo;
|
|
|
|
processed.chunks.forEach(chunk => {
|
|
currentLongname += chunk;
|
|
|
|
if (currentParent !== tree) {
|
|
currentParent.children = currentParent.children || {};
|
|
currentParent = currentParent.children;
|
|
}
|
|
|
|
if (!hasOwnProp.call(currentParent, chunk)) {
|
|
currentParent[chunk] = nameInfo[currentLongname];
|
|
}
|
|
|
|
if (currentParent[chunk]) {
|
|
currentParent[chunk].doclet = doclets ? doclets[currentLongname] : null;
|
|
currentParent = currentParent[chunk];
|
|
}
|
|
});
|
|
});
|
|
|
|
return tree;
|
|
};
|
|
|
|
/**
|
|
* Split a string that starts with a name and ends with a description into its parts. Allows the
|
|
* default value (if present) to contain brackets. Returns `null` if the name contains mismatched
|
|
* brackets.
|
|
*
|
|
* @param {string} nameDesc
|
|
* @returns {?Object} Hash with "name" and "description" properties.
|
|
*/
|
|
function splitNameMatchingBrackets(nameDesc) {
|
|
const buffer = [];
|
|
let c;
|
|
let stack = 0;
|
|
let stringEnd = null;
|
|
|
|
for (var i = 0; i < nameDesc.length; ++i) {
|
|
c = nameDesc[i];
|
|
buffer.push(c);
|
|
|
|
if (stringEnd) {
|
|
if (c === '\\' && i + 1 < nameDesc.length) {
|
|
buffer.push(nameDesc[++i]);
|
|
} else if (c === stringEnd) {
|
|
stringEnd = null;
|
|
}
|
|
} else if (c === '"' || c === "'") {
|
|
stringEnd = c;
|
|
} else if (c === '[') {
|
|
++stack;
|
|
} else if (c === ']') {
|
|
if (--stack === 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (stack || stringEnd) {
|
|
return null;
|
|
}
|
|
|
|
nameDesc.substr(i).match(REGEXP_DESCRIPTION);
|
|
|
|
return {
|
|
name: buffer.join(''),
|
|
description: RegExp.$1
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* Split a string that starts with a name and ends with a description into separate parts.
|
|
* @param {string} str - The string that contains the name and description.
|
|
* @returns {object} An object with `name` and `description` properties.
|
|
*/
|
|
exports.splitNameAndDescription = str => {
|
|
// Like: `name`, `[name]`, `name text`, `[name] text`, `name - text`, or `[name] - text`.
|
|
// To ensure that we don't get confused by leading dashes in Markdown list items, the hyphen
|
|
// must be on the same line as the name.
|
|
|
|
// Optional values get special treatment,
|
|
let result = null;
|
|
|
|
if (str[0] === '[') {
|
|
result = splitNameMatchingBrackets(str);
|
|
if (result !== null) {
|
|
return result;
|
|
}
|
|
}
|
|
|
|
str.match(REGEXP_NAME_DESCRIPTION);
|
|
|
|
return {
|
|
name: RegExp.$1,
|
|
description: RegExp.$2
|
|
};
|
|
};
|