JSDoc 3 Plugin Reference

May 18, 2022 0 Comments

Table of Contents

  1. Creating and Enabling a Plugin
  2. Authoring JSDoc 3 Plugins
    1. Event Handlers
      1. Event: parseBegin
      2. Event: fileBegin
      3. Event: beforeParse
      4. Event: jsdocCommentFound
      5. Event: symbolFound
      6. Event: newDoclet
      7. Event: fileComplete
      8. Event: parseComplete
      9. Event: processingComplete
    2. Tag Definitions
      1. The Dictionary
    3. Node Visitors
      1. Making things happen
      2. Example
    4. Throwing Errors
  3. Packaging JSDoc 3 Plugins

Creating and Enabling a Plugin

There are two steps required to create and enable a new JSDoc plugin:

  1. Create a JavaScript module to contain your plugin code.
  2. Include that module in the “plugins” array of conf.json. You can specify an absolute or relative path. If you use a relative path, JSDoc searches for the plugin in the current working directory and the JSDoc directory, in that order.

For example, if your plugin source code was saved in the “plugins/shout.js” file in the current working directory, you would include it by adding a reference to it in conf.json like so:

Example
...
"plugins": [
    "plugins/shout"
]
...

Authoring JSDoc 3 Plugins

JSDoc 3’s plugin system offers extensive control over the parsing process. A plugin can affect the parse results by doing any of the following:

  • Defining event handlers
  • Defining tags
  • Defining a parse tree node processor

Event Handlers

At the highest level, a plugin may register handlers for specific named-events that occur in the documentation generation process. JSDoc will pass the handler an event object containing pertinent information. Your plugin module should export a handlers object that contains your handler, like so:

Example
exports.handlers = {
    newDoclet: function(e) {
        //Do something when we see a new doclet
    }
}

Event: parseBegin

The parseBegin event is fired before JSDoc starts loading and parsing the source files. Your plugin can control which files JSDoc will parse by modifying the event’s contents.

Note: This event is fired in JSDoc 3.2 and later.

The event object will contain the following properties:

  • sourcefiles: An array of paths to source files that will be parsed.

Event: fileBegin

This is triggered when the parser has started on a new file. You might use this to do any per-file initialization your plugin needs to do.

The event object will contain the following properties:

  • filename: the name of the file

Event: beforeParse

This is triggered before parsing has begun. You can use this method to modify the source code that will be parsed. For instance, you might add some virtual doclets so they get added to the documentation.

The event object will contain the following properties:

  • filename: the name of the file
  • source: the contents of the file

Below is an example that adds a virtual doclet for a function to the source so that it will get parsed and added to the documentation. This might be done to document methods that will be present for end-user use, but might not be in the source code being documented, like methods provided by a third-party superclass:

Example
exports.handlers = {
    beforeParse: function(e) {
        var extraDoc = ["",
            "/**",
            "Here's a description of this function",
            "@name superFunc",
            "@memberof ui.mywidget",
            "@function",
            "*/", ""];
        e.source += extraDoc.join("\n");
    }
}

Event: jsdocCommentFound

This is fired whenever a JSDoc comment is found. It may or may not be associated with any code. You might use this to modify the contents of a comment before it is processed.

The event object will contain the following properties:

  • filename: the name of the file
  • comment: the text of the comment
  • lineno: the line number the comment was found on

Event: symbolFound

This is fired when the parser comes across a symbol in the code it thinks is important. This usually means things that one might want to document — variables, functions, object literals, object property definitions, assignments, etc., but the symbols the parser finds can be modified by a plugin (see “Node Visitors” below).

The event object will contain the following properties:

  • filename: the name of the file
  • comment: the comment associated with the symbol, if any
  • id: the unique id of the symbol
  • lineno: the line number the symbols was found on
  • range: an array containing the first and last characters of the code associated with the symbol
  • astnode: the node of the parse tree
  • code: information about the code. This usually contains “name”, “type”, and “node” properties and might also have “value”, “paramnames”, or “funcscope” properties depending on the symbol.

Event: newDoclet

This is the highest level event and is fired when a new doclet has been created. This means that a JSDoc comment or a symbol has been processed, and the actual doclet that will be passed to the template has been created.

The event object will contain the following properties:

  • doclet: the new doclet that was created

The properties of the doclet can vary depending on the comment or symbol used to create it. Additionally, tag definitions (See “Tag Definitions” below) can modify the doclet. Some common properties you’re likely to see include:

  • comment: the text of the comment (may be empty if symbol is undocumented)
  • meta: some information about the doclet, like filename, line number, etc.
  • description
  • kind
  • name
  • longname: the fully qualified name, including memberof info
  • memberof: the function/class/namespace that this is a member of
  • scope: (global|static|instance|inner)
  • undocumented: true if the symbol didn’t have a JSDoc comment
  • defaultvalue: the specified default value for a property/variable
  • type: the specified type of parameter/property/function return (e.g. Boolean)
  • params: an object containing the list of parameters to a function
  • tags: an object containing the set of tags not handled by the parser (note: this is only available if allowUnknownTags is set to true in the conf.json file for JSDoc 3)

Below is an example of a newDoclet handler that shouts the descriptions:

Example
exports.handlers = {
    newDoclet: function(e) {
        // e.doclet will refer to the newly created doclet
        // you can read and modify properties of that doclet if you wish
        if (typeof e.doclet.description === 'string') {
            e.doclet.description = e.doclet.description.toUpperCase();
        }
    }
};

Event: fileComplete

This is fired when the parser is done with a file. You might use this to perform some cleanup for your plugin.

The event object will contain the following properties:

  • filename: the name of the file
  • source: the contents of the file

Event: parseComplete

The parseComplete event is fired after JSDoc has parsed all of the specified source files.

Note: This event is fired in JSDoc 3.2 and later.

The event object will contain the following properties:

  • sourcefiles: An array of paths to source files that were parsed.

Event: processingComplete

The processingComplete event is fired after JSDoc updates the parse results to reflect inherited and borrowed symbols.

Note: This event is fired in JSDoc 3.3 and later.

The event object will contain the following properties:

  • doclets: An array of doclet objects. See the newDoclet event for details about the properties that each doclet can contain.

Tag Definitions

Adding tags to the tag dictionary is a mid-level way to affect documentation generation. Before a newDoclet event is triggered, JSDoc comment blocks are parsed to determine the description and any JSDoc tags that may be present. When a tag is found, if it has been defined in the tag dictionary, it is given a chance to modify the doclet.

Plugins can define tags by exporting a defineTags function. That function will be passed a dictionary that can be used to define tags, like so:

Example
exports.defineTags = function(dictionary) {
    //define tags here
}

The Dictionary

The dictionary provides the following methods:

  1. defineTag(title, opts) Used to define tags. The first parameter is the name of the tag (e.g. “param” or “overview”). the second is an object containing options for the tag. The options can be the following:
    • mustHaveValue (Boolean): whether or not the tag must have a value (e.g “@name TheName”)
    • mustNotHaveValue (Boolean): whether or not the tag must not have a value
    • canHaveType (Boolean): Whether or not the tag can have a type (e.g. “@param {String} name the description of name”)
    • canHaveName (Boolean): Whether or not the tag can have a name (e.g. “@param {String} name the description of name”)
    • isNamespace (Boolean): Whether or not the tag marks a doclet as representing a namespace. The “@module” tag, for instance, sets this to true.
    • onTagged (Function): A callback function executed when the tag is found. The function is passed two parameters: the doclet and the tag. Here’s an example:
      Example
      dictionary.defineTag('instance', { 
          onTagged: function(doclet, tag) {
              doclet.scope = "instance";
          }
      }); 
                      

    The defineTag method returns a Tag. The Tag object has a method “synonym” that can be used to declare synonyms to the tag. For example:

    Example
    dictionary.defineTag('exception', { <options for exception tag> })
    .synonym('throws');
            
  2. lookUp(title) Used to lookup a tag. Returns either the tag or false if it’s not defined
  3. isNamespace(kind) Used to determine if a particular doclet type represents a namespace
  4. normalise(title) Used to find the canonical name of a tag. The name passed in might be that name or a synonym

Node Visitors

At the lowest level, plugin authors can process each node in the parse tree by defining a node visitor that will visit each node, creating an opportunity to do things like modify comments and trigger parser events for any arbitrary piece of code.

Plugins can define a node visitor by exporting a nodeVisitor object that contains a visitNode function, like so:

Example
exports.nodeVisitor = {
    visitNode: function(node, e, parser, currentSourceName) {
        //do all sorts of crazy things here
    }
}

The function is called on each node with the following parameters:

  • node: the node of the parse tree
  • e: the event. If the node is one that the parser handles, this will already be populated with the same things described in the symbolFound event above. Otherwise, it will be an empty object on which to set various properties.
  • parser: the parser
  • currentSourceName: the name of the file being parsed

Making things happen

The primary reasons to implement a node visitor are to be able to document things that aren’t normally documented (like function calls that create classes) or to auto generate documentation for code that isn’t documented. For instance, a plugin might look for calls to a “_trigger” method since it knows that means an event is fired and then generate documentation for the event.

To make things happen, the visitNode function should modify properties of the event parameter. In general the goal is to construct a comment and then get an event to fire. After the parser lets all of the node visitors have a look at the node, it looks to see if the event object has a comment property and an event property. If it has both, the event named in the event property is fired. The event is usually “symbolFound” or “jsdocCommentFound”, but theoretically, a plugin could define its own events and handle them.

Example

Below is an example of what a plugin for documenting jQuery UI widgets might do. jQuery UI uses a factory function call to create widget classes. The plugin looks for that function call and creates a symbol with documentation. It also looks for any “this._trigger” function calls and automatically creates documentation for the events that are triggered:

Example
exports.nodeVisitor = {
    visitNode: function(node, e, parser, currentSourceName) {
        if (node.type === Token.OBJECTLIT && node.parent && node.parent.type === Token.CALL && isInWidgetFactory(node, 1)) {
            var widgetName = node.parent.arguments.get(0).toSource();
            e.id = 'astnode' + node.hashCode(); // the id of the object literal node
            e.comment = String(node.parent.jsDoc||'');
            e.lineno = node.parent.getLineno();
            e.filename = currentSourceName;
            e.astnode = node;
            e.code = {
                name: "" + widgetName.substring(1, widgetName.length() - 1),
                type: "class",
                node: node
            };
            e.event = "symbolFound";
            e.finishers = [parser.addDocletRef];

            addCommentTag(e, "param", "{Object=} options A set of configuration options");
        } 
        else if(isTriggerCall(node)) {
            var nameNode = node.arguments.get(0);
                eventName = String((nameNode.type == Token.STRING) ? nameNode.value : nameNode.toSource()),
                func = {},
                comment = "@event\n",
                eventKey = "";

            if (node.enclosingFunction) {
                func.id = 'astnode'+node.enclosingFunction.hashCode();
                func.doclet = parser.refs[func.id];
            }
            if(func.doclet) {
                func.doclet.addTag("fires", eventName);
                if (func.doclet.memberof) {
                    eventKey = func.doclet.memberof + "#event:" + eventName;
                    comment += "@name " + func.doclet.memberof + "#" + eventName;
                }
            }
            e.comment = comment;
            e.lineno = node.getLineno();
            e.filename = currentSourceName;
            e.event = "jsdocCommentFound";                
        }
    }
};
function isTriggerCall(node) {
    if(node.type != Token.CALL) { return false; }
    var target = node.getTarget(),
        left = target && target.left && String(target.left.toSource()),
        right = target && target.right && String(target.right.toSource());
    return (left === "this" && right === "_trigger");
}

function isInWidgetFactory(node, depth) {
    var parent = node.parent,
        d = 0;
    while(parent && (!depth || d < depth)) {
        if (parent.type === Token.CALL) {
            var target = parent.getTarget(),
                left = target && target.left && String(target.left.toSource()),
                right = target && target.right && String(target.right.toSource());
            return ((left === "$" || left === "jQuery") && right === "widget");
        } else {
            parent = parent.parent;
            d++;
        }
    }
    return false;
}

You’ll notice a “finishers” property set. The finishers property should contain an array of functions to be called after the event is fired and all the handlers have processed it. The parser provides an addDocletRef function that adds the doclet to the map (keyed off of the id property) of doclets it knows about.

Lastly, the visitors are executed in the order the plugins are listed in the conf.json file. A plugin can stop later plugins from visiting a node by setting a stopPropagation property on the event object (e.stopPropagation = true). A plugin can stop the event from firing setting a preventDefault property.

Throwing Errors

If you wish your plugin to throw an error, do it using the handle function in the jsdoc/util/error module.

Example
require('jsdoc/util/error').handle( new Error('I do not like green eggs and ham!') );

By default, this will throw the error, halting the execution of JSDoc. However, if the user enabled JSDoc’s --lenient switch, JSDoc will simply log the error to the console and continue.

Packaging JSDoc 3 Plugins

The JSDoc 3 Jakefile has an install task that can be used to install a plugin into the JSDoc directory. So running the following will install the plugin:

Example
$>jake install[path/to/YourPluginFolder]

Note: On some operating systems, including OS X, you may need to quote the target name and parameters:

Example
$>jake 'install[path/to/YourPluginFolder]'

The task is passed a directory that should look something like the following:

Example
YourPluginFolder
 |- plugins
 |   |- YourPlugin.js
 |   \- test
 |       |- fixtures
 |       |   \- YourFixtures.js
 |       \- specs
 |           \- YourTests.js
 \- templates
     \- YourTemplate
         \- publish.js