This commit is contained in:
watrabi
2025-09-18 17:55:52 -04:00
commit 977f1ff4b8
15030 changed files with 17324420 additions and 0 deletions
@@ -0,0 +1,492 @@
'use strict';
/**
* Provides methods for augmenting the parse results based on their content.
* @module jsdoc/augment
*/
var doop = require('jsdoc/util/doop');
var name = require('jsdoc/name');
var hasOwnProp = Object.prototype.hasOwnProperty;
function mapDependencies(index, propertyName) {
var dependencies = {};
var doc;
var doclets;
var kinds = ['class', 'external', 'mixin'];
var len = 0;
Object.keys(index).forEach(function(name) {
doclets = index[name];
for (var i = 0, ii = doclets.length; i < ii; i++) {
doc = doclets[i];
if (kinds.indexOf(doc.kind) !== -1) {
dependencies[name] = {};
if (hasOwnProp.call(doc, propertyName)) {
len = doc[propertyName].length;
for (var j = 0; j < len; j++) {
dependencies[name][doc[propertyName][j]] = true;
}
}
}
}
});
return dependencies;
}
function Sorter(dependencies) {
this.dependencies = dependencies;
this.visited = {};
this.sorted = [];
}
Sorter.prototype.visit = function(key) {
var self = this;
if (!(key in this.visited)) {
this.visited[key] = true;
if (this.dependencies[key]) {
Object.keys(this.dependencies[key]).forEach(function(path) {
self.visit(path);
});
}
this.sorted.push(key);
}
};
Sorter.prototype.sort = function() {
var self = this;
Object.keys(this.dependencies).forEach(function(key) {
self.visit(key);
});
return this.sorted;
};
function sort(dependencies) {
var sorter = new Sorter(dependencies);
return sorter.sort();
}
function getMembers(longname, docs, scopes) {
var candidate;
var members = [];
for (var i = 0, l = docs.length; i < l; i++) {
candidate = docs[i];
if (candidate.memberof === longname &&
(!scopes || !scopes.length || scopes.indexOf(candidate.scope) !== -1)) {
members.push(candidate);
}
}
return members;
}
function addDocletProperty(doclets, propName, value) {
for (var i = 0, l = doclets.length; i < l; i++) {
doclets[i][propName] = value;
}
}
function reparentDoclet(parent, child) {
var parts = name.shorten(child.longname);
parts.memberof = parent.longname;
child.memberof = parent.longname;
child.longname = name.combine(parts);
}
function parentIsClass(parent) {
return parent.kind === 'class';
}
function staticToInstance(doclet) {
var parts = name.shorten(doclet.longname);
parts.scope = name.SCOPE.PUNC.INSTANCE;
doclet.longname = name.combine(parts);
doclet.scope = name.SCOPE.NAMES.INSTANCE;
}
/**
* Update the list of doclets to be added to another symbol.
*
* We add only one doclet per longname. For example: If `ClassA` inherits from two classes that both
* use the same method name, `ClassA` gets docs for one method rather than two.
*
* Also, the last symbol wins for any given longname. For example: If you write `@extends Class1
* @extends Class2`, and both classes have an instance method called `myMethod`, you get the docs
* from `Class2#myMethod`.
*
* @private
* @param {Array.<module:jsdoc/doclet.Doclet>} additions - An array of doclets that will be added to
* another symbol.
* @param {Object.<string, number>} indexes - A dictionary of indexes into the `additions` array.
* Each key is a longname, and each value is the index of the longname's doclet.
* @return {void}
*/
function updateAddedDoclets(doclet, additions, indexes) {
if (typeof indexes[doclet.longname] !== 'undefined') {
// replace the existing doclet
additions[indexes[doclet.longname]] = doclet;
}
else {
// add the doclet to the array, and track its index
additions.push(doclet);
indexes[doclet.longname] = additions.length - 1;
}
}
function explicitlyInherits(doclets) {
var doclet;
var inherits = false;
for (var i = 0, l = doclets.length; i < l; i++) {
doclet = doclets[i];
if (typeof doclet.inheritdoc !== 'undefined' || typeof doclet.override !== 'undefined') {
inherits = true;
break;
}
}
return inherits;
}
function getInheritedAdditions(doclets, docs, documented) {
var additionIndexes;
var additions = [];
var doc;
var parents;
var members;
var member;
var parts;
// doclets will be undefined if the inherited symbol isn't documented
doclets = doclets || [];
for (var i = 0, ii = doclets.length; i < ii; i++) {
doc = doclets[i];
parents = doc.augments;
if (parents && doc.kind === 'class') {
// reset the lookup table of added doclet indexes by longname
additionIndexes = {};
for (var j = 0, jj = parents.length; j < jj; j++) {
members = getMembers(parents[j], docs, ['instance']);
for (var k = 0, kk = members.length; k < kk; k++) {
member = doop(members[k]);
if (!member.inherited) {
member.inherits = member.longname;
}
member.inherited = true;
// TODO: this will fail on longnames like: MyClass#"quoted#Longname"
// and nested instance members like: MyClass#MyOtherClass#myMethod;
// switch to updateLongname()!
member.memberof = doc.longname;
parts = member.longname.split('#');
parts[0] = doc.longname;
member.longname = parts.join('#');
// Indicate what the descendant is overriding. (We only care about the closest
// ancestor. For classes A > B > C, if B#a overrides A#a, and C#a inherits B#a,
// we don't want the doclet for C#a to say that it overrides A#a.)
addDocletProperty([member], 'overrides', members[k].longname);
// Add the ancestor's docs unless the descendant overrides the ancestor AND
// documents the override.
if ( !hasOwnProp.call(documented, member.longname) ) {
updateAddedDoclets(member, additions, additionIndexes);
}
// If the descendant used an @inheritdoc or @override tag, add the ancestor's
// docs, and ignore the existing doclets.
else if ( explicitlyInherits(documented[member.longname]) ) {
// Ignore any existing doclets. (This is safe because we only get here if
// `member.longname` is an own property of `documented`.)
addDocletProperty(documented[member.longname], 'ignore', true);
updateAddedDoclets(member, additions, additionIndexes);
// Remove property that's no longer accurate.
if (member.virtual) {
delete member.virtual;
}
// Remove properties that we no longer need.
if (member.inheritdoc) {
delete member.inheritdoc;
}
if (member.override) {
delete member.override;
}
}
// If the descendant overrides the ancestor and documents the override,
// update the doclets to indicate what the descendant is overriding.
else {
addDocletProperty(documented[member.longname], 'overrides',
members[k].longname);
}
}
}
}
}
return additions;
}
function updateMixes(mixedDoclet, mixedLongname) {
var idx;
var mixedName;
var names;
// take the fast path if there's no array of mixed-in longnames
if (!mixedDoclet.mixes) {
mixedDoclet.mixes = [mixedLongname];
}
else {
// find the short name of the longname we're mixing in
mixedName = name.shorten(mixedLongname).name;
// find the short name of each previously mixed-in symbol
names = mixedDoclet.mixes.map(function(m) {
return name.shorten(mixedDoclet.longname).name;
});
// if we're mixing `myMethod` into `MixinC` from `MixinB`, and `MixinB` had the method mixed
// in from `MixinA`, don't show `MixinA.myMethod` in the `mixes` list
idx = names.indexOf(mixedName);
if (idx !== -1) {
mixedDoclet.mixes.splice(idx, 1);
}
mixedDoclet.mixes.push(mixedLongname);
}
}
// TODO: try to reduce overlap with getInheritedAdditions
function getMixedInAdditions(mixinDoclets, allDoclets, commentedDoclets) {
var additionIndexes;
var additions = [];
var doclet;
var idx;
var mixedDoclet;
var mixedDoclets;
var mixes;
// mixinDoclets will be undefined if the mixed-in symbol isn't documented
mixinDoclets = mixinDoclets || [];
for (var i = 0, ii = mixinDoclets.length; i < ii; i++) {
doclet = mixinDoclets[i];
mixes = doclet.mixes;
if (mixes) {
// reset the lookup table of added doclet indexes by longname
additionIndexes = {};
for (var j = 0, jj = mixes.length; j < jj; j++) {
mixedDoclets = getMembers(mixes[j], allDoclets, ['static']);
for (var k = 0, kk = mixedDoclets.length; k < kk; k++) {
mixedDoclet = doop(mixedDoclets[k]);
updateMixes(mixedDoclet, mixedDoclet.longname);
mixedDoclet.mixed = true;
reparentDoclet(doclet, mixedDoclet);
// if we're mixing into a class, treat the mixed-in symbol as an instance member
if (parentIsClass(doclet)) {
staticToInstance(mixedDoclet);
}
updateAddedDoclets(mixedDoclet, additions, additionIndexes);
}
}
}
}
return additions;
}
function augment(doclets, propertyName, docletFinder) {
var index = doclets.index.longname;
var dependencies = sort( mapDependencies(index, propertyName) );
dependencies.forEach(function(name) {
var additions = docletFinder.call(null, index[name], doclets, doclets.index.documented);
additions.forEach(function(addition) {
var longname = addition.longname;
if ( !hasOwnProp.call(index, longname) ) {
index[longname] = [];
}
index[longname].push(addition);
doclets.push(addition);
});
});
}
/**
* Add doclets to reflect class inheritance.
*
* For example, if `ClassA` has the instance method `myMethod`, and `ClassB` inherits from `ClassA`,
* calling this method creates a new doclet for `ClassB#myMethod`.
*
* @param {!Array.<module:jsdoc/doclet.Doclet>} doclets - The doclets generated by JSDoc.
* @param {!Object} doclets.index - The doclet index added by {@link module:jsdoc/borrow.indexAll}.
* @return {void}
*/
exports.addInherited = function(doclets) {
augment(doclets, 'augments', getInheritedAdditions);
};
/**
* Add doclets to reflect mixins. When a symbol is mixed into a class, the class' version of the
* mixed-in symbol is treated as an instance member.
*
* For example:
*
* + If `MixinA` has the static method `myMethod`, and `MixinB` mixes `MixinA`, calling this method
* creates a new doclet for the static method `MixinB.myMethod`.
* + If `MixinA` has the static method `myMethod`, and `ClassA` mixes `MixinA`, calling this method
* creates a new doclet for the instance method `ClassA#myMethod`.
*
* @param {!Array.<module:jsdoc/doclet.Doclet>} doclets - The doclets generated by JSDoc.
* @param {!Object} doclets.index - The doclet index added by {@link module:jsdoc/borrow.indexAll}.
* @return {void}
*/
exports.addMixedIn = function(doclets) {
augment(doclets, 'mixes', getMixedInAdditions);
};
// TODO: move as much of this as possible to jsdoc/borrow.indexAll
/**
* Update doclets to reflect implementations of interfaces.
*
* For example, if `InterfaceA` has the instance method `myMethod`, and `ClassA` implements
* `InterfaceA`, calling this method does the following:
*
* + Updates `InterfaceA` to indicate that it is implemented by `ClassA`
* + Updates `InterfaceA#myMethod` to indicate that it is implemented by `ClassA#myMethod`
* + Updates `ClassA#myMethod` to indicate that it implements `InterfaceA#myMethod`
*
* @param {!Array.<module:jsdoc/doclet.Doclet>} docs - The doclets generated by JSDoc.
* @param {!Object} doclets.index - The doclet index added by {@link module:jsdoc/borrow.indexAll}.
* @return {void}
*/
exports.addImplemented = function(docs) {
var docMap = {};
var interfaces = {};
var implemented = {};
var memberInfo = {};
docs.forEach(function(doc) {
var memberof = doc.memberof || doc.name;
if (!hasOwnProp.call(docMap, memberof)) {
docMap[memberof] = [];
}
docMap[memberof].push(doc);
if (doc.kind === 'interface') {
interfaces[doc.longname] = doc;
}
else if (doc.implements && doc.implements.length) {
if (!hasOwnProp.call(implemented, doc.memberof)) {
implemented[memberof] = [];
}
implemented[memberof].push(doc);
}
});
// create an dictionary of interface doclets
Object.keys(interfaces).forEach(function(ifaceName) {
var iface = interfaces[ifaceName];
if (hasOwnProp.call(docMap, iface.name)) {
docMap[iface.name].forEach(function(doc) {
var members = memberInfo[doc.memberof];
if (!members) {
members = memberInfo[doc.memberof] = {};
}
members[doc.name] = doc;
});
}
});
Object.keys(implemented).forEach(function(key) {
// implemented classes namespace.
var owner = implemented[key];
owner.forEach(function(klass) {
// class's interfaces
klass.implements.forEach(function(impl) {
var interfaceMember;
var interfaceMembers = memberInfo[impl];
var member;
var members;
// mark the interface as being implemented by the class
if (hasOwnProp.call(interfaces, impl)) {
interfaces[impl].implementations = interfaces[impl].implementations || [];
interfaces[impl].implementations.push(klass.longname);
}
// if the interface has no members, skip to the next owner
if (!interfaceMembers) {
return;
}
if (!hasOwnProp.call(docMap, klass.longname)) {
docMap[klass.longname] = [];
}
members = docMap[klass.longname];
for (var i = 0, len = members.length; i < len; i++) {
member = members[i];
interfaceMember = interfaceMembers && interfaceMembers[member.name];
// if we didn't find the member name in the interface, skip to the next member
if (!interfaceMember) {
continue;
}
// mark members that implement an interface
member.implements = member.implements || [];
member.implements.push(interfaceMember.longname);
// mark interface members that the symbol implements
interfaceMember.implementations = interfaceMember.implementations || [];
interfaceMember.implementations.push(member.longname);
}
});
});
});
};
/**
* Add and update doclets to reflect all of the following:
*
* + Inherited classes
* + Mixins
* + Interface implementations
*
* Calling this method is equivalent to calling all other methods exported by this module.
*
* @return {void}
*/
exports.augmentAll = function(doclets) {
exports.addInherited(doclets);
exports.addMixedIn(doclets);
exports.addImplemented(doclets);
};
+107
View File
@@ -0,0 +1,107 @@
/**
A collection of functions relating to resolving @borrows tags in JSDoc symbols.
@module jsdoc/borrow
@author Michael Mathews <micmath@gmail.com>
@license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
'use strict';
var doop = require('jsdoc/util/doop');
var logger = require('jsdoc/util/logger');
var SCOPE = require('jsdoc/name').SCOPE;
var hasOwnProp = Object.prototype.hasOwnProperty;
// TODO: add the index at parse time, so we don't have to iterate over all the doclets again
exports.indexAll = function(doclets) {
var borrowed = [];
var doclet;
var documented = {};
var longname = {};
for (var i = 0, l = doclets.length; i < l; i++) {
doclet = doclets[i];
// track all doclets by longname
if ( !hasOwnProp.call(longname, doclet.longname) ) {
longname[doclet.longname] = [];
}
longname[doclet.longname].push(doclet);
// track longnames of documented symbols
if (!doclet.undocumented) {
if ( !hasOwnProp.call(documented, doclet.longname) ) {
documented[doclet.longname] = [];
}
documented[doclet.longname].push(doclet);
}
// track doclets with a `borrowed` property
if ( hasOwnProp.call(doclet, 'borrowed') ) {
borrowed.push(doclet);
}
}
doclets.index = {
borrowed: borrowed,
documented: documented,
longname: longname
};
};
function cloneBorrowedDoclets(doclet, doclets) {
doclet.borrowed.forEach(function(borrowed) {
var borrowedDoclets = doclets.index.longname[borrowed.from];
var borrowedAs = borrowed.as || borrowed.from;
var clonedDoclets;
var parts;
var scopePunc;
if (borrowedDoclets) {
borrowedAs = borrowedAs.replace(/^prototype\./, SCOPE.PUNC.INSTANCE);
clonedDoclets = doop(borrowedDoclets).forEach(function(clone) {
// TODO: this will fail on longnames like '"Foo#bar".baz'
parts = borrowedAs.split(SCOPE.PUNC.INSTANCE);
if (parts.length === 2) {
clone.scope = SCOPE.NAMES.INSTANCE;
scopePunc = SCOPE.PUNC.INSTANCE;
}
else {
clone.scope = SCOPE.NAMES.STATIC;
scopePunc = SCOPE.PUNC.STATIC;
}
clone.name = parts.pop();
clone.memberof = doclet.longname;
clone.longname = clone.memberof + scopePunc + clone.name;
doclets.push(clone);
});
}
});
}
// requires docs to have been indexed: docs.index must be defined here
/**
Take a copy of the docs for borrowed symbols and attach them to the
docs for the borrowing symbol. This process changes the symbols involved,
moving docs from the "borrowed" array and into the general docs, then
deleting the "borrowed" array.
*/
exports.resolveBorrows = function(doclets) {
var doclet;
if (!doclets.index) {
logger.error('Unable to resolve borrowed symbols, because the docs have not been indexed.');
return;
}
for (var i = 0, l = doclets.index.borrowed.length; i < l; i++) {
doclet = doclets.index.borrowed[i];
cloneBorrowedDoclets(doclet, doclets);
delete doclet.borrowed;
}
doclets.index.borrowed = [];
};
@@ -0,0 +1,60 @@
/**
@overview
@author Michael Mathews <micmath@gmail.com>
@license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
/**
@module jsdoc/config
*/
'use strict';
function mergeRecurse(target, source) {
Object.keys(source).forEach(function(p) {
if ( source[p].constructor === Object ) {
if ( !target[p] ) { target[p] = {}; }
mergeRecurse(target[p], source[p]);
}
else {
target[p] = source[p];
}
});
return target;
}
// required config values, override these defaults in your config.json if necessary
var defaults = {
tags: {
allowUnknownTags: true,
dictionaries: ['jsdoc', 'closure']
},
templates: {
monospaceLinks: false,
cleverLinks: false
},
source: {
includePattern: '.+\\.js(doc)?$',
excludePattern: ''
},
plugins: []
};
/**
@class
@classdesc Represents a JSDoc application configuration.
@param {string} [json] - The contents of config.json.
*/
function Config(json) {
json = JSON.parse( (json || '{}') );
this._config = mergeRecurse(defaults, json);
}
module.exports = Config;
/**
Get the merged configuration values.
*/
Config.prototype.get = function() {
return this._config;
};
+416
View File
@@ -0,0 +1,416 @@
/**
* @overview
* @author Michael Mathews <micmath@gmail.com>
* @license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
/**
* @module jsdoc/doclet
*/
'use strict';
var _ = require('underscore');
var jsdoc = {
name: require('jsdoc/name'),
src: {
astnode: require('jsdoc/src/astnode'),
Syntax: require('jsdoc/src/syntax').Syntax
},
tag: {
Tag: require('jsdoc/tag').Tag,
dictionary: require('jsdoc/tag/dictionary')
}
};
var path = require('jsdoc/path');
var Syntax = jsdoc.src.Syntax;
var util = require('util');
function applyTag(doclet, tag) {
if (tag.title === 'name') {
doclet.name = tag.value;
}
if (tag.title === 'kind') {
doclet.kind = tag.value;
}
if (tag.title === 'description') {
doclet.description = tag.value;
}
}
// use the meta info about the source code to guess what the doclet kind should be
function codeToKind(code) {
var parent;
var isFunction = jsdoc.src.astnode.isFunction;
// default
var kind = 'member';
if (code.type === Syntax.FunctionDeclaration || code.type === Syntax.FunctionExpression) {
kind = 'function';
}
else if (code.node && code.node.parent) {
parent = code.node.parent;
if ( isFunction(parent) ) {
kind = 'param';
}
}
return kind;
}
function unwrap(docletSrc) {
if (!docletSrc) { return ''; }
// note: keep trailing whitespace for @examples
// extra opening/closing stars are ignored
// left margin is considered a star and a space
// use the /m flag on regex to avoid having to guess what this platform's newline is
docletSrc =
docletSrc.replace(/^\/\*\*+/, '') // remove opening slash+stars
.replace(/\**\*\/$/, '\\Z') // replace closing star slash with end-marker
.replace(/^\s*(\* ?|\\Z)/gm, '') // remove left margin like: spaces+star or spaces+end-marker
.replace(/\s*\\Z$/g, ''); // remove end-marker
return docletSrc;
}
/**
* Convert the raw source of the doclet comment into an array of pseudo-Tag objects.
* @private
*/
function toTags(docletSrc) {
var parsedTag;
var tagData = [];
var tagText;
var tagTitle;
// split out the basic tags, keep surrounding whitespace
// like: @tagTitle tagBody
docletSrc
// replace splitter ats with an arbitrary sequence
.replace(/^(\s*)@(\S)/gm, '$1\\@$2')
// then split on that arbitrary sequence
.split('\\@')
.forEach(function($) {
if ($) {
parsedTag = $.match(/^(\S+)(?:\s+(\S[\s\S]*))?/);
if (parsedTag) {
tagTitle = parsedTag[1];
tagText = parsedTag[2];
if (tagTitle) {
tagData.push({
title: tagTitle,
text: tagText
});
}
}
}
});
return tagData;
}
function fixDescription(docletSrc) {
if (!/^\s*@/.test(docletSrc) && docletSrc.replace(/\s/g, '').length) {
docletSrc = '@description ' + docletSrc;
}
return docletSrc;
}
/**
* Replace the existing tag dictionary with a new tag dictionary.
*
* Used for testing only.
*
* @private
* @param {module:jsdoc/tag/dictionary.Dictionary} dict - The new tag dictionary.
*/
exports._replaceDictionary = function _replaceDictionary(dict) {
jsdoc.tag.dictionary = dict;
require('jsdoc/tag')._replaceDictionary(dict);
require('jsdoc/util/templateHelper')._replaceDictionary(dict);
};
/**
* @class
* @classdesc Represents a single JSDoc comment.
* @param {string} docletSrc - The raw source code of the jsdoc comment.
* @param {object=} meta - Properties describing the code related to this comment.
*/
var Doclet = exports.Doclet = function(docletSrc, meta) {
var newTags = [];
/** The original text of the comment from the source code. */
this.comment = docletSrc;
this.setMeta(meta);
docletSrc = unwrap(docletSrc);
docletSrc = fixDescription(docletSrc);
newTags = toTags.call(this, docletSrc);
for (var i = 0, l = newTags.length; i < l; i++) {
this.addTag(newTags[i].title, newTags[i].text);
}
this.postProcess();
};
/** Called once after all tags have been added. */
Doclet.prototype.postProcess = function() {
var i;
var l;
if (!this.preserveName) {
jsdoc.name.resolve(this);
}
if (this.name && !this.longname) {
this.setLongname(this.name);
}
if (this.memberof === '') {
delete this.memberof;
}
if (!this.kind && this.meta && this.meta.code) {
this.addTag( 'kind', codeToKind(this.meta.code) );
}
if (this.variation && this.longname && !/\)$/.test(this.longname) ) {
this.longname += '(' + this.variation + ')';
}
// add in any missing param names
if (this.params && this.meta && this.meta.code && this.meta.code.paramnames) {
for (i = 0, l = this.params.length; i < l; i++) {
if (!this.params[i].name) {
this.params[i].name = this.meta.code.paramnames[i] || '';
}
}
}
};
/**
* Add a tag to the doclet.
*
* @param {string} title - The title of the tag being added.
* @param {string} [text] - The text of the tag being added.
*/
Doclet.prototype.addTag = function(title, text) {
var tagDef = jsdoc.tag.dictionary.lookUp(title),
newTag = new jsdoc.tag.Tag(title, text, this.meta);
if (tagDef && tagDef.onTagged) {
tagDef.onTagged(this, newTag);
}
if (!tagDef) {
this.tags = this.tags || [];
this.tags.push(newTag);
}
applyTag(this, newTag);
};
function removeGlobal(longname) {
var globalRegexp = new RegExp('^' + jsdoc.name.LONGNAMES.GLOBAL + '\\.?');
return longname.replace(globalRegexp, '');
}
/**
* Set the doclet's `memberof` property.
*
* @param {string} sid - The longname of the doclet's parent symbol.
*/
Doclet.prototype.setMemberof = function(sid) {
/**
* The longname of the symbol that contains this one, if any.
* @type string
*/
this.memberof = removeGlobal(sid)
.replace(/\.prototype/g, jsdoc.name.SCOPE.PUNC.INSTANCE);
};
/**
* Set the doclet's `longname` property.
*
* @param {string} name - The longname for the doclet.
*/
Doclet.prototype.setLongname = function(name) {
/**
* The fully resolved symbol name.
* @type string
*/
this.longname = removeGlobal(name);
if (jsdoc.tag.dictionary.isNamespace(this.kind)) {
this.longname = jsdoc.name.applyNamespace(this.longname, this.kind);
}
};
/**
* Get the full path to the source file that is associated with a doclet.
*
* @private
* @param {module:jsdoc/doclet.Doclet} The doclet to check for a filepath.
* @return {string} The path to the doclet's source file, or an empty string if the path is not
* available.
*/
function getFilepath(doclet) {
if (!doclet || !doclet.meta || !doclet.meta.filename) {
return '';
}
return path.join(doclet.meta.path || '', doclet.meta.filename);
}
/**
* Set the doclet's `scope` property. Must correspond to a scope name that is defined in
* {@link module:jsdoc/name.SCOPE.NAMES}.
*
* @param {module:jsdoc/name.SCOPE.NAMES} scope - The scope for the doclet relative to the symbol's
* parent.
* @throws {Error} If the scope name is not recognized.
*/
Doclet.prototype.setScope = function(scope) {
var errorMessage;
var filepath;
var scopeNames = _.values(jsdoc.name.SCOPE.NAMES);
if (scopeNames.indexOf(scope) === -1) {
filepath = getFilepath(this);
errorMessage = util.format('The scope name "%s" is not recognized. Use one of the ' +
'following values: %j', scope, scopeNames);
if (filepath) {
errorMessage += util.format(' (Source file: %s)', filepath);
}
throw new Error(errorMessage);
}
this.scope = scope;
};
/**
* Add a symbol to this doclet's `borrowed` array.
*
* @param {string} source - The longname of the symbol that is the source.
* @param {string} target - The name the symbol is being assigned to.
*/
Doclet.prototype.borrow = function(source, target) {
var about = { from: source };
if (target) {
about.as = target;
}
if (!this.borrowed) {
/**
* A list of symbols that are borrowed by this one, if any.
* @type Array.<string>
*/
this.borrowed = [];
}
this.borrowed.push(about);
};
Doclet.prototype.mix = function(source) {
/**
* A list of symbols that are mixed into this one, if any.
* @type Array.<string>
*/
this.mixes = this.mixes || [];
this.mixes.push(source);
};
/**
* Add a symbol to the doclet's `augments` array.
*
* @param {string} base - The longname of the base symbol.
*/
Doclet.prototype.augment = function(base) {
/**
* A list of symbols that are augmented by this one, if any.
* @type Array.<string>
*/
this.augments = this.augments || [];
this.augments.push(base);
};
/**
* Set the `meta` property of this doclet.
*
* @param {object} meta
*/
Doclet.prototype.setMeta = function(meta) {
/**
* Information about the source code associated with this doclet.
* @namespace
*/
this.meta = this.meta || {};
if (meta.range) {
/**
* The positions of the first and last characters of the code associated with this doclet.
* @type Array.<number>
*/
this.meta.range = meta.range.slice(0);
}
if (meta.lineno) {
/**
* The name of the file containing the code associated with this doclet.
* @type string
*/
this.meta.filename = path.basename(meta.filename);
/**
* The line number of the code associated with this doclet.
* @type number
*/
this.meta.lineno = meta.lineno;
var pathname = path.dirname(meta.filename);
if (pathname && pathname !== '.') {
this.meta.path = pathname;
}
}
/**
* Information about the code symbol.
* @namespace
*/
this.meta.code = this.meta.code || {};
if (meta.id) { this.meta.code.id = meta.id; }
if (meta.code) {
if (meta.code.name) {
/** The name of the symbol in the source code. */
this.meta.code.name = meta.code.name;
}
if (meta.code.type) {
/** The type of the symbol in the source code. */
this.meta.code.type = meta.code.type;
}
// the AST node is only enumerable in debug mode, which reduces clutter for the
// --explain/-X option
if (meta.code.node) {
Object.defineProperty(this.meta.code, 'node', {
value: meta.code.node,
enumerable: global.env.opts.debug ? true : false
});
}
if (meta.code.funcscope) {
this.meta.code.funcscope = meta.code.funcscope;
}
if (meta.code.value) {
/** The value of the symbol in the source code. */
this.meta.code.value = meta.code.value;
}
if (meta.code.paramnames) {
this.meta.code.paramnames = meta.code.paramnames.slice(0);
}
}
};
+74
View File
@@ -0,0 +1,74 @@
/**
* Extended version of the standard `fs` module.
* @module jsdoc/fs
*/
'use strict';
var fs = require('fs');
var path = require('path');
var runtime = require('jsdoc/util/runtime');
var ls = exports.ls = function(dir, recurse, _allFiles, _path) {
var file;
var files;
var isFile;
// first pass
if (_path === undefined) {
_allFiles = [];
_path = [dir];
}
if (!_path.length) {
return _allFiles;
}
if (recurse === undefined) {
recurse = 1;
}
try {
isFile = fs.statSync(dir).isFile();
}
catch (e) {
isFile = false;
}
if (isFile) {
files = [dir];
}
else {
files = fs.readdirSync(dir);
}
for (var i = 0, l = files.length; i < l; i++) {
file = String(files[i]);
// skip dot files
if (file.match(/^\.[^\.\/\\]/)) {
continue;
}
if ( fs.statSync(path.join(_path.join('/'), file)).isDirectory() ) {
// it's a directory
_path.push(file);
if (_path.length - 1 < recurse) {
ls(_path.join('/'), recurse, _allFiles, _path);
}
_path.pop();
}
else {
// it's a file
_allFiles.push( path.normalize(path.join(_path.join('/'), file)) );
}
}
return _allFiles;
};
// export the VM-specific implementations of the extra methods
// TODO: document extra methods here
var extras = require( runtime.getModulePath('fs') );
Object.keys(extras).forEach(function(extra) {
exports[extra] = extras[extra];
});
+432
View File
@@ -0,0 +1,432 @@
/**
A collection of functions relating to JSDoc symbol name manipulation.
@module jsdoc/name
@author Michael Mathews <micmath@gmail.com>
@license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
'use strict';
var _ = require('underscore');
var escape = require('escape-string-regexp');
var hasOwnProp = Object.prototype.hasOwnProperty;
/**
* Longnames that have a special meaning in JSDoc.
*
* @enum {string}
* @static
* @memberof module:jsdoc/name
*/
var LONGNAMES = exports.LONGNAMES = {
/** Longname used for doclets that do not have a longname, such as anonymous functions. */
ANONYMOUS: '<anonymous>',
/** Longname that represents global scope. */
GLOBAL: '<global>'
};
// Module namespace prefix.
var MODULE_NAMESPACE = 'module:';
/**
* Names and punctuation marks that identify doclet scopes.
*
* @enum {string}
* @static
* @memberof module:jsdoc/name
*/
var SCOPE = exports.SCOPE = {
NAMES: {
GLOBAL: 'global',
INNER: 'inner',
INSTANCE: 'instance',
STATIC: 'static'
},
PUNC: {
INNER: '~',
INSTANCE: '#',
STATIC: '.'
}
};
// For backwards compatibility, this enum must use lower-case keys
var scopeToPunc = exports.scopeToPunc = {
'inner': SCOPE.PUNC.INNER,
'instance': SCOPE.PUNC.INSTANCE,
'static': SCOPE.PUNC.STATIC
};
var puncToScope = exports.puncToScope = _.invert(scopeToPunc);
var DEFAULT_SCOPE = SCOPE.NAMES.STATIC;
var SCOPE_PUNC = _.values(SCOPE.PUNC);
var SCOPE_PUNC_STRING = '[' + SCOPE_PUNC.join() + ']';
var REGEXP_LEADING_SCOPE = new RegExp('^(' + SCOPE_PUNC_STRING + ')');
var REGEXP_TRAILING_SCOPE = new RegExp('(' + SCOPE_PUNC_STRING + ')$');
var DESCRIPTION = '(?:(?:[ \\t]*\\-\\s*|\\s+)(\\S[\\s\\S]*))?$';
var REGEXP_DESCRIPTION = new RegExp(DESCRIPTION);
var REGEXP_NAME_DESCRIPTION = new RegExp('^(\\[[^\\]]+\\]|\\S+)' + DESCRIPTION);
function nameIsLongname(name, memberof) {
var regexp = new RegExp('^' + escape(memberof) + SCOPE_PUNC_STRING);
return regexp.test(name);
}
function prototypeToPunc(name) {
return name.replace(/(?:^|\.)prototype\.?/g, SCOPE.PUNC.INSTANCE);
}
// TODO: deprecate exports.resolve in favor of a better name
/**
Resolves the longname, memberof, variation and name values of the given doclet.
@param {module:jsdoc/doclet.Doclet} doclet
*/
exports.resolve = function(doclet) {
var about = {};
var memberof = doclet.memberof || '';
var name = doclet.name ? String(doclet.name) : '';
var parentDoc;
// change MyClass.prototype.instanceMethod to MyClass#instanceMethod
// (but not in function params, which lack doclet.kind)
// TODO: check for specific doclet.kind values (probably function, class, and module)
if (name && doclet.kind) {
name = prototypeToPunc(name);
}
doclet.name = name;
// member of a var in an outer scope?
if (name && !memberof && doclet.meta.code && doclet.meta.code.funcscope) {
name = doclet.longname = doclet.meta.code.funcscope + SCOPE.PUNC.INNER + name;
}
if (memberof || doclet.forceMemberof) { // @memberof tag given
memberof = prototypeToPunc(memberof);
// the name is a complete longname, like @name foo.bar, @memberof foo
if (name && nameIsLongname(name, memberof) && name !== memberof) {
about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
}
// the name and memberof are identical and refer to a module,
// like @name module:foo, @memberof module:foo (probably a member like 'var exports')
else if (name && name === memberof && name.indexOf(MODULE_NAMESPACE) === 0) {
about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
}
// the name and memberof are identical, like @name foo, @memberof foo
else if (name && name === memberof) {
doclet.scope = doclet.scope || DEFAULT_SCOPE;
name = memberof + scopeToPunc[doclet.scope] + name;
about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
}
// like @memberof foo# or @memberof foo~
else if (name && REGEXP_TRAILING_SCOPE.test(memberof) ) {
about = exports.shorten(memberof + name, (doclet.forceMemberof ? memberof : undefined));
}
else if (name && doclet.scope) {
about = exports.shorten(memberof + (scopeToPunc[doclet.scope] || '') + name,
(doclet.forceMemberof ? memberof : undefined));
}
}
else { // no @memberof
about = exports.shorten(name);
}
if (about.name) {
doclet.name = about.name;
}
if (about.memberof) {
doclet.setMemberof(about.memberof);
}
if (about.longname && (!doclet.longname || doclet.longname === doclet.name)) {
doclet.setLongname(about.longname);
}
if (doclet.scope === SCOPE.NAMES.GLOBAL) { // via @global tag?
doclet.setLongname(doclet.name);
delete doclet.memberof;
}
else if (about.scope) {
if (about.memberof === LONGNAMES.GLOBAL) { // via @memberof <global> ?
doclet.scope = SCOPE.NAMES.GLOBAL;
}
else {
doclet.scope = puncToScope[about.scope];
}
}
else if (doclet.name && doclet.memberof && !doclet.longname) {
if ( REGEXP_LEADING_SCOPE.test(doclet.name) ) {
doclet.scope = puncToScope[RegExp.$1];
doclet.name = doclet.name.substr(1);
}
else {
doclet.scope = DEFAULT_SCOPE;
}
doclet.setLongname(doclet.memberof + scopeToPunc[doclet.scope] + doclet.name);
}
if (about.variation) {
doclet.variation = about.variation;
}
// if we never found a longname, just use an empty string
if (!doclet.longname) {
doclet.longname = '';
}
};
/**
@method module:jsdoc/name.applyNamespace
@param {string} longname The full longname of the symbol.
@param {string} ns The namespace to be applied.
@returns {string} The longname with the namespace applied.
*/
exports.applyNamespace = function(longname, ns) {
var nameParts = exports.shorten(longname),
name = nameParts.name;
longname = nameParts.longname;
if ( !/^[a-zA-Z]+?:.+$/i.test(name) ) {
longname = longname.replace( new RegExp(escape(name) + '$'), ns + ':' + name );
}
return longname;
};
// TODO: docs
function atomize(longname, sliceChars, forcedMemberof) {
var i;
var memberof = '';
var name = '';
var parts;
var partsRegExp;
var scopePunc = '';
var token;
var tokens = [];
var variation;
// quoted strings in a longname are atomic, so we convert them to tokens
longname = longname.replace(/(\[?["'].+?["']\]?)/g, function($) {
var dot = '';
if ( /^\[/.test($) ) {
dot = '.';
$ = $.replace( /^\[/g, '' ).replace( /\]$/g, '' );
}
token = '@{' + tokens.length + '}@';
tokens.push($);
return dot + token; // foo["bar"] => foo.@{1}@
});
longname = prototypeToPunc(longname);
if (forcedMemberof !== undefined) {
partsRegExp = new RegExp('^(.*?)([' + sliceChars.join() + ']?)$');
name = longname.substr(forcedMemberof.length);
parts = forcedMemberof.match(partsRegExp);
if (parts[1]) {
memberof = parts[1] || forcedMemberof;
}
if (parts[2]) {
scopePunc = parts[2];
}
}
else if (longname) {
parts = (longname.match(new RegExp('^(:?(.+)([' + sliceChars.join() + ']))?(.+?)$')) || [])
.reverse();
name = parts[0] || '';
scopePunc = parts[1] || '';
memberof = parts[2] || '';
}
// like /** @name foo.bar(2) */
if ( /(.+)\(([^)]+)\)$/.test(name) ) {
name = RegExp.$1;
variation = RegExp.$2;
}
// restore quoted strings
i = tokens.length;
while (i--) {
longname = longname.replace('@{' + i + '}@', tokens[i]);
memberof = memberof.replace('@{' + i + '}@', tokens[i]);
scopePunc = scopePunc.replace('@{' + i + '}@', tokens[i]);
name = name.replace('@{' + i + '}@', tokens[i]);
}
return {
longname: longname,
memberof: memberof,
scope: scopePunc,
name: name,
variation: variation
};
}
// TODO: deprecate exports.shorten in favor of a better name
/**
Given a longname like "a.b#c(2)", slice it up into an object
containing the memberof, the scope, the name, and variation.
@param {string} longname
@param {string} forcedMemberof
@returns {object} Representing the properties of the given name.
*/
exports.shorten = function(longname, forcedMemberof) {
return atomize(longname, SCOPE_PUNC, forcedMemberof);
};
// TODO: docs
exports.combine = function(parts) {
return '' +
(parts.memberof || '') +
(parts.scope || '') +
(parts.name || '') +
(parts.variation || '');
};
function stripVariation(name) {
return name.replace(/\([^)]\)$/, '');
}
function splitLongname(longname, options) {
var chunks = [];
var currentNameInfo;
var nameInfo = {};
var previousName = longname;
var splitters = SCOPE_PUNC.concat('/');
options = _.defaults(options || {}, {
includeVariation: true
});
do {
if (!options.includeVariation) {
previousName = stripVariation(previousName);
}
currentNameInfo = nameInfo[previousName] = atomize(previousName, splitters);
previousName = currentNameInfo.memberof;
chunks.push(currentNameInfo.scope + currentNameInfo.name);
} while (previousName);
return {
chunks: chunks.reverse(),
nameInfo: nameInfo
};
}
// TODO: docs
exports.longnamesToTree = function longnamesToTree(longnames, doclets) {
var splitOptions = { includeVariation: false };
var tree = {};
longnames.forEach(function(longname) {
var chunk;
var currentLongname = '';
var currentNavItem = tree;
var nameInfo;
var processed;
// don't try to add empty longnames to the tree
if (!longname) {
return;
}
processed = splitLongname(longname, splitOptions);
nameInfo = processed.nameInfo;
processed.chunks.forEach(function(chunk) {
currentLongname += chunk;
if (!hasOwnProp.call(currentNavItem, chunk)) {
currentNavItem[chunk] = nameInfo[currentLongname];
}
if (currentNavItem[chunk]) {
currentNavItem[chunk].doclet = doclets ? doclets[currentLongname] : null;
currentNavItem[chunk].children = currentNavItem[chunk].children || {};
currentNavItem = currentNavItem[chunk].children;
}
});
});
return tree;
};
/**
Split a string that starts with a name and ends with a description into its parts.
Allows the defaultvalue (if present) to contain brackets. If the name is found to have
mismatched brackets, null is returned.
@param {string} nameDesc
@returns {object} Hash with "name" and "description" properties.
*/
function splitNameMatchingBrackets(nameDesc) {
var buffer = [];
var c;
var stack = 0;
var stringEnd = null;
for (var i = 0; i < nameDesc.length; ++i) {
c = nameDesc[i];
buffer.push(c);
if (stringEnd) {
if (c === '\\' && i + 1 < nameDesc.length) {
buffer.push(nameDesc[++i]);
} else if (c === stringEnd) {
stringEnd = null;
}
} else if (c === '"' || c === "'") {
stringEnd = c;
} else if (c === '[') {
++stack;
} else if (c === ']') {
if (--stack === 0) {
break;
}
}
}
if (stack || stringEnd) {
return null;
}
nameDesc.substr(i).match(REGEXP_DESCRIPTION);
return {
name: buffer.join(''),
description: RegExp.$1
};
}
// TODO: deprecate exports.splitName in favor of a better name
/**
Split a string that starts with a name and ends with a description into its parts.
@param {string} nameDesc
@returns {object} Hash with "name" and "description" properties.
*/
exports.splitName = function(nameDesc) {
// like: name, [name], name text, [name] text, name - text, or [name] - text
// the hyphen must be on the same line as the name; this prevents us from treating a Markdown
// dash as a separator
// optional values get special treatment
var result = null;
if (nameDesc[0] === '[') {
result = splitNameMatchingBrackets(nameDesc);
if (result !== null) {
return result;
}
}
nameDesc.match(REGEXP_NAME_DESCRIPTION);
return {
name: RegExp.$1,
description: RegExp.$2
};
};
@@ -0,0 +1,302 @@
/**
* Parse the command line arguments.
* @module jsdoc/opts/argparser
* @author Michael Mathews <micmath@gmail.com>
* @license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
'use strict';
var _ = require('underscore');
var util = require('util');
var hasOwnProp = Object.prototype.hasOwnProperty;
/**
* Create an instance of the parser.
* @classdesc A parser to interpret the key-value pairs entered on the command line.
* @constructor
* @alias module:jsdoc/opts/argparser
*/
var ArgParser = function() {
this._options = [];
this._shortNameIndex = {};
this._longNameIndex = {};
};
ArgParser.prototype._getOptionByShortName = function(name) {
if (hasOwnProp.call(this._shortNameIndex, name)) {
return this._options[this._shortNameIndex[name]];
}
return null;
};
ArgParser.prototype._getOptionByLongName = function(name) {
if (hasOwnProp.call(this._longNameIndex, name)) {
return this._options[this._longNameIndex[name]];
}
return null;
};
ArgParser.prototype._addOption = function(option) {
var currentIndex;
var longName = option.longName;
var shortName = option.shortName;
this._options.push(option);
currentIndex = this._options.length - 1;
if (shortName) {
this._shortNameIndex[shortName] = currentIndex;
}
if (longName) {
this._longNameIndex[longName] = currentIndex;
}
return this;
};
/**
* Provide information about a legal option.
* @param {character} shortName The short name of the option, entered like: -T.
* @param {string} longName The equivalent long name of the option, entered like: --test.
* @param {boolean} hasValue Does this option require a value? Like: -t templatename
* @param {string} helpText A brief description of the option.
* @param {boolean} [canHaveMultiple=false] Set to `true` if the option can be provided more than once.
* @param {function} [coercer] A function to coerce the given value to a specific type.
* @return {this}
* @example
* myParser.addOption('t', 'template', true, 'The path to the template.');
* myParser.addOption('h', 'help', false, 'Show the help message.');
*/
ArgParser.prototype.addOption = function(shortName, longName, hasValue, helpText, canHaveMultiple, coercer) {
var option = {
shortName: shortName,
longName: longName,
hasValue: hasValue,
helpText: helpText,
canHaveMultiple: (canHaveMultiple || false),
coercer: coercer
};
return this._addOption(option);
};
// TODO: refactor addOption to accept objects, then get rid of this method
/**
* Provide information about an option that should not cause an error if present, but that is always
* ignored (for example, an option that was used in previous versions but is no longer supported).
*
* @private
* @param {string} shortName - The short name of the option with a leading hyphen (for example,
* `-v`).
* @param {string} longName - The long name of the option with two leading hyphens (for example,
* `--version`).
*/
ArgParser.prototype.addIgnoredOption = function(shortName, longName) {
var option = {
shortName: shortName,
longName: longName,
ignore: true
};
return this._addOption(option);
};
function padding(length) {
return new Array(length + 1).join(' ');
}
function padLeft(str, length) {
return padding(length) + str;
}
function padRight(str, length) {
return str + padding(length);
}
function findMaxLength(arr) {
var max = 0;
arr.forEach(function(item) {
if (item.length > max) {
max = item.length;
}
});
return max;
}
function concatWithMaxLength(items, maxLength) {
var result = '';
// to prevent endless loops, always use the first item, regardless of length
result += items.shift();
while ( items.length && (result.length + items[0].length < maxLength) ) {
result += ' ' + items.shift();
}
return result;
}
// we want to format names and descriptions like this:
// | -f, --foo Very long description very long description very long |
// | description very long description. |
function formatHelpInfo(options) {
var MARGIN_LENGTH = 4;
var results = [];
var maxLength = process.stdout.columns;
var maxNameLength = findMaxLength(options.names);
var maxDescriptionLength = findMaxLength(options.descriptions);
var wrapDescriptionAt = maxLength - (MARGIN_LENGTH * 3) - maxNameLength;
// build the string for each option
options.names.forEach(function(name, i) {
var result;
var partialDescription;
var words;
// add a left margin to the name
result = padLeft(options.names[i], MARGIN_LENGTH);
// and a right margin, with extra padding so the descriptions line up with one another
result = padRight(result, maxNameLength - options.names[i].length + MARGIN_LENGTH);
// split the description on spaces
words = options.descriptions[i].split(' ');
// add as much of the description as we can fit on the first line
result += concatWithMaxLength(words, wrapDescriptionAt);
// if there's anything left, keep going until we've consumed the description
while (words.length) {
partialDescription = padding( maxNameLength + (MARGIN_LENGTH * 2) );
partialDescription += concatWithMaxLength(words, wrapDescriptionAt);
result += '\n' + partialDescription;
}
results.push(result);
});
return results;
}
/**
* Generate a summary of all the options with corresponding help text.
* @returns {string}
*/
ArgParser.prototype.help = function() {
var options = {
names: [],
descriptions: []
};
this._options.forEach(function(option) {
var name = '';
// don't show ignored options
if (option.ignore) {
return;
}
if (option.shortName) {
name += '-' + option.shortName + (option.longName ? ', ' : '');
}
if (option.longName) {
name += '--' + option.longName;
}
if (option.hasValue) {
name += ' <value>';
}
options.names.push(name);
options.descriptions.push(option.helpText);
});
return 'Options:\n' + formatHelpInfo(options).join('\n');
};
/**
* Get the options.
* @param {Array.<string>} args An array, like ['-x', 'hello']
* @param {Object} [defaults={}] An optional collection of default values.
* @returns {Object} The keys will be the longNames, or the shortName if no longName is defined for
* that option. The values will be the values provided, or `true` if the option accepts no value.
*/
ArgParser.prototype.parse = function(args, defaults) {
var result = defaults && _.defaults({}, defaults) || {};
result._ = [];
for (var i = 0, leni = args.length; i < leni; i++) {
var arg = '' + args[i],
next = (i < leni - 1) ? '' + args[i + 1] : null,
option,
shortName = null,
longName,
name,
value = null;
// like -t
if (arg.charAt(0) === '-') {
// like --template
if (arg.charAt(1) === '-') {
name = longName = arg.slice(2);
option = this._getOptionByLongName(longName);
}
else {
name = shortName = arg.slice(1);
option = this._getOptionByShortName(shortName);
}
if (option === null) {
throw new Error( util.format('Unknown command-line option "%s".', name) );
}
if (option.hasValue) {
value = next;
i++;
if (value === null || value.charAt(0) === '-') {
throw new Error( util.format('The command-line option "%s" requires a value.', name) );
}
}
else {
value = true;
}
// skip ignored options now that we've consumed the option text
if (option.ignore) {
continue;
}
if (option.longName && shortName) {
name = option.longName;
}
if (typeof option.coercer === 'function') {
value = option.coercer(value);
}
// Allow for multiple options of the same type to be present
if (option.canHaveMultiple && hasOwnProp.call(result, name)) {
var val = result[name];
if (val instanceof Array) {
val.push(value);
} else {
result[name] = [val, value];
}
}
else {
result[name] = value;
}
}
else {
result._.push(arg);
}
}
return result;
};
module.exports = ArgParser;
@@ -0,0 +1,141 @@
/**
* @module jsdoc/opts/args
* @requires jsdoc/opts/argparser
* @author Michael Mathews <micmath@gmail.com>
* @license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
'use strict';
var ArgParser = require('jsdoc/opts/argparser');
var querystring = require('querystring');
var util = require('util');
var ourOptions;
var argParser = new ArgParser();
var hasOwnProp = Object.prototype.hasOwnProperty;
// cast strings to booleans or integers where appropriate
function castTypes(item) {
var integer;
var result = item;
switch (result) {
case 'true':
result = true;
break;
case 'false':
result = false;
break;
default:
// might be an integer
integer = parseInt(result, 10);
if (String(integer) === result && integer !== 'NaN') {
result = integer;
}
}
return result;
}
// check for strings that we need to cast to other types
function fixTypes(item) {
var result = item;
// recursively process arrays and objects
if ( util.isArray(result) ) {
for (var i = 0, l = result.length; i < l; i++) {
result[i] = fixTypes(result[i]);
}
}
else if (typeof result === 'object') {
Object.keys(result).forEach(function(prop) {
result[prop] = fixTypes(result[prop]);
});
}
else {
result = castTypes(result);
}
return result;
}
function parseQuery(str) {
var result = querystring.parse(str);
Object.keys(result).forEach(function(prop) {
result[prop] = fixTypes(result[prop]);
});
return result;
}
argParser.addOption('t', 'template', true, 'The path to the template to use. Default: path/to/jsdoc/templates/default');
argParser.addOption('c', 'configure', true, 'The path to the configuration file. Default: path/to/jsdoc/conf.json');
argParser.addOption('e', 'encoding', true, 'Assume this encoding when reading all source files. Default: utf8');
argParser.addOption('T', 'test', false, 'Run all tests and quit.');
argParser.addOption('d', 'destination', true, 'The path to the output folder. Use "console" to dump data to the console. Default: ./out/');
argParser.addOption('p', 'private', false, 'Display symbols marked with the @private tag. Default: false');
argParser.addOption('r', 'recurse', false, 'Recurse into subdirectories when scanning for source code files.');
argParser.addOption('h', 'help', false, 'Print this message and quit.');
argParser.addOption('X', 'explain', false, 'Dump all found doclet internals to console and quit.');
argParser.addOption('q', 'query', true, 'A query string to parse and store in env.opts.query. Example: foo=bar&baz=true', false, parseQuery);
argParser.addOption('u', 'tutorials', true, 'Directory in which JSDoc should search for tutorials.');
argParser.addOption('P', 'package', true, 'The path to the project\'s package file. Default: path/to/sourcefiles/package.json');
argParser.addOption('R', 'readme', true, 'The path to the project\'s README file. Default: path/to/sourcefiles/README.md');
argParser.addOption('v', 'version', false, 'Display the version number and quit.');
argParser.addOption('', 'debug', false, 'Log information for debugging JSDoc. On Rhino, launches the debugger when passed as the first option.');
argParser.addOption('', 'verbose', false, 'Log detailed information to the console as JSDoc runs.');
argParser.addOption('', 'pedantic', false, 'Treat errors as fatal errors, and treat warnings as errors. Default: false');
// Options specific to tests
argParser.addOption(null, 'match', true, 'Only run tests containing <value>.', true);
argParser.addOption(null, 'nocolor', false, 'Do not use color in console output from tests.');
// Options that are no longer supported and should be ignored
argParser.addIgnoredOption('l', 'lenient'); // removed in JSDoc 3.3.0
/**
* Set the options for this app.
* @throws {Error} Illegal arguments will throw errors.
* @param {string|String[]} args The command line arguments for this app.
*/
exports.parse = function(args) {
args = args || [];
if (typeof args === 'string' || args.constructor === String) {
args = String(args).split(/\s+/g);
}
ourOptions = argParser.parse(args);
return ourOptions;
};
/**
* Retrieve help message for options.
*/
exports.help = function() {
return argParser.help();
};
/**
* Get a named option.
* @variation (name)
* @param {string} name The name of the option.
* @return {string} The value associated with the given name.
*//**
* Get all the options for this app.
* @return {Object} A collection of key/values representing all the options.
*/
exports.get = function(name) {
if (typeof name === 'undefined') {
return ourOptions;
}
else if ( hasOwnProp.call(ourOptions, name) ) {
return ourOptions[name];
}
};
@@ -0,0 +1,254 @@
'use strict';
var logger = require('jsdoc/util/logger');
/**
* Provides access to information about a JavaScript package.
*
* @module jsdoc/package
* @see https://www.npmjs.org/doc/files/package.json.html
*/
// Collect all of the license information from a `package.json` file.
function getLicenses(packageInfo) {
var licenses = packageInfo.licenses ? packageInfo.licenses.slice(0) : [];
if (packageInfo.license) {
licenses.push({ type: packageInfo.license });
}
return licenses;
}
/**
* Information about where to report bugs in the package.
*
* @typedef {Object} module:jsdoc/package.Package~BugInfo
* @property {string} email - The email address for reporting bugs.
* @property {string} url - The URL for reporting bugs.
*/
/**
* Information about a package's software license.
*
* @typedef {Object} module:jsdoc/package.Package~LicenseInfo
* @property {string} type - An identifier for the type of license.
* @property {string} url - The URL for the complete text of the license.
*/
/**
* Information about a package author or contributor.
*
* @typedef {Object} module:jsdoc/package.Package~PersonInfo
* @property {string} name - The person's full name.
* @property {string} email - The person's email address.
* @property {string} url - The URL of the person's website.
*/
/**
* Information about a package's version-control repository.
*
* @typedef {Object} module:jsdoc/package.Package~RepositoryInfo
* @property {string} type - The type of version-control system that the repository uses (for
* example, `git` or `svn`).
* @property {string} url - The URL for the repository.
*/
/**
* Information about a JavaScript package. JSDoc can extract package information from
* `package.json` files that follow the
* [npm specification](https://www.npmjs.org/doc/files/package.json.html).
*
* **Note**: JSDoc does not validate or normalize the contents of `package.json` files. If your
* `package.json` file does not follow the npm specification, some properties of the `Package`
* object may not use the format documented here.
*
* @class
* @param {string} json - The contents of the `package.json` file.
*/
exports.Package = function(json) {
var packageInfo;
/**
* The string identifier that is shared by all `Package` objects.
*
* @readonly
* @default
* @type {string}
*/
this.kind = 'package';
try {
packageInfo = JSON.parse(json || '{}');
}
catch (e) {
logger.error('Unable to parse the package file: %s', e.message);
packageInfo = {};
}
if (packageInfo.name) {
/**
* The package name.
*
* @type {string}
*/
this.name = packageInfo.name;
}
/**
* The unique longname for this `Package` object.
*
* @type {string}
*/
this.longname = this.kind + ':' + this.name;
if (packageInfo.author) {
/**
* The author of this package. Contains either a
* {@link module:jsdoc/package.Package~PersonInfo PersonInfo} object or a string with
* information about the author.
*
* @type {(module:jsdoc/package.Package~PersonInfo|string)}
* @since 3.3.0
*/
this.author = packageInfo.author;
}
if (packageInfo.bugs) {
/**
* Information about where to report bugs in the project. May contain a URL, as a string, or
* an object with more detailed information.
*
* @type {(string|module:jsdoc/package.Package~BugInfo)}
* @since 3.3.0
*/
this.bugs = packageInfo.bugs;
}
if (packageInfo.contributors) {
/**
* The contributors to this package.
*
* @type {Array.<(module:jsdoc/package.Package~PersonInfo|string)>}
* @since 3.3.0
*/
this.contributors = packageInfo.contributors;
}
if (packageInfo.dependencies) {
/**
* The dependencies for this package.
*
* @type {Object}
* @since 3.3.0
*/
this.dependencies = packageInfo.dependencies;
}
if (packageInfo.description) {
/**
* A brief description of the package.
*
* @type {string}
*/
this.description = packageInfo.description;
}
if (packageInfo.devDependencies) {
/**
* The development dependencies for this package.
*
* @type {Object}
* @since 3.3.0
*/
this.devDependencies = packageInfo.devDependencies;
}
if (packageInfo.engines) {
/**
* The JavaScript engines that this package supports. Each key is a string that identifies the
* engine (for example, `node`). Each value is a
* [semver](https://www.npmjs.org/doc/misc/semver.html)-compliant version number for the engine.
*
* @type {Object}
* @since 3.3.0
*/
this.engines = packageInfo.engines;
}
/**
* The source files associated with the package.
*
* New `Package` objects always contain an empty array, regardless of whether the `package.json`
* file includes a `files` property.
*
* After JSDoc parses your input files, it sets this property to a list of paths to your input
* files.
*
* @type {Array.<string>}
*/
this.files = [];
if (packageInfo.homepage) {
/**
* The URL for the package's homepage.
*
* @type {string}
* @since 3.3.0
*/
this.homepage = packageInfo.homepage;
}
if (packageInfo.keywords) {
/**
* Keywords to help users find the package.
*
* @type {Array.<string>}
* @since 3.3.0
*/
this.keywords = packageInfo.keywords;
}
if (packageInfo.license || packageInfo.licenses) {
/**
* The licenses used by this package. Combines information from the `package.json` file's
* `license` property and the deprecated `licenses` property.
*
* @type {Array.<module:jsdoc/package.Package~LicenseInfo>}
*/
this.licenses = getLicenses(packageInfo);
}
if (packageInfo.main) {
/**
* The module ID that provides the primary entry point to the package. For example, if your
* package is a CommonJS module, and the value of this property is `foo`, users should be able
* to load your module with `require('foo')`.
*
* @type {string}
* @since 3.3.0
*/
this.main = packageInfo.main;
}
if (packageInfo.repository) {
/**
* The version-control repository for the package.
*
* @type {module:jsdoc/package.Package~RepositoryInfo}
* @since 3.3.0
*/
this.repository = packageInfo.repository;
}
if (packageInfo.version) {
/**
* The [semver](https://www.npmjs.org/doc/misc/semver.html)-compliant version number of the
* package.
*
* @type {string}
* @since 3.2.0
*/
this.version = packageInfo.version;
}
};
+134
View File
@@ -0,0 +1,134 @@
/*global env: true */
/**
* Extended version of the standard `path` module.
* @module jsdoc/path
*/
'use strict';
var fs = require('fs');
var path = require('path');
var runtime = require('jsdoc/util/runtime');
function prefixReducer(previousPath, current) {
var currentPath = [];
// if previousPath is defined, but has zero length, there's no common prefix; move along
if (previousPath && !previousPath.length) {
return currentPath;
}
currentPath = path.resolve(global.env.pwd, current).split(path.sep) || [];
if (previousPath && currentPath.length) {
// remove chunks that exceed the previous path's length
currentPath = currentPath.slice(0, previousPath.length);
// if a chunk doesn't match the previous path, remove everything from that chunk on
for (var i = 0, l = currentPath.length; i < l; i++) {
if (currentPath[i] !== previousPath[i]) {
currentPath.splice(i, currentPath.length - i);
break;
}
}
}
return currentPath;
}
/**
* Find the common prefix for an array of paths. If there is a common prefix, a trailing separator
* is appended to the prefix. Relative paths are resolved relative to the current working directory.
*
* For example, assuming that the current working directory is `/Users/jsdoc`:
*
* + For the single path `foo/bar/baz/qux.js`, the common prefix is `foo/bar/baz/`.
* + For paths `foo/bar/baz/qux.js`, `foo/bar/baz/quux.js`, and `foo/bar/baz.js`, the common prefix
* is `/Users/jsdoc/foo/bar/`.
* + For paths `../jsdoc/foo/bar/baz/qux/quux/test.js`, `/Users/jsdoc/foo/bar/bazzy.js`, and
* `../../Users/jsdoc/foo/bar/foobar.js`, the common prefix is `/Users/jsdoc/foo/bar/`.
* + For paths `foo/bar/baz/qux.js` and `../../Library/foo/bar/baz.js`, there is no common prefix,
* and an empty string is returned.
*
* @param {Array.<string>} paths - The paths to search for a common prefix.
* @return {string} The common prefix, or an empty string if there is no common prefix.
*/
exports.commonPrefix = function(paths) {
var segments;
var prefix = '';
paths = paths || [];
// if there's only one path, its resolved dirname (plus a trailing slash) is the common prefix
if (paths.length === 1) {
prefix = path.resolve(global.env.pwd, paths[0]);
if ( path.extname(prefix) ) {
prefix = path.dirname(prefix);
}
prefix += path.sep;
}
else {
segments = paths.reduce(prefixReducer, undefined) || [];
// if there's anything left (other than a placeholder for a leading slash), add a
// placeholder for a trailing slash
if ( segments.length && (segments.length > 1 || segments[0] !== '') ) {
segments.push('');
}
prefix = segments.join(path.sep);
}
return prefix;
};
/**
* Retrieve the fully qualified path to the requested resource.
*
* If the resource path is specified as a relative path, JSDoc searches for the path in the
* directory where the JSDoc configuration file is located, then in the current working directory,
* and finally in the JSDoc directory.
*
* If the resource path is specified as a fully qualified path, JSDoc uses the path as-is.
*
* @param {string} filepath - The path to the requested resource. May be an absolute path; a path
* relative to the JSDoc directory; or a path relative to the current working directory.
* @param {string} [filename] - The filename of the requested resource.
* @return {string} The fully qualified path (or, on Rhino, a URI) to the requested resource.
* Includes the filename if one was provided.
*/
exports.getResourcePath = function(filepath, filename) {
var result = null;
function pathExists(_path) {
try {
fs.readdirSync(_path);
}
catch(e) {
return false;
}
return true;
}
// absolute paths are normalized by path.resolve on the first pass
[path.dirname(global.env.opts.configure || ''), env.pwd, env.dirname].forEach(function(_path) {
if (!result && _path) {
_path = path.resolve(_path, filepath);
if ( pathExists(_path) ) {
result = _path;
}
}
});
if (result) {
result = filename ? path.join(result, filename) : result;
}
return result;
};
Object.keys(path).forEach(function(member) {
exports[member] = path[member];
});
@@ -0,0 +1,53 @@
/*global app: true */
/**
* Utility functions to support the JSDoc plugin framework.
* @module jsdoc/plugins
*/
'use strict';
var logger = require('jsdoc/util/logger');
var path = require('jsdoc/path');
function addHandlers(handlers, parser) {
Object.keys(handlers).forEach(function(eventName) {
parser.on(eventName, handlers[eventName]);
});
}
exports.installPlugins = function(plugins, parser) {
var dictionary = require('jsdoc/tag/dictionary');
var eventName;
var plugin;
for (var i = 0, l = plugins.length; i < l; i++) {
plugin = require(plugins[i]);
// allow user-defined plugins to...
// ...register event handlers
if (plugin.handlers) {
addHandlers(plugin.handlers, parser);
}
// ...define tags
if (plugin.defineTags) {
plugin.defineTags(dictionary);
}
// ...add a Rhino node visitor (deprecated in JSDoc 3.3)
if (plugin.nodeVisitor) {
if ( !parser.addNodeVisitor ) {
logger.error('Unable to add the Rhino node visitor from %s, because JSDoc ' +
'is not using the Rhino JavaScript parser.', plugins[i]);
}
else {
parser.addNodeVisitor(plugin.nodeVisitor);
}
}
// ...add a Mozilla Parser API node visitor
if (plugin.astNodeVisitor) {
parser.addAstNodeVisitor(plugin.astNodeVisitor);
}
}
};
@@ -0,0 +1,26 @@
/*global env: true */
/**
* Make the contents of a README file available to include in the output.
* @module jsdoc/readme
* @author Michael Mathews <micmath@gmail.com>
* @author Ben Blank <ben.blank@gmail.com>
*/
'use strict';
var fs = require('jsdoc/fs'),
markdown = require('jsdoc/util/markdown');
/**
* @class
* @classdesc Represents a README file.
* @param {string} path - The filepath to the README.
*/
function ReadMe(path) {
var content = fs.readFileSync(path, env.opts.encoding),
parse = markdown.getParser();
this.html = parse(content);
}
module.exports = ReadMe;
+759
View File
@@ -0,0 +1,759 @@
/**
* @overview Schema for validating JSDoc doclets.
*
* @author Michael Mathews <micmath@gmail.com>
* @author Jeff Williams <jeffrey.l.williams@gmail.com>
* @license Apache License 2.0 - See file 'LICENSE.md' in this project.
* @see <http://tools.ietf.org/html/draft-zyp-json-schema-03>
*/
'use strict';
// JSON schema types
var ARRAY = 'array';
var BOOLEAN = 'boolean';
var INTEGER = 'integer';
var NULL = 'null';
var NUMBER = 'number';
var OBJECT = 'object';
var STRING = 'string';
var UNDEFINED = 'undefined';
var BOOLEAN_OPTIONAL = [BOOLEAN, NULL, UNDEFINED];
var STRING_OPTIONAL = [STRING, NULL, UNDEFINED];
var EVENT_REGEXP = /event\:[\S]+/;
var PACKAGE_REGEXP = /package\:[\S]+/;
// information about the code associated with a doclet
var META_SCHEMA = exports.META_SCHEMA = {
type: OBJECT,
optional: true,
additionalProperties: false,
properties: {
code: {
type: OBJECT,
additionalProperties: false,
properties: {
funcscope: {
type: STRING,
optional: true
},
id: {
type: STRING,
optional: true
},
name: {
type: STRING,
optional: true
},
node: {
type: OBJECT,
optional: true
},
paramnames: {
type: ARRAY,
optional: true,
uniqueItems: true,
items: {
type: STRING
}
},
type: {
type: STRING,
optional: true
},
value: {
optional: true
}
}
},
filename: {
title: 'The name of the file that contains the code associated with this doclet.',
type: STRING,
optional: true
},
lineno: {
title: 'The line number of the code associated with this doclet.',
type: NUMBER,
optional: true
},
path: {
title: 'The path in which the code associated with this doclet is located.',
type: STRING,
optional: true
},
range: {
title: 'The positions of the first and last characters of the code associated with ' +
'this doclet.',
type: ARRAY,
optional: true,
minItems: 2,
maxItems: 2,
items: {
type: NUMBER
}
},
vars: {
type: OBJECT
}
}
};
// type property containing type names
var TYPE_PROPERTY_SCHEMA = exports.TYPE_PROPERTY_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
names: {
type: ARRAY,
minItems: 1,
items: {
type: STRING
}
},
// type parser output
parsedType: {
type: OBJECT,
additionalProperties: true
}
}
};
// enumeration properties
var ENUM_PROPERTY_SCHEMA = exports.ENUM_PROPERTY_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
comment: {
type: STRING
},
defaultvalue: {
type: STRING_OPTIONAL,
optional: true
},
description: {
type: STRING_OPTIONAL,
optional: true
},
kind: {
type: STRING,
// TODO: get this from a real enum somewhere
enum: ['member']
},
longname: {
type: STRING
},
memberof: {
type: STRING,
optional: true
},
meta: META_SCHEMA,
name: {
type: STRING
},
// is this member nullable? (derived from the type expression)
nullable: {
type: BOOLEAN_OPTIONAL
},
// is this member optional? (derived from the type expression)
optional: {
type: BOOLEAN_OPTIONAL
},
scope: {
type: STRING,
// TODO: get this from a real enum somewhere
enum: ['static']
},
type: TYPE_PROPERTY_SCHEMA,
// can this member be provided more than once? (derived from the type expression)
variable: {
type: BOOLEAN_OPTIONAL
}
}
};
// function parameter, or object property defined with @property tag
var PARAM_SCHEMA = exports.PARAM_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
// what is the default value for this parameter?
defaultvalue: {
type: STRING_OPTIONAL,
optional: true
},
// a description of the parameter
description: {
type: STRING_OPTIONAL,
optional: true
},
// what name does this parameter have within the function?
name: {
type: STRING
},
// can the value for this parameter be null?
nullable: {
type: BOOLEAN_OPTIONAL,
optional: true
},
// is a value for this parameter optional?
optional: {
type: BOOLEAN_OPTIONAL,
optional: true
},
// what are the types of value expected for this parameter?
type: TYPE_PROPERTY_SCHEMA,
// can this parameter be repeated?
variable: {
type: BOOLEAN_OPTIONAL,
optional: true
}
}
};
var DOCLET_SCHEMA = exports.DOCLET_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
// what access privileges are allowed
access: {
type: STRING,
optional: true,
// TODO: define this as an enumeration elsewhere
enum: [
'private',
'protected'
]
},
alias: {
type: STRING,
optional: true
},
augments: {
type: ARRAY,
optional: true,
uniqueItems: true,
items: {
type: STRING
}
},
author: {
type: ARRAY,
optional: true,
items: {
type: STRING
}
},
borrowed: {
type: ARRAY,
optional: true,
uniqueItems: true,
items: {
type: OBJECT,
additionalProperties: false,
properties: {
// name of the target
as: {
type: STRING,
optional: true
},
// name of the source
from: {
type: STRING
}
}
}
},
// a description of the class that this constructor belongs to
classdesc: {
type: STRING,
optional: true
},
comment: {
type: STRING
},
copyright: {
type: STRING,
optional: true
},
defaultvalue: {
optional: true
},
defaultvaluetype: {
type: STRING,
optional: true,
enum: [OBJECT, ARRAY]
},
// is usage of this symbol deprecated?
deprecated: {
type: [STRING, BOOLEAN],
optional: true
},
// a description
description: {
type: STRING_OPTIONAL,
optional: true
},
// something else to consider
examples: {
type: ARRAY,
optional: true,
items: {
type: STRING
}
},
exceptions: {
type: ARRAY,
optional: true,
items: PARAM_SCHEMA
},
// the path to another constructor
extends: {
type: ARRAY,
optional: true,
uniqueItems: true,
items: {
type: STRING
}
},
// the path to another doc object
fires: {
type: ARRAY,
optional: true,
uniqueItems: true,
items: {
type: STRING,
pattern: EVENT_REGEXP
}
},
forceMemberof: {
type: BOOLEAN_OPTIONAL,
optional: true
},
ignore: {
type: BOOLEAN,
optional: true
},
implementations: {
type: ARRAY,
optional: true,
items: {
type: STRING
}
},
implements: {
type: ARRAY,
optional: true,
items: {
type: STRING
}
},
inheritdoc: {
type: STRING,
optional: true
},
inherited: {
type: BOOLEAN,
optional: true
},
inherits: {
type: STRING,
optional: true,
dependency: {
inherited: true
}
},
isEnum: {
type: BOOLEAN,
optional: true
},
// what kind of symbol is this?
kind: {
type: STRING,
// TODO: define this as an enumeration elsewhere
enum: [
'class',
'constant',
'event',
'external',
'file',
'function',
'interface',
'member',
'mixin',
'module',
'namespace',
'package',
'param',
'typedef'
]
},
license: {
type: STRING,
optional: true
},
listens: {
type: ARRAY,
optional: true,
uniqueItems: true,
items: {
type: STRING,
pattern: EVENT_REGEXP
}
},
longname: {
type: STRING
},
// probably a leading substring of the path
memberof: {
type: STRING,
optional: true
},
// information about this doc
meta: META_SCHEMA,
// was this doclet mixed in?
mixed: {
type: BOOLEAN,
optional: true
},
mixes: {
type: ARRAY,
optional: true,
uniqueItems: true,
items: {
type: STRING
}
},
// probably a trailing substring of the path
name: {
type: STRING
},
// is this member nullable? (derived from the type expression)
nullable: {
type: BOOLEAN_OPTIONAL
},
// is this member optional? (derived from the type expression)
optional: {
type: BOOLEAN_OPTIONAL
},
// does this member explicitly override the parent?
override: {
type: BOOLEAN,
optional: true
},
overrides: {
type: STRING,
optional: true
},
// are there function parameters associated with this doc?
params: {
type: ARRAY,
optional: true,
uniqueItems: true,
items: PARAM_SCHEMA
},
preserveName: {
type: BOOLEAN,
optional: true
},
properties: {
type: ARRAY,
optional: true,
uniqueItems: true,
minItems: 1,
items: {
anyOf: [ENUM_PROPERTY_SCHEMA, PARAM_SCHEMA]
}
},
readonly: {
type: BOOLEAN,
optional: true
},
// the symbol being documented requires another symbol
requires: {
type: ARRAY,
optional: true,
uniqueItems: true,
minItems: 1,
items: {
type: STRING
}
},
returns: {
type: ARRAY,
optional: true,
minItems: 1,
items: PARAM_SCHEMA
},
// what sort of parent scope does this symbol have?
scope: {
type: STRING,
enum: [
// TODO: make these an enumeration
'global',
'inner',
'instance',
'static'
]
},
// something else to consider
see: {
type: ARRAY,
optional: true,
minItems: 1,
items: {
type: STRING
}
},
// at what previous version was this doc added?
since: {
type: STRING,
optional: true
},
summary: {
type: STRING,
optional: true
},
// arbitrary tags associated with this doc
tags: {
type: ARRAY,
optional: true,
minItems: 1,
items: {
type: OBJECT,
additionalProperties: false,
properties: {
originalTitle: {
type: STRING
},
text: {
type: STRING,
optional: true
},
title: {
type: STRING
},
value: {
type: [STRING, OBJECT],
optional: true,
properties: PARAM_SCHEMA
}
}
}
},
'this': {
type: STRING,
optional: true
},
todo: {
type: ARRAY,
optional: true,
minItems: 1,
items: {
type: STRING
}
},
// extended tutorials
tutorials: {
type: ARRAY,
optional: true,
minItems: 1,
items: {
type: STRING
}
},
// what type is the value that this doc is associated with, like `number`
type: TYPE_PROPERTY_SCHEMA,
undocumented: {
type: BOOLEAN,
optional: true
},
// can this member be provided more than once? (derived from the type expression)
variable: {
type: BOOLEAN_OPTIONAL
},
variation: {
type: STRING,
optional: true
},
// what is the version of this doc
version: {
type: STRING,
optional: true
},
// is a member left to be implemented during inheritance?
virtual: {
type: BOOLEAN,
optional: true
},
// Platform support
platforms: {
type: ARRAY,
optional: true,
minItems: 1,
items: {
type: STRING
}
}
}
};
var CONTACT_INFO_SCHEMA = exports.CONTACT_INFO_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
email: {
type: STRING,
optional: true
},
name: {
type: STRING,
optional: true
},
url: {
type: STRING,
optional: true,
format: 'uri'
}
}
};
var BUGS_SCHEMA = exports.BUGS_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
email: {
type: STRING,
optional: true
},
url: {
type: STRING,
optional: true,
format: 'uri'
}
}
};
var PACKAGE_SCHEMA = exports.PACKAGE_SCHEMA = {
type: OBJECT,
additionalProperties: false,
properties: {
author: {
anyOf: [STRING, CONTACT_INFO_SCHEMA],
optional: true
},
bugs: {
anyOf: [STRING, BUGS_SCHEMA],
optional: true
},
contributors: {
type: ARRAY,
optional: true,
minItems: 0,
items: {
anyOf: [STRING, CONTACT_INFO_SCHEMA]
}
},
dependencies: {
type: OBJECT,
optional: true
},
description: {
type: STRING,
optional: true
},
devDependencies: {
type: OBJECT,
optional: true
},
engines: {
type: OBJECT,
optional: true
},
files: {
type: ARRAY,
uniqueItems: true,
minItems: 0,
items: {
type: STRING
}
},
homepage: {
type: STRING,
optional: true,
format: 'uri'
},
keywords: {
type: ARRAY,
optional: true,
minItems: 0,
items: {
type: STRING
}
},
kind: {
type: STRING,
enum: ['package']
},
licenses: {
type: ARRAY,
optional: true,
minItems: 1,
items: {
type: OBJECT,
additionalProperties: false,
properties: {
type: {
type: STRING,
optional: true
},
url: {
type: STRING,
optional: true,
format: 'uri'
}
}
}
},
longname: {
type: STRING,
optional: true,
pattern: PACKAGE_REGEXP
},
main: {
type: STRING,
optional: true
},
name: {
type: STRING,
optional: true
},
repository: {
type: OBJECT,
optional: true,
additionalProperties: false,
properties: {
type: {
type: STRING,
optional: true
},
// we don't use `format: 'uri'` here because repo URLs are atypical
url: {
type: STRING,
optional: true
}
}
},
version: {
type: STRING,
optional: true
}
}
};
var DOCLETS_SCHEMA = exports.DOCLETS_SCHEMA = {
type: ARRAY,
items: {
anyOf: [DOCLET_SCHEMA, PACKAGE_SCHEMA]
}
};
@@ -0,0 +1,419 @@
'use strict';
var esprima = require('esprima');
var jsdoc = {
src: {
syntax: require('jsdoc/src/syntax'),
Walker: require('jsdoc/src/walker').Walker
},
util: {
logger: require('jsdoc/util/logger')
}
};
var Syntax = jsdoc.src.syntax.Syntax;
// TODO: should set e.stopPropagation == true for consistency with Rhino, right?
var VISITOR_CONTINUE = true;
var VISITOR_STOP = false;
// TODO: docs; empty array means any node type, otherwise only the node types in the array
var acceptsLeadingComments = (function() {
var accepts = {};
// these nodes always accept leading comments
var commentable = [
Syntax.AssignmentExpression,
Syntax.CallExpression,
Syntax.FunctionDeclaration,
Syntax.FunctionExpression,
Syntax.MemberExpression,
Syntax.Property,
Syntax.TryStatement,
Syntax.VariableDeclaration,
Syntax.VariableDeclarator,
Syntax.WithStatement
];
for (var i = 0, l = commentable.length; i < l; i++) {
accepts[commentable[i]] = [];
}
// these nodes accept leading comments if they have specific types of parent nodes
// like: function foo(/** @type {string} */ bar) {}
accepts[Syntax.Identifier] = [
Syntax.CatchClause,
Syntax.FunctionDeclaration,
Syntax.FunctionExpression
];
// like: var Foo = Class.create(/** @lends Foo */{ // ... })
accepts[Syntax.ObjectExpression] = [
Syntax.CallExpression,
Syntax.Property,
Syntax.ReturnStatement
];
return accepts;
})();
// TODO: docs
function canAcceptComment(node) {
var canAccept = false;
var spec = acceptsLeadingComments[node.type];
if (spec) {
// empty array means we don't care about the parent type
if (spec.length === 0) {
canAccept = true;
}
// we can accept the comment if the spec contains the type of the node's parent
else if (node.parent) {
canAccept = spec.indexOf(node.parent.type) !== -1;
}
}
return canAccept;
}
// TODO: docs
// check whether node1 is before node2
function isBefore(beforeRange, afterRange) {
return beforeRange[1] <= afterRange[0];
}
// TODO: docs
function isWithin(innerRange, outerRange) {
return innerRange[0] >= outerRange[0] && innerRange[1] <= outerRange[1];
}
// TODO: docs
function isJsdocComment(comment) {
return comment && (comment.type === 'Block') && (comment.value[0] === '*');
}
/**
* Add the raw comment string to a block comment node.
*
* @private
* @param {!Object} comment - A comment node with `type` and `value` properties.
*/
function addRawComment(comment) {
comment.raw = comment.raw || ('/*' + comment.value + '*/');
return comment;
}
// TODO: docs
function scrubComments(comments) {
var comment;
var scrubbed = [];
for (var i = 0, l = comments.length; i < l; i++) {
comment = comments[i];
if ( isJsdocComment(comment) ) {
scrubbed.push( addRawComment(comment) );
}
}
return scrubbed;
}
// TODO: docs
var AstBuilder = exports.AstBuilder = function() {};
function parse(source, filename, esprimaOpts) {
var ast;
try {
ast = esprima.parse(source, esprimaOpts);
}
catch (e) {
jsdoc.util.logger.error('Unable to parse %s: %s', filename, e.message);
}
return ast;
}
// TODO: docs
AstBuilder.prototype.build = function(source, filename) {
var ast;
var esprimaOpts = {
comment: true,
loc: true,
range: true,
tokens: true
};
ast = parse(source, filename, esprimaOpts);
if (ast) {
this._postProcess(filename, ast);
}
return ast;
};
// TODO: docs
function atomSorter(a, b) {
var aRange = a.range;
var bRange = b.range;
var result = 0;
// does a end before b starts?
if ( isBefore(aRange, bRange) ) {
result = -1;
}
// does a enclose b?
else if ( isWithin(bRange, aRange) ) {
result = -1;
}
// does a start before b?
else if (aRange[0] < bRange[0]) {
result = -1;
}
// are the ranges non-identical? if so, b must be first
else if ( aRange[0] !== bRange[0] || aRange[1] !== bRange[1] ) {
result = 1;
}
return result;
}
// TODO: docs
// TODO: export?
function CommentAttacher(comments, tokens) {
this._comments = comments || [];
this._tokens = tokens || [];
this._tokenIndex = 0;
this._previousNode = null;
this._astRoot = null;
this._resetPendingComments()
._resetCandidates();
}
// TODO: docs
CommentAttacher.prototype._resetPendingComments = function() {
this._pendingComments = [];
this._pendingCommentRange = null;
return this;
};
// TODO: docs
CommentAttacher.prototype._resetCandidates = function() {
this._candidates = [];
return this;
};
// TODO: docs
CommentAttacher.prototype._nextComment = function() {
return this._comments[0] || null;
};
// TODO: docs
CommentAttacher.prototype._nextToken = function() {
return this._tokens[this._tokenIndex] || null;
};
// TODO: docs
// find the index of the atom whose end position is closest to (but not after) the specified
// position
CommentAttacher.prototype._nextIndexBefore = function(atoms, startIndex, position) {
var atom;
var newIndex = startIndex;
for (var i = newIndex, l = atoms.length; i < l; i++) {
atom = atoms[i];
if (atom.range[1] > position) {
break;
}
else {
newIndex = i;
}
}
return newIndex;
};
// TODO: docs
CommentAttacher.prototype._advanceTokenIndex = function(node) {
var position = node.range[0];
this._tokenIndex = this._nextIndexBefore(this._tokens, this._tokenIndex, position);
return this;
};
// TODO: docs
CommentAttacher.prototype._fastForwardComments = function(node) {
var position = node.range[0];
var commentIndex = this._nextIndexBefore(this._comments, 0, position);
// all comments before the node (except the last one) are pended
if (commentIndex > 0) {
this._pendingComments = this._pendingComments.concat( this._comments.splice(0,
commentIndex) );
}
};
CommentAttacher.prototype._attachPendingCommentsAsLeading = function(target) {
target.leadingComments = (target.leadingComments || []).concat(this._pendingComments);
};
CommentAttacher.prototype._attachPendingCommentsAsTrailing = function(target) {
target.trailingComments = (target.trailingComments || []).concat(this._pendingComments);
};
// TODO: docs
CommentAttacher.prototype._attachPendingComments = function(currentNode) {
var target;
if (!this._pendingComments.length) {
return this;
}
// if there are one or more candidate nodes, attach the pending comments before the last
// candidate node
if (this._candidates.length > 0) {
target = this._candidates[this._candidates.length - 1];
this._attachPendingCommentsAsLeading(target);
}
// if we don't have a previous node, attach pending comments before the AST root; this should
// mean that we haven't encountered any other nodes yet, or that the source file contains
// JSDoc comments but not code
else if (!this._previousNode) {
target = this._astRoot;
this._attachPendingCommentsAsLeading(target);
}
// otherwise, the comments must come after the current node (or the last node of the AST, if
// we've run out of nodes)
else {
this._attachPendingCommentsAsTrailing(currentNode || this._previousNode);
}
// update the previous node
this._previousNode = currentNode;
this._resetPendingComments()
._resetCandidates();
return this;
};
// TODO: docs
CommentAttacher.prototype._isEligible = function(node) {
var atoms;
var token;
var isEligible = false;
var comment = this._nextComment();
if (comment) {
atoms = [node, comment];
token = this._nextToken();
if (token) {
atoms.push(token);
}
atoms.sort(atomSorter);
// a candidate node must immediately follow the comment
if (atoms.indexOf(node) === atoms.indexOf(comment) + 1) {
isEligible = true;
}
}
return isEligible;
};
// TODO: docs
// TODO: do we ever get multiple candidate nodes?
CommentAttacher.prototype.visit = function(node) {
var isEligible;
// bail if we're out of comments
if ( !this._nextComment() ) {
return VISITOR_STOP;
}
// set the AST root if necessary
this._astRoot = this._astRoot || node;
// move to the next token, and fast-forward past comments that can no longer be attached
this._advanceTokenIndex(node);
this._fastForwardComments(node);
// now we can check whether the current node is in the right position to accept the next comment
isEligible = this._isEligible(node);
// attach the pending comments, if any
this._attachPendingComments(node);
// okay, now that we've done all that bookkeeping, we can check whether the current node accepts
// leading comments and add it to the candidate list if needed
if ( isEligible && canAcceptComment(node) ) {
// make sure we don't go past the end of the outermost target node
if (!this._pendingCommentRange) {
this._pendingCommentRange = node.range.slice(0);
}
this._candidates.push(node);
// we have a candidate node, so pend the current comment
this._pendingComments.push(this._comments.splice(0, 1)[0]);
}
return VISITOR_CONTINUE;
};
// TODO: docs
CommentAttacher.prototype.finish = function() {
var length = this._comments.length;
// any leftover comments are pended
if (length) {
this._pendingComments = this._pendingComments.concat( this._comments.splice(0, length) );
}
// attach the pending comments, if any
this._attachPendingComments();
};
// TODO: docs
// TODO: refactor to make this extensible
/**
* @private
* @param {string} filename - The full path to the source file.
* @param {Object} ast - An abstract syntax tree that conforms to the Mozilla Parser API.
*/
AstBuilder.prototype._postProcess = function(filename, ast) {
var attachComments = !!ast.comments && !!ast.comments.length;
var commentAttacher;
var scrubbed;
var visitor;
var walker;
if (!attachComments) {
return;
}
scrubbed = scrubComments(ast.comments.slice(0));
commentAttacher = new CommentAttacher(scrubbed.slice(0), ast.tokens);
visitor = {
visit: function(node) {
return commentAttacher.visit(node);
}
};
walker = new jsdoc.src.Walker();
walker.recurse(ast, visitor, filename);
commentAttacher.finish();
// replace the comments with the filtered comments
ast.comments = scrubbed;
// we no longer need the tokens
ast.tokens = [];
};
@@ -0,0 +1,299 @@
// TODO: docs
'use strict';
var Syntax = require('jsdoc/src/syntax').Syntax;
var util = require('util');
// Counter for generating unique node IDs.
var uid = 100000000;
/**
* Check whether an AST node represents a function.
*
* @param {Object} node - The AST node to check.
* @return {boolean} Set to `true` if the node is a function or `false` in all other cases.
*/
var isFunction = exports.isFunction = function(node) {
return node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression;
};
/**
* Check whether an AST node creates a new scope.
*
* @param {Object} node - The AST node to check.
* @return {Boolean} Set to `true` if the node creates a new scope, or `false` in all other cases.
*/
var isScope = exports.isScope = function(node) {
// TODO: handle blocks with "let" declarations
return !!node && typeof node === 'object' && ( node.type === Syntax.CatchClause ||
isFunction(node) );
};
// TODO: docs
var addNodeProperties = exports.addNodeProperties = function(node) {
var debugEnabled = !!global.env.opts.debug;
var newProperties = {};
if (!node || typeof node !== 'object') {
return null;
}
if (!node.nodeId) {
newProperties.nodeId = {
value: 'astnode' + uid++,
enumerable: debugEnabled
};
}
if (!node.parent && node.parent !== null) {
newProperties.parent = {
// `null` means 'no parent', so use `undefined` for now
value: undefined,
writable: true
};
}
if (!node.enclosingScope && node.enclosingScope !== null) {
newProperties.enclosingScope = {
// `null` means 'no enclosing scope', so use `undefined` for now
value: undefined,
writable: true
};
}
if (debugEnabled && !node.parentId) {
newProperties.parentId = {
enumerable: true,
get: function() {
return this.parent ? this.parent.nodeId : null;
}
};
}
if (debugEnabled && !node.enclosingScopeId) {
newProperties.enclosingScopeId = {
enumerable: true,
get: function() {
return this.enclosingScope ? this.enclosingScope.nodeId : null;
}
};
}
Object.defineProperties(node, newProperties);
return node;
};
// TODO: docs
var nodeToString = exports.nodeToString = function(node) {
var tempObject;
var str = '';
switch (node.type) {
case Syntax.ArrayExpression:
tempObject = [];
node.elements.forEach(function(el, i) {
// handle sparse arrays. use `null` to represent missing values, consistent with
// JSON.stringify([,]).
if (!el) {
tempObject[i] = null;
}
// preserve literal values so that the JSON form shows the correct type
else if (el.type === Syntax.Literal) {
tempObject[i] = el.value;
}
else {
tempObject[i] = nodeToString(el);
}
});
str = JSON.stringify(tempObject);
break;
case Syntax.AssignmentExpression:
str = nodeToString(node.left);
break;
case Syntax.FunctionDeclaration:
// falls through
case Syntax.FunctionExpression:
str = 'function';
break;
case Syntax.Identifier:
str = node.name;
break;
case Syntax.Literal:
str = String(node.value);
break;
case Syntax.MemberExpression:
// could be computed (like foo['bar']) or not (like foo.bar)
str = nodeToString(node.object);
if (node.computed) {
str += util.format('[%s]', node.property.raw);
}
else {
str += '.' + nodeToString(node.property);
}
break;
case Syntax.ObjectExpression:
tempObject = {};
node.properties.forEach(function(prop) {
var key = prop.key.name;
// preserve literal values so that the JSON form shows the correct type
if (prop.value.type === Syntax.Literal) {
tempObject[key] = prop.value.value;
}
else {
tempObject[key] = nodeToString(prop);
}
});
str = JSON.stringify(tempObject);
break;
case Syntax.ThisExpression:
str = 'this';
break;
case Syntax.UnaryExpression:
// like -1. in theory, operator can be prefix or postfix. in practice, any value with a
// valid postfix operator (such as -- or ++) is not a UnaryExpression.
str = nodeToString(node.argument);
// workaround for https://code.google.com/p/esprima/issues/detail?id=526
if (node.prefix === true || node.prefix === undefined) {
str = node.operator + str;
}
else {
// this shouldn't happen
throw new Error( util.format('Found a UnaryExpression with a postfix operator: %j',
node) );
}
break;
case Syntax.VariableDeclarator:
str = nodeToString(node.id);
break;
default:
str = '';
}
return str;
};
// TODO: docs
var getParamNames = exports.getParamNames = function(node) {
if (!node || !node.params) {
return [];
}
return node.params.map(function(param) {
return nodeToString(param);
});
};
// TODO: docs
var isAccessor = exports.isAccessor = function(node) {
return !!node && typeof node === 'object' && node.type === Syntax.Property &&
(node.kind === 'get' || node.kind === 'set');
};
// TODO: docs
var isAssignment = exports.isAssignment = function(node) {
return !!node && typeof node === 'object' && (node.type === Syntax.AssignmentExpression ||
node.type === Syntax.VariableDeclarator);
};
// TODO: docs
/**
* Retrieve information about the node, including its name and type.
*/
var getInfo = exports.getInfo = function(node) {
var info = {};
switch (node.type) {
// like: "foo = 'bar'" (after declaring foo)
// like: "MyClass.prototype.myMethod = function() {}" (after declaring MyClass)
case Syntax.AssignmentExpression:
info.node = node.right;
info.name = nodeToString(node.left);
info.type = info.node.type;
info.value = nodeToString(info.node);
// if the assigned value is a function, we need to capture the parameter names here
info.paramnames = getParamNames(node.right);
break;
// like: "function foo() {}"
case Syntax.FunctionDeclaration:
info.node = node;
info.name = nodeToString(node.id);
info.type = info.node.type;
info.paramnames = getParamNames(node);
break;
// like the function in: "var foo = function() {}"
case Syntax.FunctionExpression:
info.node = node;
// TODO: should we add a name for, e.g., "var foo = function bar() {}"?
info.name = '';
info.type = info.node.type;
info.paramnames = getParamNames(node);
break;
// like the param "bar" in: "function foo(bar) {}"
case Syntax.Identifier:
info.node = node;
info.name = nodeToString(info.node);
info.type = info.node.type;
break;
// like "a.b.c"
case Syntax.MemberExpression:
info.node = node;
info.name = nodeToString(info.node);
info.type = info.node.type;
break;
// like "a: 0" in "var foo = {a: 0}"
case Syntax.Property:
info.node = node.value;
info.name = nodeToString(node.key);
info.value = nodeToString(info.node);
if ( isAccessor(node) ) {
info.type = nodeToString(info.node);
info.paramnames = getParamNames(info.node);
}
else {
info.type = info.node.type;
}
break;
// like: "var i = 0" (has init property)
// like: "var i" (no init property)
case Syntax.VariableDeclarator:
info.node = node.init || node.id;
info.name = node.id.name;
if (node.init) {
info.type = info.node.type;
info.value = nodeToString(info.node);
}
break;
default:
info.node = node;
info.type = info.node.type;
}
return info;
};
@@ -0,0 +1,67 @@
/*global env: true */
/**
@module jsdoc/src/filter
@author Michael Mathews <micmath@gmail.com>
@license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
'use strict';
var path = require('jsdoc/path');
var pwd = env.pwd;
function makeRegExp(config) {
var regExp = null;
if (config) {
regExp = (typeof config === 'string') ? new RegExp(config) : config;
}
return regExp;
}
/**
@constructor
@param {object} opts
@param {string[]} opts.exclude - Specific files to exclude.
@param {string|RegExp} opts.includePattern
@param {string|RegExp} opts.excludePattern
*/
exports.Filter = function(opts) {
this.exclude = opts.exclude && Array.isArray(opts.exclude) ?
opts.exclude.map(function($) {
return path.resolve(pwd, $);
}) :
null;
this.includePattern = makeRegExp(opts.includePattern);
this.excludePattern = makeRegExp(opts.excludePattern);
};
/**
@param {string} filepath - The filepath to check.
@returns {boolean} Should the given file be included?
*/
exports.Filter.prototype.isIncluded = function(filepath) {
var included = true;
filepath = path.resolve(pwd, filepath);
if ( this.includePattern && !this.includePattern.test(filepath) ) {
included = false;
}
if ( this.excludePattern && this.excludePattern.test(filepath) ) {
included = false;
}
if (this.exclude) {
this.exclude.forEach(function(exclude) {
if ( filepath.indexOf(exclude) === 0 ) {
included = false;
}
});
}
return included;
};
@@ -0,0 +1,325 @@
/**
* @module jsdoc/src/handlers
*/
'use strict';
var escape = require('escape-string-regexp');
var jsdoc = {
doclet: require('jsdoc/doclet'),
name: require('jsdoc/name'),
util: {
logger: require('jsdoc/util/logger')
}
};
var util = require('util');
var currentModule = null;
var SCOPE_NAMES = jsdoc.name.SCOPE.NAMES;
var SCOPE_PUNC = jsdoc.name.SCOPE.PUNC;
var unresolvedName = /^((?:module.)?exports|this)(\.|$)/;
function CurrentModule(doclet) {
this.doclet = doclet;
this.longname = doclet.longname;
this.originalName = doclet.meta.code.name || '';
}
function filterByLongname(doclet) {
// you can't document prototypes
if ( /#$/.test(doclet.longname) ) {
return true;
}
return false;
}
function createDoclet(comment, e) {
var doclet;
var err;
try {
doclet = new jsdoc.doclet.Doclet(comment, e);
}
catch (error) {
err = new Error( util.format('cannot create a doclet for the comment "%s": %s',
comment.replace(/[\r\n]/g, ''), error.message) );
jsdoc.util.logger.error(err);
doclet = new jsdoc.doclet.Doclet('', e);
}
return doclet;
}
/**
* Create a doclet for a `symbolFound` event. The doclet represents an actual symbol that is defined
* in the code.
*
* Here's why this function is useful. A JSDoc comment can define a symbol name by including:
*
* + A `@name` tag
* + Another tag that accepts a name, such as `@function`
*
* When the JSDoc comment defines a symbol name, we treat it as a "virtual comment" for a symbol
* that isn't actually present in the code. And if a virtual comment is attached to a symbol, it's
* possible that the comment and symbol have nothing to do with one another.
*
* To handle this case, this function checks the new doclet to see if we've already added a name
* property by parsing the JSDoc comment. If so, this method creates a replacement doclet that
* ignores the attached JSDoc comment and only looks at the code.
*
* @private
*/
function createSymbolDoclet(comment, e) {
var doclet = createDoclet(comment, e);
if (doclet.name) {
// try again, without the comment
e.comment = '@undocumented';
doclet = createDoclet(e.comment, e);
}
return doclet;
}
function setCurrentModule(doclet) {
if (doclet.kind === 'module') {
currentModule = new CurrentModule(doclet);
}
}
function setModuleScopeMemberOf(doclet) {
// handle module symbols that are _not_ assigned to module.exports
if (currentModule && currentModule.longname !== doclet.name) {
// if we don't already know the scope, it must be an inner member
if (!doclet.scope) {
doclet.addTag('inner');
}
// if the doclet isn't a memberof anything yet, and it's not a global, it must be a memberof
// the current module
if (!doclet.memberof && doclet.scope !== SCOPE_NAMES.GLOBAL) {
doclet.addTag('memberof', currentModule.longname);
}
}
}
function setDefaultScope(doclet) {
// module doclets don't get a default scope
if (!doclet.scope && doclet.kind !== 'module') {
doclet.setScope(SCOPE_NAMES.GLOBAL);
}
}
function addDoclet(parser, newDoclet) {
var e;
if (newDoclet) {
setCurrentModule(newDoclet);
e = { doclet: newDoclet };
parser.emit('newDoclet', e);
if ( !e.defaultPrevented && !filterByLongname(e.doclet) ) {
parser.addResult(e.doclet);
}
}
}
function processAlias(parser, doclet, astNode) {
var memberofName;
if (doclet.alias === '{@thisClass}') {
memberofName = parser.resolveThis(astNode);
// "class" refers to the owner of the prototype, not the prototype itself
if ( /^(.+?)(\.prototype|#)$/.test(memberofName) ) {
memberofName = RegExp.$1;
}
doclet.alias = memberofName;
}
doclet.addTag('name', doclet.alias);
doclet.postProcess();
}
// TODO: separate code that resolves `this` from code that resolves the module object
function findSymbolMemberof(parser, doclet, astNode, nameStartsWith, trailingPunc) {
var memberof = '';
var nameAndPunc = nameStartsWith + (trailingPunc || '');
var scopePunc = '';
// remove stuff that indicates module membership (but don't touch the name `module.exports`,
// which identifies the module object itself)
if (doclet.name !== 'module.exports') {
doclet.name = doclet.name.replace(nameAndPunc, '');
}
// like `bar` in:
// exports.bar = 1;
// module.exports.bar = 1;
// module.exports = MyModuleObject; MyModuleObject.bar = 1;
if (nameStartsWith !== 'this' && currentModule && doclet.name !== 'module.exports') {
memberof = currentModule.longname;
scopePunc = SCOPE_PUNC.STATIC;
}
// like: module.exports = 1;
else if (doclet.name === 'module.exports' && currentModule) {
doclet.addTag('name', currentModule.longname);
doclet.postProcess();
}
else {
memberof = parser.resolveThis(astNode);
// like the following at the top level of a module:
// this.foo = 1;
if (nameStartsWith === 'this' && currentModule && !memberof) {
memberof = currentModule.longname;
scopePunc = SCOPE_PUNC.STATIC;
}
else {
scopePunc = SCOPE_PUNC.INSTANCE;
}
}
return {
memberof: memberof,
scopePunc: scopePunc
};
}
function addSymbolMemberof(parser, doclet, astNode) {
var basename;
var memberof;
var memberofInfo;
var moduleOriginalName = '';
var resolveTargetRegExp;
var scopePunc;
var unresolved;
if (!astNode) {
return;
}
// check to see if the doclet name is an unresolved reference to the module object, or to `this`
// TODO: handle cases where the module object is shadowed in the current scope
if (currentModule) {
moduleOriginalName = '|' + currentModule.originalName;
}
resolveTargetRegExp = new RegExp('^((?:module.)?exports|this' + moduleOriginalName +
')(\\.|$)');
unresolved = resolveTargetRegExp.exec(doclet.name);
if (unresolved) {
memberofInfo = findSymbolMemberof(parser, doclet, astNode, unresolved[1], unresolved[2]);
memberof = memberofInfo.memberof;
scopePunc = memberofInfo.scopePunc;
if (memberof) {
doclet.name = doclet.name ?
memberof + scopePunc + doclet.name :
memberof;
}
}
else {
memberofInfo = parser.astnodeToMemberof(astNode);
if ( Array.isArray(memberofInfo) ) {
basename = memberofInfo[1];
memberof = memberofInfo[0];
}
else {
memberof = memberofInfo;
}
}
// if we found a memberof name, apply it to the doclet
if (memberof) {
doclet.addTag('memberof', memberof);
if (basename) {
doclet.name = (doclet.name || '')
.replace(new RegExp('^' + escape(basename) + '.'), '');
}
}
// otherwise, add the defaults for a module (if we're currently in a module)
else {
setModuleScopeMemberOf(doclet);
}
}
function newSymbolDoclet(parser, docletSrc, e) {
var memberofName = null;
var newDoclet = createSymbolDoclet(docletSrc, e);
// if there's an alias, use that as the symbol name
if (newDoclet.alias) {
processAlias(parser, newDoclet, e.astnode);
}
// otherwise, get the symbol name from the code
else if (e.code && e.code.name) {
newDoclet.addTag('name', e.code.name);
if (!newDoclet.memberof) {
addSymbolMemberof(parser, newDoclet, e.astnode);
}
newDoclet.postProcess();
}
else {
return false;
}
// set the scope to global unless any of the following are true:
// a) the doclet is a memberof something
// b) the doclet represents a module
// c) we're in a module that exports only this symbol
if ( !newDoclet.memberof && newDoclet.kind !== 'module' &&
(!currentModule || currentModule.longname !== newDoclet.name) ) {
newDoclet.scope = SCOPE_NAMES.GLOBAL;
}
addDoclet(parser, newDoclet);
e.doclet = newDoclet;
}
/**
* Attach these event handlers to a particular instance of a parser.
* @param parser
*/
exports.attachTo = function(parser) {
// Handle JSDoc "virtual comments" that include one of the following:
// + A `@name` tag
// + Another tag that accepts a name, such as `@function`
parser.on('jsdocCommentFound', function(e) {
var comments = e.comment.split(/@also\b/g);
var newDoclet;
for (var i = 0, l = comments.length; i < l; i++) {
newDoclet = createDoclet(comments[i], e);
// we're only interested in virtual comments here
if (!newDoclet.name) {
continue;
}
// add the default scope/memberof for a module (if we're in a module)
setModuleScopeMemberOf(newDoclet);
newDoclet.postProcess();
// if we _still_ don't have a scope, use the default
setDefaultScope(newDoclet);
addDoclet(parser, newDoclet);
e.doclet = newDoclet;
}
});
// Handle named symbols in the code. May or may not have a JSDoc comment attached.
parser.on('symbolFound', function(e) {
var comments = e.comment.split(/@also\b/g);
for (var i = 0, l = comments.length; i < l; i++) {
newSymbolDoclet(parser, comments[i], e);
}
});
parser.on('fileComplete', function(e) {
currentModule = null;
});
};
@@ -0,0 +1,524 @@
/**
* @module jsdoc/src/parser
*/
'use strict';
var events = require('events');
var fs = require('jsdoc/fs');
var jsdoc = {
doclet: require('jsdoc/doclet'),
name: require('jsdoc/name'),
src: {
astnode: require('jsdoc/src/astnode'),
syntax: require('jsdoc/src/syntax')
},
util: {
doop: require('jsdoc/util/doop'),
runtime: require('jsdoc/util/runtime')
}
};
var logger = require('jsdoc/util/logger');
var path = require('jsdoc/path');
var util = require('util');
var hasOwnProp = Object.prototype.hasOwnProperty;
var Syntax = jsdoc.src.syntax.Syntax;
// TODO: docs
var PARSERS = exports.PARSERS = {
esprima: 'jsdoc/src/parser',
rhino: 'rhino/jsdoc/src/parser'
};
/*eslint-disable no-script-url */
// Prefix for JavaScript strings that were provided in lieu of a filename.
var SCHEMA = 'javascript:';
/*eslint-enable no-script-url */
// TODO: docs
exports.createParser = function(type) {
var modulePath;
if (!type) {
type = jsdoc.util.runtime.isRhino() ? 'rhino' : 'esprima';
}
if (PARSERS[type]) {
modulePath = PARSERS[type];
}
else {
modulePath = path.join( path.getResourcePath(path.dirname(type)), path.basename(type) );
}
try {
return new ( require(modulePath) ).Parser();
}
catch (e) {
logger.fatal('Unable to create the parser type "' + type + '": ' + e);
}
};
// TODO: docs
/**
* @class
* @alias module:jsdoc/src/parser.Parser
* @mixes module:events.EventEmitter
*
* @example <caption>Create a new parser.</caption>
* var jsdocParser = new (require('jsdoc/src/parser').Parser)();
*/
var Parser = exports.Parser = function(builderInstance, visitorInstance, walkerInstance) {
this.clear();
this._astBuilder = builderInstance || new (require('jsdoc/src/astbuilder')).AstBuilder();
this._visitor = visitorInstance || new (require('jsdoc/src/visitor')).Visitor(this);
this._walker = walkerInstance || new (require('jsdoc/src/walker')).Walker();
Object.defineProperties(this, {
astBuilder: {
get: function() {
return this._astBuilder;
}
},
visitor: {
get: function() {
return this._visitor;
}
},
walker: {
get: function() {
return this._walker;
}
}
});
};
util.inherits(Parser, events.EventEmitter);
// TODO: docs
Parser.prototype.clear = function() {
this._resultBuffer = [];
this.refs = {};
this.refs[jsdoc.name.LONGNAMES.GLOBAL] = {};
this.refs[jsdoc.name.LONGNAMES.GLOBAL].meta = {};
};
// TODO: update docs
/**
* Parse the given source files for JSDoc comments.
* @param {Array.<string>} sourceFiles An array of filepaths to the JavaScript sources.
* @param {string} [encoding=utf8]
*
* @fires module:jsdoc/src/parser.Parser.parseBegin
* @fires module:jsdoc/src/parser.Parser.fileBegin
* @fires module:jsdoc/src/parser.Parser.jsdocCommentFound
* @fires module:jsdoc/src/parser.Parser.symbolFound
* @fires module:jsdoc/src/parser.Parser.newDoclet
* @fires module:jsdoc/src/parser.Parser.fileComplete
* @fires module:jsdoc/src/parser.Parser.parseComplete
*
* @example <caption>Parse two source files.</caption>
* var myFiles = ['file1.js', 'file2.js'];
* var docs = jsdocParser.parse(myFiles);
*/
Parser.prototype.parse = function(sourceFiles, encoding) {
encoding = encoding || global.env.conf.encoding || 'utf8';
var filename = '';
var sourceCode = '';
var parsedFiles = [];
var e = {};
if (typeof sourceFiles === 'string') {
sourceFiles = [sourceFiles];
}
e.sourcefiles = sourceFiles;
logger.debug('Parsing source files: %j', sourceFiles);
this.emit('parseBegin', e);
for (var i = 0, l = sourceFiles.length; i < l; i++) {
sourceCode = '';
if (sourceFiles[i].indexOf(SCHEMA) === 0) {
sourceCode = sourceFiles[i].substr(SCHEMA.length);
filename = '[[string' + i + ']]';
}
else {
filename = sourceFiles[i];
try {
sourceCode = fs.readFileSync(filename, encoding);
}
catch(e) {
logger.error('Unable to read and parse the source file %s: %s', filename, e);
}
}
if (sourceCode.length) {
this._parseSourceCode(sourceCode, filename);
parsedFiles.push(filename);
}
}
this.emit('parseComplete', {
sourcefiles: parsedFiles,
doclets: this._resultBuffer
});
logger.debug('Finished parsing source files.');
return this._resultBuffer;
};
// TODO: docs
Parser.prototype.fireProcessingComplete = function(doclets) {
this.emit('processingComplete', { doclets: doclets });
};
// TODO: docs
Parser.prototype.results = function() {
return this._resultBuffer;
};
// TODO: update docs
/**
* @param {Object} o The parse result to add to the result buffer.
*/
Parser.prototype.addResult = function(o) {
this._resultBuffer.push(o);
};
// TODO: docs
Parser.prototype.addAstNodeVisitor = function(visitor) {
this._visitor.addAstNodeVisitor(visitor);
};
// TODO: docs
Parser.prototype.getAstNodeVisitors = function() {
return this._visitor.getAstNodeVisitors();
};
// TODO: docs
function pretreat(code) {
return code
// comment out hashbang at the top of the file, like: #!/usr/bin/env node
.replace(/^(\#\![\S \t]+\r?\n)/, '// $1')
// to support code minifiers that preserve /*! comments, treat /*!* as equivalent to /**
.replace(/\/\*\!\*/g, '/**')
// merge adjacent doclets
.replace(/\*\/\/\*\*+/g, '@also');
}
/** @private */
Parser.prototype._parseSourceCode = function(sourceCode, sourceName) {
var ast;
var globalScope;
var e = {
filename: sourceName
};
this.emit('fileBegin', e);
logger.printInfo('Parsing %s ...', sourceName);
if (!e.defaultPrevented) {
e = {
filename: sourceName,
source: sourceCode
};
this.emit('beforeParse', e);
sourceCode = e.source;
sourceName = e.filename;
sourceCode = pretreat(e.source);
ast = this._astBuilder.build(sourceCode, sourceName);
if (ast) {
this._walkAst(ast, this._visitor, sourceName);
}
}
this.emit('fileComplete', e);
logger.info('complete.');
};
/** @private */
Parser.prototype._walkAst = function(ast, visitor, sourceName) {
this._walker.recurse(ast, visitor, sourceName);
};
// TODO: docs
Parser.prototype.addDocletRef = function(e) {
var node;
if (e && e.code && e.code.node) {
node = e.code.node;
// allow lookup from value => doclet
if (e.doclet) {
this.refs[node.nodeId] = e.doclet;
}
// keep references to undocumented anonymous functions, too, as they might have scoped vars
else if (
(node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression) &&
!this.refs[node.nodeId] ) {
this.refs[node.nodeId] = {
longname: jsdoc.name.LONGNAMES.ANONYMOUS,
meta: {
code: e.code
}
};
}
}
};
// TODO: docs
Parser.prototype._getDoclet = function(id) {
if ( hasOwnProp.call(this.refs, id) ) {
return this.refs[id];
}
return null;
};
// TODO: docs
/**
* @param {string} name - The symbol's longname.
* @return {string} The symbol's basename.
*/
Parser.prototype.getBasename = function(name) {
if (name !== undefined) {
return name.replace(/^([$a-z_][$a-z_0-9]*).*?$/i, '$1');
}
};
// TODO: docs
function definedInScope(doclet, basename) {
return !!doclet && !!doclet.meta && !!doclet.meta.vars && !!basename &&
hasOwnProp.call(doclet.meta.vars, basename);
}
// TODO: docs
/**
* Given a node, determine what the node is a member of.
* @param {node} node
* @returns {string} The long name of the node that this is a member of.
*/
Parser.prototype.astnodeToMemberof = function(node) {
var basename;
var doclet;
var scope;
var result = '';
var type = node.type;
if ( (type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression ||
type === Syntax.VariableDeclarator) && node.enclosingScope ) {
doclet = this._getDoclet(node.enclosingScope.nodeId);
if (!doclet) {
result = jsdoc.name.LONGNAMES.ANONYMOUS + jsdoc.name.SCOPE.PUNC.INNER;
}
else {
result = (doclet.longname || doclet.name) + jsdoc.name.SCOPE.PUNC.INNER;
}
}
else {
// check local references for aliases
scope = node;
basename = this.getBasename( jsdoc.src.astnode.nodeToString(node) );
// walk up the scope chain until we find the scope in which the node is defined
while (scope.enclosingScope) {
doclet = this._getDoclet(scope.enclosingScope.nodeId);
if ( doclet && definedInScope(doclet, basename) ) {
result = [doclet.meta.vars[basename], basename];
break;
}
else {
// move up
scope = scope.enclosingScope;
}
}
// do we know that it's a global?
doclet = this.refs[jsdoc.name.LONGNAMES.GLOBAL];
if ( doclet && definedInScope(doclet, basename) ) {
result = [doclet.meta.vars[basename], basename];
}
// have we seen the node's parent? if so, use that
else if (node.parent) {
doclet = this._getDoclet(node.parent.nodeId);
// set the result if we found a doclet. (if we didn't, the AST node may describe a
// global symbol.)
if (doclet) {
result = doclet.longname || doclet.name;
}
}
}
return result;
};
// TODO: docs
/**
* Resolve what "this" refers to relative to a node.
* @param {node} node - The "this" node
* @returns {string} The longname of the enclosing node.
*/
Parser.prototype.resolveThis = function(node) {
var doclet;
var result;
// In general, if there's an enclosing scope, we use the enclosing scope to resolve `this`.
// For object properties, we use the node's parent (the object) instead.
if (node.type !== Syntax.Property && node.enclosingScope) {
doclet = this._getDoclet(node.enclosingScope.nodeId);
if (!doclet) {
result = jsdoc.name.LONGNAMES.ANONYMOUS; // TODO handle global this?
}
else if (doclet['this']) {
result = doclet['this'];
}
// like: Foo.constructor = function(n) { /** blah */ this.name = n; }
else if (doclet.kind === 'function' && doclet.memberof) {
result = doclet.memberof;
}
// like: var foo = function(n) { /** blah */ this.bar = n; }
else if ( doclet.kind === 'member' && jsdoc.src.astnode.isAssignment(node) ) {
result = doclet.longname || doclet.name;
}
// walk up to the closest class we can find
else if (doclet.kind === 'class' || doclet.kind === 'module') {
result = doclet.longname || doclet.name;
}
else if (node.enclosingScope) {
result = this.resolveThis(node.enclosingScope);
}
}
else if (node.parent) {
doclet = this.refs[node.parent.nodeId];
// TODO: is this behavior correct? when do we get here?
if (!doclet) {
result = ''; // global?
}
else {
result = doclet.longname || doclet.name;
}
}
// TODO: is this behavior correct? when do we get here?
else {
result = ''; // global?
}
return result;
};
/**
* Given an AST node representing an object property, find the doclets for the parent object or
* objects.
*
* If the object is part of a simple assignment (for example, `var foo = { x: 1 }`), this method
* returns a single doclet (in this case, the doclet for `foo`).
*
* If the object is part of a chained assignment (for example, `var foo = exports.FOO = { x: 1 }`,
* this method returns multiple doclets (in this case, the doclets for `foo` and `exports.FOO`).
*
* @param {Object} node - An AST node representing an object property.
* @return {Array.<jsdoc/doclet.Doclet>} An array of doclets for the parent object or objects, or
* an empty array if no doclets are found.
*/
Parser.prototype.resolvePropertyParents = function(node) {
var currentAncestor = node.parent;
var nextAncestor = currentAncestor ? currentAncestor.parent : null;
var doclet;
var doclets = [];
while (currentAncestor) {
doclet = this._getDoclet(currentAncestor.nodeId);
if (doclet) {
doclets.push(doclet);
}
// if the next ancestor is an assignment expression (for example, `exports.FOO` in
// `var foo = exports.FOO = { x: 1 }`, keep walking upwards
if (nextAncestor && nextAncestor.type === Syntax.AssignmentExpression) {
nextAncestor = nextAncestor.parent;
currentAncestor = currentAncestor.parent;
}
// otherwise, we're done
else {
currentAncestor = null;
}
}
return doclets;
};
// TODO: docs
/**
* Resolve what function a var is limited to.
* @param {astnode} node
* @param {string} basename The leftmost name in the long name: in foo.bar.zip the basename is foo.
*/
Parser.prototype.resolveVar = function(node, basename) {
var doclet;
var result;
var scope = node.enclosingScope;
// HACK: return an empty string for function declarations so they don't end up in anonymous
// scope (see #685 and #693)
if (node.type === Syntax.FunctionDeclaration) {
result = '';
}
else if (!scope) {
result = ''; // global
}
else {
doclet = this._getDoclet(scope.nodeId);
if ( definedInScope(doclet, basename) ) {
result = doclet.longname;
}
else {
result = this.resolveVar(scope, basename);
}
}
return result;
};
// TODO: docs
Parser.prototype.resolveEnum = function(e) {
var doclets = this.resolvePropertyParents(e.code.node.parent);
doclets.forEach(function(doclet) {
if (doclet && doclet.isEnum) {
doclet.properties = doclet.properties || [];
// members of an enum inherit the enum's type
if (doclet.type && !e.doclet.type) {
// clone the type to prevent circular refs
e.doclet.type = jsdoc.util.doop(doclet.type);
}
delete e.doclet.undocumented;
e.doclet.defaultvalue = e.doclet.meta.code.value;
// add the doclet to the parent's properties
doclet.properties.push(e.doclet);
}
});
};
// TODO: document other events
/**
* Fired once for each JSDoc comment in the current source code.
* @event jsdocCommentFound
* @memberof module:jsdoc/src/parser.Parser
* @type {Object}
* @property {string} comment The text content of the JSDoc comment
* @property {number} lineno The line number associated with the found comment.
* @property {string} filename The file name associated with the found comment.
*/
@@ -0,0 +1,70 @@
/*global env: true */
/**
@module jsdoc/src/scanner
@requires module:fs
@author Michael Mathews <micmath@gmail.com>
@license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
'use strict';
var fs = require('jsdoc/fs');
var logger = require('jsdoc/util/logger');
var path = require('jsdoc/path');
/**
@constructor
@mixes module:events
*/
exports.Scanner = function() {};
exports.Scanner.prototype = Object.create( require('events').EventEmitter.prototype );
/**
Recursively searches the given searchPaths for js files.
@param {Array.<string>} searchPaths
@param {number} [depth=1]
@fires sourceFileFound
*/
exports.Scanner.prototype.scan = function(searchPaths, depth, filter) {
var currentFile;
var isFile;
var filePaths = [];
var pwd = env.pwd;
var self = this;
searchPaths = searchPaths || [];
depth = depth || 1;
searchPaths.forEach(function($) {
var filepath = path.resolve( pwd, decodeURIComponent($) );
try {
currentFile = fs.statSync(filepath);
}
catch (e) {
logger.error('Unable to find the source file or directory %s', filepath);
return;
}
if ( currentFile.isFile() ) {
filePaths.push(filepath);
}
else {
filePaths = filePaths.concat( fs.ls(filepath, depth) );
}
});
filePaths = filePaths.filter(function($) {
return filter.isIncluded($);
});
filePaths = filePaths.filter(function($) {
var e = { fileName: $ };
self.emit('sourceFileFound', e);
return !e.defaultPrevented;
});
return filePaths;
};
@@ -0,0 +1,67 @@
'use strict';
// TODO: docs
exports.Syntax = {
ArrayExpression: 'ArrayExpression',
ArrayPattern: 'ArrayPattern',
ArrowFunctionExpression: 'ArrowFunctionExpression',
AssignmentExpression: 'AssignmentExpression',
BinaryExpression: 'BinaryExpression',
BlockStatement: 'BlockStatement',
BreakStatement: 'BreakStatement',
CallExpression: 'CallExpression',
CatchClause: 'CatchClause',
ClassBody: 'ClassBody',
ClassDeclaration: 'ClassDeclaration',
ClassExpression: 'ClassExpression',
ComprehensionBlock: 'ComprehensionBlock',
ComprehensionExpression: 'ComprehensionExpression',
ConditionalExpression: 'ConditionalExpression',
ContinueStatement: 'ContinueStatement',
DebuggerStatement: 'DebuggerStatement',
DoWhileStatement: 'DoWhileStatement',
EmptyStatement: 'EmptyStatement',
ExportBatchSpecifier: 'ExportBatchSpecifier',
ExportDeclaration: 'ExportDeclaration',
ExportSpecifier: 'ExportSpecifier',
ExpressionStatement: 'ExpressionStatement',
ForInStatement: 'ForInStatement',
ForOfStatement: 'ForOfStatement',
ForStatement: 'ForStatement',
FunctionDeclaration: 'FunctionDeclaration',
FunctionExpression: 'FunctionExpression',
Identifier: 'Identifier',
IfStatement: 'IfStatement',
ImportDeclaration: 'ImportDeclaration',
ImportSpecifier: 'ImportSpecifier',
LabeledStatement: 'LabeledStatement',
LetStatement: 'LetStatement', // TODO: update Rhino to use VariableDeclaration
Literal: 'Literal',
LogicalExpression: 'LogicalExpression',
MemberExpression: 'MemberExpression',
MethodDefinition: 'MethodDefinition',
ModuleDeclaration: 'ModuleDeclaration',
NewExpression: 'NewExpression',
ObjectExpression: 'ObjectExpression',
ObjectPattern: 'ObjectPattern',
Program: 'Program',
Property: 'Property',
ReturnStatement: 'ReturnStatement',
SequenceExpression: 'SequenceExpression',
SpreadElement: 'SpreadElement',
SwitchCase: 'SwitchCase',
SwitchStatement: 'SwitchStatement',
TaggedTemplateExpression: 'TaggedTemplateExpression',
TemplateElement: 'TemplateElement',
TemplateLiteral: 'TemplateLiteral',
ThisExpression: 'ThisExpression',
ThrowStatement: 'ThrowStatement',
TryStatement: 'TryStatement',
UnaryExpression: 'UnaryExpression',
UpdateExpression: 'UpdateExpression',
VariableDeclaration: 'VariableDeclaration',
VariableDeclarator: 'VariableDeclarator',
WhileStatement: 'WhileStatement',
WithStatement: 'WithStatement',
YieldExpression: 'YieldExpression'
};
@@ -0,0 +1,426 @@
/**
* @module jsdoc/src/visitor
*/
'use strict';
// TODO: consider exporting more stuff so users can override it
var jsdoc = {
doclet: require('jsdoc/doclet'),
name: require('jsdoc/name'),
src: {
astnode: require('jsdoc/src/astnode'),
syntax: require('jsdoc/src/syntax')
},
util: {
logger: require('jsdoc/util/logger')
}
};
var util = require('util');
var hasOwnProp = Object.prototype.hasOwnProperty;
var Syntax = jsdoc.src.syntax.Syntax;
// TODO: docs
function getLeadingComment(node) {
var comment = null;
var leadingComments = node.leadingComments;
if (Array.isArray(leadingComments) && leadingComments.length && leadingComments[0].raw) {
comment = leadingComments[0].raw;
}
return comment;
}
// TODO: docs
function makeVarsFinisher(scopeDoclet) {
return function(e) {
// no need to evaluate all things related to scopeDoclet again, just use it
if (scopeDoclet && e.doclet && e.doclet.alias) {
scopeDoclet.meta.vars[e.code.name] = e.doclet.longname;
}
};
}
/**
* For function parameters that have inline documentation, create a function that will merge the
* inline documentation into the function's doclet. If the parameter is already documented in the
* function's doclet, the inline documentation will be ignored.
*
* @private
* @param {module:jsdoc/src/parser.Parser} parser - The JSDoc parser.
* @return {function} A function that merges a parameter's inline documentation into the function's
* doclet.
*/
function makeInlineParamsFinisher(parser) {
return function(e) {
var documentedParams;
var knownParams;
var param;
var parentDoclet;
var i = 0;
if (e.doclet && e.doclet.meta && e.doclet.meta.code && e.doclet.meta.code.node &&
e.doclet.meta.code.node.parent) {
parentDoclet = parser._getDoclet(e.doclet.meta.code.node.parent.nodeId);
}
if (!parentDoclet) {
return;
}
// we only want to use the doclet if it's param-specific (but not, for example, if it's
// a param tagged with `@exports` in an AMD module)
if (e.doclet.kind !== 'param') {
return;
}
parentDoclet.params = parentDoclet.params || [];
documentedParams = parentDoclet.params;
knownParams = parentDoclet.meta.code.paramnames;
while (true) {
param = documentedParams[i];
// is the param already documented? if so, we don't need to use the doclet
if (param && param.name === e.doclet.name) {
e.doclet.undocumented = true;
break;
}
// if we ran out of documented params, or we're at the parameter's actual position,
// splice in the param at the current index
if ( !param || i === knownParams.indexOf(e.doclet.name) ) {
documentedParams.splice(i, 0, {
type: e.doclet.type,
description: '',
name: e.doclet.name
});
// the doclet is no longer needed
e.doclet.undocumented = true;
break;
}
i++;
}
};
}
// TODO: docs
function SymbolFound(node, filename, extras) {
var self = this;
extras = extras || {};
this.id = extras.id || node.nodeId;
this.comment = extras.comment || getLeadingComment(node) || '@undocumented';
this.lineno = extras.lineno || node.loc.start.line;
this.range = extras.range || node.range;
this.filename = extras.filename || filename;
this.astnode = extras.astnode || node;
this.code = extras.code;
this.event = extras.event || 'symbolFound';
this.finishers = extras.finishers || [];
// make sure the event includes properties that don't have default values
Object.keys(extras).forEach(function(key) {
self[key] = extras[key];
});
}
// TODO: docs
function JsdocCommentFound(comment, filename) {
this.comment = comment.raw;
this.lineno = comment.loc.start.line;
this.filename = filename;
this.range = comment.range;
Object.defineProperty(this, 'event', {
value: 'jsdocCommentFound'
});
}
// TODO: docs
var Visitor = exports.Visitor = function(parser) {
this._parser = parser;
// Mozilla Parser API node visitors added by plugins
this._nodeVisitors = [];
// built-in visitors
this._visitors = [
this.visitNodeComments,
this.visitNode
];
};
// TODO: docs
Visitor.prototype.addAstNodeVisitor = function(visitor) {
this._nodeVisitors.push(visitor);
};
// TODO: docs
Visitor.prototype.removeAstNodeVisitor = function(visitor) {
var idx = this._nodeVisitors.indexOf(visitor);
if (idx !== -1) {
this._nodeVisitors.splice(idx, 1);
}
};
// TODO: docs
Visitor.prototype.getAstNodeVisitors = function() {
return this._nodeVisitors;
};
// TODO: docs; visitor signature is (node, parser, filename)
Visitor.prototype.visit = function(node, filename) {
var i;
var l;
for (i = 0, l = this._visitors.length; i < l; i++) {
this._visitors[i].call(this, node, this._parser, filename);
}
return true;
};
// TODO: docs
/**
* Verify that a block comment exists and that its leading delimiter does not contain three or more
* asterisks.
*
* @private
* @memberof module:jsdoc/src/parser.Parser
*/
function isValidJsdoc(commentSrc) {
return commentSrc && commentSrc.indexOf('/***') !== 0;
}
// TODO: docs
function hasJsdocComments(node) {
return (node && node.leadingComments && node.leadingComments.length) ||
(node && node.trailingComments && node.trailingComments.length);
}
// TODO: docs
function removeCommentDelimiters(comment) {
return comment.substring(2, comment.length - 2);
}
// TODO: docs
function updateCommentNode(commentNode, comment) {
commentNode.raw = comment;
commentNode.value = removeCommentDelimiters(comment);
}
// TODO: docs
Visitor.prototype.visitNodeComments = function(node, parser, filename) {
var comment;
var comments;
var e;
var BLOCK_COMMENT = 'Block';
if ( !hasJsdocComments(node) && (!node.type || node.type !== BLOCK_COMMENT) ) {
return true;
}
comments = (node.type === BLOCK_COMMENT) ? [node] : [];
if (node.leadingComments && node.leadingComments.length) {
comments = comments.concat( node.leadingComments.slice(0) );
}
if (node.trailingComments && node.trailingComments.length) {
comments = comments.concat( node.trailingComments.slice(0) );
}
for (var i = 0, l = comments.length; i < l; i++) {
comment = comments[i];
if ( isValidJsdoc(comment.raw) ) {
e = new JsdocCommentFound(comment, filename);
parser.emit(e.event, e, parser);
if (e.comment !== comment.raw) {
updateCommentNode(comment, e.comment);
}
}
}
return true;
};
// TODO: docs
Visitor.prototype.visitNode = function(node, parser, filename) {
var i;
var l;
var e = this.makeSymbolFoundEvent(node, parser, filename);
if (this._nodeVisitors && this._nodeVisitors.length) {
for (i = 0, l = this._nodeVisitors.length; i < l; i++) {
this._nodeVisitors[i].visitNode(node, e, parser, filename);
if (e.stopPropagation) {
break;
}
}
}
if (!e.preventDefault && e.comment && isValidJsdoc(e.comment)) {
parser.emit(e.event, e, parser);
}
// add the node to the parser's lookup table
parser.addDocletRef(e);
for (i = 0, l = e.finishers.length; i < l; i++) {
e.finishers[i].call(parser, e);
}
return true;
};
// TODO: docs
// TODO: note that it's essential to call this function before you try to resolve names!
function trackVars(parser, node, e) {
var enclosingScopeId = node.enclosingScope ? node.enclosingScope.nodeId :
jsdoc.name.LONGNAMES.GLOBAL;
var doclet = parser.refs[enclosingScopeId];
if (doclet) {
doclet.meta.vars = doclet.meta.vars || {};
doclet.meta.vars[e.code.name] = null;
e.finishers.push( makeVarsFinisher(doclet) );
}
}
// TODO: docs
Visitor.prototype.makeSymbolFoundEvent = function(node, parser, filename) {
var logger = jsdoc.util.logger;
var e;
var basename;
var i;
var l;
var parent;
var extras = {
code: jsdoc.src.astnode.getInfo(node)
};
switch (node.type) {
// like: i = 0;
case Syntax.AssignmentExpression:
e = new SymbolFound(node, filename, extras);
trackVars(parser, node, e);
basename = parser.getBasename(e.code.name);
if (basename !== 'this') {
e.code.funcscope = parser.resolveVar(node, basename);
}
break;
// like: function foo() {}
case Syntax.FunctionDeclaration:
// falls through
// like: var foo = function() {};
case Syntax.FunctionExpression:
e = new SymbolFound(node, filename, extras);
trackVars(parser, node, e);
basename = parser.getBasename(e.code.name);
e.code.funcscope = parser.resolveVar(node, basename);
break;
// like "bar" in: function foo(/** @type {string} */ bar) {}
// or "module" in: define("MyModule", function(/** @exports MyModule */ module) {}
// This is an extremely common type of node; we only care about function parameters with
// inline comments. No need to fire an event unless the node is already commented.
case Syntax.Identifier:
parent = node.parent;
if ( node.leadingComments && parent && jsdoc.src.astnode.isFunction(parent) ) {
extras.finishers = [makeInlineParamsFinisher(parser)];
e = new SymbolFound(node, filename, extras);
trackVars(parser, node, e);
}
break;
// like "obj.prop" in: /** @typedef {string} */ obj.prop;
// Closure Compiler uses this pattern extensively for enums.
// No need to fire an event unless the node is already commented.
case Syntax.MemberExpression:
if (node.leadingComments) {
e = new SymbolFound(node, filename, extras);
}
break;
// like the object literal in: function Foo = Class.create(/** @lends Foo */ {});
case Syntax.ObjectExpression:
e = new SymbolFound(node, filename, extras);
break;
// like "bar: true" in: var foo = { bar: true };
// like "get bar() {}" in: var foo = { get bar() {} };
case Syntax.Property:
if ( node.kind !== ('get' || 'set') ) {
extras.finishers = [parser.resolveEnum];
}
e = new SymbolFound(node, filename, extras);
break;
// like: var i = 0;
case Syntax.VariableDeclarator:
e = new SymbolFound(node, filename, extras);
trackVars(parser, node, e);
basename = parser.getBasename(e.code.name);
break;
// for now, log a warning for all ES6 nodes, since we don't do anything useful with them
case Syntax.ArrowFunctionExpression:
case Syntax.ClassBody:
case Syntax.ClassDeclaration:
case Syntax.ClassExpression:
case Syntax.ExportBatchSpecifier:
case Syntax.ExportDeclaration:
case Syntax.ExportSpecifier:
case Syntax.ImportDeclaration:
case Syntax.ImportSpecifier:
case Syntax.MethodDefinition:
case Syntax.ModuleDeclaration:
case Syntax.SpreadElement:
case Syntax.TaggedTemplateExpression:
case Syntax.TemplateElement:
case Syntax.TemplateLiteral:
logger.warn('JSDoc does not currently handle %s nodes. Source file: %s, line %s',
node.type, filename, (node.loc && node.loc.start) ? node.loc.start.line : '??');
break;
default:
// ignore
}
if (!e) {
e = {
finishers: []
};
}
return e;
};
@@ -0,0 +1,539 @@
/**
* Traversal utilities for ASTs that are compatible with the Mozilla Parser API. Adapted from
* [Acorn](http://marijnhaverbeke.nl/acorn/).
*
* @module jsdoc/src/walker
* @license MIT
*/
'use strict';
var astnode = require('jsdoc/src/astnode');
var doclet = require('jsdoc/doclet');
var Syntax = require('jsdoc/src/syntax').Syntax;
/**
* Check whether an AST node creates a new scope.
*
* @private
* @param {Object} node - The AST node to check.
* @return {Boolean} Set to `true` if the node creates a new scope, or `false` in all other cases.
*/
function isScopeNode(node) {
// TODO: handle blocks with "let" declarations
return node && typeof node === 'object' && (node.type === Syntax.CatchClause ||
node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression);
}
// TODO: docs
function getCurrentScope(scopes) {
return scopes[scopes.length - 1] || null;
}
// TODO: docs
function moveComments(source, target) {
if (source.leadingComments) {
target.leadingComments = source.leadingComments.slice(0);
source.leadingComments = [];
}
}
function leafNode(node, parent, state, cb) {}
// TODO: docs
var walkers = exports.walkers = {};
walkers[Syntax.ArrayExpression] = function arrayExpression(node, parent, state, cb) {
for (var i = 0, l = node.elements.length; i < l; i++) {
var e = node.elements[i];
if (e) {
cb(e, node, state);
}
}
};
// TODO: verify correctness
walkers[Syntax.ArrayPattern] = function arrayPattern(node, parent, state, cb) {
for (var i = 0, l = node.elements.length; i < l; i++) {
var e = node.elements[i];
// must be an identifier or an expression
if (e && e.type !== Syntax.Identifier) {
cb(e, node, state);
}
}
};
walkers[Syntax.ArrowFunctionExpression] =
function arrowFunctionExpression(node, parent, state, cb) {
var i;
var l;
// used for function declarations, so we include it here
if (node.id) {
cb(node.id, node, state);
}
for (i = 0, l = node.params.length; i < l; i++) {
cb(node.params[i], node, state);
}
for (i = 0, l = node.defaults.length; i < l; i++) {
cb(node.defaults[i], node, state);
}
cb(node.body, node, state);
if (node.rest) {
cb(node.rest, node, state);
}
};
walkers[Syntax.AssignmentExpression] = function assignmentExpression(node, parent, state, cb) {
cb(node.left, node, state);
cb(node.right, node, state);
};
walkers[Syntax.BinaryExpression] = function binaryExpression(node, parent, state, cb) {
cb(node.left, node, state);
cb(node.right, node, state);
};
walkers[Syntax.BlockStatement] = function blockStatement(node, parent, state, cb) {
for (var i = 0, l = node.body.length; i < l; i++) {
cb(node.body[i], node, state);
}
};
walkers[Syntax.BreakStatement] = leafNode;
walkers[Syntax.CallExpression] = function callExpression(node, parent, state, cb) {
var i;
var l;
cb(node.callee, node, state);
if (node.arguments) {
for (i = 0, l = node.arguments.length; i < l; i++) {
cb(node.arguments[i], node, state);
}
}
};
walkers[Syntax.CatchClause] = leafNode;
walkers[Syntax.ClassBody] = walkers[Syntax.BlockStatement];
walkers[Syntax.ClassDeclaration] = function classDeclaration(node, parent, state, cb) {
if (node.id) {
cb(node.id, node, state);
}
if (node.superClass) {
cb(node.superClass, node, state);
}
if (node.body) {
cb(node.body, node, state);
}
};
walkers[Syntax.ClassExpression] = walkers[Syntax.ClassDeclaration];
// TODO: verify correctness
walkers[Syntax.ComprehensionBlock] = walkers[Syntax.AssignmentExpression];
// TODO: verify correctness
walkers[Syntax.ComprehensionExpression] =
function comprehensionExpression(node, parent, state, cb) {
cb(node.body, node, state);
if (node.filter) {
cb(node.filter, node, state);
}
for (var i = 0, l = node.blocks.length; i < l; i++) {
cb(node.blocks[i], node, state);
}
};
walkers[Syntax.ConditionalExpression] = function conditionalExpression(node, parent, state, cb) {
cb(node.test, node, state);
cb(node.consequent, node, state);
cb(node.alternate, node, state);
};
walkers[Syntax.ContinueStatement] = leafNode;
walkers[Syntax.DebuggerStatement] = leafNode;
walkers[Syntax.DoWhileStatement] = function doWhileStatement(node, parent, state, cb) {
cb(node.test, node, state);
cb(node.body, node, state);
};
walkers[Syntax.EmptyStatement] = leafNode;
walkers[Syntax.ExportBatchSpecifier] = leafNode;
walkers[Syntax.ExportDeclaration] = function exportDeclaration(node, parent, state, cb) {
var i;
var l;
if (node.declaration) {
for (i = 0, l = node.declaration.length; i < l; i++) {
cb(node.declaration[i], node, state);
}
}
if (node.specifiers) {
for (i = 0, l = node.specifiers.length; i < l; i++) {
cb(node.specifiers[i], node, state);
}
}
if (node.source) {
cb(node.source, node, state);
}
};
walkers[Syntax.ExportSpecifier] = function exportSpecifier(node, parent, state, cb) {
if (node.id) {
cb(node.id, node, state);
}
if (node.name) {
cb(node.name, node, state);
}
};
walkers[Syntax.ExpressionStatement] = function expressionStatement(node, parent, state, cb) {
cb(node.expression, node, state);
};
walkers[Syntax.ForInStatement] = function forInStatement(node, parent, state, cb) {
cb(node.left, node, state);
cb(node.right, node, state);
cb(node.body, node, state);
};
walkers[Syntax.ForOfStatement] = walkers[Syntax.ForInStatement];
walkers[Syntax.ForStatement] = function forStatement(node, parent, state, cb) {
if (node.init) {
cb(node.init, node, state);
}
if (node.test) {
cb(node.test, node, state);
}
if (node.update) {
cb(node.update, node, state);
}
cb(node.body, node, state);
};
walkers[Syntax.FunctionDeclaration] = walkers[Syntax.ArrowFunctionExpression];
walkers[Syntax.FunctionExpression] = walkers[Syntax.ArrowFunctionExpression];
walkers[Syntax.Identifier] = leafNode;
walkers[Syntax.IfStatement] = function ifStatement(node, parent, state, cb) {
cb(node.test, node, state);
cb(node.consequent, node, state);
if (node.alternate) {
cb(node.alternate, node, state);
}
};
walkers[Syntax.ImportDeclaration] = function importDeclaration(node, parent, state, cb) {
var i;
var l;
if (node.specifiers) {
for (i = 0, l = node.specifiers.length; i < l; i++) {
cb(node.specifiers[i], node, state);
}
}
if (node.source) {
cb(node.source, node, state);
}
};
walkers[Syntax.ImportSpecifier] = walkers[Syntax.ExportSpecifier];
walkers[Syntax.LabeledStatement] = function labeledStatement(node, parent, state, cb) {
cb(node.body, node, state);
};
// TODO: add scope info??
walkers[Syntax.LetStatement] = function letStatement(node, parent, state, cb) {
for (var i = 0, l = node.head.length; i < l; i++) {
var head = node.head[i];
cb(head.id, node, state);
if (head.init) {
cb(head.init, node, state);
}
}
cb(node.body, node, state);
};
walkers[Syntax.Literal] = leafNode;
walkers[Syntax.LogicalExpression] = walkers[Syntax.BinaryExpression];
walkers[Syntax.MemberExpression] = function memberExpression(node, parent, state, cb) {
cb(node.object, node, state);
if (node.property) {
cb(node.property, node, state);
}
};
walkers[Syntax.MethodDefinition] = function methodDefinition(node, parent, state, cb) {
if (node.key) {
cb(node.key, node, state);
}
if (node.value) {
cb(node.value, node, state);
}
};
walkers[Syntax.ModuleDeclaration] = function moduleDeclaration(node, parent, state, cb) {
if (node.id) {
cb(node.id, node, state);
}
if (node.source) {
cb(node.source, node, state);
}
if (node.body) {
cb(node.body, node, state);
}
};
walkers[Syntax.NewExpression] = walkers[Syntax.CallExpression];
walkers[Syntax.ObjectExpression] = function objectExpression(node, parent, state, cb) {
for (var i = 0, l = node.properties.length; i < l; i++) {
cb(node.properties[i], node, state);
}
};
walkers[Syntax.ObjectPattern] = walkers[Syntax.ObjectExpression];
walkers[Syntax.Program] = walkers[Syntax.BlockStatement];
walkers[Syntax.Property] = function property(node, parent, state, cb) {
// move leading comments from key to property node
moveComments(node.key, node);
cb(node.value, node, state);
};
walkers[Syntax.ReturnStatement] = function returnStatement(node, parent, state, cb) {
if (node.argument) {
cb(node.argument, node, state);
}
};
walkers[Syntax.SequenceExpression] = function sequenceExpression(node, parent, state, cb) {
for (var i = 0, l = node.expressions.length; i < l; i++) {
cb(node.expressions[i], node, state);
}
};
walkers[Syntax.SpreadElement] = function spreadElement(node, parent, state, cb) {
if (node.argument) {
cb(node.argument, node, state);
}
};
walkers[Syntax.SwitchCase] = function switchCase(node, parent, state, cb) {
if (node.test) {
cb(node.test, node, state);
}
for (var i = 0, l = node.consequent.length; i < l; i++) {
cb(node.consequent[i], node, state);
}
};
walkers[Syntax.SwitchStatement] = function switchStatement(node, parent, state, cb) {
cb(node.discriminant, node, state);
for (var i = 0, l = node.cases.length; i < l; i++) {
cb(node.cases[i], node, state);
}
};
walkers[Syntax.TaggedTemplateExpression] =
function taggedTemplateExpression(node, parent, state, cb) {
if (node.tag) {
cb(node.tag, node, state);
}
if (node.quasi) {
cb(node.quasi, node, state);
}
};
walkers[Syntax.TemplateElement] = leafNode;
walkers[Syntax.TemplateLiteral] = function templateLiteral(node, parent, state, cb) {
var i;
var l;
if (node.quasis && node.quasis.length) {
for (i = 0, l = node.quasis.length; i < l; i++) {
cb(node.quasis[i], node, state);
}
}
if (node.expressions && node.expressions.length) {
for (i = 0, l = node.expressions.length; i < l; i++) {
cb(node.expressions[i], node, state);
}
}
};
walkers[Syntax.ThisExpression] = leafNode;
walkers[Syntax.ThrowStatement] = function throwStatement(node, parent, state, cb) {
cb(node.argument, node, state);
};
walkers[Syntax.TryStatement] = function tryStatement(node, parent, state, cb) {
var i;
var l;
cb(node.block, node, state);
// handle Esprima ASTs, which deviate from the spec a bit
if ( node.handlers && Array.isArray(node.handlers) && node.handlers[0] ) {
cb(node.handlers[0].body, node, state);
}
else if (node.handler) {
cb(node.handler.body, node, state);
}
if (node.guardedHandlers) {
for (i = 0, l = node.guardedHandlers.length; i < l; i++) {
cb(node.guardedHandlers[i].body, node, state);
}
}
if (node.finalizer) {
cb(node.finalizer, node, state);
}
};
walkers[Syntax.UnaryExpression] = function unaryExpression(node, parent, state, cb) {
cb(node.argument, node, state);
};
walkers[Syntax.UpdateExpression] = walkers[Syntax.UnaryExpression];
walkers[Syntax.VariableDeclaration] = function variableDeclaration(node, parent, state, cb) {
// move leading comments to first declarator
moveComments(node, node.declarations[0]);
for (var i = 0, l = node.declarations.length; i < l; i++) {
cb(node.declarations[i], node, state);
}
};
walkers[Syntax.VariableDeclarator] = function variableDeclarator(node, parent, state, cb) {
cb(node.id, node, state);
if (node.init) {
cb(node.init, node, state);
}
};
walkers[Syntax.WhileStatement] = walkers[Syntax.DoWhileStatement];
walkers[Syntax.WithStatement] = function withStatement(node, parent, state, cb) {
cb(node.object, node, state);
cb(node.body, node, state);
};
walkers[Syntax.YieldExpression] = function(node, parent, state, cb) {
if (node.argument) {
cb(node.argument, node, state);
}
};
/**
* Create a walker that can traverse an AST that is consistent with the Mozilla Parser API.
*
* @todo docs
* @memberof module:jsdoc/src/walker
*/
var Walker = exports.Walker = function(walkerFuncs) {
this._walkers = walkerFuncs || walkers;
};
// TODO: docs
Walker.prototype._recurse = function(filename, ast) {
var self = this;
var state = {
filename: filename,
nodes: [],
scopes: []
};
function cb(node, parent, state) {
var currentScope;
var isScope = astnode.isScope(node);
// for efficiency, if the node has a `parent` property, assume that we've already
// added the required properties
if (typeof node.parent !== 'undefined') {
astnode.addNodeProperties(node);
}
node.parent = parent || null;
currentScope = getCurrentScope(state.scopes);
if (currentScope) {
node.enclosingScope = currentScope;
}
if (isScope) {
state.scopes.push(node);
}
state.nodes.push(node);
self._walkers[node.type](node, parent, state, cb);
if (isScope) {
state.scopes.pop();
}
}
cb(ast, null, state);
return state;
};
// TODO: docs
// TODO: skip the AST root node to be consistent with Rhino?
Walker.prototype.recurse = function(ast, visitor, filename) {
var shouldContinue;
var state = this._recurse(filename, ast);
if (visitor) {
for (var i = 0, l = state.nodes.length; i < l; i++) {
shouldContinue = visitor.visit.call(visitor, state.nodes[i], filename);
if (!shouldContinue) {
break;
}
}
}
return ast;
};
+196
View File
@@ -0,0 +1,196 @@
/*global env: true */
/**
@overview
@author Michael Mathews <micmath@gmail.com>
@license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
/**
Functionality related to JSDoc tags.
@module jsdoc/tag
@requires jsdoc/tag/dictionary
@requires jsdoc/tag/validator
@requires jsdoc/tag/type
*/
'use strict';
var jsdoc = {
tag: {
dictionary: require('jsdoc/tag/dictionary'),
validator: require('jsdoc/tag/validator'),
type: require('jsdoc/tag/type')
},
util: {
logger: require('jsdoc/util/logger')
}
};
var path = require('jsdoc/path');
var util = require('util');
// Check whether the text is the same as a symbol name with leading or trailing whitespace. If so,
// the whitespace must be preserved, and the text cannot be trimmed.
function mustPreserveWhitespace(text, meta) {
return meta && meta.code && meta.code.name === text && text.match(/(?:^\s+)|(?:\s+$)/);
}
function trim(text, opts, meta) {
var indentMatcher;
var match;
opts = opts || {};
text = text || '';
if ( mustPreserveWhitespace(text, meta) ) {
text = util.format('"%s"', text);
}
else if (opts.keepsWhitespace) {
text = text.replace(/^[\n\r\f]+|[\n\r\f]+$/g, '');
if (opts.removesIndent) {
match = text.match(/^([ \t]+)/);
if (match && match[1]) {
indentMatcher = new RegExp('^' + match[1], 'gm');
text = text.replace(indentMatcher, '');
}
}
}
else {
text = text.replace(/^\s+|\s+$/g, '');
}
return text;
}
function addHiddenProperty(obj, propName, propValue) {
Object.defineProperty(obj, propName, {
value: propValue,
writable: true,
enumerable: !!global.env.opts.debug,
configurable: true
});
}
function parseType(tag, tagDef, meta) {
try {
return jsdoc.tag.type.parse(tag.text, tagDef.canHaveName, tagDef.canHaveType);
}
catch (e) {
jsdoc.util.logger.error(
'Unable to parse a tag\'s type expression%s with tag title "%s" and text "%s": %s',
meta.filename ? ( ' for source file ' + path.join(meta.path, meta.filename) ) : '',
tag.originalTitle,
tag.text,
e.message
);
return {};
}
}
function processTagText(tag, tagDef, meta) {
var tagType;
if (tagDef.onTagText) {
tag.text = tagDef.onTagText(tag.text);
}
if (tagDef.canHaveType || tagDef.canHaveName) {
/** The value property represents the result of parsing the tag text. */
tag.value = {};
tagType = parseType(tag, tagDef, meta);
// It is possible for a tag to *not* have a type but still have
// optional or defaultvalue, e.g. '@param [foo]'.
// Although tagType.type.length == 0 we should still copy the other properties.
if (tagType.type) {
if (tagType.type.length) {
tag.value.type = {
names: tagType.type
};
addHiddenProperty(tag.value.type, 'parsedType', tagType.parsedType);
}
['optional', 'nullable', 'variable', 'defaultvalue'].forEach(function(prop) {
if (typeof tagType[prop] !== 'undefined') {
tag.value[prop] = tagType[prop];
}
});
}
if (tagType.text && tagType.text.length) {
tag.value.description = tagType.text;
}
if (tagDef.canHaveName) {
// note the dash is a special case: as a param name it means "no name"
if (tagType.name && tagType.name !== '-') { tag.value.name = tagType.name; }
}
}
else {
tag.value = tag.text;
}
}
/**
* Replace the existing tag dictionary with a new tag dictionary.
*
* Used for testing only. Do not call this method directly. Instead, call
* {@link module:jsdoc/doclet._replaceDictionary}, which also updates this module's tag dictionary.
*
* @private
* @param {module:jsdoc/tag/dictionary.Dictionary} dict - The new tag dictionary.
*/
exports._replaceDictionary = function _replaceDictionary(dict) {
jsdoc.tag.dictionary = dict;
};
/**
Constructs a new tag object. Calls the tag validator.
@class
@classdesc Represents a single doclet tag.
@param {string} tagTitle
@param {string=} tagBody
@param {object=} meta
*/
var Tag = exports.Tag = function(tagTitle, tagBody, meta) {
var tagDef;
var trimOpts;
meta = meta || {};
this.originalTitle = trim(tagTitle);
/** The title of the tag (for example, `title` in `@title text`). */
this.title = jsdoc.tag.dictionary.normalise(this.originalTitle);
tagDef = jsdoc.tag.dictionary.lookUp(this.title);
trimOpts = {
keepsWhitespace: tagDef.keepsWhitespace,
removesIndent: tagDef.removesIndent
};
/**
* The text following the tag (for example, `text` in `@title text`).
*
* Whitespace is trimmed from the tag text as follows:
*
* + If the tag's `keepsWhitespace` option is falsy, all leading and trailing whitespace are
* removed.
* + If the tag's `keepsWhitespace` option is set to `true`, leading and trailing whitespace are
* not trimmed, unless the `removesIndent` option is also enabled.
* + If the tag's `removesIndent` option is set to `true`, any indentation that is shared by
* every line in the string is removed. This option is ignored unless `keepsWhitespace` is set
* to `true`.
*
* **Note**: If the tag text is the name of a symbol, and the symbol's name includes leading or
* trailing whitespace (for example, the property names in `{ ' ': true, ' foo ': false }`),
* the tag text is not trimmed. Instead, the tag text is wrapped in double quotes to prevent the
* whitespace from being trimmed.
*/
this.text = trim(tagBody, trimOpts, meta);
if (this.text) {
processTagText(this, tagDef, meta);
}
jsdoc.tag.validator.validate(this, tagDef, meta);
};
@@ -0,0 +1,119 @@
/** @module jsdoc/tag/dictionary */
'use strict';
var definitions = require('jsdoc/tag/dictionary/definitions');
var hasOwnProp = Object.prototype.hasOwnProperty;
var dictionary;
/** @private */
function TagDefinition(dictionary, title, etc) {
var self = this;
etc = etc || {};
this.title = dictionary.normalise(title);
Object.defineProperty(this, '_dictionary', {
value: dictionary
});
Object.keys(etc).forEach(function(p) {
self[p] = etc[p];
});
}
/** @private */
TagDefinition.prototype.synonym = function(synonymName) {
this._dictionary.defineSynonym(this.title, synonymName);
return this; // chainable
};
/**
* @class
* @alias module:jsdoc/tag/dictionary.Dictionary
*/
function Dictionary() {
this._tags = {};
this._tagSynonyms = {};
// The longnames for `Package` objects include a `package` namespace. There's no `package` tag,
// though, so we declare the namespace here.
this._namespaces = ['package'];
}
/** @function */
Dictionary.prototype._defineNamespace = function defineNamespace(title) {
title = this.normalise(title || '');
if (title && this._namespaces.indexOf(title) === -1) {
this._namespaces.push(title);
}
return this;
};
/** @function */
Dictionary.prototype.defineTag = function defineTag(title, opts) {
var tagDef = new TagDefinition(this, title, opts);
this._tags[tagDef.title] = tagDef;
if (opts && opts.isNamespace) {
this._defineNamespace(tagDef.title);
}
return this._tags[tagDef.title];
};
/** @function */
Dictionary.prototype.defineSynonym = function defineSynonym(title, synonym) {
this._tagSynonyms[synonym.toLowerCase()] = this.normalise(title);
};
/** @function */
Dictionary.prototype.getNamespaces = function getNamespaces() {
return this._namespaces.slice(0);
};
/** @function */
Dictionary.prototype.lookUp = function lookUp(title) {
title = this.normalise(title);
if ( hasOwnProp.call(this._tags, title) ) {
return this._tags[title];
}
return false;
};
/** @function */
Dictionary.prototype.isNamespace = function isNamespace(kind) {
if (kind) {
kind = this.normalise(kind);
if (this._namespaces.indexOf(kind) !== -1) {
return true;
}
}
return false;
};
/** @function */
Dictionary.prototype.normalise = function normalise(title) {
var canonicalName = title.toLowerCase();
if ( hasOwnProp.call(this._tagSynonyms, canonicalName) ) {
return this._tagSynonyms[canonicalName];
}
return canonicalName;
};
// initialize the default dictionary
dictionary = new Dictionary();
definitions.defineTags(dictionary);
// make the constructor available for unit-testing purposes
dictionary.Dictionary = Dictionary;
/** @type {module:jsdoc/tag/dictionary.Dictionary} */
module.exports = dictionary;
@@ -0,0 +1,900 @@
/**
Define tags that are known in JSDoc.
@module jsdoc/tag/dictionary/definitions
@author Michael Mathews <micmath@gmail.com>
@license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
'use strict';
var _ = require('underscore');
var jsdoc = {
name: require('jsdoc/name'),
src: {
astnode: require('jsdoc/src/astnode')
},
tag: {
type: require('jsdoc/tag/type')
},
util: {
doop: require('jsdoc/util/doop'),
logger: require('jsdoc/util/logger')
}
};
var path = require('jsdoc/path');
var Syntax = require('jsdoc/src/syntax').Syntax;
var hasOwnProp = Object.prototype.hasOwnProperty;
var DEFINITIONS = {
closure: 'closureTags',
jsdoc: 'jsdocTags'
};
var MODULE_NAMESPACE = 'module:';
// Clone a tag definition, excluding synonyms.
function cloneTagDef(tagDef, extras) {
var newTagDef = jsdoc.util.doop(tagDef);
delete newTagDef.synonyms;
return (extras ? _.extend(newTagDef, extras) : newTagDef);
}
function getSourcePaths() {
var sourcePaths = global.env.sourceFiles.slice(0) || [];
if (global.env.opts._) {
global.env.opts._.forEach(function(sourcePath) {
var resolved = path.resolve(global.env.pwd, sourcePath);
if (sourcePaths.indexOf(resolved) === -1) {
sourcePaths.push(resolved);
}
});
}
return sourcePaths;
}
function filepathMinusPrefix(filepath) {
var sourcePaths = getSourcePaths();
var commonPrefix = path.commonPrefix(sourcePaths);
var result = '';
if (filepath) {
filepath = path.normalize(filepath);
// always use forward slashes in the result
result = (filepath + path.sep).replace(commonPrefix, '')
.replace(/\\/g, '/');
}
if (result.length > 0 && result[result.length - 1] !== '/') {
result += '/';
}
return result;
}
/** @private */
function setDocletKindToTitle(doclet, tag) {
doclet.addTag( 'kind', tag.title );
}
function setDocletScopeToTitle(doclet, tag) {
try {
doclet.setScope(tag.title);
}
catch(e) {
jsdoc.util.logger.error(e.message);
}
}
function setDocletNameToValue(doclet, tag) {
if (tag.value && tag.value.description) { // as in a long tag
doclet.addTag('name', tag.value.description);
}
else if (tag.text) { // or a short tag
doclet.addTag('name', tag.text);
}
}
function setDocletNameToValueName(doclet, tag) {
if (tag.value && tag.value.name) {
doclet.addTag('name', tag.value.name);
}
}
function setDocletDescriptionToValue(doclet, tag) {
if (tag.value) {
doclet.addTag('description', tag.value);
}
}
function setDocletTypeToValueType(doclet, tag) {
if (tag.value && tag.value.type) {
// Add the type names and other type properties (such as `optional`).
// Don't overwrite existing properties.
Object.keys(tag.value).forEach(function(prop) {
if ( !hasOwnProp.call(doclet, prop) ) {
doclet[prop] = tag.value[prop];
}
});
}
}
function setNameToFile(doclet, tag) {
var name;
if (doclet.meta.filename) {
name = filepathMinusPrefix(doclet.meta.path) + doclet.meta.filename;
doclet.addTag('name', name);
}
}
function setDocletMemberof(doclet, tag) {
if (tag.value && tag.value !== '<global>') {
doclet.setMemberof(tag.value);
}
}
function applyNamespace(docletOrNs, tag) {
if (typeof docletOrNs === 'string') { // ns
tag.value = jsdoc.name.applyNamespace(tag.value, docletOrNs);
}
else { // doclet
if (!docletOrNs.name) {
return; // error?
}
docletOrNs.longname = jsdoc.name.applyNamespace(docletOrNs.name, tag.title);
}
}
function setDocletNameToFilename(doclet, tag) {
var name = '';
if (doclet.meta.path) {
name = filepathMinusPrefix(doclet.meta.path);
}
name += doclet.meta.filename.replace(/\.js$/i, '');
doclet.name = name;
}
function parseTypeText(text) {
var tagType = jsdoc.tag.type.parse(text, false, true);
return tagType.typeExpression || text;
}
function parseBorrows(doclet, tag) {
var m = /^(\S+)(?:\s+as\s+(\S+))?$/.exec(tag.text);
if (m) {
if (m[1] && m[2]) {
return { target: m[1], source: m[2] };
}
else if (m[1]) {
return { target: m[1] };
}
} else {
return {};
}
}
function stripModuleNamespace(name) {
return name.replace(/^module\:/, '');
}
function firstWordOf(string) {
var m = /^(\S+)/.exec(string);
if (m) { return m[1]; }
else { return ''; }
}
// Core JSDoc tags that are shared with other tag dictionaries.
var baseTags = exports.baseTags = {
abstract: {
mustNotHaveValue: true,
onTagged: function(doclet, tag) {
// we call this `virtual` because `abstract` is a reserved word
doclet.virtual = true;
},
synonyms: ['virtual']
},
access: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
// only valid values are private and protected, public is default
if ( /^(private|protected)$/i.test(tag.value) ) {
doclet.access = tag.value.toLowerCase();
}
else {
delete doclet.access;
}
}
},
alias: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.alias = tag.value;
}
},
// Special separator tag indicating that multiple doclets should be generated for the same
// comment. Used internally (and by some JSDoc users, although it's not officially supported).
// In the following example, the parser will replace `//**` with an `@also` tag:
// /**
// * Foo.
// *//**
// * Foo with a param.
// * @param {string} bar
// */
// function foo(bar) {}
also: {
onTagged: function(doclet, tag) {
// let the parser handle it; we define the tag here to avoid "not a known tag" errors
}
},
augments: {
mustHaveValue: true,
// Allow augments value to be specified as a normal type, e.g. {Type}
onTagText: parseTypeText,
onTagged: function(doclet, tag) {
doclet.augment( firstWordOf(tag.value) );
},
synonyms: ['extends']
},
author: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.author = doclet.author || [];
doclet.author.push(tag.value);
}
},
// this symbol has a member that should use the same docs as another symbol
borrows: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
var borrows = parseBorrows(doclet, tag);
doclet.borrow(borrows.target, borrows.source);
}
},
class: {
onTagged: function(doclet, tag) {
doclet.addTag('kind', 'class');
// We treat the @class tag as a @classdesc tag if all of the following are true:
// - Both @class and @constructor tags are present
// - There's no @classdesc tag
// - There are multiple words after @class
if (tag.value &&
tag.originalTitle === 'class' &&
/@construct(?:s|or)\b/i.test(doclet.comment) &&
!/@classdesc\b/i.test(doclet.comment) &&
tag.value.match(/\S+\s+\S+/)) {
doclet.addTag('classdesc', tag.value);
}
else {
setDocletNameToValue(doclet, tag);
}
},
synonyms: ['constructor']
},
classdesc: {
onTagged: function(doclet, tag) {
doclet.classdesc = tag.value;
}
},
constant: {
canHaveType: true,
canHaveName: true,
onTagged: function(doclet, tag) {
setDocletKindToTitle(doclet, tag);
setDocletNameToValueName(doclet, tag);
setDocletTypeToValueType(doclet, tag);
},
synonyms: ['const']
},
constructs: {
onTagged: function(doclet, tag) {
var ownerClassName;
if (!tag.value) {
// this can be resolved later in the handlers
ownerClassName = '{@thisClass}';
}
else {
ownerClassName = firstWordOf(tag.value);
}
doclet.addTag('alias', ownerClassName);
doclet.addTag('kind', 'class');
}
},
copyright: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.copyright = tag.value;
}
},
default: {
onTagged: function(doclet, tag) {
var type;
var value;
var nodeToString = jsdoc.src.astnode.nodeToString;
if (tag.value) {
doclet.defaultvalue = tag.value;
}
else if (doclet.meta && doclet.meta.code && doclet.meta.code.value) {
type = doclet.meta.code.type;
value = doclet.meta.code.value;
switch (type) {
case Syntax.ArrayExpression:
doclet.defaultvalue = nodeToString(doclet.meta.code.node);
doclet.defaultvaluetype = 'array';
break;
case Syntax.Literal:
doclet.defaultvalue = String(value);
break;
case Syntax.ObjectExpression:
doclet.defaultvalue = nodeToString(doclet.meta.code.node);
doclet.defaultvaluetype = 'object';
break;
default:
// do nothing
break;
}
}
},
synonyms: ['defaultvalue']
},
deprecated: {
// value is optional
onTagged: function(doclet, tag) {
doclet.deprecated = tag.value || true;
}
},
description: {
mustHaveValue: true,
synonyms: ['desc']
},
enum: {
canHaveType: true,
onTagged: function(doclet, tag) {
doclet.kind = 'member';
doclet.isEnum = true;
setDocletTypeToValueType(doclet, tag);
}
},
event: {
isNamespace: true,
onTagged: function(doclet, tag) {
setDocletKindToTitle(doclet, tag);
setDocletNameToValue(doclet, tag);
}
},
example: {
keepsWhitespace: true,
removesIndent: true,
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.examples = doclet.examples || [];
doclet.examples.push(tag.value);
}
},
exports: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
var modName = firstWordOf(tag.value);
// in case the user wrote something like `/** @exports module:foo */`:
doclet.addTag( 'alias', stripModuleNamespace(modName) );
doclet.addTag('kind', 'module');
}
},
external: {
canHaveType: true,
isNamespace: true,
onTagged: function(doclet, tag) {
setDocletKindToTitle(doclet, tag);
if (tag.value && tag.value.type) {
setDocletTypeToValueType(doclet, tag);
doclet.addTag('name', doclet.type.names[0]);
}
else {
setDocletNameToValue(doclet, tag);
}
},
synonyms: ['host']
},
file: {
onTagged: function(doclet, tag) {
setNameToFile(doclet, tag);
setDocletKindToTitle(doclet, tag);
setDocletDescriptionToValue(doclet, tag);
doclet.preserveName = true;
},
synonyms: ['fileoverview', 'overview']
},
fires: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.fires = doclet.fires || [];
applyNamespace('event', tag);
doclet.fires.push(tag.value);
},
synonyms: ['emits']
},
function: {
onTagged: function(doclet, tag) {
setDocletKindToTitle(doclet, tag);
setDocletNameToValue(doclet, tag);
},
synonyms: ['func', 'method']
},
global: {
mustNotHaveValue: true,
onTagged: function(doclet, tag) {
doclet.scope = jsdoc.name.SCOPE.NAMES.GLOBAL;
delete doclet.memberof;
}
},
ignore: {
mustNotHaveValue: true,
onTagged: function(doclet, tag) {
doclet.ignore = true;
}
},
implements: {
mustHaveValue: true,
onTagText: parseTypeText,
onTagged: function(doclet, tag) {
doclet.implements = doclet.implements || [];
doclet.implements.push(tag.value);
}
},
inheritdoc: {
mustNotHaveValue: true,
onTagged: function(doclet, tag) {
// use an empty string so JSDoc can support `@inheritdoc Foo#bar` in the future
doclet.inheritdoc = '';
}
},
inner: {
onTagged: function(doclet, tag) {
setDocletScopeToTitle(doclet, tag);
}
},
instance: {
onTagged: function(doclet, tag) {
setDocletScopeToTitle(doclet, tag);
}
},
interface: {
canHaveName: true,
onTagged: function(doclet, tag) {
doclet.addTag('kind', 'interface');
if (tag.value) {
setDocletNameToValueName(doclet, tag);
}
}
},
kind: {
mustHaveValue: true
},
lends: {
onTagged: function(doclet, tag) {
doclet.alias = tag.value || jsdoc.name.LONGNAMES.GLOBAL;
doclet.addTag('undocumented');
}
},
license: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.license = tag.value;
}
},
listens: {
mustHaveValue: true,
onTagged: function (doclet, tag) {
doclet.listens = doclet.listens || [];
applyNamespace('event', tag);
doclet.listens.push(tag.value);
}
},
member: {
canHaveType: true,
canHaveName: true,
onTagged: function(doclet, tag) {
setDocletKindToTitle(doclet, tag);
setDocletNameToValueName(doclet, tag);
setDocletTypeToValueType(doclet, tag);
},
synonyms: ['var']
},
memberof: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
if (tag.originalTitle === 'memberof!') {
doclet.forceMemberof = true;
if (tag.value === jsdoc.name.LONGNAMES.GLOBAL) {
doclet.addTag('global');
delete doclet.memberof;
}
}
setDocletMemberof(doclet, tag);
},
synonyms: ['memberof!']
},
// this symbol mixes in all of the specified object's members
mixes: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
var source = firstWordOf(tag.value);
doclet.mix(source);
}
},
mixin: {
onTagged: function(doclet, tag) {
setDocletKindToTitle(doclet, tag);
setDocletNameToValue(doclet, tag);
}
},
module: {
canHaveType: true,
isNamespace: true,
onTagged: function(doclet, tag) {
setDocletKindToTitle(doclet, tag);
setDocletNameToValue(doclet, tag);
if (!doclet.name) {
setDocletNameToFilename(doclet, tag);
}
// in case the user wrote something like `/** @module module:foo */`:
doclet.name = stripModuleNamespace(doclet.name);
setDocletTypeToValueType(doclet, tag);
}
},
name: {
mustHaveValue: true
},
namespace: {
canHaveType: true,
onTagged: function(doclet, tag) {
setDocletKindToTitle(doclet, tag);
setDocletNameToValue(doclet, tag);
setDocletTypeToValueType(doclet, tag);
}
},
param: {
canHaveType: true,
canHaveName: true,
onTagged: function(doclet, tag) {
doclet.params = doclet.params || [];
doclet.params.push(tag.value || {});
},
synonyms: ['arg', 'argument']
},
private: {
mustNotHaveValue: true,
onTagged: function(doclet, tag) {
doclet.access = 'private';
}
},
property: {
mustHaveValue: true,
canHaveType: true,
canHaveName: true,
onTagged: function(doclet, tag) {
doclet.properties = doclet.properties || [];
doclet.properties.push(tag.value);
},
synonyms: ['prop']
},
protected: {
mustNotHaveValue: true,
onTagged: function(doclet, tag) {
doclet.access = 'protected';
}
},
public: {
mustNotHaveValue: true,
onTagged: function(doclet, tag) {
delete doclet.access; // public is default
}
},
readonly: {
mustNotHaveValue: true,
onTagged: function(doclet, tag) {
doclet.readonly = true;
}
},
requires: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
var requiresName;
// inline link tags are passed through as-is so that `@requires {@link foo}` works
if ( require('jsdoc/tag/inline').isInlineTag(tag.value, 'link\\S*') ) {
requiresName = tag.value;
}
// otherwise, assume it's a module
else {
requiresName = firstWordOf(tag.value);
if (requiresName.indexOf(MODULE_NAMESPACE) !== 0) {
requiresName = MODULE_NAMESPACE + requiresName;
}
}
doclet.requires = doclet.requires || [];
doclet.requires.push(requiresName);
}
},
returns: {
mustHaveValue: true,
canHaveType: true,
onTagged: function(doclet, tag) {
doclet.returns = doclet.returns || [];
doclet.returns.push(tag.value);
},
synonyms: ['return']
},
see: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.see = doclet.see || [];
doclet.see.push(tag.value);
}
},
since: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.since = tag.value;
}
},
static: {
onTagged: function(doclet, tag) {
setDocletScopeToTitle(doclet, tag);
}
},
summary: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.summary = tag.value;
}
},
'this': {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet['this'] = firstWordOf(tag.value);
}
},
todo: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.todo = doclet.todo || [];
doclet.todo.push(tag.value);
}
},
platforms: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.platforms = doclet.platforms || [];
doclet.platforms = doclet.platforms.concat(tag.value.split(' '));
}
},
throws: {
mustHaveValue: true,
canHaveType: true,
onTagged: function(doclet, tag) {
doclet.exceptions = doclet.exceptions || [];
doclet.exceptions.push(tag.value);
},
synonyms: ['exception']
},
tutorial: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.tutorials = doclet.tutorials || [];
doclet.tutorials.push(tag.value);
}
},
type: {
mustHaveValue: true,
mustNotHaveDescription: true,
canHaveType: true,
onTagText: function(text) {
var closeIdx;
var openIdx;
var OPEN_BRACE = '{';
var CLOSE_BRACE = '}';
// remove line breaks
text = text.replace(/[\f\n\r]/g, '');
// Text must be a type expression; for backwards compatibility, we add braces if they're
// missing. But do NOT add braces to things like `@type {string} some pointless text`.
openIdx = text.indexOf(OPEN_BRACE);
closeIdx = text.indexOf(CLOSE_BRACE);
// a type expression is at least one character long
if ( openIdx !== 0 || closeIdx <= openIdx + 1) {
text = OPEN_BRACE + text + CLOSE_BRACE;
}
return text;
},
onTagged: function(doclet, tag) {
if (tag.value && tag.value.type) {
setDocletTypeToValueType(doclet, tag);
// for backwards compatibility, we allow @type for functions to imply return type
if (doclet.kind === 'function') {
doclet.addTag('returns', tag.text);
}
}
}
},
typedef: {
canHaveType: true,
canHaveName: true,
onTagged: function(doclet, tag) {
setDocletKindToTitle(doclet, tag);
if (tag.value) {
setDocletNameToValueName(doclet, tag);
// callbacks are always type {function}
if (tag.originalTitle === 'callback') {
doclet.type = {
names: [
'function'
]
};
}
else {
setDocletTypeToValueType(doclet, tag);
}
}
},
synonyms: ['callback']
},
undocumented: {
mustNotHaveValue: true,
onTagged: function(doclet, tag) {
doclet.undocumented = true;
doclet.comment = '';
}
},
variation: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.variation = tag.value;
}
},
version: {
mustHaveValue: true,
onTagged: function(doclet, tag) {
doclet.version = tag.value;
}
}
};
// Tag dictionary for JSDoc.
var jsdocTags = exports.jsdocTags = baseTags;
// Tag dictionary for Google Closure Compiler.
var closureTags = exports.closureTags = {
const: cloneTagDef(baseTags.constant),
constructor: cloneTagDef(baseTags.class),
deprecated: cloneTagDef(baseTags.deprecated),
enum: cloneTagDef(baseTags.enum),
extends: cloneTagDef(baseTags.augments),
final: cloneTagDef(baseTags.readonly),
implements: cloneTagDef(baseTags.implements),
inheritdoc: cloneTagDef(baseTags.inheritdoc),
interface: cloneTagDef(baseTags.interface, {
canHaveName: false,
mustNotHaveValue: true
}),
lends: cloneTagDef(baseTags.lends),
license: cloneTagDef(baseTags.license),
// Closure Compiler only
override: {
mustNotHaveValue: true,
onTagged: function(doclet, tag) {
doclet.override = true;
}
},
param: cloneTagDef(baseTags.param),
private: {
canHaveType: true,
onTagged: function(doclet, tag) {
doclet.access = 'private';
if (tag.value && tag.value.type) {
setDocletTypeToValueType(doclet, tag);
}
}
},
protected: {
canHaveType: true,
onTagged: function(doclet, tag) {
doclet.access = 'protected';
if (tag.value && tag.value.type) {
setDocletTypeToValueType(doclet, tag);
}
}
},
return: cloneTagDef(baseTags.returns),
'this': cloneTagDef(baseTags['this']),
throws: cloneTagDef(baseTags.throws),
type: cloneTagDef(baseTags.type, {
mustNotHaveDescription: false
}),
typedef: cloneTagDef(baseTags.typedef)
};
function addTagDefinitions(dictionary, tagDefs) {
Object.keys(tagDefs).forEach(function(tagName) {
var tagDef;
tagDef = tagDefs[tagName];
dictionary.defineTag(tagName, tagDef);
if (tagDef.synonyms) {
tagDef.synonyms.forEach(function(synonym) {
dictionary.defineSynonym(tagName, synonym);
});
}
});
}
/**
* Populate the given dictionary with the appropriate JSDoc tag definitions.
*
* If the `tagDefinitions` parameter is omitted, JSDoc uses its configuration settings to decide
* which tags to add to the dictionary.
*
* If the `tagDefinitions` parameter is included, JSDoc adds only the tag definitions from the
* `tagDefinitions` object. The configuration settings are ignored.
*
* @param {module:jsdoc/tag/dictionary} dictionary
* @param {Object} [tagDefinitions] - A dictionary whose values define the rules for a JSDoc tag.
*/
exports.defineTags = function(dictionary, tagDefinitions) {
var dictionaries;
if (!tagDefinitions) {
dictionaries = global.env.conf.tags.dictionaries;
if (!dictionaries) {
jsdoc.util.logger.error('The configuration setting "tags.dictionaries" is undefined. ' +
'Unable to load tag definitions.');
return;
}
else {
dictionaries = dictionaries.slice(0).reverse();
}
dictionaries.forEach(function(dictName) {
var tagDefs = exports[DEFINITIONS[dictName]];
if (!tagDefs) {
jsdoc.util.logger.error('The configuration setting "tags.dictionaries" contains ' +
'the unknown dictionary name %s. Ignoring the dictionary.', dictName);
return;
}
addTagDefinitions(dictionary, tagDefs);
});
}
else {
addTagDefinitions(dictionary, tagDefinitions);
}
};
@@ -0,0 +1,141 @@
/**
* @module jsdoc/tag/inline
*
* @author Jeff Williams <jeffrey.l.williams@gmail.com>
* @license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
'use strict';
/**
* Information about an inline tag that was found within a string.
*
* @typedef {Object} InlineTagInfo
* @memberof module:jsdoc/tag/inline
* @property {?string} completeTag - The entire inline tag, including its enclosing braces.
* @property {?string} tag - The tag whose text was found.
* @property {?string} text - The tag text that was found.
*/
/**
* Information about the results of replacing inline tags within a string.
*
* @typedef {Object} InlineTagResult
* @memberof module:jsdoc/tag/inline
* @property {Array.<module:jsdoc/tag/inline.InlineTagInfo>} tags - The inline tags that were found.
* @property {string} newString - The updated text string after extracting or replacing the inline
* tags.
*/
/**
* Text-replacing function for strings that contain an inline tag.
*
* @callback InlineTagReplacer
* @memberof module:jsdoc/tag/inline
* @param {string} string - The complete string containing the inline tag.
* @param {module:jsdoc/tag/inline.InlineTagInfo} tagInfo - Information about the inline tag.
* @return {string} An updated version of the complete string.
*/
/**
* Create a regexp that matches a specific inline tag, or all inline tags.
*
* @private
* @memberof module:jsdoc/tag/inline
* @param {?string} tagName - The inline tag that the regexp will match. May contain regexp
* characters. If omitted, matches any string.
* @param {?string} prefix - A prefix for the regexp. Defaults to an empty string.
* @param {?string} suffix - A suffix for the regexp. Defaults to an empty string.
* @returns {RegExp} A regular expression that matches the requested inline tag.
*/
function regExpFactory(tagName, prefix, suffix) {
tagName = tagName || '\\S+';
prefix = prefix || '';
suffix = suffix || '';
return new RegExp(prefix + '\\{@' + tagName + '\\s+((?:.|\n)+?)\\}' + suffix, 'gi');
}
/**
* Check whether a string is an inline tag. You can check for a specific inline tag or for any valid
* inline tag.
*
* @param {string} string - The string to check.
* @param {?string} tagName - The inline tag to match. May contain regexp characters. If this
* parameter is omitted, this method returns `true` for any valid inline tag.
* @returns {boolean} Set to `true` if the string is a valid inline tag or `false` in all other
* cases.
*/
exports.isInlineTag = function(string, tagName) {
return regExpFactory(tagName, '^', '$').test(string);
};
/**
* Replace all instances of multiple inline tags with other text.
*
* @param {string} string - The string in which to replace the inline tags.
* @param {Object} replacers - The functions that are used to replace text in the string. The keys
* must contain tag names (for example, `link`), and the values must contain functions with the
* type {@link module:jsdoc/tag/inline.InlineTagReplacer}.
* @return {module:jsdoc/tag/inline.InlineTagResult} The updated string, as well as information
* about the inline tags that were found.
*/
exports.replaceInlineTags = function(string, replacers) {
var tagInfo = [];
function replaceMatch(replacer, tag, match, text) {
var matchedTag = {
completeTag: match,
tag: tag,
text: text
};
tagInfo.push(matchedTag);
return replacer(string, matchedTag);
}
string = string || '';
Object.keys(replacers).forEach(function(replacer) {
var tagRegExp = regExpFactory(replacer);
var matches;
// call the replacer once for each match
while ( (matches = tagRegExp.exec(string)) !== null ) {
string = replaceMatch(replacers[replacer], replacer, matches[0], matches[1]);
}
});
return {
tags: tagInfo,
newString: string.trim()
};
};
/**
* Replace all instances of an inline tag with other text.
*
* @param {string} string - The string in which to replace the inline tag.
* @param {string} tag - The name of the inline tag to replace.
* @param {module:jsdoc/tag/inline.InlineTagReplacer} replacer - The function that is used to
* replace text in the string.
* @return {module:jsdoc/tag/inline.InlineTagResult} The updated string, as well as information
* about the inline tags that were found.
*/
exports.replaceInlineTag = function(string, tag, replacer) {
var replacers = {};
replacers[tag] = replacer;
return exports.replaceInlineTags(string, replacers);
};
/**
* Extract inline tags from a string, replacing them with an empty string.
*
* @param {string} string - The string from which to extract text.
* @param {?string} tag - The inline tag to extract.
* @return {module:jsdoc/tag/inline.InlineTagResult} The updated string, as well as information
* about the inline tags that were found.
*/
exports.extractInlineTag = function(string, tag) {
return exports.replaceInlineTag(string, tag, function(str, tagInfo) {
return str.replace(tagInfo.completeTag, '');
});
};
@@ -0,0 +1,308 @@
/**
* @module jsdoc/tag/type
*
* @author Michael Mathews <micmath@gmail.com>
* @author Jeff Williams <jeffrey.l.williams@gmail.com>
* @license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
'use strict';
var catharsis = require('catharsis');
var jsdoc = {
name: require('jsdoc/name'),
tag: {
inline: require('jsdoc/tag/inline')
}
};
var util = require('util');
/**
* Information about a type expression extracted from tag text.
*
* @typedef TypeExpressionInfo
* @memberof module:jsdoc/tag/type
* @property {string} expression - The type expression.
* @property {string} text - The updated tag text.
*/
/** @private */
function unescapeBraces(text) {
return text.replace(/\\\{/g, '{')
.replace(/\\\}/g, '}');
}
/**
* Extract a type expression from the tag text.
*
* @private
* @param {string} string - The tag text.
* @return {module:jsdoc/tag/type.TypeExpressionInfo} The type expression and updated tag text.
*/
function extractTypeExpression(string) {
var completeExpression;
var count = 0;
var position = 0;
var expression = '';
var startIndex = string.search(/\{[^@]/);
var textStartIndex;
if (startIndex !== -1) {
// advance to the first character in the type expression
position = textStartIndex = startIndex + 1;
count++;
while (position < string.length) {
switch (string[position]) {
case '\\':
// backslash is an escape character, so skip the next character
position++;
break;
case '{':
count++;
break;
case '}':
count--;
break;
default:
// do nothing
}
if (count === 0) {
completeExpression = string.slice(startIndex, position + 1);
expression = string.slice(textStartIndex, position).trim();
break;
}
position++;
}
}
string = completeExpression ? string.replace(completeExpression, '') : string;
return {
expression: unescapeBraces(expression),
newString: string.trim()
};
}
/** @private */
function getTagInfo(tagValue, canHaveName, canHaveType) {
var name = '';
var typeExpression = '';
var text = tagValue;
var expressionAndText;
var nameAndDescription;
var typeOverride;
if (canHaveType) {
expressionAndText = extractTypeExpression(text);
typeExpression = expressionAndText.expression;
text = expressionAndText.newString;
}
if (canHaveName) {
nameAndDescription = jsdoc.name.splitName(text);
name = nameAndDescription.name;
text = nameAndDescription.description;
}
// an inline @type tag, like {@type Foo}, overrides the type expression
if (canHaveType) {
typeOverride = jsdoc.tag.inline.extractInlineTag(text, 'type');
if (typeOverride.tags && typeOverride.tags[0]) {
typeExpression = typeOverride.tags[0].text;
}
text = typeOverride.newString;
}
return {
name: name,
typeExpression: typeExpression,
text: text
};
}
/**
* Information provided in a JSDoc tag.
*
* @typedef {Object} TagInfo
* @memberof module:jsdoc/tag/type
* @property {string} TagInfo.defaultvalue - The default value of the member.
* @property {string} TagInfo.name - The name of the member (for example, `myParamName`).
* @property {boolean} TagInfo.nullable - Indicates whether the member can be set to `null` or
* `undefined`.
* @property {boolean} TagInfo.optional - Indicates whether the member is optional.
* @property {string} TagInfo.text - Descriptive text for the member (for example, `The user's email
* address.`).
* @property {Array.<string>} TagInfo.type - The type or types that the member can contain (for
* example, `string` or `MyNamespace.MyClass`).
* @property {string} TagInfo.typeExpression - The type expression that was parsed to identify the
* types.
* @property {boolean} TagInfo.variable - Indicates whether the number of members that are provided
* can vary (for example, in a function that accepts any number of parameters).
*/
// TODO: move to module:jsdoc/name?
/**
* Extract JSDoc-style type information from the name specified in the tag info, including the
* member name; whether the member is optional; and the default value of the member.
*
* @private
* @param {module:jsdoc/tag/type.TagInfo} tagInfo - Information contained in the tag.
* @return {module:jsdoc/tag/type.TagInfo} Updated information from the tag.
*/
function parseName(tagInfo) {
// like '[foo]' or '[ foo ]' or '[foo=bar]' or '[ foo=bar ]' or '[ foo = bar ]'
// or 'foo=bar' or 'foo = bar'
if ( /^(\[)?\s*(.+?)\s*(\])?$/.test(tagInfo.name) ) {
tagInfo.name = RegExp.$2;
// were the "optional" brackets present?
if (RegExp.$1 && RegExp.$3) {
tagInfo.optional = true;
}
// like 'foo=bar' or 'foo = bar'
if ( /^(.+?)\s*=\s*(.+)$/.test(tagInfo.name) ) {
tagInfo.name = RegExp.$1;
tagInfo.defaultvalue = RegExp.$2;
}
}
return tagInfo;
}
/** @private */
function getTypeStrings(parsedType, isOutermostType) {
var applications;
var typeString;
var types = [];
var TYPES = catharsis.Types;
switch (parsedType.type) {
case TYPES.AllLiteral:
types.push('*');
break;
case TYPES.FunctionType:
types.push('function');
break;
case TYPES.NameExpression:
types.push(parsedType.name);
break;
case TYPES.NullLiteral:
types.push('null');
break;
case TYPES.RecordType:
types.push('Object');
break;
case TYPES.TypeApplication:
// if this is the outermost type, we strip the modifiers; otherwise, we keep them
if (isOutermostType) {
applications = parsedType.applications.map(function(application) {
return catharsis.stringify(application);
}).join(', ');
typeString = util.format( '%s.<%s>', getTypeStrings(parsedType.expression),
applications );
types.push(typeString);
}
else {
types.push( catharsis.stringify(parsedType) );
}
break;
case TYPES.TypeUnion:
parsedType.elements.forEach(function(element) {
types = types.concat( getTypeStrings(element) );
});
break;
case TYPES.UndefinedLiteral:
types.push('undefined');
break;
case TYPES.UnknownLiteral:
types.push('?');
break;
default:
// this shouldn't happen
throw new Error( util.format('unrecognized type %s in parsed type: %j', parsedType.type,
parsedType) );
}
return types;
}
/**
* Extract JSDoc-style and Closure Compiler-style type information from the type expression
* specified in the tag info.
*
* @private
* @param {module:jsdoc/tag/type.TagInfo} tagInfo - Information contained in the tag.
* @return {module:jsdoc/tag/type.TagInfo} Updated information from the tag.
*/
function parseTypeExpression(tagInfo) {
var errorMessage;
var parsedType;
// don't try to parse empty type expressions
if (!tagInfo.typeExpression) {
return tagInfo;
}
try {
parsedType = catharsis.parse(tagInfo.typeExpression, {jsdoc: true});
}
catch (e) {
// always re-throw so the caller has a chance to report which file was bad
throw new Error( util.format('Invalid type expression "%s": %s', tagInfo.typeExpression,
e.message) );
}
tagInfo.type = tagInfo.type.concat( getTypeStrings(parsedType, true) );
tagInfo.parsedType = parsedType;
// Catharsis and JSDoc use the same names for 'optional' and 'nullable'...
['optional', 'nullable'].forEach(function(key) {
if (parsedType[key] !== null && parsedType[key] !== undefined) {
tagInfo[key] = parsedType[key];
}
});
// ...but not 'variable'.
if (parsedType.repeatable !== null && parsedType.repeatable !== undefined) {
tagInfo.variable = parsedType.repeatable;
}
return tagInfo;
}
// TODO: allow users to add/remove type parsers (perhaps via plugins)
var typeParsers = [parseName, parseTypeExpression];
/**
* Parse the value of a JSDoc tag.
*
* @param {string} tagValue - The value of the tag. For example, the tag `@param {string} name` has
* a value of `{string} name`.
* @param {boolean} canHaveName - Indicates whether the value can include a symbol name.
* @param {boolean} canHaveType - Indicates whether the value can include a type expression that
* describes the symbol.
* @return {module:jsdoc/tag/type.TagInfo} Information obtained from the tag.
* @throws {Error} Thrown if a type expression cannot be parsed.
*/
exports.parse = function(tagValue, canHaveName, canHaveType) {
if (typeof tagValue !== 'string') { tagValue = ''; }
var tagInfo = getTagInfo(tagValue, canHaveName, canHaveType);
tagInfo.type = tagInfo.type || [];
typeParsers.forEach(function(parser) {
tagInfo = parser.call(this, tagInfo);
});
// if we wanted a type, but the parsers didn't add any type names, use the type expression
if (canHaveType && !tagInfo.type.length && tagInfo.typeExpression) {
tagInfo.type = [tagInfo.typeExpression];
}
return tagInfo;
};
@@ -0,0 +1,56 @@
/*global env: true */
/**
@module jsdoc/tag/validator
@requires jsdoc/tag/dictionary
@author Michael Mathews <micmath@gmail.com>
@license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
'use strict';
var dictionary = require('jsdoc/tag/dictionary');
var format = require('util').format;
var logger = require('jsdoc/util/logger');
function buildMessage(tagName, meta, desc) {
var result = format('The @%s tag %s. File: %s, line: %s', tagName, desc, meta.filename,
meta.lineno);
if (meta.comment) {
result += '\n' + meta.comment;
}
return result;
}
/**
* Validate the given tag.
*/
exports.validate = function(tag, tagDef, meta) {
// handle cases where the tag definition does not exist
if (!tagDef) {
// log an error if unknown tags are not allowed
if (!env.conf.tags.allowUnknownTags) {
logger.error( buildMessage(tag.title, meta, 'is not a known tag') );
}
// stop validation, since there's nothing to validate against
return;
}
// check for errors that make the tag useless
if (!tagDef && !env.conf.tags.allowUnknownTags) {
logger.error( buildMessage(tag.title, meta, 'is not a known tag') );
}
else if (!tag.text && tagDef.mustHaveValue) {
logger.error( buildMessage(tag.title, meta, 'requires a value') );
}
// check for minor issues that are usually harmless
else if (tag.text && tagDef.mustNotHaveValue) {
logger.warn( buildMessage(tag.title, meta,
'does not permit a value; the value will be ignored') );
}
else if (tag.value && tag.value.description && tagDef.mustNotHaveDescription) {
logger.warn( buildMessage(tag.title, meta,
'does not permit a description; the description will be ignored') );
}
};
@@ -0,0 +1,83 @@
/**
* @file Wrapper for underscore's template utility to allow loading templates from files.
* @author Rafał Wrzeszcz <rafal.wrzeszcz@wrzasq.pl>
* @author <a href="mailto:matthewkastor@gmail.com">Matthew Christopher Kastor-Inare III</a>
* @license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
'use strict';
var _ = require('underscore'),
fs = require('jsdoc/fs'),
path = require('path');
/**
@module jsdoc/template
*/
/**
@class
@classdesc Underscore template helper.
@param {string} path - Templates directory.
*/
exports.Template = function(path) {
this.path = path;
this.layout = null;
this.cache = {};
// override default template tag settings
this.settings = {
evaluate: /<\?js([\s\S]+?)\?>/g,
interpolate: /<\?js=([\s\S]+?)\?>/g,
escape: /<\?js~([\s\S]+?)\?>/g
};
};
/** Loads template from given file.
@param {string} file - Template filename.
@return {function} Returns template closure.
*/
exports.Template.prototype.load = function(file) {
return _.template(fs.readFileSync(file, 'utf8'), null, this.settings);
};
/**
Renders template using given data.
This is low-level function, for rendering full templates use {@link Template.render()}.
@param {string} file - Template filename.
@param {object} data - Template variables (doesn't have to be object, but passing variables dictionary is best way and most common use).
@return {string} Rendered template.
*/
exports.Template.prototype.partial = function(file, data) {
file = path.resolve(this.path, file);
// load template into cache
if (!(file in this.cache)) {
this.cache[file] = this.load(file);
}
// keep template helper context
return this.cache[file].call(this, data);
};
/**
Renders template with given data.
This method automaticaly applies layout if set.
@param {string} file - Template filename.
@param {object} data - Template variables (doesn't have to be object, but passing variables dictionary is best way and most common use).
@return {string} Rendered template.
*/
exports.Template.prototype.render = function(file, data) {
// main content
var content = this.partial(file, data);
// apply layout
if (this.layout) {
data.content = content;
content = this.partial(this.layout, data);
}
return content;
};
@@ -0,0 +1,140 @@
/**
@overview
@author Rafał Wrzeszcz <rafal.wrzeszcz@wrzasq.pl>
@license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
'use strict';
var markdown = require('jsdoc/util/markdown');
var util = require('util');
var hasOwnProp = Object.prototype.hasOwnProperty;
/** Removes child tutorial from the parent. Does *not* unset child.parent though.
@param {Tutorial} parent - parent tutorial.
@param {Tutorial} child - Old child.
@private
*/
function removeChild(parent, child) {
var index = parent.children.indexOf(child);
if (index !== -1) {
parent.children.splice(index, 1);
}
}
/** Adds a child to the parent tutorial. Does *not* set child.parent though.
@param {Tutorial} parent - parent tutorial.
@param {Tutorial} child - New child.
@private
*/
function addChild(parent, child) {
parent.children.push(child);
}
/**
@module jsdoc/tutorial
*/
/**
@class
@classdesc Represents a single JSDoc tutorial.
@param {string} name - Tutorial name.
@param {string} content - Text content.
@param {number} type - Source formating.
*/
exports.Tutorial = function(name, content, type) {
this.title = this.name = name;
this.content = content;
this.type = type;
// default values
this.parent = null;
this.children = [];
};
/** Moves children from current parent to different one.
@param {?Tutorial} parent - New parent. If null, the tutorial has no parent.
*/
exports.Tutorial.prototype.setParent = function(parent) {
// removes node from old parent
if (this.parent) {
removeChild(this.parent, this);
}
this.parent = parent;
if (parent) {
addChild(parent, this);
}
};
/** Removes children from current node.
@param {Tutorial} child - Old child.
*/
exports.Tutorial.prototype.removeChild = function(child) {
child.setParent(null);
};
/** Adds new children to current node.
@param {Tutorial} child - New child.
*/
exports.Tutorial.prototype.addChild = function(child) {
child.setParent(this);
};
/** Prepares source.
@return {string} HTML source.
*/
exports.Tutorial.prototype.parse = function() {
switch (this.type) {
// nothing to do
case exports.TYPES.HTML:
return this.content;
// markdown
case exports.TYPES.MARKDOWN:
var mdParse = markdown.getParser();
return mdParse(this.content);
// uhm... should we react somehow?
// if not then this case can be merged with TYPES.HTML
default:
return this.content;
}
};
/**
* @class
* @classdesc Represents the root tutorial.
* @extends {module:jsdoc/tutorial.Tutorial}
*/
exports.RootTutorial = function() {
exports.RootTutorial.super_.call(this, '', '');
this._tutorials = {};
};
util.inherits(exports.RootTutorial, exports.Tutorial);
/**
* Retrieve a tutorial by name.
* @param {string} name - Tutorial name.
* @return {module:jsdoc/tutorial.Tutorial} Tutorial instance.
*/
exports.RootTutorial.prototype.getByName = function(name) {
return hasOwnProp.call(this._tutorials, name) && this._tutorials[name];
};
/**
* Add a child tutorial to the root.
* @param {module:jsdoc/tutorial.Tutorial} child - Child tutorial.
*/
exports.RootTutorial.prototype._addTutorial = function(child) {
this._tutorials[child.name] = child;
};
/** Tutorial source types.
@enum {number}
*/
exports.TYPES = {
HTML: 1,
MARKDOWN: 2
};
@@ -0,0 +1,192 @@
/**
@overview
@author Rafa&#322; Wrzeszcz <rafal.wrzeszcz@wrzasq.pl>
@license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
/**
@module jsdoc/tutorial/resolver
*/
'use strict';
var logger = require('jsdoc/util/logger');
var fs = require('jsdoc/fs');
var path = require('path');
var tutorial = require('jsdoc/tutorial');
var hasOwnProp = Object.prototype.hasOwnProperty;
// TODO: make this an instance member of `RootTutorial`?
var conf = {};
var finder = /^(.*)\.(x(?:ht)?ml|html?|md|markdown|json)$/i;
/** checks if `conf` is the metadata for a single tutorial.
* A tutorial's metadata has a property 'title' and/or a property 'children'.
* @param {object} json - the object we want to test (typically from JSON.parse)
* @returns {boolean} whether `json` could be the metadata for a tutorial.
*/
function isTutorialJSON(json) {
// if conf.title exists or conf.children exists, it is metadata for a tutorial
return (hasOwnProp.call(json, 'title') || hasOwnProp.call(json, 'children'));
}
/**
* Root tutorial.
* @type {module:jsdoc/tutorial.Root}
*/
exports.root = new tutorial.RootTutorial();
/** Helper function that adds tutorial configuration to the `conf` variable.
* This helps when multiple tutorial configurations are specified in one object,
* or when a tutorial's children are specified as tutorial configurations as
* opposed to an array of tutorial names.
*
* Recurses as necessary to ensure all tutorials are added.
*
* @param {string} name - if `meta` is a configuration for a single tutorial,
* this is that tutorial's name.
* @param {object} meta - object that contains tutorial information.
* Can either be for a single tutorial, or for multiple
* (where each key in `meta` is the tutorial name and each
* value is the information for a single tutorial).
* Additionally, a tutorial's 'children' property may
* either be an array of strings (names of the child tutorials),
* OR an object giving the configuration for the child tutorials.
*/
function addTutorialConf(name, meta) {
var i;
var l;
var names;
if (isTutorialJSON(meta)) {
// if the children are themselves tutorial defintions as opposed to an
// array of strings, add each child.
if (hasOwnProp.call(meta, 'children') && !Array.isArray(meta.children)) {
names = Object.keys(meta.children);
for (i = 0, l = names.length; i < l; ++i) {
addTutorialConf(names[i], meta.children[names[i]]);
}
// replace with an array of names.
meta.children = names;
}
// check if the tutorial has already been defined...
if (hasOwnProp.call(conf, name)) {
logger.warn('Metadata for the tutorial %s is defined more than once. Only the first definition will be used.', name );
} else {
conf[name] = meta;
}
} else {
// keys are tutorial names, values are `Tutorial` instances
names = Object.keys(meta);
for (i = 0, l = names.length; i < l; ++i) {
addTutorialConf(names[i], meta[names[i]]);
}
}
}
/**
* Add a tutorial.
* @param {module:jsdoc/tutorial.Tutorial} current - Tutorial to add.
*/
exports.addTutorial = function(current) {
if (exports.root.getByName(current.name)) {
logger.warn('The tutorial %s is defined more than once. Only the first definition will be used.', current.name);
} else {
// by default, the root tutorial is the parent
current.setParent(exports.root);
exports.root._addTutorial(current);
}
};
/**
* Load tutorials from the given path.
* @param {string} filepath - Tutorials directory.
*/
exports.load = function(filepath) {
var content;
var current;
var files = fs.ls(filepath, global.env.opts.recurse ? 10 : undefined);
var name;
var match;
var type;
// tutorials handling
files.forEach(function(file) {
match = file.match(finder);
// any filetype that can apply to tutorials
if (match) {
name = path.basename(match[1]);
content = fs.readFileSync(file, global.env.opts.encoding);
switch (match[2].toLowerCase()) {
// HTML type
case 'xml':
case 'xhtml':
case 'html':
case 'htm':
type = tutorial.TYPES.HTML;
break;
// Markdown typs
case 'md':
case 'markdown':
type = tutorial.TYPES.MARKDOWN;
break;
// configuration file
case 'json':
var meta = JSON.parse(content);
addTutorialConf(name, meta);
// don't add this as a tutorial
return;
// how can it be? check `finder' regexp
default:
// not a file we want to work with
return;
}
current = new tutorial.Tutorial(name, content, type);
exports.addTutorial(current);
}
});
};
/** Resolves hierarchical structure.
*/
exports.resolve = function() {
var item;
var current;
Object.keys(conf).forEach(function(name) {
current = exports.root.getByName(name);
// TODO: should we complain about this?
if (!current) {
return;
}
item = conf[name];
// set title
if (item.title) {
current.title = item.title;
}
// add children
if (item.children) {
item.children.forEach(function(child) {
var childTutorial = exports.root.getByName(child);
if (!childTutorial) {
logger.error('Missing child tutorial: %s', child);
}
else {
childTutorial.setParent(current);
}
});
}
});
};
@@ -0,0 +1,83 @@
/*global Set */
/**
Deep clone a simple object. Ignores non-enumerable properties.
@private
*/
'use strict';
var setDefined = typeof Set !== 'undefined';
function addItem(set, item) {
if (setDefined) {
set.add(item);
}
else if (set.indexOf(item) === -1) {
set.push(item);
}
}
function hasItem(set, item) {
if (setDefined) {
return set.has(item);
}
else {
return set.indexOf(item) !== -1;
}
}
// TODO: can we remove the circular-ref checking? pretty sure it's not needed anymore...
// if we need this here for some reason I'm forgetting, we should share code with jsdoc/util/dumper
function doop(o, seen) {
var clone;
var descriptor;
var props;
var i;
var l;
if (!seen) {
seen = setDefined ? new Set() : [];
}
if (o instanceof Object && o.constructor !== Function) {
if ( hasItem(seen, o) ) {
clone = '<CircularRef>';
}
else {
addItem(seen, o);
if ( Array.isArray(o) ) {
clone = [];
for (i = 0, l = o.length; i < l; i++) {
clone[i] = (o[i] instanceof Object) ? doop(o[i], seen) : o[i];
}
}
else {
clone = Object.create( Object.getPrototypeOf(o) );
props = Object.keys(o);
for (i = 0, l = props.length; i < l; i++) {
descriptor = Object.getOwnPropertyDescriptor(o, props[i]);
if (descriptor.value) {
descriptor.value = doop(descriptor.value, seen);
}
Object.defineProperty(clone, props[i], descriptor);
}
}
}
return clone;
}
return o;
}
// Wrapper to avoid exposing the 'seen' parameter outside of this module.
function doopWrapper(o) {
return doop(o);
}
// for backwards compatibility
doopWrapper.doop = doopWrapper;
module.exports = doopWrapper;
@@ -0,0 +1,139 @@
/*global Set */
/**
* Recursively print out all names and values in a data structure.
* @module jsdoc/util/dumper
* @author Michael Mathews <micmath@gmail.com>
* @license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
'use strict';
var util = require('util');
var OBJECT_WALKER_KEY = 'hasBeenSeenByWalkerDumper';
var SET_DEFINED = (typeof Set !== 'undefined');
function ObjectWalker() {
this.seenItems = SET_DEFINED ? new Set() : [];
}
ObjectWalker.prototype.seen = function(object) {
var result;
if (SET_DEFINED) {
result = this.seenItems.has(object);
}
else {
result = object[OBJECT_WALKER_KEY];
}
return result;
};
ObjectWalker.prototype.markAsSeen = function(object) {
if (SET_DEFINED) {
this.seenItems.add(object);
}
else {
object[OBJECT_WALKER_KEY] = true;
this.seenItems.push(object);
}
};
ObjectWalker.prototype.removeSeenFlag = function(obj) {
if (SET_DEFINED) {
this.seenItems.delete(obj);
}
else {
delete obj[OBJECT_WALKER_KEY];
}
};
// some objects are unwalkable, like Java native objects
ObjectWalker.prototype.isUnwalkable = function(o) {
return (o && typeof o === 'object' && typeof o.constructor === 'undefined');
};
ObjectWalker.prototype.isFunction = function(o) {
return (o && typeof o === 'function' || o instanceof Function);
};
ObjectWalker.prototype.isObject = function(o) {
return o && o instanceof Object ||
(o && typeof o.constructor !== 'undefined' && o.constructor.name === 'Object');
};
ObjectWalker.prototype.checkCircularRefs = function(o, func) {
if ( this.seen(o) ) {
return '<CircularRef>';
}
else {
this.markAsSeen(o);
return func(o);
}
};
ObjectWalker.prototype.walk = function(o) {
var result;
var self = this;
if ( this.isUnwalkable(o) ) {
result = '<Object>';
}
else if ( o === undefined ) {
result = null;
}
else if ( Array.isArray(o) ) {
result = this.checkCircularRefs(o, function(arr) {
var newArray = [];
arr.forEach(function(item) {
newArray.push( self.walk(item) );
});
self.removeSeenFlag(arr);
return newArray;
});
}
else if ( util.isRegExp(o) ) {
result = '<RegExp ' + o + '>';
}
else if ( util.isDate(o) ) {
result = '<Date ' + o.toUTCString() + '>';
}
else if ( util.isError(o) ) {
result = { message: o.message };
}
else if ( this.isFunction(o) ) {
result = '<Function' + (o.name ? ' ' + o.name : '') + '>';
}
else if ( this.isObject(o) && o !== null ) {
result = this.checkCircularRefs(o, function(obj) {
var newObj = {};
Object.keys(obj).forEach(function(key) {
if (!SET_DEFINED && key === OBJECT_WALKER_KEY) { return; }
newObj[key] = self.walk(obj[key]);
});
self.removeSeenFlag(obj);
return newObj;
});
}
// should be safe to JSON.stringify() everything else
else {
result = o;
}
return result;
};
/**
* @param {*} object
*/
exports.dump = function(object) {
var walker = new ObjectWalker();
return JSON.stringify(walker.walk(object), null, 4);
};
@@ -0,0 +1,35 @@
/*global env: true */
/**
* Helper functions for handling errors.
*
* @deprecated As of JSDoc 3.3.0. This module may be removed in a future release. Use the module
* {@link module:jsdoc/util/logger} to log warnings and errors.
* @module jsdoc/util/error
*/
'use strict';
/**
* Log an exception as an error.
*
* Prior to JSDoc 3.3.0, this method would either log the exception (if lenient mode was enabled) or
* re-throw the exception (default).
*
* In JSDoc 3.3.0 and later, lenient mode has been replaced with strict mode, which is disabled by
* default. If strict mode is enabled, calling the `handle` method causes JSDoc to exit immediately,
* just as if the exception had been re-thrown.
*
* @deprecated As of JSDoc 3.3.0. This module may be removed in a future release.
* @param {Error} e - The exception to log.
* @memberof module:jsdoc/util/error
*/
exports.handle = function(e) {
var logger = require('jsdoc/util/logger');
var msg = e ? ( e.message || JSON.stringify(e) ) : '';
// include the error type if it's an Error object
if (e instanceof Error) {
msg = e.name + ': ' + msg;
}
logger.error(msg);
};
@@ -0,0 +1,232 @@
/**
* Logging tools for JSDoc.
*
* Log messages are printed to the console based on the current logging level. By default, messages
* at level `{@link module:jsdoc/util/logger.LEVELS.ERROR}` or above are logged; all other messages
* are ignored.
*
* In addition, the module object emits an event whenever a logger method is called, regardless of
* the current logging level. The event's name is the string `logger:` followed by the logger's name
* (for example, `logger:error`). The event handler receives an array of arguments that were passed
* to the logger method.
*
* Each logger method accepts a `message` parameter that may contain zero or more placeholders. Each
* placeholder is replaced by the corresponding argument following the message. If the placeholder
* does not have a corresponding argument, the placeholder is not replaced.
*
* The following placeholders are supported:
*
* + `%s`: String.
* + `%d`: Number.
* + `%j`: JSON.
*
* @module jsdoc/util/logger
* @extends module:events.EventEmitter
* @example
* var logger = require('jsdoc/util/logger');
*
* var data = {
* foo: 'bar'
* };
* var name = 'baz';
*
* logger.warn('%j %s', data, name); // prints '{"foo":"bar"} baz'
* @see http://nodejs.org/api/util.html#util_util_format_format
*/
'use strict';
var runtime = require('jsdoc/util/runtime');
var util = require('util');
function Logger() {}
util.inherits(Logger, require('events').EventEmitter);
var logger = module.exports = new Logger();
/**
* Logging levels for the JSDoc logger. The default logging level is
* {@link module:jsdoc/util/logger.LEVELS.ERROR}.
*
* @enum
* @type {number}
*/
var LEVELS = logger.LEVELS = {
/** Do not log any messages. */
SILENT: 0,
/** Log fatal errors that prevent JSDoc from running. */
FATAL: 10,
/** Log all errors, including errors from which JSDoc can recover. */
ERROR: 20,
/**
* Log the following messages:
*
* + Warnings
* + Errors
*/
WARN: 30,
/**
* Log the following messages:
*
* + Informational messages
* + Warnings
* + Errors
*/
INFO: 40,
/**
* Log the following messages:
*
* + Debugging messages
* + Informational messages
* + Warnings
* + Errors
*/
DEBUG: 50,
/** Log all messages. */
VERBOSE: 1000
};
var DEFAULT_LEVEL = LEVELS.WARN;
var logLevel = DEFAULT_LEVEL;
var PREFIXES = {
DEBUG: 'DEBUG: ',
ERROR: 'ERROR: ',
FATAL: 'FATAL: ',
WARN: 'WARNING: '
};
// Add a prefix to a log message if necessary.
function addPrefix(args, prefix) {
var updatedArgs;
if (prefix && typeof args[0] === 'string') {
updatedArgs = args.slice(0);
updatedArgs[0] = prefix + updatedArgs[0];
}
return updatedArgs || args;
}
// TODO: document events
function wrapLogFunction(name, func) {
var eventName = 'logger:' + name;
var upperCaseName = name.toUpperCase();
var level = LEVELS[upperCaseName];
var prefix = PREFIXES[upperCaseName];
return function() {
var loggerArgs;
var args = Array.prototype.slice.call(arguments, 0);
if (logLevel >= level) {
loggerArgs = addPrefix(args, prefix);
func.apply(null, loggerArgs);
}
args.unshift(eventName);
logger.emit.apply(logger, args);
};
}
// Print a message to STDOUT without a terminating newline.
function printToStdout() {
var args = Array.prototype.slice.call(arguments, 0);
process.stdout.write( util.format.apply(util, args) );
}
/**
* Log a message at log level {@link module:jsdoc/util/logger.LEVELS.DEBUG}.
*
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.debug = wrapLogFunction('debug', console.info);
/**
* Print a string at log level {@link module:jsdoc/util/logger.LEVELS.DEBUG}. The string is not
* terminated by a newline.
*
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.printDebug = wrapLogFunction('debug', printToStdout);
/**
* Log a message at log level {@link module:jsdoc/util/logger.LEVELS.ERROR}.
*
* @name module:jsdoc/util/logger.error
* @function
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.error = wrapLogFunction('error', console.error);
/**
* Log a message at log level {@link module:jsdoc/util/logger.LEVELS.FATAL}.
*
* @name module:jsdoc/util/logger.fatal
* @function
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.fatal = wrapLogFunction('fatal', console.error);
/**
* Log a message at log level {@link module:jsdoc/util/logger.LEVELS.INFO}.
*
* @name module:jsdoc/util/logger.info
* @function
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.info = wrapLogFunction('info', console.info);
/**
* Print a string at log level {@link module:jsdoc/util/logger.LEVELS.INFO}. The string is not
* terminated by a newline.
*
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.printInfo = wrapLogFunction('info', printToStdout);
/**
* Log a message at log level {@link module:jsdoc/util/logger.LEVELS.VERBOSE}.
*
* @name module:jsdoc/util/logger.verbose
* @function
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.verbose = wrapLogFunction('verbose', console.info);
/**
* Print a string at log level {@link module:jsdoc/util/logger.LEVELS.VERBOSE}. The string is not
* terminated by a newline.
*
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.printVerbose = wrapLogFunction('verbose', printToStdout);
/**
* Log a message at log level {@link module:jsdoc/util/logger.LEVELS.WARN}.
*
* @name module:jsdoc/util/logger.warn
* @function
* @param {string} message - The message to log.
* @param {...*=} values - The values that will replace the message's placeholders.
*/
logger.warn = wrapLogFunction('warn', console.warn);
/**
* Set the log level.
*
* @param {module:jsdoc/util/logger.LEVELS} level - The log level to use.
*/
logger.setLevel = function setLevel(level) {
logLevel = (level !== undefined) ? level : DEFAULT_LEVEL;
};
/**
* Get the current log level.
*
* @return {module:jsdoc/util/logger.LEVELS} The current log level.
*/
logger.getLevel = function getLevel() {
return logLevel;
};
@@ -0,0 +1,156 @@
/*global env */
/**
* Provides access to Markdown-related functions.
* @module jsdoc/util/markdown
* @author Michael Mathews <micmath@gmail.com>
* @author Ben Blank <ben.blank@gmail.com>
*/
'use strict';
var util = require('util');
/**
* Enumeration of Markdown parsers that are available.
* @enum {String}
*/
var parserNames = {
/**
* The "[markdown-js](https://github.com/evilstreak/markdown-js)" (aka "evilstreak") parser.
*
* @deprecated Replaced by "marked," as markdown-js does not support inline HTML.
*/
evilstreak: 'marked',
/**
* The "GitHub-flavored Markdown" parser.
* @deprecated Replaced by "marked."
*/
gfm: 'marked',
/**
* The "[Marked](https://github.com/chjj/marked)" parser.
*/
marked: 'marked'
};
/**
* Escape underscores that occur within {@ ... } in order to protect them
* from the markdown parser(s).
* @param {String} source the source text to sanitize.
* @returns {String} `source` where underscores within {@ ... } have been
* protected with a preceding backslash (i.e. \_) -- the markdown parsers
* will strip the backslash and protect the underscore.
*/
function escapeUnderscores(source) {
return source.replace(/\{@[^}\r\n]+\}/g, function (wholeMatch) {
return wholeMatch.replace(/(^|[^\\])_/g, '$1\\_');
});
}
/**
* Escape HTTP/HTTPS URLs so that they are not automatically converted to HTML links.
*
* @param {string} source - The source text to escape.
* @return {string} The source text with escape characters added to HTTP/HTTPS URLs.
*/
function escapeUrls(source) {
return source.replace(/(https?)\:\/\//g, '$1:\\/\\/');
}
/**
* Unescape HTTP/HTTPS URLs after Markdown parsing is complete.
*
* @param {string} source - The source text to unescape.
* @return {string} The source text with escape characters removed from HTTP/HTTPS URLs.
*/
function unescapeUrls(source) {
return source.replace(/(https?)\:\\\/\\\//g, '$1://');
}
/**
* Escape characters in text within a code block.
*
* @param {string} source - The source text to escape.
* @return {string} The escaped source text.
*/
function escapeCode(source) {
return source.replace(/</g, '&lt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
/**
* Retrieve a function that accepts a single parameter containing Markdown source. The function uses
* the specified parser to transform the Markdown source to HTML, then returns the HTML as a string.
*
* @private
* @param {String} parserName The name of the selected parser.
* @param {Object} [conf] Configuration for the selected parser, if any.
* @returns {Function} A function that accepts Markdown source, feeds it to the selected parser, and
* returns the resulting HTML.
*/
function getParseFunction(parserName, conf) {
var logger = require('jsdoc/util/logger');
var marked = require('marked');
var markedRenderer;
var parserFunction;
conf = conf || {};
if (parserName === parserNames.marked) {
// Marked generates an "id" attribute for headers; this custom renderer suppresses it
markedRenderer = new marked.Renderer();
markedRenderer.heading = function(text, level) {
return util.format('<h%s>%s</h%s>', level, text, level);
};
// Allow prettyprint to work on inline code samples
markedRenderer.code = function(code, language) {
var langClass = language ? ' lang-' + language : '';
return util.format( '<pre class="prettyprint source%s"><code>%s</code></pre>',
langClass, escapeCode(code) );
};
parserFunction = function(source) {
var result;
source = escapeUnderscores(source);
source = escapeUrls(source);
result = marked(source, { renderer: markedRenderer })
.replace(/\s+$/, '')
.replace(/&#39;/g, "'");
result = unescapeUrls(result);
return result;
};
parserFunction._parser = parserNames.marked;
return parserFunction;
}
else {
logger.error('Unrecognized Markdown parser "%s". Markdown support is disabled.',
parserName);
}
}
/**
* Retrieve a Markdown parsing function based on the value of the `conf.json` file's
* `env.conf.markdown` property. The parsing function accepts a single parameter containing Markdown
* source. The function uses the parser specified in `conf.json` to transform the Markdown source to
* HTML, then returns the HTML as a string.
*
* @returns {function} A function that accepts Markdown source, feeds it to the selected parser, and
* returns the resulting HTML.
*/
exports.getParser = function() {
var conf = env.conf.markdown;
if (conf && conf.parser) {
return getParseFunction(parserNames[conf.parser], conf);
}
else {
// marked is the default parser
return getParseFunction(parserNames.marked, conf);
}
};
@@ -0,0 +1,117 @@
/*global env, java */
/**
* Helper functions to enable JSDoc to run on multiple JavaScript runtimes.
*
* @module jsdoc/util/runtime
* @private
*/
'use strict';
var os = require('os');
// These strings represent directory names; do not modify them!
/** @private */
var RHINO = exports.RHINO = 'rhino';
/** @private */
var NODE = exports.NODE = 'node';
/**
* The JavaScript runtime that is executing JSDoc:
*
* + `module:jsdoc/util/runtime~RHINO`: Mozilla Rhino.
* + `module:jsdoc/util/runtime~NODE`: Node.js.
*
* @private
*/
var runtime = (function() {
if (global.Packages && typeof global.Packages === 'object' &&
Object.prototype.toString.call(global.Packages) === '[object JavaPackage]') {
return RHINO;
} else if (require && require.main && module) {
return NODE;
} else {
// unknown runtime
throw new Error('Unable to identify the current JavaScript runtime.');
}
})();
/**
* Check whether Mozilla Rhino is running JSDoc.
* @return {boolean} Set to `true` if the current runtime is Mozilla Rhino.
*/
exports.isRhino = function() {
return runtime === RHINO;
};
/**
* Check whether Node.js is running JSDoc.
* @return {boolean} Set to `true` if the current runtime is Node.js.
*/
exports.isNode = function() {
return runtime === NODE;
};
function initializeRhino(args) {
// the JSDoc dirname is the main module URI, minus the filename, converted to a path
var uriParts = require.main.uri.split('/');
uriParts.pop();
env.dirname = String( new java.io.File(new java.net.URI(uriParts.join('/'))) );
env.pwd = String( java.lang.System.getenv().get('PWD') );
env.args = args;
require(env.dirname + '/rhino/rhino-shim.js');
}
function initializeNode(args) {
var fs = require('fs');
var path = require('path');
var jsdocPath = args[0];
var pwd = args[1];
// resolve the path if it's a symlink
if ( fs.statSync(jsdocPath).isSymbolicLink() ) {
jsdocPath = path.resolve( path.dirname(jsdocPath), fs.readlinkSync(jsdocPath) );
}
env.dirname = jsdocPath;
env.pwd = pwd;
env.args = process.argv.slice(2);
}
exports.initialize = function(args) {
switch (runtime) {
case RHINO:
initializeRhino(args);
break;
case NODE:
initializeNode(args);
break;
default:
throw new Error('Cannot initialize the unknown JavaScript runtime "' + runtime + '"!');
}
};
/**
* Retrieve the identifier for the current JavaScript runtime.
*
* @private
* @return {string} The runtime identifier.
*/
exports.getRuntime = function() {
return runtime;
};
/**
* Get the require path for the runtime-specific implementation of a module.
*
* @param {string} partialPath - The partial path to the module. Use the same format as when calling
* `require()`.
* @return {object} The require path for the runtime-specific implementation of the module.
*/
exports.getModulePath = function(partialPath) {
var path = require('path');
return path.join(env.dirname, runtime, partialPath);
};
@@ -0,0 +1,920 @@
/*global env: true */
/**
* @module jsdoc/util/templateHelper
*/
'use strict';
var catharsis = require('catharsis');
var dictionary = require('jsdoc/tag/dictionary');
var name = require('jsdoc/name');
var util = require('util');
var hasOwnProp = Object.prototype.hasOwnProperty;
var MODULE_NAMESPACE = 'module:';
var files = {};
var ids = {};
// each container gets its own html file
var containers = ['class', 'module', 'external', 'namespace', 'mixin', 'interface'];
var tutorials;
/** Sets tutorials map.
@param {jsdoc.tutorial.Tutorial} root - Root tutorial node.
*/
exports.setTutorials = function(root) {
tutorials = root;
};
exports.globalName = name.SCOPE.NAMES.GLOBAL;
exports.fileExtension = '.html';
exports.scopeToPunc = name.scopeToPunc;
var linkMap = {
// two-way lookup
longnameToUrl: {},
urlToLongname: {},
// one-way lookup (IDs are only unique per file)
longnameToId: {}
};
// two-way lookup
var tutorialLinkMap = {
nameToUrl: {},
urlToName: {}
};
var longnameToUrl = exports.longnameToUrl = linkMap.longnameToUrl;
var longnameToId = exports.longnameToId = linkMap.longnameToId;
var registerLink = exports.registerLink = function(longname, fileUrl) {
linkMap.longnameToUrl[longname] = fileUrl;
linkMap.urlToLongname[fileUrl] = longname;
};
var registerId = exports.registerId = function(longname, fragment) {
linkMap.longnameToId[longname] = fragment;
};
function getNamespace(kind) {
if (dictionary.isNamespace(kind)) {
return kind + ':';
}
return '';
}
function formatNameForLink(doclet, options) {
var newName = getNamespace(doclet.kind) + (doclet.name || '') + (doclet.variation || '');
var scopePunc = exports.scopeToPunc[doclet.scope] || '';
// Only prepend the scope punctuation if it's not the same character that marks the start of a
// fragment ID. Using `#` in HTML5 fragment IDs is legal, but URLs like `foo.html##bar` are
// just confusing.
if (scopePunc !== '#') {
newName = scopePunc + newName;
}
return newName;
}
function makeUniqueFilename(filename, str) {
var key = filename.toLowerCase();
var nonUnique = true;
// don't allow filenames to begin with an underscore
if (!filename.length || filename[0] === '_') {
filename = '-' + filename;
key = filename.toLowerCase();
}
// append enough underscores to make the filename unique
while (nonUnique) {
if ( hasOwnProp.call(files, key) ) {
filename += '_';
key = filename.toLowerCase();
} else {
nonUnique = false;
}
}
files[key] = str;
return filename;
}
/**
* Convert a string to a unique filename, including an extension.
*
* Filenames are cached to ensure that they are used only once. For example, if the same string is
* passed in twice, two different filenames will be returned.
*
* Also, filenames are not considered unique if they are capitalized differently but are otherwise
* identical.
* @param {string} str The string to convert.
* @return {string} The filename to use for the string.
*/
var getUniqueFilename = exports.getUniqueFilename = function(str) {
var namespaces = dictionary.getNamespaces().join('|');
var basename = (str || '')
// use - instead of : in namespace prefixes
.replace(new RegExp('^(' + namespaces + '):'), '$1-')
// replace characters that can cause problems on some filesystems
.replace(/[\\\/?*:|'"<>]/g, '_')
// use - instead of ~ to denote 'inner'
.replace(/~/g, '-')
// use _ instead of # to denote 'instance'
.replace(/\#/g, '_')
// use _ instead of / (for example, in module names)
.replace(/\//g, '_')
// remove the variation, if any
.replace(/\([\s\S]*\)$/, '')
// make sure we don't create hidden files, or files whose names start with a dash
.replace(/^[\.\-]/, '');
// in case we've now stripped the entire basename (uncommon, but possible):
basename = basename.length ? basename : '_';
return makeUniqueFilename(basename, str) + exports.fileExtension;
};
/**
* Get a longname's filename if one has been registered; otherwise, generate a unique filename, then
* register the filename.
* @private
*/
function getFilename(longname) {
var fileUrl;
if ( hasOwnProp.call(longnameToUrl, longname) ) {
fileUrl = longnameToUrl[longname];
}
else {
fileUrl = getUniqueFilename(longname);
registerLink(longname, fileUrl);
}
return fileUrl;
}
/**
* Check whether a symbol is the only symbol exported by a module (as in
* `module.exports = function() {};`).
*
* @private
* @param {module:jsdoc/doclet.Doclet} doclet - The doclet for the symbol.
* @return {boolean} `true` if the symbol is the only symbol exported by a module; otherwise,
* `false`.
*/
function isModuleExports(doclet) {
return doclet.longname && doclet.longname === doclet.name &&
doclet.longname.indexOf(MODULE_NAMESPACE) === 0 && doclet.kind !== 'module';
}
function makeUniqueId(filename, id) {
var key;
var nonUnique = true;
key = id.toLowerCase();
// HTML5 IDs cannot contain whitespace characters
id = id.replace(/\s/g, '');
// append enough underscores to make the identifier unique
while (nonUnique) {
if ( hasOwnProp.call(ids, filename) && hasOwnProp.call(ids[filename], key) ) {
id += '_';
key = id.toLowerCase();
}
else {
nonUnique = false;
}
}
ids[filename] = ids[filename] || {};
ids[filename][key] = id;
return id;
}
/**
* Get a doclet's ID if one has been registered; otherwise, generate a unique ID, then register
* the ID.
* @private
*/
function getId(longname, id) {
if ( hasOwnProp.call(longnameToId, longname) ) {
id = longnameToId[longname];
}
else if (!id) {
// no ID required
return '';
}
else {
id = makeUniqueId(longname, id);
registerId(longname, id);
}
return id;
}
/**
* Convert a doclet to an identifier that is unique for a specified filename.
*
* Identifiers are not considered unique if they are capitalized differently but are otherwise
* identical.
*
* @method
* @param {string} filename - The file in which the identifier will be used.
* @param {string} doclet - The doclet to convert.
* @return {string} A unique identifier based on the file and doclet.
*/
var getUniqueId = exports.getUniqueId = makeUniqueId;
var htmlsafe = exports.htmlsafe = function(str) {
return str.replace(/&/g, '&amp;')
.replace(/</g, '&lt;');
};
function parseType(longname) {
var err;
try {
return catharsis.parse(longname, {jsdoc: true});
}
catch (e) {
err = new Error('unable to parse ' + longname + ': ' + e.message);
require('jsdoc/util/logger').error(err);
return longname;
}
}
function stringifyType(parsedType, cssClass, linkMap) {
return require('catharsis').stringify(parsedType, {
cssClass: cssClass,
htmlSafe: true,
links: linkMap
});
}
function hasUrlPrefix(text) {
return (/^(http|ftp)s?:\/\//).test(text);
}
function isComplexTypeExpression(expr) {
// record types, type unions, and type applications all count as "complex"
return expr.search(/[{(|]/) !== -1 || expr.search(/</) > 0;
}
function fragmentHash(fragmentId) {
if (!fragmentId) {
return '';
}
return '#' + fragmentId;
}
/**
* Build an HTML link to the symbol with the specified longname. If the longname is not
* associated with a URL, this method simply returns the link text, if provided, or the longname.
*
* The `longname` parameter can also contain a URL rather than a symbol's longname.
*
* This method supports type applications that can contain one or more types, such as
* `Array.<MyClass>` or `Array.<(MyClass|YourClass)>`. In these examples, the method attempts to
* replace `Array`, `MyClass`, and `YourClass` with links to the appropriate types. The link text
* is ignored for type applications.
*
* @param {string} longname - The longname (or URL) that is the target of the link.
* @param {string=} linkText - The text to display for the link, or `longname` if no text is
* provided.
* @param {Object} options - Options for building the link.
* @param {string=} options.cssClass - The CSS class (or classes) to include in the link's `<a>`
* tag.
* @param {string=} options.fragmentId - The fragment identifier (for example, `name` in
* `foo.html#name`) to append to the link target.
* @param {string=} options.linkMap - The link map in which to look up the longname.
* @param {boolean=} options.monospace - Indicates whether to display the link text in a monospace
* font.
* @return {string} The HTML link, or the link text if the link is not available.
*/
function buildLink(longname, linkText, options) {
var classString = options.cssClass ? util.format(' class="%s"', options.cssClass) : '';
var fileUrl;
var fragmentString = fragmentHash(options.fragmentId);
var stripped;
var text;
var parsedType;
// handle cases like:
// @see <http://example.org>
// @see http://example.org
stripped = longname ? longname.replace(/^<|>$/g, '') : '';
if ( hasUrlPrefix(stripped) ) {
fileUrl = stripped;
text = linkText || stripped;
}
// handle complex type expressions that may require multiple links
// (but skip anything that looks like an inline tag)
else if (longname && isComplexTypeExpression(longname) && /\{\@.+\}/.test(longname) === false) {
parsedType = parseType(longname);
return stringifyType(parsedType, options.cssClass, options.linkMap);
}
else {
fileUrl = hasOwnProp.call(options.linkMap, longname) ? options.linkMap[longname] : '';
text = linkText || longname;
}
text = options.monospace ? '<code>' + text + '</code>' : text;
if (!fileUrl) {
return text;
}
else {
return util.format('<a href="%s"%s>%s</a>', encodeURI(fileUrl + fragmentString),
classString, text);
}
}
/**
* Retrieve an HTML link to the symbol with the specified longname. If the longname is not
* associated with a URL, this method simply returns the link text, if provided, or the longname.
*
* The `longname` parameter can also contain a URL rather than a symbol's longname.
*
* This method supports type applications that can contain one or more types, such as
* `Array.<MyClass>` or `Array.<(MyClass|YourClass)>`. In these examples, the method attempts to
* replace `Array`, `MyClass`, and `YourClass` with links to the appropriate types. The link text
* is ignored for type applications.
*
* @param {string} longname - The longname (or URL) that is the target of the link.
* @param {string=} linkText - The text to display for the link, or `longname` if no text is
* provided.
* @param {string=} cssClass - The CSS class (or classes) to include in the link's `<a>` tag.
* @param {string=} fragmentId - The fragment identifier (for example, `name` in `foo.html#name`) to
* append to the link target.
* @return {string} The HTML link, or a plain-text string if the link is not available.
*/
var linkto = exports.linkto = function(longname, linkText, cssClass, fragmentId) {
return buildLink(longname, linkText, {
cssClass: cssClass,
fragmentId: fragmentId,
linkMap: longnameToUrl
});
};
function useMonospace(tag, text) {
var cleverLinks;
var monospaceLinks;
var result;
if ( hasUrlPrefix(text) ) {
result = false;
}
else if (tag === 'linkplain') {
result = false;
}
else if (tag === 'linkcode') {
result = true;
}
else {
cleverLinks = env.conf.templates.cleverLinks;
monospaceLinks = env.conf.templates.monospaceLinks;
if (monospaceLinks || cleverLinks) {
result = true;
}
}
return result || false;
}
function splitLinkText(text) {
var linkText;
var target;
var splitIndex;
// if a pipe is not present, we split on the first space
splitIndex = text.indexOf('|');
if (splitIndex === -1) {
splitIndex = text.search(/\s/);
}
if (splitIndex !== -1) {
linkText = text.substr(splitIndex + 1);
// Normalize subsequent newlines to a single space.
linkText = linkText.replace(/\n+/, ' ');
target = text.substr(0, splitIndex);
}
return {
linkText: linkText,
target: target || text
};
}
var tutorialToUrl = exports.tutorialToUrl = function(tutorial) {
var fileUrl;
var node = tutorials.getByName(tutorial);
// no such tutorial
if (!node) {
require('jsdoc/util/logger').error( new Error('No such tutorial: ' + tutorial) );
return null;
}
// define the URL if necessary
if (!hasOwnProp.call(tutorialLinkMap.nameToUrl, node.name)) {
fileUrl = 'tutorial-' + getUniqueFilename(node.name);
tutorialLinkMap.nameToUrl[node.name] = fileUrl;
tutorialLinkMap.urlToName[fileUrl] = node.name;
}
return tutorialLinkMap.nameToUrl[node.name];
};
/**
* Retrieve a link to a tutorial, or the name of the tutorial if the tutorial is missing. If the
* `missingOpts` parameter is supplied, the names of missing tutorials will be prefixed by the
* specified text and wrapped in the specified HTML tag and CSS class.
*
* @todo Deprecate missingOpts once we have a better error-reporting mechanism.
* @param {string} tutorial The name of the tutorial.
* @param {string} content The link text to use.
* @param {object} [missingOpts] Options for displaying the name of a missing tutorial.
* @param {string} missingOpts.classname The CSS class to wrap around the tutorial name.
* @param {string} missingOpts.prefix The prefix to add to the tutorial name.
* @param {string} missingOpts.tag The tag to wrap around the tutorial name.
* @return {string} An HTML link to the tutorial, or the name of the tutorial with the specified
* options.
*/
var toTutorial = exports.toTutorial = function(tutorial, content, missingOpts) {
if (!tutorial) {
require('jsdoc/util/logger').error( new Error('Missing required parameter: tutorial') );
return null;
}
var node = tutorials.getByName(tutorial);
// no such tutorial
if (!node) {
missingOpts = missingOpts || {};
var tag = missingOpts.tag;
var classname = missingOpts.classname;
var link = tutorial;
if (missingOpts.prefix) {
link = missingOpts.prefix + link;
}
if (tag) {
link = '<' + tag + (classname ? (' class="' + classname + '">') : '>') + link;
link += '</' + tag + '>';
}
return link;
}
content = content || node.title;
return '<a href="' + tutorialToUrl(tutorial) + '">' + content + '</a>';
};
/** Find symbol {@link ...} and {@tutorial ...} strings in text and turn into html links */
exports.resolveLinks = function(str) {
var replaceInlineTags = require('jsdoc/tag/inline').replaceInlineTags;
function extractLeadingText(string, completeTag) {
var tagIndex = string.indexOf(completeTag);
var leadingText = null;
var leadingTextRegExp = /\[(.+?)\]/g;
var leadingTextInfo = leadingTextRegExp.exec(string);
// did we find leading text, and if so, does it immediately precede the tag?
while (leadingTextInfo && leadingTextInfo.length) {
if (leadingTextInfo.index + leadingTextInfo[0].length === tagIndex) {
string = string.replace(leadingTextInfo[0], '');
leadingText = leadingTextInfo[1];
break;
}
leadingTextInfo = leadingTextRegExp.exec(string);
}
return {
leadingText: leadingText,
string: string
};
}
function processLink(string, tagInfo) {
var leading = extractLeadingText(string, tagInfo.completeTag);
var linkText = leading.leadingText;
var monospace;
var split;
var target;
string = leading.string;
split = splitLinkText(tagInfo.text);
target = split.target;
linkText = linkText || split.linkText;
monospace = useMonospace(tagInfo.tag, tagInfo.text);
return string.replace( tagInfo.completeTag, buildLink(target, linkText, {
linkMap: longnameToUrl,
monospace: monospace
}) );
}
function processTutorial(string, tagInfo) {
var leading = extractLeadingText(string, tagInfo.completeTag);
string = leading.string;
return string.replace( tagInfo.completeTag, toTutorial(tagInfo.text, leading.leadingText) );
}
var replacers = {
link: processLink,
linkcode: processLink,
linkplain: processLink,
tutorial: processTutorial
};
return replaceInlineTags(str, replacers).newString;
};
/** Convert tag text like "Jane Doe <jdoe@example.org>" into a mailto link */
exports.resolveAuthorLinks = function(str) {
var author;
var matches = str.match(/^\s?([\s\S]+)\b\s+<(\S+@\S+)>\s?$/);
if (matches && matches.length === 3) {
author = '<a href="mailto:' + matches[2] + '">' + htmlsafe(matches[1]) + '</a>';
}
else {
author = htmlsafe(str);
}
return author;
};
/**
* Find items in a TaffyDB database that match the specified key-value pairs.
* @param {TAFFY} data The TaffyDB database to search.
* @param {object|function} spec Key-value pairs to match against (for example,
* `{ longname: 'foo' }`), or a function that returns `true` if a value matches or `false` if it
* does not match.
* @return {array<object>} The matching items.
*/
var find = exports.find = function(data, spec) {
return data(spec).get();
};
/**
* Retrieve all of the following types of members from a set of doclets:
*
* + Classes
* + Externals
* + Globals
* + Mixins
* + Modules
* + Namespaces
* + Events
* @param {TAFFY} data The TaffyDB database to search.
* @return {object} An object with `classes`, `externals`, `globals`, `mixins`, `modules`,
* `events`, and `namespaces` properties. Each property contains an array of objects.
*/
exports.getMembers = function(data) {
var members = {
classes: find( data, {kind: 'class'} ),
externals: find( data, {kind: 'external'} ),
events: find( data, {kind: 'event'} ),
globals: find(data, {
kind: ['member', 'function', 'constant', 'typedef'],
memberof: { isUndefined: true }
}),
mixins: find( data, {kind: 'mixin'} ),
modules: find( data, {kind: 'module'} ),
namespaces: find( data, {kind: 'namespace'} ),
interfaces: find( data, {kind: 'interface'} )
};
// strip quotes from externals, since we allow quoted names that would normally indicate a
// namespace hierarchy (as in `@external "jquery.fn"`)
// TODO: we should probably be doing this for other types of symbols, here or elsewhere; see
// jsdoc3/jsdoc#396
members.externals = members.externals.map(function(doclet) {
doclet.name = doclet.name.replace(/(^"|"$)/g, '');
return doclet;
});
// functions that are also modules (as in `module.exports = function() {};`) are not globals
members.globals = members.globals.filter(function(doclet) {
return !isModuleExports(doclet);
});
return members;
};
/**
* Retrieve the member attributes for a doclet (for example, `virtual`, `static`, and
* `readonly`).
* @param {object} d The doclet whose attributes will be retrieved.
* @return {array<string>} The member attributes for the doclet.
*/
exports.getAttribs = function(d) {
var attribs = [];
if (!d) {
return attribs;
}
if (d.virtual) {
attribs.push('abstract');
}
if (d.access && d.access !== 'public') {
attribs.push(d.access);
}
if (d.scope && d.scope !== 'instance' && d.scope !== name.SCOPE.NAMES.GLOBAL) {
if (d.kind === 'function' || d.kind === 'member' || d.kind === 'constant') {
attribs.push(d.scope);
}
}
if (d.readonly === true) {
if (d.kind === 'member') {
attribs.push('readonly');
}
}
if (d.kind === 'constant') {
attribs.push('constant');
}
if (d.nullable === true) {
attribs.push('nullable');
}
else if (d.nullable === false) {
attribs.push('non-null');
}
return attribs;
};
/**
* Retrieve links to allowed types for the member.
*
* @param {Object} d - The doclet whose types will be retrieved.
* @param {string} [cssClass] - The CSS class to include in the `class` attribute for each link.
* @return {Array.<string>} HTML links to allowed types for the member.
*/
exports.getSignatureTypes = function(d, cssClass) {
var types = [];
if (d.type && d.type.names) {
types = d.type.names;
}
if (types && types.length) {
types = types.map(function(t) {
return linkto(t, htmlsafe(t), cssClass);
});
}
return types;
};
/**
* Retrieve names of the parameters that the member accepts. If a value is provided for `optClass`,
* the names of optional parameters will be wrapped in a `<span>` tag with that class.
* @param {object} d The doclet whose parameter names will be retrieved.
* @param {string} [optClass] The class to assign to the `<span>` tag that is wrapped around the
* names of optional parameters. If a value is not provided, optional parameter names will not be
* wrapped with a `<span>` tag. Must be a legal value for a CSS class name.
* @return {array<string>} An array of parameter names, with or without `<span>` tags wrapping the
* names of optional parameters.
*/
exports.getSignatureParams = function(d, optClass) {
var pnames = [];
if (d.params) {
d.params.forEach(function(p) {
if (p.name && p.name.indexOf('.') === -1) {
if (p.optional && optClass) {
pnames.push('<span class="' + optClass + '">' + p.name + '</span>');
}
else {
pnames.push(p.name);
}
}
});
}
return pnames;
};
/**
* Retrieve links to types that the member can return.
*
* @param {Object} d - The doclet whose types will be retrieved.
* @param {string} [cssClass] - The CSS class to include in the `class` attribute for each link.
* @return {Array.<string>} HTML links to types that the member can return.
*/
exports.getSignatureReturns = function(d, cssClass) {
var returnTypes = [];
if (d.returns) {
d.returns.forEach(function(r) {
if (r && r.type && r.type.names) {
if (!returnTypes.length) {
returnTypes = r.type.names;
}
}
});
}
if (returnTypes && returnTypes.length) {
returnTypes = returnTypes.map(function(r) {
return linkto(r, htmlsafe(r), cssClass);
});
}
return returnTypes;
};
/**
* Retrieve an ordered list of doclets for a symbol's ancestors.
*
* @param {TAFFY} data - The TaffyDB database to search.
* @param {Object} doclet - The doclet whose ancestors will be retrieved.
* @return {Array.<module:jsdoc/doclet.Doclet>} A array of ancestor doclets, sorted from most to
* least distant.
*/
exports.getAncestors = function(data, doclet) {
var ancestors = [];
var doc = doclet;
while (doc) {
doc = find(data, {longname: doc.memberof})[0];
if (doc) {
ancestors.unshift(doc);
}
}
return ancestors;
};
/**
* Retrieve links to a member's ancestors.
*
* @param {TAFFY} data - The TaffyDB database to search.
* @param {Object} doclet - The doclet whose ancestors will be retrieved.
* @param {string} [cssClass] - The CSS class to include in the `class` attribute for each link.
* @return {Array.<string>} HTML links to a member's ancestors.
*/
exports.getAncestorLinks = function(data, doclet, cssClass) {
var ancestors = exports.getAncestors(data, doclet);
var links = [];
ancestors.forEach(function(ancestor) {
var linkText = (exports.scopeToPunc[ancestor.scope] || '') + ancestor.name;
var link = linkto(ancestor.longname, linkText, cssClass);
links.push(link);
});
if (links.length) {
links[links.length - 1] += (exports.scopeToPunc[doclet.scope] || '');
}
return links;
};
/**
* Iterates through all the doclets in `data`, ensuring that if a method
* @listens to an event, then that event has a 'listeners' array with the
* longname of the listener in it.
*
* @param {TAFFY} data - The TaffyDB database to search.
*/
exports.addEventListeners = function(data) {
// TODO: do this on the *pruned* data
// find all doclets that @listen to something.
var listeners = find(data, function () { return this.listens && this.listens.length; });
if (!listeners.length) {
return;
}
var doc,
l,
_events = {}; // just a cache to prevent me doing so many lookups
listeners.forEach(function (listener) {
l = listener.listens;
l.forEach(function (eventLongname) {
doc = _events[eventLongname] || find(data, {longname: eventLongname, kind: 'event'})[0];
if (doc) {
if (!doc.listeners) {
doc.listeners = [listener.longname];
} else {
doc.listeners.push(listener.longname);
}
_events[eventLongname] = _events[eventLongname] || doc;
}
});
});
};
/**
* Remove members that will not be included in the output, including:
*
* + Undocumented members.
* + Members tagged `@ignore`.
* + Members of anonymous classes.
* + Members tagged `@private`, unless the `private` option is enabled.
* @param {TAFFY} data The TaffyDB database to prune.
* @return {TAFFY} The pruned database.
*/
exports.prune = function(data) {
data({undocumented: true}).remove();
data({ignore: true}).remove();
if (!env.opts.private) { data({access: 'private'}).remove(); }
data({memberof: '<anonymous>'}).remove();
return data;
};
/**
* Create a URL that points to the generated documentation for the doclet.
*
* If a doclet corresponds to an output file (for example, if the doclet represents a class), the
* URL will consist of a filename.
*
* If a doclet corresponds to a smaller portion of an output file (for example, if the doclet
* represents a method), the URL will consist of a filename and a fragment ID.
*
* @param {module:jsdoc/doclet.Doclet} doclet - The doclet that will be used to create the URL.
* @return {string} The URL to the generated documentation for the doclet.
*/
exports.createLink = function(doclet) {
var fakeContainer;
var filename;
var fileUrl;
var fragment = '';
var longname = doclet.longname;
var match;
// handle doclets in which doclet.longname implies that the doclet gets its own HTML file, but
// doclet.kind says otherwise. this happens due to mistagged JSDoc (for example, a module that
// somehow has doclet.kind set to `member`).
// TODO: generate a warning (ideally during parsing!)
if (containers.indexOf(doclet.kind) === -1) {
match = /(\S+):/.exec(longname);
if (match && containers.indexOf(match[1]) !== -1) {
fakeContainer = match[1];
}
}
// the doclet gets its own HTML file
if ( containers.indexOf(doclet.kind) !== -1 || isModuleExports(doclet) ) {
filename = getFilename(longname);
}
// mistagged version of a doclet that gets its own HTML file
else if ( containers.indexOf(doclet.kind) === -1 && fakeContainer ) {
filename = getFilename(doclet.memberof || longname);
if (doclet.name !== doclet.longname) {
fragment = formatNameForLink(doclet);
fragment = getId(longname, fragment);
}
}
// the doclet is within another HTML file
else {
filename = getFilename(doclet.memberof || exports.globalName);
if ( (doclet.name !== doclet.longname) || (doclet.scope === name.SCOPE.NAMES.GLOBAL) ) {
fragment = formatNameForLink(doclet);
fragment = getId(longname, fragment);
}
}
fileUrl = encodeURI( filename + fragmentHash(fragment) );
return fileUrl;
};
// TODO: docs
exports.longnamesToTree = name.longnamesToTree;
/**
* Replace the existing tag dictionary with a new tag dictionary.
*
* Used for testing only. Do not call this method directly. Instead, call
* {@link module:jsdoc/doclet._replaceDictionary}, which also updates this module's tag dictionary.
*
* @private
* @param {module:jsdoc/tag/dictionary.Dictionary} dict - The new tag dictionary.
*/
exports._replaceDictionary = function _replaceDictionary(dict) {
dictionary = dict;
};