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.
@type Object
@example if (env.opts.help) { print 'Helpful message.'; }
@example if (env.opts.help) { console.log('Helpful message.'); }
*/
opts: {}
};
@ -101,7 +101,7 @@ function main() {
var handlers = require('jsdoc/src/handlers');
var include = require('jsdoc/util/include');
var Package = require('jsdoc/package').Package;
var path = require('path');
var path = require('jsdoc/path');
var plugins = require('jsdoc/plugins');
var Readme = require('jsdoc/readme');
var resolver = require('jsdoc/tutorial/resolver');
@ -121,64 +121,6 @@ function main() {
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 = {
destination: './out/',
encoding: 'utf8'
@ -274,19 +216,23 @@ function main() {
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 {
template = require(env.opts.template + '/publish');
}
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
if (template.publish && typeof template.publish === 'function') {
// 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(
taffy(docs),
env.opts,
@ -301,7 +247,7 @@ function main() {
'deprecated and may not be supported in future versions. ' +
'Please update the template to use "exports.publish" instead.' );
// 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(
taffy(docs),
env.opts,
@ -309,7 +255,7 @@ function main() {
);
}
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
*/
var fs = require('fs');
var path = require('path');
var vm = require('jsdoc/util/vm');
function prefixReducer(previousPath, current) {
@ -60,6 +62,75 @@ exports.commonPrefix = function(paths) {
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) {
exports[member] = path[member];
});

View File

@ -4,6 +4,9 @@
* @module jsdoc/plugins
*/
var error = require('jsdoc/util/error');
var path = require('jsdoc/path');
var hasOwnProp = Object.prototype.hasOwnProperty;
exports.installPlugins = function(plugins, p) {
@ -12,28 +15,35 @@ exports.installPlugins = function(plugins, p) {
var eventName;
var plugin;
var pluginPath;
// allow user-defined plugins to...
for (var i = 0, leni = plugins.length; i < leni; i++) {
plugin = require(plugins[i]);
for (var i = 0, l = plugins.length; i < l; i++) {
pluginPath = path.getResourcePath(path.dirname(plugins[i]), path.basename(plugins[i]));
if (!pluginPath) {
error.handle(new Error('Unable to find the plugin "' + plugins[i] + '"'));
}
else {
plugin = require(pluginPath);
//...register event handlers
if (plugin.handlers) {
for (eventName in plugin.handlers) {
if ( hasOwnProp.call(plugin.handlers, eventName) ) {
parser.on(eventName, plugin.handlers[eventName]);
// allow user-defined plugins to...
//...register event handlers
if (plugin.handlers) {
for (eventName in plugin.handlers) {
if ( hasOwnProp.call(plugin.handlers, eventName) ) {
parser.on(eventName, plugin.handlers[eventName]);
}
}
}
}
//...define tags
if (plugin.defineTags) {
plugin.defineTags(dictionary);
}
//...define tags
if (plugin.defineTags) {
plugin.defineTags(dictionary);
}
//...add a node visitor
if (plugin.nodeVisitor) {
parser.addNodeVisitor(plugin.nodeVisitor);
//...add a node visitor
if (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.
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"
file, you would include it by adding a reference to it in conf.json like so:
For example, if your plugin source code was saved in the "plugins/shout.js"
file in the current working directory, you would include it by adding a
reference to it in conf.json like so:
...
"plugins": [
@ -350,19 +354,21 @@ the `jsdoc/util/error` module:
require('jsdoc/util/error').handle( new Error('I do not like green eggs and ham!') );
By default this will throw the error, halting the execution of `jsdoc`. However,
if the user used the `--lenient` switch when they ran `jsdoc` it will simply log
the error to the console and continue.
By default, this will throw the error, halting the execution of JSDoc. However,
if the user enabled JSDoc's `--lenient` switch, JSDoc will simply log the error
to the console and continue.
Packaging JSDoc 3 Plugins
----
The JSDoc 3 Jakefile has an ```install``` task that can be used to install a plugin
into the jsdoc 3 installation. So running the following will install the plugin:
The JSDoc 3 Jakefile has an ```install``` task that can be used to install a
plugin into the JSDoc directory. So running the following will install the
plugin:
$>jake install[path/to/YourPluginFolder]
_note: on some 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]'
@ -379,6 +385,3 @@ The task is passed a directory that should look something like the following:
\- templates
\- YourTemplate
\- 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 = {
fileBegin: jasmine.createSpy('fileBegin'),
beforeParse: jasmine.createSpy('beforeParse'),
jsdocCommentFound: jasmine.createSpy('jsdocCommentFound'),
symbolFound: jasmine.createSpy('symbolFound'),
newDoclet: jasmine.createSpy('newDoclet'),
fileComplete: jasmine.createSpy('fileComplete')
fileBegin: function() {
myGlobal.jsdocPluginsTest.plugin1.fileBegin = true;
},
beforeParse: function() {
myGlobal.jsdocPluginsTest.plugin1.beforeParse = true;
},
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) {
@ -17,7 +32,8 @@ exports.defineTags = function(dictionary) {
};
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;
})
};
}
};

View File

@ -1,12 +1,29 @@
var myGlobal = require('jsdoc/util/global');
myGlobal.jsdocPluginsTest.plugin2 = {};
exports.handlers = {
fileBegin: jasmine.createSpy('fileBegin'),
beforeParse: jasmine.createSpy('beforeParse'),
jsdocCommentFound: jasmine.createSpy('jsdocCommentFound'),
symbolFound: jasmine.createSpy('symbolFound'),
newDoclet: jasmine.createSpy('newDoclet'),
fileComplete: jasmine.createSpy('fileComplete')
fileBegin: function() {
myGlobal.jsdocPluginsTest.plugin2.fileBegin = true;
},
beforeParse: function() {
myGlobal.jsdocPluginsTest.plugin2.beforeParse = true;
},
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 = {
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() {
var os = require('os');
@ -24,6 +24,11 @@ describe('jsdoc/path', 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() {
beforeEach(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 */
describe("plugins", function() {
var myGlobal = require('jsdoc/util/global');
myGlobal.jsdocPluginsTest = myGlobal.jsdocPluginsTest || {};
require('jsdoc/plugins').installPlugins(['test/fixtures/testPlugin1',
'test/fixtures/testPlugin2'], app.jsdoc.parser);
var plugin1 = require('test/fixtures/testPlugin1'),
plugin2 = require('test/fixtures/testPlugin2'),
docSet;
docSet = jasmine.getDocSetFromFile("test/fixtures/plugins.js", app.jsdoc.parser);
var docSet = jasmine.getDocSetFromFile("test/fixtures/plugins.js", app.jsdoc.parser);
it("should fire the plugin's event handlers", function() {
expect(plugin1.handlers.fileBegin).toHaveBeenCalled();
expect(plugin1.handlers.beforeParse).toHaveBeenCalled();
expect(plugin1.handlers.jsdocCommentFound).toHaveBeenCalled();
expect(plugin1.handlers.symbolFound).toHaveBeenCalled();
expect(plugin1.handlers.newDoclet).toHaveBeenCalled();
expect(plugin1.handlers.fileComplete).toHaveBeenCalled();
expect(myGlobal.jsdocPluginsTest.plugin1.fileBegin).toBeDefined();
expect(myGlobal.jsdocPluginsTest.plugin1.fileBegin).toEqual(true);
expect(myGlobal.jsdocPluginsTest.plugin1.beforeParse).toBeDefined();
expect(myGlobal.jsdocPluginsTest.plugin1.beforeParse).toEqual(true);
expect(myGlobal.jsdocPluginsTest.plugin1.jsdocCommentFound).toBeDefined();
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(plugin2.handlers.beforeParse).toHaveBeenCalled();
expect(plugin2.handlers.jsdocCommentFound).toHaveBeenCalled();
expect(plugin2.handlers.symbolFound).toHaveBeenCalled();
expect(plugin2.handlers.newDoclet).toHaveBeenCalled();
expect(plugin2.handlers.fileComplete).toHaveBeenCalled();
expect(myGlobal.jsdocPluginsTest.plugin2.fileBegin).toBeDefined();
expect(myGlobal.jsdocPluginsTest.plugin2.fileBegin).toEqual(true);
expect(myGlobal.jsdocPluginsTest.plugin2.beforeParse).toBeDefined();
expect(myGlobal.jsdocPluginsTest.plugin2.beforeParse).toEqual(true);
expect(myGlobal.jsdocPluginsTest.plugin2.jsdocCommentFound).toBeDefined();
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() {
@ -34,10 +45,11 @@ describe("plugins", 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() {
expect(plugin2.nodeVisitor.visitNode).not.toHaveBeenCalled();
expect(myGlobal.jsdocPluginsTest.plugin2.visitNode).not.toBeDefined();
});
});
});