iflrandevu/rsc/scripts/jsdoc/plugins/README.md

390 lines
15 KiB
Markdown
Raw Normal View History

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:
...
"plugins": [
"plugins/shout"
]
...
Authoring JSDoc 3 Plugins
----
The plugin system for JSDoc 3 is pretty powerful and provides plugin authors
multiple methods, from high-level to low-level, of affecting document generation:
- 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:
exports.handlers = {
newDoclet: function(e) {
//Do something when we see a new doclet
}
}
#### 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:
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 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 JSDoc3)
Below is an example of a newDoclet handler that shouts the descriptions:
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
### 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:
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:
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:
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:
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:
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:
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:
$>jake install[path/to/YourPluginFolder]
**Note**: On some operating systems, including OS X, you may need to quote the
target name and parameters:
$>jake 'install[path/to/YourPluginFolder]'
The task is passed a directory that should look something like the following:
YourPluginFolder
|- plugins
| |- YourPlugin.js
| \- test
| |- fixtures
| | \- YourFixtures.js
| \- specs
| \- YourTests.js
\- templates
\- YourTemplate
\- publish.js