mirror of
https://github.com/copyrighttxt/watrbx-game-engine.git
synced 2026-09-08 14:27:49 +00:00
GEEKING
This commit is contained in:
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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];
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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ł 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, '<')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(/'/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, '&')
|
||||
.replace(/</g, '<');
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user