diff --git a/jsdoc.js b/jsdoc.js index a0077d33..45a7d0d6 100644 --- a/jsdoc.js +++ b/jsdoc.js @@ -143,7 +143,8 @@ function main() { parser: require('jsdoc/opts/parser'), } }, - resolver; + resolver, + dictionary = require('jsdoc/tag/dictionary'); env.opts = jsdoc.opts.parser.parse(env.args); @@ -184,16 +185,30 @@ function main() { exit(0); } - // allow user-defined plugins to register listeners + // allow user-defined plugins to... if (env.conf.plugins) { for (var i = 0, leni = env.conf.plugins.length; i < leni; i++) { var plugin = require(env.conf.plugins[i]); - for (var eventName in plugin) { - app.jsdoc.parser.on(eventName, plugin[eventName]); + + //...register event handlers + if (plugin.handlers) { + for (var eventName in plugin.handlers) { + app.jsdoc.parser.on(eventName, plugin.handlers[eventName]); + } + } + + //...define tags + if (plugin.defineTags) { + plugin.defineTags(dictionary); + } + + //...add a node visitor + if (plugin.nodeVisitor) { + app.jsdoc.parser.addNodeVisitor(plugin.nodeVisitor); } } } - + // any source file named package.json is treated special for (var i = 0, l = env.opts._.length; i < l; i++ ) { if (/\bpackage\.json$/i.test(env.opts._[i])) { diff --git a/lib/js.jar b/lib/js.jar index 878b0d94..37316edf 100644 Binary files a/lib/js.jar and b/lib/js.jar differ diff --git a/plugins/README.md b/plugins/README.md index b3b3addd..c5a8bd21 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -18,19 +18,327 @@ file, you would include it in conf.json like so: Authoring JSDoc 3 Plugins ---- -The plugin system for JSDoc 3 is event-based, meaning you register an interest -in a specific named-event with a handler function that will be called by JSDoc -whenever that event occurs. JSDoc will pass an event object into your handler as -the argument: this can be used to access information about the event. +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: -For example, to handle events fired whenever a new doclet is created by JSDoc, -you would add a handler for the "newDoclet" event to your plugin's exported -functions, like so: +- Defining event handlers +- Defining tags +- Defining a parse tree node processor - exports.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 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.comment += 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 +- 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', { + + }) + .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. \ No newline at end of file diff --git a/plugins/commentConvert.js b/plugins/commentConvert.js index b601166d..e97a291a 100644 --- a/plugins/commentConvert.js +++ b/plugins/commentConvert.js @@ -4,15 +4,18 @@ @author Michael Mathews */ - /// - /// Convert ///-style comments into jsdoc comments. - /// @param e - /// @param e.filename - /// @param e.source - /// -exports.beforeParse = function(e) { - e.source = e.source.replace(/(\n[ \t]*\/\/\/[^\n]*)+/g, function($) { - var replacement = '\n/**' + $.replace(/^[ \t]*\/\/\//mg, '').replace(/(\n$|$)/, '*/$1'); - return replacement; - }); + +exports.handlers = { + /// + /// Convert ///-style comments into jsdoc comments. + /// @param e + /// @param e.filename + /// @param e.source + /// + beforeParse: function(e) { + e.source = e.source.replace(/(\n[ \t]*\/\/\/[^\n]*)+/g, function($) { + var replacement = '\n/**' + $.replace(/^[ \t]*\/\/\//mg, '').replace(/(\n$|$)/, '*/$1'); + return replacement; + }); + } }; \ No newline at end of file diff --git a/plugins/escapeHtml.js b/plugins/escapeHtml.js index aaef19d2..7b561682 100644 --- a/plugins/escapeHtml.js +++ b/plugins/escapeHtml.js @@ -4,14 +4,17 @@ @author Michael Mathews */ -/** - Translate HTML tags in descriptions into safe entities. - */ -exports.newDoclet = function(e) { - if (e.doclet.description) { - e.doclet.description = e.doclet.description - .replace(/&/g,'&') - .replace(/'); + +exports.handlers = { + /** + Translate HTML tags in descriptions into safe entities. + */ + newDoclet: function(e) { + if (e.doclet.description) { + e.doclet.description = e.doclet.description + .replace(/&/g,'&') + .replace(/'); + } } }; diff --git a/plugins/markdown.js b/plugins/markdown.js index c4d41f50..382c0d50 100644 --- a/plugins/markdown.js +++ b/plugins/markdown.js @@ -6,15 +6,17 @@ var mdParser = require('evilstreak/markdown'); -/** - Translate markdown syntax in a new doclet's description into HTML. Is run - by JSDoc 3 whenever a "newDoclet" event fires. - */ -exports.newDoclet = function(e) { - if (e.doclet.description) { - e.doclet.description = mdParser.toHTML(e.doclet.description) - .replace( /&/g, "&" ) // because markdown escapes these - .replace( /</g, "<" ) - .replace( />/g, ">" ); +exports.handlers = { + /** + Translate markdown syntax in a new doclet's description into HTML. Is run + by JSDoc 3 whenever a "newDoclet" event fires. + */ + newDoclet: function(e) { + if (e.doclet.description) { + e.doclet.description = mdParser.toHTML(e.doclet.description) + .replace( /&/g, "&" ) // because markdown escapes these + .replace( /</g, "<" ) + .replace( />/g, ">" ); + } } -}; +}; \ No newline at end of file diff --git a/plugins/shout.js b/plugins/shout.js index c15a79e1..bb9c2c5d 100644 --- a/plugins/shout.js +++ b/plugins/shout.js @@ -4,11 +4,13 @@ @author Michael Mathews */ -/** - Make your descriptions more shoutier. - */ -exports.newDoclet = function(e) { - if (typeof e.doclet.description === 'string') { - e.doclet.description = e.doclet.description.toUpperCase(); +exports.handlers = { + /** + Make your descriptions more shoutier. + */ + newDoclet: function(e) { + if (typeof e.doclet.description === 'string') { + e.doclet.description = e.doclet.description.toUpperCase(); + } } }; \ No newline at end of file diff --git a/plugins/sourcetag.js b/plugins/sourcetag.js index 89255451..7c065d1b 100644 --- a/plugins/sourcetag.js +++ b/plugins/sourcetag.js @@ -3,38 +3,40 @@ @author Michael Mathews */ -/** - Support @source tag. Expected value like: - { "filename": "myfile.js", "lineno": 123 } - Modifies the corresponding meta values on the given doclet. - */ -exports.newDoclet = function(e) { - var tags = e.doclet.tags, - tag, - value; - - //console.log(e.doclet); - // any user-defined tags in this doclet? - if (typeof tags !== 'undefined') { - // only interested in the @source tags - tags = tags.filter(function($) { - return $.title === 'source'; - }); - - if (tags.length) { - // take the first one - tag = tags[0]; +exports.handlers = { + /** + Support @source tag. Expected value like: + { "filename": "myfile.js", "lineno": 123 } + Modifies the corresponding meta values on the given doclet. + */ + newDoclet: function(e) { + var tags = e.doclet.tags, + tag, + value; - try { - value = JSON.parse(tag.value); - } - catch(e) { - throw new Error('@source tag expects a valid JSON value, like { "filename": "myfile.js", "lineno": 123 }.'); - } + //console.log(e.doclet); + // any user-defined tags in this doclet? + if (typeof tags !== 'undefined') { + // only interested in the @source tags + tags = tags.filter(function($) { + return $.title === 'source'; + }); - !e.doclet.meta && (e.doclet.meta = {}); - e.doclet.meta.filename = value.filename || ''; - e.doclet.meta.lineno = value.lineno || ''; - } + if (tags.length) { + // take the first one + tag = tags[0]; + + try { + value = JSON.parse(tag.value); + } + catch(e) { + throw new Error('@source tag expects a valid JSON value, like { "filename": "myfile.js", "lineno": 123 }.'); + } + + !e.doclet.meta && (e.doclet.meta = {}); + e.doclet.meta.filename = value.filename || ''; + e.doclet.meta.lineno = value.lineno || ''; + } + } } }; \ No newline at end of file diff --git a/rhino_modules/jsdoc/src/parser.js b/rhino_modules/jsdoc/src/parser.js index fc78bb52..5118052f 100644 --- a/rhino_modules/jsdoc/src/parser.js +++ b/rhino_modules/jsdoc/src/parser.js @@ -19,6 +19,7 @@ var Token = Packages.org.mozilla.javascript.Token, exports.Parser = function() { this._resultBuffer = []; this.refs = {}; + this._visitors = []; } require('common/util').mixin(exports.Parser.prototype, require('common/events')); @@ -91,6 +92,20 @@ exports.Parser.prototype.clear = function() { this._resultBuffer = []; } +/** + * Adds a node visitor to use in parsing + */ +exports.Parser.prototype.addNodeVisitor = function(visitor) { + this._visitors.push(visitor); +} + +/** + * Get the node visitors used in parsing + */ +exports.Parser.prototype.getVisitors = function() { + return this._visitors; +} + /** @private */ exports.Parser.prototype._parseSourceCode = function(sourceCode, sourceName) { var e = {filename: sourceName}; @@ -103,7 +118,7 @@ exports.Parser.prototype._parseSourceCode = function(sourceCode, sourceName) { currentSourceName = sourceName = e.filename; sourceCode = pretreat(e.source); - + var ast = parserFactory().parse(sourceCode, sourceName, 1); ast.visit( new Packages.org.mozilla.javascript.ast.NodeVisitor({ @@ -135,6 +150,9 @@ function pretreat(code) { .replace(/ยป/g, '*/'); } +var tkn = { NAMEDFUNCTIONSTATEMENT: -1001 }; +exports.Parser.tkn = tkn; + /** * Given a node, determine what the node is a member of. * @param {astnode} node @@ -143,7 +161,7 @@ function pretreat(code) { exports.Parser.prototype.astnodeToMemberof = function(node) { var memberof = {}; - if (node.type === Token.VAR || node.type === Token.FUNCTION || node.type == tkn.NAMEDFUNCTIONTATEMENT) { + if (node.type === Token.VAR || node.type === Token.FUNCTION || node.type == tkn.NAMEDFUNCTIONSTATEMENT) { if (node.enclosingFunction) { // an inner var or func memberof.id = 'astnode'+node.enclosingFunction.hashCode(); memberof.doclet = this.refs[memberof.id]; @@ -247,6 +265,31 @@ exports.Parser.prototype.resolveVar = function(node, basename) { return this.resolveVar(enclosingFunction, basename); } +exports.Parser.prototype.addDocletRef = function(e) { + var node = e.code.node; + if (e.doclet) { + currentParser.refs['astnode'+node.hashCode()] = e.doclet; // allow lookup from value => doclet + } + else if ((node.type == Token.FUNCTION || node.type == tkn.NAMEDFUNCTIONSTATEMENT) && !currentParser.refs['astnode'+node.hashCode()]) { // keep references to undocumented anonymous functions too as they might have scoped vars + currentParser.refs['astnode'+node.hashCode()] = { + longname: '', + meta: { code: e.code } + }; + } +} + +exports.Parser.prototype.resolveEnum = function(e) { + var parent = currentParser.resolvePropertyParent(e.code.node); + if (parent && parent.doclet.isEnum) { + if (!parent.doclet.properties) { parent.doclet.properties = []; } + // members of an enum inherit the enum's type + if (parent.doclet.type && !e.doclet.type) { e.doclet.type = parent.doclet.type; } + delete e.doclet.undocumented; + e.doclet.defaultvalue = e.doclet.meta.code.value; + parent.doclet.properties.push(e.doclet); + } +} + /** @private */ function visitNode(node) { var e, @@ -267,12 +310,13 @@ function visitNode(node) { lineno: comment.getLineno(), filename: currentSourceName }; - + if ( isValidJsdoc(commentSrc) ) { currentParser.fire('jsdocCommentFound', e, currentParser); } } } + e = null; } else if (node.type === Token.ASSIGN) { e = { @@ -281,20 +325,14 @@ function visitNode(node) { lineno: node.left.getLineno(), filename: currentSourceName, astnode: node, - code: aboutNode(node) + code: aboutNode(node), + event: "symbolFound", + finishers: [currentParser.addDocletRef] }; var basename = e.code.name.replace(/^([$a-z_][$a-z_0-9]*).*?$/i, '$1'); if (basename !== 'this') e.code.funcscope = currentParser.resolveVar(node, basename); - - if ( isValidJsdoc(e.comment) ) { - currentParser.fire('symbolFound', e, currentParser); - } - - if (e.doclet) { - currentParser.refs['astnode'+e.code.node.hashCode()] = e.doclet; // allow lookup from value => doclet - } } else if (node.type === Token.COLON) { // assignment within an object literal e = { @@ -303,26 +341,10 @@ function visitNode(node) { lineno: node.left.getLineno(), filename: currentSourceName, astnode: node, - code: aboutNode(node) + code: aboutNode(node), + event: "symbolFound", + finishers: [currentParser.addDocletRef, currentParser.resolveEnum] }; - - if ( isValidJsdoc(e.comment) ) { - currentParser.fire('symbolFound', e, currentParser); - } - - if (e.doclet) { - currentParser.refs['astnode'+e.code.node.hashCode()] = e.doclet; // allow lookup from value => doclet - } - - var parent = currentParser.resolvePropertyParent(node); - if (parent && parent.doclet.isEnum) { - if (!parent.doclet.properties) { parent.doclet.properties = []; } - // members of an enum inherit the enum's type - if (parent.doclet.type && !e.doclet.type) { e.doclet.type = parent.doclet.type; } - delete e.doclet.undocumented; - e.doclet.defaultvalue = e.doclet.meta.code.value; - parent.doclet.properties.push(e.doclet); - } } else if (node.type == Token.VAR || node.type == Token.LET || node.type == Token.CONST) { @@ -341,7 +363,9 @@ function visitNode(node) { lineno: node.getLineno(), filename: currentSourceName, astnode: node, - code: aboutNode(node) + code: aboutNode(node), + event: "symbolFound", + finishers: [currentParser.addDocletRef] }; // keep track of vars in a function scope @@ -354,26 +378,20 @@ function visitNode(node) { funcDoc.meta.vars.push(e.code.name); } } - - if ( isValidJsdoc(e.comment) ) { - currentParser.fire('symbolFound', e, currentParser); - } - - if (e.doclet) { - currentParser.refs['astnode'+e.code.node.hashCode()] = e.doclet; // allow lookup from value => doclet - } } - else if (node.type == Token.FUNCTION || node.type == tkn.NAMEDFUNCTIONTATEMENT) { + else if (node.type == Token.FUNCTION || node.type == tkn.NAMEDFUNCTIONSTATEMENT) { e = { id: 'astnode'+node.hashCode(), // the id of the COLON node comment: String(node.jsDoc||'@undocumented'), lineno: node.getLineno(), filename: currentSourceName, astnode: node, - code: aboutNode(node) + code: aboutNode(node), + event: "symbolFound", + finishers: [currentParser.addDocletRef] }; - e.code.name = (node.type == tkn.NAMEDFUNCTIONTATEMENT)? '' : String(node.name) || ''; + e.code.name = (node.type == tkn.NAMEDFUNCTIONSTATEMENT)? '' : String(node.name) || ''; //console.log(':: e.code.name is '+e.code.name); // keep track of vars in a function scope if (node.enclosingFunction) { @@ -388,22 +406,22 @@ function visitNode(node) { var basename = e.code.name.replace(/^([$a-z_][$a-z_0-9]*).*?$/i, '$1'); e.code.funcscope = currentParser.resolveVar(node, basename); - - if ( isValidJsdoc(e.comment) ) { - currentParser.fire('symbolFound', e, currentParser); - } - - if (e.doclet) { - currentParser.refs['astnode'+e.code.node.hashCode()] = e.doclet; // allow lookup from value => doclet - } - else if (!currentParser.refs['astnode'+e.code.node.hashCode()]) { // keep references to undocumented anonymous functions too as they might have scoped vars - currentParser.refs['astnode'+e.code.node.hashCode()] = { - longname: '', - meta: { code: e.code } - }; - } } - + + if (!e) { e = {finishers: []}; } + for(var i = 0, l = currentParser._visitors.length; i < l; i++) { + currentParser._visitors[i].visitNode(node, e, currentParser, currentSourceName); + if (e.stopPropagation) { break; } + } + + if (!e.preventDefault && isValidJsdoc(e.comment)) { + currentParser.fire(e.event, e, currentParser); + } + + for (var i = 0, l = e.finishers.length; i < l; i++) { + e.finishers[i].call(currentParser, e); + } + return true; } @@ -419,7 +437,7 @@ function parserFactory() { ce.initFromContext(cx); return new Packages.org.mozilla.javascript.Parser(ce, ce.getErrorReporter()); } -var tkn = { NAMEDFUNCTIONTATEMENT: -1001 }; + /** * Attempts to find the name and type of the given node. * @private @@ -428,8 +446,8 @@ var tkn = { NAMEDFUNCTIONTATEMENT: -1001 }; function aboutNode(node) { about = {}; - if (node.type == Token.FUNCTION || node.type == tkn.NAMEDFUNCTIONTATEMENT) { - about.name = node.type == tkn.NAMEDFUNCTIONTATEMENT? '' : '' + node.name; + if (node.type == Token.FUNCTION || node.type == tkn.NAMEDFUNCTIONSTATEMENT) { + about.name = node.type == tkn.NAMEDFUNCTIONSTATEMENT? '' : '' + node.name; about.type = 'function'; about.node = node; } @@ -437,15 +455,15 @@ function aboutNode(node) { about.name = nodeToString(node.target); if (node.initializer) { // like var i = 0; about.node = node.initializer; - about.value = nodeToString(about.node); + about.value = nodeToString(about.node); about.type = getTypeName(node.initializer); if (about.type === 'FUNCTION' && about.node.name) { - about.node.type = tkn.NAMEDFUNCTIONTATEMENT; + about.node.type = tkn.NAMEDFUNCTIONSTATEMENT; } } else { // like var i; about.node = node.target; - about.value = nodeToString(about.node); + about.value = nodeToString(about.node); about.type = 'undefined'; } } @@ -459,11 +477,11 @@ function aboutNode(node) { } } about.node = node.right; - about.value = nodeToString(about.node); + about.value = nodeToString(about.node); about.type = getTypeName(node.right); if (about.type === 'FUNCTION' && about.node.name) { - about.node.type = tkn.NAMEDFUNCTIONTATEMENT; + about.node.type = tkn.NAMEDFUNCTIONSTATEMENT; } } else { @@ -510,7 +528,7 @@ function nodeToString(node) { else if (node.type === Token.STRING) { str = node.value; } - else if (node.type === Token.NUMBER) { + else if (node.type === Token.NUMBER) { str = node.value; } else if (node.type === Token.THIS) { @@ -546,7 +564,7 @@ function getTypeName(node) { @memberof module:src/parser.Parser */ function isValidJsdoc(commentSrc) { - return commentSrc.indexOf('/***') !== 0; /*** ignore comments that start with many stars ***/ + return commentSrc && commentSrc.indexOf('/***') !== 0; /*** ignore comments that start with many stars ***/ } /**