Update plugin chain to support JSDoc import types
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
"allowUnknownTags": true
|
||||
},
|
||||
"source": {
|
||||
"includePattern": ".+\\.js(doc)?$",
|
||||
"includePattern": ".+\\.js$",
|
||||
"excludePattern": "(^|\\/|\\\\)_",
|
||||
"include": [
|
||||
"src/ol"
|
||||
@@ -15,8 +15,10 @@
|
||||
},
|
||||
"plugins": [
|
||||
"plugins/markdown",
|
||||
"config/jsdoc/api/plugins/normalize-exports",
|
||||
"config/jsdoc/api/plugins/convert-types",
|
||||
"config/jsdoc/api/plugins/normalize-longnames",
|
||||
"config/jsdoc/api/plugins/inline-options",
|
||||
"config/jsdoc/api/plugins/inheritdoc",
|
||||
"config/jsdoc/api/plugins/events",
|
||||
"config/jsdoc/api/plugins/observable",
|
||||
"config/jsdoc/api/plugins/api"
|
||||
|
||||
@@ -95,17 +95,15 @@ exports.handlers = {
|
||||
newDoclet: function(e) {
|
||||
const doclet = e.doclet;
|
||||
if (doclet.stability) {
|
||||
modules[doclet.longname.split('~').shift()] = true;
|
||||
modules[doclet.longname.split(/[~\.]/).shift()] = true;
|
||||
api.push(doclet);
|
||||
}
|
||||
// Mark explicity defined namespaces - needed in parseComplete to keep
|
||||
// namespaces that we need as containers for api items.
|
||||
if (/.*\.jsdoc$/.test(doclet.meta.filename) && doclet.kind == 'namespace') {
|
||||
doclet.namespace_ = true;
|
||||
}
|
||||
if (doclet.kind == 'class') {
|
||||
modules[doclet.longname.split('~').shift()] = true;
|
||||
classes[doclet.longname] = doclet;
|
||||
modules[doclet.longname.split(/[~\.]/).shift()] = true;
|
||||
if (!(doclet.longname in classes)) {
|
||||
classes[doclet.longname] = doclet;
|
||||
}
|
||||
}
|
||||
if (doclet.name === doclet.longname && !doclet.memberof) {
|
||||
// Make sure anonymous default exports are documented
|
||||
doclet.setMemberof(doclet.longname);
|
||||
@@ -116,7 +114,7 @@ exports.handlers = {
|
||||
const doclets = e.doclets;
|
||||
for (let i = doclets.length - 1; i >= 0; --i) {
|
||||
const doclet = doclets[i];
|
||||
if (doclet.stability || doclet.namespace_) {
|
||||
if (doclet.stability) {
|
||||
if (doclet.kind == 'class') {
|
||||
includeAugments(doclet);
|
||||
}
|
||||
|
||||
139
config/jsdoc/api/plugins/convert-types.js
Normal file
139
config/jsdoc/api/plugins/convert-types.js
Normal file
@@ -0,0 +1,139 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const importRegEx = /(typeof )?import\("([^"]*)"\)\.([^ \.\|\}><,\)=\n]*)([ \.\|\}><,\)=\n])/g;
|
||||
const typedefRegEx = /@typedef \{[^\}]*\} ([^ \r?\n?]*)/;
|
||||
|
||||
const defaultExports = {};
|
||||
const fileNodes = {};
|
||||
|
||||
function getDefaultExportName(moduleId, parser) {
|
||||
if (!defaultExports[moduleId]) {
|
||||
if (!fileNodes[moduleId]) {
|
||||
const classDeclarations = {};
|
||||
const absolutePath = path.join(process.cwd(), 'src', moduleId + '.js');
|
||||
const file = fs.readFileSync(absolutePath, 'UTF-8');
|
||||
const node = fileNodes[moduleId] = parser.astBuilder.build(file, absolutePath);
|
||||
if (node.program && node.program.body) {
|
||||
const nodes = node.program.body;
|
||||
for (let i = 0, ii = nodes.length; i < ii; ++i) {
|
||||
const node = nodes[i];
|
||||
if (node.type === 'ClassDeclaration') {
|
||||
classDeclarations[node.id.name] = node;
|
||||
} else if (node.type === 'ExportDefaultDeclaration') {
|
||||
const classDeclaration = classDeclarations[node.declaration.name];
|
||||
if (classDeclaration) {
|
||||
defaultExports[moduleId] = classDeclaration.id.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!defaultExports[moduleId]) {
|
||||
defaultExports[moduleId] = '';
|
||||
}
|
||||
return defaultExports[moduleId];
|
||||
}
|
||||
|
||||
exports.astNodeVisitor = {
|
||||
|
||||
visitNode: function(node, e, parser, currentSourceName) {
|
||||
if (node.type === 'File') {
|
||||
const modulePath = path.relative(path.join(process.cwd(), 'src'), currentSourceName).replace(/\.js$/, '');
|
||||
fileNodes[modulePath] = node;
|
||||
const identifiers = {};
|
||||
if (node.program && node.program.body) {
|
||||
const nodes = node.program.body;
|
||||
for (let i = 0, ii = nodes.length; i < ii; ++i) {
|
||||
let node = nodes[i];
|
||||
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
||||
node = node.declaration;
|
||||
}
|
||||
if (node.type === 'ImportDeclaration') {
|
||||
node.specifiers.forEach(specifier => {
|
||||
let defaultImport = false;
|
||||
switch (specifier.type) {
|
||||
case 'ImportDefaultSpecifier':
|
||||
defaultImport = true;
|
||||
// fallthrough
|
||||
case 'ImportSpecifier':
|
||||
identifiers[specifier.local.name] = {
|
||||
defaultImport,
|
||||
value: node.source.value
|
||||
};
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
} else if (node.type === 'ClassDeclaration') {
|
||||
if (node.id && node.id.name) {
|
||||
identifiers[node.id.name] = {
|
||||
value: path.basename(currentSourceName)
|
||||
};
|
||||
}
|
||||
|
||||
// Add class inheritance information because JSDoc does not honor
|
||||
// the ES6 class's `extends` keyword
|
||||
if (node.superClass && node.leadingComments) {
|
||||
const leadingComment = node.leadingComments[node.leadingComments.length - 1];
|
||||
const lines = leadingComment.value.split(/\r?\n/);
|
||||
lines.push(lines[lines.length - 1]);
|
||||
const identifier = identifiers[node.superClass.name];
|
||||
if (identifier) {
|
||||
const absolutePath = path.resolve(path.dirname(currentSourceName), identifier.value);
|
||||
const moduleId = path.relative(path.join(process.cwd(), 'src'), absolutePath).replace(/\.js$/, '');
|
||||
const exportName = identifier.defaultImport ? getDefaultExportName(moduleId, parser) : node.superClass.name;
|
||||
lines[lines.length - 2] = ' * @extends ' + `module:${moduleId}${exportName ? '~' + exportName : ''}`;
|
||||
} else {
|
||||
lines[lines.length - 2] = ' * @extends ' + node.superClass.name;
|
||||
}
|
||||
leadingComment.value = lines.join('\n');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node.comments) {
|
||||
node.comments.forEach(comment => {
|
||||
//TODO Handle typeof, to indicate that a constructor instead of an
|
||||
// instance is needed.
|
||||
comment.value = comment.value.replace(/typeof /g, '');
|
||||
|
||||
// Convert `import("path/to/module").export` to
|
||||
// `module:path/to/module~Name`
|
||||
let importMatch;
|
||||
while ((importMatch = importRegEx.exec(comment.value))) {
|
||||
importRegEx.lastIndex = 0;
|
||||
const rel = path.resolve(path.dirname(currentSourceName), importMatch[2]);
|
||||
const importModule = path.relative(path.join(process.cwd(), 'src'), rel).replace(/\.js$/, '');
|
||||
const exportName = importMatch[3] === 'default' ? getDefaultExportName(importModule, parser) : importMatch[3];
|
||||
const replacement = `module:${importModule}${exportName ? '~' + exportName : ''}`;
|
||||
comment.value = comment.value.replace(importMatch[0], replacement + importMatch[4]);
|
||||
}
|
||||
|
||||
// Treat `@typedef`s like named exports
|
||||
const typedefMatch = comment.value.replace(/\r?\n?\s*\*\s/g, ' ').match(typedefRegEx);
|
||||
if (typedefMatch) {
|
||||
identifiers[typedefMatch[1]] = {
|
||||
value: path.basename(currentSourceName)
|
||||
};
|
||||
}
|
||||
|
||||
// Replace local types with the full `module:` path
|
||||
Object.keys(identifiers).forEach(key => {
|
||||
const regex = new RegExp(`(@fires |[\{<\|,] ?)${key}`, 'g');
|
||||
if (regex.test(comment.value)) {
|
||||
const identifier = identifiers[key];
|
||||
const absolutePath = path.resolve(path.dirname(currentSourceName), identifier.value);
|
||||
const moduleId = path.relative(path.join(process.cwd(), 'src'), absolutePath).replace(/\.js$/, '');
|
||||
const exportName = identifier.defaultImport ? getDefaultExportName(moduleId, parser) : key;
|
||||
comment.value = comment.value.replace(regex, '$1' + `module:${moduleId}${exportName ? '~' + exportName : ''}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
109
config/jsdoc/api/plugins/inheritdoc.js
Executable file
109
config/jsdoc/api/plugins/inheritdoc.js
Executable file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* This is a hack to prevent inheritDoc tags from entirely removing
|
||||
* documentation of the method that inherits the documentation.
|
||||
*
|
||||
* TODO: Remove this hack when https://github.com/jsdoc3/jsdoc/issues/53
|
||||
* is addressed.
|
||||
*/
|
||||
|
||||
|
||||
exports.defineTags = function(dictionary) {
|
||||
dictionary.defineTag('inheritDoc', {
|
||||
mustHaveValue: false,
|
||||
canHaveType: false,
|
||||
canHaveName: false,
|
||||
onTagged: function(doclet, tag) {
|
||||
doclet.inheritdoc = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const lookup = {};
|
||||
const incompleteByClass = {};
|
||||
const keepKeys = ['comment', 'meta', 'name', 'memberof', 'longname', 'augment',
|
||||
'stability'];
|
||||
|
||||
exports.handlers = {
|
||||
|
||||
newDoclet: function(e) {
|
||||
const doclet = e.doclet;
|
||||
let incompletes;
|
||||
if (!(doclet.longname in lookup)) {
|
||||
lookup[doclet.longname] = [];
|
||||
}
|
||||
lookup[doclet.longname].push(doclet);
|
||||
if (doclet.inheritdoc) {
|
||||
if (!(doclet.memberof in incompleteByClass)) {
|
||||
incompleteByClass[doclet.memberof] = [];
|
||||
}
|
||||
incompletes = incompleteByClass[doclet.memberof];
|
||||
if (incompletes.indexOf(doclet.name) == -1) {
|
||||
incompletes.push(doclet.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
parseComplete: function(e) {
|
||||
let ancestors, candidate, candidates, doclet, i, j, k, l, key;
|
||||
let incompleteDoclet, stability, incomplete, incompletes;
|
||||
const doclets = e.doclets;
|
||||
for (i = doclets.length - 1; i >= 0; --i) {
|
||||
doclet = doclets[i];
|
||||
if (doclet.augments) {
|
||||
ancestors = [].concat(doclet.augments);
|
||||
}
|
||||
incompletes = incompleteByClass[doclet.longname];
|
||||
if (ancestors && incompletes) {
|
||||
// collect ancestors from the whole hierarchy
|
||||
for (j = 0; j < ancestors.length; ++j) {
|
||||
candidates = lookup[ancestors[j]];
|
||||
if (candidates) {
|
||||
for (k = candidates.length - 1; k >= 0; --k) {
|
||||
candidate = candidates[k];
|
||||
if (candidate.augments) {
|
||||
ancestors = ancestors.concat(candidate.augments);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// walk through all inheritDoc members
|
||||
for (j = incompletes.length - 1; j >= 0; --j) {
|
||||
incomplete = incompletes[j];
|
||||
candidates = lookup[doclet.longname + '#' + incomplete];
|
||||
if (candidates) {
|
||||
// get the incomplete doclet that needs to be augmented
|
||||
for (k = candidates.length - 1; k >= 0; --k) {
|
||||
incompleteDoclet = candidates[k];
|
||||
if (incompleteDoclet.inheritdoc) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// find the documented ancestor
|
||||
for (k = ancestors.length - 1; k >= 0; --k) {
|
||||
candidates = lookup[ancestors[k] + '#' + incomplete];
|
||||
if (candidates) {
|
||||
for (l = candidates.length - 1; l >= 0; --l) {
|
||||
candidate = candidates[l];
|
||||
if (candidate && !candidate.inheritdoc) {
|
||||
stability = candidate.stability || incompleteDoclet.stability;
|
||||
if (stability) {
|
||||
incompleteDoclet.stability = stability;
|
||||
for (key in candidate) {
|
||||
if (candidate.hasOwnProperty(key) &&
|
||||
keepKeys.indexOf(key) == -1) {
|
||||
incompleteDoclet[key] = candidate[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* @filedesc
|
||||
* Expands module path type to point to default export when no name is given
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let moduleRoot;
|
||||
|
||||
function addDefaultExportPath(obj) {
|
||||
if (!Array.isArray(obj)) {
|
||||
obj = obj.names;
|
||||
}
|
||||
obj.forEach((name, index) => {
|
||||
const matches = name.match(/module\:([^>|),\.<]|)+/g);
|
||||
if (matches) {
|
||||
matches.forEach(module => {
|
||||
if (!/[~\.]/.test(module)) {
|
||||
const checkFile = path.resolve(moduleRoot, module.replace(/^module\:/, ''));
|
||||
const file = fs.readFileSync(require.resolve(checkFile), 'utf-8');
|
||||
const lines = file.split('\n');
|
||||
let hasDefaultExport = false;
|
||||
for (let i = 0, ii = lines.length; i < ii; ++i) {
|
||||
hasDefaultExport = hasDefaultExport || /^export default [^\{]/.test(lines[i]);
|
||||
const match = lines[i].match(/^export default ([A-Za-z_$][A-Za-z0-9_$]+);$/);
|
||||
if (match) {
|
||||
// Use variable name if default export is assigned to a variable.
|
||||
obj[index] = name = name.replace(module, `${module}~${match[1]}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (hasDefaultExport) {
|
||||
// Duplicate last part if default export is not assigned to a variable.
|
||||
obj[index] = name = name.replace(module, `${module}~${module.split('/').pop()}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function replaceLinks(comment) {
|
||||
const matches = comment.match(/\{@link [^\} #]+}/g);
|
||||
if (matches) {
|
||||
const modules = matches.map(m => {
|
||||
const mm = m.match(/(module:[^\}]+)}$/);
|
||||
if (mm) {
|
||||
return mm[1];
|
||||
}
|
||||
}).filter(m => !!m);
|
||||
const newModules = modules.concat();
|
||||
addDefaultExportPath(newModules);
|
||||
modules.forEach((module, i) => {
|
||||
comment = comment.replace(module, newModules[i]);
|
||||
});
|
||||
}
|
||||
return comment;
|
||||
}
|
||||
|
||||
exports.handlers = {
|
||||
|
||||
/**
|
||||
* Adds default export to module path types without name
|
||||
* @param {Object} e Event object.
|
||||
*/
|
||||
newDoclet: function(e) {
|
||||
const doclet = e.doclet;
|
||||
if (doclet.kind == 'module') {
|
||||
const levelsUp = doclet.longname.replace(/^module\:/, '').split('/');
|
||||
if (doclet.meta.filename != 'index.js') {
|
||||
levelsUp.pop();
|
||||
}
|
||||
const pathArgs = [doclet.meta.path].concat(levelsUp.map(() => '../'));
|
||||
moduleRoot = path.resolve.apply(null, pathArgs);
|
||||
} else {
|
||||
if (doclet.description) {
|
||||
doclet.description = replaceLinks(doclet.description);
|
||||
}
|
||||
if (doclet.classdesc) {
|
||||
doclet.classdesc = replaceLinks(doclet.classdesc);
|
||||
}
|
||||
|
||||
const module = doclet.longname.split('#').shift();
|
||||
if (module.indexOf('module:') == 0 && module.indexOf('.') !== -1) {
|
||||
doclet.longname = doclet.longname.replace(module, module.replace('.', '~'));
|
||||
}
|
||||
if (doclet.augments) {
|
||||
addDefaultExportPath(doclet.augments);
|
||||
}
|
||||
if (doclet.params) {
|
||||
doclet.params.forEach(p => addDefaultExportPath(p.type));
|
||||
}
|
||||
if (doclet.returns) {
|
||||
doclet.returns.forEach(r => addDefaultExportPath(r.type));
|
||||
}
|
||||
if (doclet.properties) {
|
||||
doclet.properties.forEach(p => addDefaultExportPath(p.type));
|
||||
}
|
||||
if (doclet.type) {
|
||||
addDefaultExportPath(doclet.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
21
config/jsdoc/api/plugins/normalize-longnames.js
Normal file
21
config/jsdoc/api/plugins/normalize-longnames.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @filedesc
|
||||
* Normalize module path to make no distinction between static and member at
|
||||
* the module level.
|
||||
*/
|
||||
|
||||
exports.handlers = {
|
||||
|
||||
/**
|
||||
* Adds default export to module path types without name
|
||||
* @param {Object} e Event object.
|
||||
*/
|
||||
newDoclet: function(e) {
|
||||
const doclet = e.doclet;
|
||||
const module = doclet.longname.split('#').shift();
|
||||
if (module.indexOf('module:') == 0 && module.indexOf('.') !== -1) {
|
||||
doclet.longname = doclet.longname.replace(module, module.replace('.', '~'));
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Changes @enum annotations into @typedef.
|
||||
*/
|
||||
|
||||
// types that are undefined or typedefs containing undefined
|
||||
let undefinedLikes = null;
|
||||
|
||||
/**
|
||||
* Changes the description of the param if it is required.
|
||||
* @param {Object} doclet The doclet.
|
||||
* @returns {Object} The modified doclet.
|
||||
*/
|
||||
function markRequiredIfNeeded(doclet) {
|
||||
const memberof = doclet.memberof;
|
||||
if (!memberof) {
|
||||
return doclet;
|
||||
}
|
||||
|
||||
const types = doclet.type.names;
|
||||
let isRequiredParam = true;
|
||||
|
||||
// iterate over all types that are like-undefined (see above for explanation)
|
||||
for (let idx = undefinedLikes.length - 1; idx >= 0; idx--) {
|
||||
const undefinedLike = undefinedLikes[idx];
|
||||
// … if the current types contains a type that is undefined-like,
|
||||
// it is not required.
|
||||
if (types.indexOf(undefinedLike) != -1) {
|
||||
isRequiredParam = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRequiredParam) {
|
||||
const reqSnippet = '<span class="required-option">Required.</span></p>';
|
||||
const endsWithP = /<\/p>$/i;
|
||||
let description = doclet.description;
|
||||
if (description && endsWithP.test(description)) {
|
||||
description = description.replace(endsWithP, ' ' + reqSnippet);
|
||||
} else if (doclet.description === undefined) {
|
||||
description = '<p>' + reqSnippet;
|
||||
}
|
||||
doclet.description = description;
|
||||
}
|
||||
return doclet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all doclets and finds the names of types that contain
|
||||
* undefined. Stores the names in the global variable undefinedLikes, so
|
||||
* that e.g. markRequiredIfNeeded can use these.
|
||||
* @param {Array} doclets The doclets.
|
||||
*/
|
||||
function findTypesLikeUndefined(doclets) {
|
||||
undefinedLikes = ['undefined']; // include type 'undefined' explicitly
|
||||
for (let i = doclets.length - 1; i >= 0; --i) {
|
||||
const doclet = doclets[i];
|
||||
if (doclet.kind === 'typedef') {
|
||||
const types = doclet.type.names;
|
||||
if (types.indexOf('undefined') !== -1) {
|
||||
// the typedef contains 'undefined', so it self is undefinedLike.
|
||||
undefinedLikes.push(doclet.longname);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.handlers = {
|
||||
|
||||
newDoclet: function(e) {
|
||||
const doclet = e.doclet;
|
||||
if (doclet.isEnum) {
|
||||
// We never export enums, so we document them like typedefs
|
||||
doclet.kind = 'typedef';
|
||||
delete doclet.isEnum;
|
||||
}
|
||||
},
|
||||
|
||||
parseComplete: function(e) {
|
||||
const doclets = e.doclets;
|
||||
findTypesLikeUndefined(doclets);
|
||||
for (let i = doclets.length - 1; i >= 0; --i) {
|
||||
markRequiredIfNeeded(doclets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user