allow plugins to be installed outside of the JSDoc directory (#277)

This commit is contained in:
Jeff Williams 2013-01-27 07:56:43 -08:00
parent 5df93a0136
commit 87cd24728f
8 changed files with 217 additions and 133 deletions

View File

@ -42,7 +42,7 @@ env = {
/** /**
The command line arguments, parsed into a key/value hash. The command line arguments, parsed into a key/value hash.
@type Object @type Object
@example if (env.opts.help) { print 'Helpful message.'; } @example if (env.opts.help) { console.log('Helpful message.'); }
*/ */
opts: {} opts: {}
}; };
@ -101,7 +101,7 @@ function main() {
var handlers = require('jsdoc/src/handlers'); var handlers = require('jsdoc/src/handlers');
var include = require('jsdoc/util/include'); var include = require('jsdoc/util/include');
var Package = require('jsdoc/package').Package; var Package = require('jsdoc/package').Package;
var path = require('path'); var path = require('jsdoc/path');
var plugins = require('jsdoc/plugins'); var plugins = require('jsdoc/plugins');
var Readme = require('jsdoc/readme'); var Readme = require('jsdoc/readme');
var resolver = require('jsdoc/tutorial/resolver'); var resolver = require('jsdoc/tutorial/resolver');
@ -121,64 +121,6 @@ function main() {
var template; var template;
/**
* If required by the current VM, convert a path to a URI that meets the operating system's
* requirements. Otherwise, return the original path.
* @function
* @param {string} path The path to convert.
* @return {string} A URI that meets the operating system's requirements, or the original path.
*/
var pathToUri = vm.getModule('jsdoc').pathToUri;
/**
* If required by the current VM, convert a URI to a path that meets the operating system's
* requirements. Otherwise, assume the "URI" is really a path, and return the original path.
* @function
* @param {string} uri The URI to convert.
* @return {string} A path that meets the operating system's requirements.
*/
var uriToPath = vm.getModule('jsdoc').uriToPath;
/**
Retrieve the fully resolved path to the requested template.
@param {string} template - The path to the requested template. May be an absolute path;
a path relative to the current working directory; or a path relative to the JSDoc directory.
@return {string} The fully resolved path (or, on Rhino, a URI) to the requested template.
*/
function getTemplatePath(template) {
var result;
template = template || 'templates/default';
function pathExists(_path) {
try {
fs.readdirSync(_path);
}
catch(e) {
return false;
}
return true;
}
// first, try resolving it relative to the current working directory (or just normalize it
// if it's an absolute path)
result = path.resolve(template);
if ( !pathExists(result) ) {
// next, try resolving it relative to the JSDoc directory
result = path.resolve(__dirname, template);
if ( !pathExists(result) ) {
result = null;
}
}
if (result) {
result = pathToUri(result);
}
return result;
}
defaultOpts = { defaultOpts = {
destination: './out/', destination: './out/',
encoding: 'utf8' encoding: 'utf8'
@ -274,19 +216,23 @@ function main() {
resolver.resolve(); resolver.resolve();
} }
env.opts.template = getTemplatePath(env.opts.template) || env.opts.template; env.opts.template = (function() {
var publish = env.opts.template || 'templates/default';
// if we don't find it, keep the user-specified value so the error message is useful
return path.getResourcePath(publish) || env.opts.template;
})();
try { try {
template = require(env.opts.template + '/publish'); template = require(env.opts.template + '/publish');
} }
catch(e) { catch(e) {
throw new Error("Unable to load template: " + e.message || e); throw new Error('Unable to load template: ' + e.message || e);
} }
// templates should include a publish.js file that exports a "publish" function // templates should include a publish.js file that exports a "publish" function
if (template.publish && typeof template.publish === 'function') { if (template.publish && typeof template.publish === 'function') {
// convert this from a URI back to a path if necessary // convert this from a URI back to a path if necessary
env.opts.template = uriToPath(env.opts.template); env.opts.template = path._uriToPath(env.opts.template);
template.publish( template.publish(
taffy(docs), taffy(docs),
env.opts, env.opts,
@ -301,7 +247,7 @@ function main() {
'deprecated and may not be supported in future versions. ' + 'deprecated and may not be supported in future versions. ' +
'Please update the template to use "exports.publish" instead.' ); 'Please update the template to use "exports.publish" instead.' );
// convert this from a URI back to a path if necessary // convert this from a URI back to a path if necessary
env.opts.template = uriToPath(env.opts.template); env.opts.template = path._uriToPath(env.opts.template);
publish( publish(
taffy(docs), taffy(docs),
env.opts, env.opts,
@ -309,7 +255,7 @@ function main() {
); );
} }
else { else {
throw new Error( env.opts.template + " does not export a 'publish' function." ); throw new Error( env.opts.template + ' does not export a "publish" function.' );
} }
} }
} }

View File

@ -3,7 +3,9 @@
* @module jsdoc/path * @module jsdoc/path
*/ */
var fs = require('fs');
var path = require('path'); var path = require('path');
var vm = require('jsdoc/util/vm');
function prefixReducer(previousPath, current) { function prefixReducer(previousPath, current) {
@ -60,6 +62,75 @@ exports.commonPrefix = function(paths) {
return common.join(path.sep); return common.join(path.sep);
}; };
// TODO: do we need this?
/**
* If required by the current VM, convert a path to a URI that meets the operating system's
* requirements. Otherwise, return the original path.
* @function
* @private
* @param {string} path The path to convert.
* @return {string} A URI that meets the operating system's requirements, or the original path.
*/
var pathToUri = vm.getModule('jsdoc').pathToUri;
// TODO: do we need this? if so, any way to stop exporting it?
/**
* If required by the current VM, convert a URI to a path that meets the operating system's
* requirements. Otherwise, assume the "URI" is really a path, and return the original path.
* @function
* @private
* @param {string} uri The URI to convert.
* @return {string} A path that meets the operating system's requirements.
*/
exports._uriToPath = vm.getModule('jsdoc').uriToPath;
/**
* 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 current
* working directory, then 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;
function pathExists(_path) {
try {
fs.readdirSync(_path);
}
catch(e) {
return false;
}
return true;
}
// first, try resolving it relative to the current working directory (or just normalize it
// if it's an absolute path)
result = path.resolve(filepath);
if ( !pathExists(result) ) {
// next, try resolving it relative to the JSDoc directory
result = path.resolve(__dirname, filepath);
if ( !pathExists(result) ) {
result = null;
}
}
if (result) {
result = filename ? path.join(result, filename) : result;
result = pathToUri(result);
}
return result;
};
Object.keys(path).forEach(function(member) { Object.keys(path).forEach(function(member) {
exports[member] = path[member]; exports[member] = path[member];
}); });

View File

@ -4,6 +4,9 @@
* @module jsdoc/plugins * @module jsdoc/plugins
*/ */
var error = require('jsdoc/util/error');
var path = require('jsdoc/path');
var hasOwnProp = Object.prototype.hasOwnProperty; var hasOwnProp = Object.prototype.hasOwnProperty;
exports.installPlugins = function(plugins, p) { exports.installPlugins = function(plugins, p) {
@ -12,28 +15,35 @@ exports.installPlugins = function(plugins, p) {
var eventName; var eventName;
var plugin; var plugin;
var pluginPath;
// allow user-defined plugins to... for (var i = 0, l = plugins.length; i < l; i++) {
for (var i = 0, leni = plugins.length; i < leni; i++) { pluginPath = path.getResourcePath(path.dirname(plugins[i]), path.basename(plugins[i]));
plugin = require(plugins[i]); if (!pluginPath) {
error.handle(new Error('Unable to find the plugin "' + plugins[i] + '"'));
}
else {
plugin = require(pluginPath);
//...register event handlers // allow user-defined plugins to...
if (plugin.handlers) { //...register event handlers
for (eventName in plugin.handlers) { if (plugin.handlers) {
if ( hasOwnProp.call(plugin.handlers, eventName) ) { for (eventName in plugin.handlers) {
parser.on(eventName, plugin.handlers[eventName]); if ( hasOwnProp.call(plugin.handlers, eventName) ) {
parser.on(eventName, plugin.handlers[eventName]);
}
} }
} }
}
//...define tags //...define tags
if (plugin.defineTags) { if (plugin.defineTags) {
plugin.defineTags(dictionary); plugin.defineTags(dictionary);
} }
//...add a node visitor //...add a node visitor
if (plugin.nodeVisitor) { if (plugin.nodeVisitor) {
parser.addNodeVisitor(plugin.nodeVisitor); parser.addNodeVisitor(plugin.nodeVisitor);
}
} }
} }
}; };

View File

@ -1,13 +1,17 @@
Adding a Plugin Creating and Enabling a Plugin
---- ----
There are two steps required to install a new plugin: There are two steps required to create and enable a new JSDoc plugin:
1. Create a JavaScript module to contain your plugin code. 1. Create a JavaScript module to contain your plugin code.
2. Include the name of that module in the "plugins" array of `conf.json`. 2. Include that module in the "plugins" array of `conf.json`. You can specify
an absolute or relative path. If you use a relative path, JSDoc searches for
the plugin in the current working directory and the JSDoc directory, in that
order.
For example, if your plugin source code was saved in the "plugins/shout.js" For example, if your plugin source code was saved in the "plugins/shout.js"
file, you would include it by adding a reference to it in conf.json like so: file in the current working directory, you would include it by adding a
reference to it in conf.json like so:
... ...
"plugins": [ "plugins": [
@ -350,19 +354,21 @@ the `jsdoc/util/error` module:
require('jsdoc/util/error').handle( new Error('I do not like green eggs and ham!') ); require('jsdoc/util/error').handle( new Error('I do not like green eggs and ham!') );
By default this will throw the error, halting the execution of `jsdoc`. However, By default, this will throw the error, halting the execution of JSDoc. However,
if the user used the `--lenient` switch when they ran `jsdoc` it will simply log if the user enabled JSDoc's `--lenient` switch, JSDoc will simply log the error
the error to the console and continue. to the console and continue.
Packaging JSDoc 3 Plugins Packaging JSDoc 3 Plugins
---- ----
The JSDoc 3 Jakefile has an ```install``` task that can be used to install a plugin The JSDoc 3 Jakefile has an ```install``` task that can be used to install a
into the jsdoc 3 installation. So running the following will install the plugin: plugin into the JSDoc directory. So running the following will install the
plugin:
$>jake install[path/to/YourPluginFolder] $>jake install[path/to/YourPluginFolder]
_note: on some systems (like MacOS X), you may need to quote the target name and parameters_: **Note**: On some operating systems, including OS X, you may need to quote the
target name and parameters:
$>jake 'install[path/to/YourPluginFolder]' $>jake 'install[path/to/YourPluginFolder]'
@ -379,6 +385,3 @@ The task is passed a directory that should look something like the following:
\- templates \- templates
\- YourTemplate \- YourTemplate
\- publish.js \- publish.js
Basically everything is copied over into the jsdoc installation directory, the
directory should contain anything you want to put there.

View File

@ -1,10 +1,25 @@
var myGlobal = require('jsdoc/util/global');
myGlobal.jsdocPluginsTest.plugin1 = {};
exports.handlers = { exports.handlers = {
fileBegin: jasmine.createSpy('fileBegin'), fileBegin: function() {
beforeParse: jasmine.createSpy('beforeParse'), myGlobal.jsdocPluginsTest.plugin1.fileBegin = true;
jsdocCommentFound: jasmine.createSpy('jsdocCommentFound'), },
symbolFound: jasmine.createSpy('symbolFound'), beforeParse: function() {
newDoclet: jasmine.createSpy('newDoclet'), myGlobal.jsdocPluginsTest.plugin1.beforeParse = true;
fileComplete: jasmine.createSpy('fileComplete') },
jsdocCommentFound: function() {
myGlobal.jsdocPluginsTest.plugin1.jsdocCommentFound = true;
},
symbolFound: function() {
myGlobal.jsdocPluginsTest.plugin1.symbolFound = true;
},
newDoclet: function() {
myGlobal.jsdocPluginsTest.plugin1.newDoclet = true;
},
fileComplete: function() {
myGlobal.jsdocPluginsTest.plugin1.fileComplete = true;
}
}; };
exports.defineTags = function(dictionary) { exports.defineTags = function(dictionary) {
@ -17,7 +32,8 @@ exports.defineTags = function(dictionary) {
}; };
exports.nodeVisitor = { exports.nodeVisitor = {
visitNode: jasmine.createSpy("plugin 1 visitNode").andCallFake(function(node, e, parser, currentSourceName) { visitNode: function(node, e, parser, currentSourceName) {
myGlobal.jsdocPluginsTest.plugin1.visitNode = true;
e.stopPropagation = true; e.stopPropagation = true;
}) }
}; };

View File

@ -1,12 +1,29 @@
var myGlobal = require('jsdoc/util/global');
myGlobal.jsdocPluginsTest.plugin2 = {};
exports.handlers = { exports.handlers = {
fileBegin: jasmine.createSpy('fileBegin'), fileBegin: function() {
beforeParse: jasmine.createSpy('beforeParse'), myGlobal.jsdocPluginsTest.plugin2.fileBegin = true;
jsdocCommentFound: jasmine.createSpy('jsdocCommentFound'), },
symbolFound: jasmine.createSpy('symbolFound'), beforeParse: function() {
newDoclet: jasmine.createSpy('newDoclet'), myGlobal.jsdocPluginsTest.plugin2.beforeParse = true;
fileComplete: jasmine.createSpy('fileComplete') },
jsdocCommentFound: function() {
myGlobal.jsdocPluginsTest.plugin2.jsdocCommentFound = true;
},
symbolFound: function() {
myGlobal.jsdocPluginsTest.plugin2.symbolFound = true;
},
newDoclet: function() {
myGlobal.jsdocPluginsTest.plugin2.newDoclet = true;
},
fileComplete: function() {
myGlobal.jsdocPluginsTest.plugin2.fileComplete = true;
}
}; };
exports.nodeVisitor = { exports.nodeVisitor = {
visitNode: jasmine.createSpy("plugin 2 visitNode") visitNode: function() {
}; myGlobal.jsdocPluginsTest.plugin2.visitNode = true;
}
};

View File

@ -1,4 +1,4 @@
/*global beforeEach: true, describe: true, expect: true, it: true, spyOn: true */ /*global beforeEach: true, describe: true, expect: true, it: true, spyOn: true, xdescribe: true */
describe('jsdoc/path', function() { describe('jsdoc/path', function() {
var os = require('os'); var os = require('os');
@ -24,6 +24,11 @@ describe('jsdoc/path', function() {
expect(typeof path.commonPrefix).toEqual('function'); expect(typeof path.commonPrefix).toEqual('function');
}); });
it('should export a "getResourcePath" function', function() {
expect(path.getResourcePath).toBeDefined();
expect(typeof path.getResourcePath).toEqual('function');
});
describe('commonPrefix', function() { describe('commonPrefix', function() {
beforeEach(function() { beforeEach(function() {
spyOn(process, 'cwd').andCallFake(function() { spyOn(process, 'cwd').andCallFake(function() {
@ -83,4 +88,8 @@ describe('jsdoc/path', function() {
} }
}); });
}); });
xdescribe('getResourcePath', function() {
// TODO
});
}); });

View File

@ -1,29 +1,40 @@
/*global app: true, describe: true, expect: true, it: true, jasmine: true */ /*global app: true, describe: true, expect: true, it: true, jasmine: true */
describe("plugins", function() { describe("plugins", function() {
var myGlobal = require('jsdoc/util/global');
myGlobal.jsdocPluginsTest = myGlobal.jsdocPluginsTest || {};
require('jsdoc/plugins').installPlugins(['test/fixtures/testPlugin1', require('jsdoc/plugins').installPlugins(['test/fixtures/testPlugin1',
'test/fixtures/testPlugin2'], app.jsdoc.parser); 'test/fixtures/testPlugin2'], app.jsdoc.parser);
var plugin1 = require('test/fixtures/testPlugin1'), var docSet = jasmine.getDocSetFromFile("test/fixtures/plugins.js", app.jsdoc.parser);
plugin2 = require('test/fixtures/testPlugin2'),
docSet;
docSet = jasmine.getDocSetFromFile("test/fixtures/plugins.js", app.jsdoc.parser);
it("should fire the plugin's event handlers", function() { it("should fire the plugin's event handlers", function() {
expect(plugin1.handlers.fileBegin).toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin1.fileBegin).toBeDefined();
expect(plugin1.handlers.beforeParse).toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin1.fileBegin).toEqual(true);
expect(plugin1.handlers.jsdocCommentFound).toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin1.beforeParse).toBeDefined();
expect(plugin1.handlers.symbolFound).toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin1.beforeParse).toEqual(true);
expect(plugin1.handlers.newDoclet).toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin1.jsdocCommentFound).toBeDefined();
expect(plugin1.handlers.fileComplete).toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin1.jsdocCommentFound).toEqual(true);
expect(myGlobal.jsdocPluginsTest.plugin1.symbolFound).toBeDefined();
expect(myGlobal.jsdocPluginsTest.plugin1.symbolFound).toEqual(true);
expect(myGlobal.jsdocPluginsTest.plugin1.newDoclet).toBeDefined();
expect(myGlobal.jsdocPluginsTest.plugin1.newDoclet).toEqual(true);
expect(myGlobal.jsdocPluginsTest.plugin1.fileComplete).toBeDefined();
expect(myGlobal.jsdocPluginsTest.plugin1.fileComplete).toEqual(true);
expect(plugin2.handlers.fileBegin).toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin2.fileBegin).toBeDefined();
expect(plugin2.handlers.beforeParse).toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin2.fileBegin).toEqual(true);
expect(plugin2.handlers.jsdocCommentFound).toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin2.beforeParse).toBeDefined();
expect(plugin2.handlers.symbolFound).toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin2.beforeParse).toEqual(true);
expect(plugin2.handlers.newDoclet).toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin2.jsdocCommentFound).toBeDefined();
expect(plugin2.handlers.fileComplete).toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin2.jsdocCommentFound).toEqual(true);
expect(myGlobal.jsdocPluginsTest.plugin2.symbolFound).toBeDefined();
expect(myGlobal.jsdocPluginsTest.plugin2.symbolFound).toEqual(true);
expect(myGlobal.jsdocPluginsTest.plugin2.newDoclet).toBeDefined();
expect(myGlobal.jsdocPluginsTest.plugin2.newDoclet).toEqual(true);
expect(myGlobal.jsdocPluginsTest.plugin2.fileComplete).toBeDefined();
expect(myGlobal.jsdocPluginsTest.plugin2.fileComplete).toEqual(true);
}); });
it("should add the plugin's tag definitions to the dictionary", function() { it("should add the plugin's tag definitions to the dictionary", function() {
@ -34,10 +45,11 @@ describe("plugins", function() {
}); });
it("should call the plugin's visitNode function", function() { it("should call the plugin's visitNode function", function() {
expect(plugin1.nodeVisitor.visitNode).toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin1.visitNode).toBeDefined();
expect(myGlobal.jsdocPluginsTest.plugin1.visitNode).toEqual(true);
}); });
it("should not call a second plugin's visitNode function if the first stopped propagation", function() { it("should not call a second plugin's visitNode function if the first stopped propagation", function() {
expect(plugin2.nodeVisitor.visitNode).not.toHaveBeenCalled(); expect(myGlobal.jsdocPluginsTest.plugin2.visitNode).not.toBeDefined();
}); });
}); });