API documentation generator for JavaScript that parses source code and JSDoc comments to produce HTML documentation
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
JSDoc's plugin system provides extensive customization through event handlers, custom JSDoc tags, and AST node visitors. It includes built-in plugins for common tasks and supports custom plugin development.
Functions for loading and installing plugins into the JSDoc parser.
/**
* Install an array of plugins into a parser instance
* @param plugins - Array of plugin module paths
* @param parser - Parser instance to install plugins into
*/
function installPlugins(plugins: string[], parser: Parser): void;Standard interface that all JSDoc plugins must implement.
interface Plugin {
/**
* Event handlers for parser events
* Maps event names to handler functions
*/
handlers?: {
[eventName: string]: (e: any) => void;
};
/**
* Define custom JSDoc tags
* @param dictionary - Tag dictionary to add definitions to
*/
defineTags?(dictionary: TagDictionary): void;
/**
* AST node visitor function
* Called for each AST node during parsing
*/
astNodeVisitor?: (
node: ASTNode,
e: any,
parser: Parser,
currentSourceName: string
) => void;
}Processes Markdown syntax in JSDoc comments and converts to HTML.
// Plugin: plugins/markdown.js
const plugin = {
handlers: {
newDoclet: function(e) {
// Process Markdown in description
if (e.doclet.description) {
e.doclet.description = markdown.parse(e.doclet.description);
}
}
}
};Usage in configuration:
{
"plugins": ["plugins/markdown"]
}Converts certain comment types to JSDoc comments.
// Plugin: plugins/commentConvert.js
const plugin = {
handlers: {
beforeParse: function(e) {
// Convert // comments to /** */ format in certain cases
e.source = convertComments(e.source);
}
}
};Escapes HTML content in JSDoc comments to prevent XSS.
// Plugin: plugins/escapeHtml.js
const plugin = {
handlers: {
newDoclet: function(e) {
if (e.doclet.description) {
e.doclet.description = escapeHtml(e.doclet.description);
}
}
}
};Helps document function overloads by grouping related doclets.
// Plugin: plugins/overloadHelper.js
const plugin = {
handlers: {
parseComplete: function(e) {
// Group overloaded functions
groupOverloads(e.doclets);
}
}
};Adds source code information to doclets.
// Plugin: plugins/sourcetag.js
const plugin = {
defineTags: function(dictionary) {
dictionary.defineTag('source', {
onTagged: function(doclet, tag) {
doclet.source = tag.value;
}
});
}
};Creates summary descriptions from the first sentence of descriptions.
// Plugin: plugins/summarize.js
const plugin = {
handlers: {
newDoclet: function(e) {
if (e.doclet.description && !e.doclet.summary) {
e.doclet.summary = extractFirstSentence(e.doclet.description);
}
}
}
};Debug plugin that logs all parser events to console.
// Plugin: plugins/eventDumper.js
const plugin = {
handlers: {
parseBegin: (e) => console.log('parseBegin:', e),
fileBegin: (e) => console.log('fileBegin:', e),
jsdocCommentFound: (e) => console.log('jsdocCommentFound:', e),
newDoclet: (e) => console.log('newDoclet:', e),
parseComplete: (e) => console.log('parseComplete:', e)
}
};Escapes HTML content in JSDoc comments to prevent XSS attacks.
// Plugin: plugins/escapeHtml.js
const plugin = {
handlers: {
newDoclet: function(e) {
if (e.doclet.description) {
e.doclet.description = escapeHtml(e.doclet.description);
}
if (e.doclet.summary) {
e.doclet.summary = escapeHtml(e.doclet.summary);
}
}
}
};Processes only comment content, filtering out code elements.
// Plugin: plugins/commentsOnly.js
const plugin = {
handlers: {
beforeParse: function(e) {
// Filter source to include only comments
e.source = extractCommentsOnly(e.source);
}
}
};Plugins that enhance template functionality:
Rails-style template syntax support for JSDoc templates.
// Plugin: plugins/railsTemplate.js
const plugin = {
handlers: {
parseBegin: function(e) {
// Configure Rails-style template syntax
configureRailsTemplates();
}
}
};Underscore.js template integration for JSDoc.
// Plugin: plugins/underscore.js
const plugin = {
handlers: {
parseBegin: function(e) {
// Set up Underscore.js template engine
configureUnderscoreTemplates();
}
}
};Partial template support for modular template organization.
// Plugin: plugins/partial.js
const plugin = {
handlers: {
processingComplete: function(e) {
// Process partial templates
processPartialTemplates(e.doclets);
}
}
};Plugins for text manipulation and formatting:
Converts specific text elements to uppercase for emphasis.
// Plugin: plugins/shout.js
const plugin = {
handlers: {
newDoclet: function(e) {
if (e.doclet.description) {
e.doclet.description = processShoutText(e.doclet.description);
}
}
}
};// my-custom-plugin.js
exports.handlers = {
newDoclet: function(e) {
if (e.doclet.kind === 'function') {
// Custom processing for functions
e.doclet.customProperty = 'processed';
}
}
};
exports.defineTags = function(dictionary) {
dictionary.defineTag('custom', {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.customTag = tag.value;
}
});
};// event-logger-plugin.js
const fs = require('fs');
exports.handlers = {
parseBegin: function(e) {
console.log('Starting parse of:', e.sourcefiles.length, 'files');
},
newDoclet: function(e) {
if (e.doclet.kind === 'function' && !e.doclet.description) {
console.warn(`Undocumented function: ${e.doclet.longname}`);
}
},
parseComplete: function(e) {
const stats = {
total: e.doclets.length,
documented: e.doclets.filter(d => !d.undocumented).length,
functions: e.doclets.filter(d => d.kind === 'function').length,
classes: e.doclets.filter(d => d.kind === 'class').length
};
fs.writeFileSync('doc-stats.json', JSON.stringify(stats, null, 2));
}
};// version-tag-plugin.js
exports.defineTags = function(dictionary) {
dictionary.defineTag('version', {
mustHaveValue: true,
canHaveType: false,
canHaveName: false,
onTagged: function(doclet, tag) {
doclet.version = tag.value;
}
});
dictionary.defineTag('since', {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.since = tag.value;
}
});
};// ast-visitor-plugin.js
exports.astNodeVisitor = function(node, e, parser, currentSourceName) {
// Process specific node types
if (node.type === 'CallExpression') {
// Track function calls
if (node.callee && node.callee.name === 'deprecated') {
// Mark enclosing function as deprecated
const doclet = parser._getDocletById(node.parent.nodeId);
if (doclet) {
doclet.deprecated = true;
}
}
}
if (node.type === 'ClassDeclaration') {
// Process class declarations
console.log(`Found class: ${node.id ? node.id.name : 'anonymous'}`);
}
};// configurable-plugin.js
let pluginConfig = {
logLevel: 'info',
outputFile: 'plugin-output.json',
processPrivate: false
};
exports.handlers = {
parseBegin: function(e) {
// Load configuration from JSDoc config
const jsdocConfig = require('jsdoc/env').conf;
if (jsdocConfig.plugins && jsdocConfig.plugins.configurablePlugin) {
pluginConfig = { ...pluginConfig, ...jsdocConfig.plugins.configurablePlugin };
}
},
newDoclet: function(e) {
if (!pluginConfig.processPrivate && e.doclet.access === 'private') {
return;
}
if (pluginConfig.logLevel === 'debug') {
console.log('Processing doclet:', e.doclet.longname);
}
}
};{
"plugins": [
"plugins/markdown",
"plugins/overloadHelper",
"./custom-plugins/my-plugin"
],
"custom-plugin-config": {
"logLevel": "debug",
"outputFile": "custom-output.json"
}
}Plugins are loaded in the order specified in the configuration:
// Parser lifecycle events
interface ParserEvents {
parseBegin: { sourcefiles: string[] };
fileBegin: { filename: string };
beforeParse: { filename: string; source: string };
jsdocCommentFound: {
comment: string;
lineno: number;
filename: string;
};
symbolFound: any;
newDoclet: { doclet: Doclet };
fileComplete: { filename: string };
parseComplete: {
sourcefiles: string[];
doclets: Doclet[];
};
processingComplete: { doclets: Doclet[] };
}// Comprehensive event handling
exports.handlers = {
parseBegin: function(e) {
this.startTime = Date.now();
console.log(`Starting parse of ${e.sourcefiles.length} files`);
},
fileBegin: function(e) {
console.log(`Processing file: ${e.filename}`);
},
jsdocCommentFound: function(e) {
// Validate comment format
if (!e.comment.includes('@')) {
console.warn(`Minimal JSDoc at ${e.filename}:${e.lineno}`);
}
},
newDoclet: function(e) {
// Enhance doclets
if (e.doclet.kind === 'function') {
e.doclet.isFunction = true;
}
},
parseComplete: function(e) {
const duration = Date.now() - this.startTime;
console.log(`Parse complete in ${duration}ms: ${e.doclets.length} doclets`);
}
};const plugins = require('jsdoc/plugins');
const parser = require('jsdoc/src/parser').createParser();
// Install plugins
const pluginPaths = [
'plugins/markdown',
'plugins/overloadHelper',
'./my-custom-plugin'
];
plugins.installPlugins(pluginPaths, parser);// test-plugin.js
const testPlugin = {
handlers: {
newDoclet: function(e) {
e.doclet.tested = true;
}
}
};
// Install and test
const parser = require('jsdoc/src/parser').createParser();
parser.on('newDoclet', testPlugin.handlers.newDoclet);
const doclets = parser.parse(['test-file.js']);
console.log('All doclets tested:', doclets.every(d => d.tested));// good-plugin.js
exports.handlers = {
newDoclet: function(e) {
// Always check for existence
if (e.doclet && e.doclet.description) {
// Avoid modifying original strings
e.doclet.processedDescription = processDescription(e.doclet.description);
}
}
};
exports.defineTags = function(dictionary) {
// Provide complete tag definitions
dictionary.defineTag('customTag', {
mustHaveValue: true,
canHaveType: false,
canHaveName: false,
onTagged: function(doclet, tag) {
// Validate tag value
if (typeof tag.value === 'string' && tag.value.length > 0) {
doclet.customTag = tag.value;
}
}
});
};
function processDescription(description) {
// Safe processing logic
try {
return description.replace(/\[note\]/g, '<strong>Note:</strong>');
} catch (error) {
console.warn('Description processing failed:', error.message);
return description;
}
}