diff --git a/jsdoc.js b/jsdoc.js index b717479b..cdbed258 100644 --- a/jsdoc.js +++ b/jsdoc.js @@ -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.' ); } } } diff --git a/lib/jsdoc/path.js b/lib/jsdoc/path.js index 3f036632..9e3c4d48 100644 --- a/lib/jsdoc/path.js +++ b/lib/jsdoc/path.js @@ -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]; }); diff --git a/lib/jsdoc/plugins.js b/lib/jsdoc/plugins.js index 82cd6e18..da85646e 100644 --- a/lib/jsdoc/plugins.js +++ b/lib/jsdoc/plugins.js @@ -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); + } } } }; diff --git a/plugins/README.md b/plugins/README.md index ecc46784..49842476 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -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. diff --git a/test/fixtures/testPlugin1.js b/test/fixtures/testPlugin1.js index 9436152c..3b3e26b7 100644 --- a/test/fixtures/testPlugin1.js +++ b/test/fixtures/testPlugin1.js @@ -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; - }) -}; \ No newline at end of file + } +}; diff --git a/test/fixtures/testPlugin2.js b/test/fixtures/testPlugin2.js index 475999f7..bd7189f4 100644 --- a/test/fixtures/testPlugin2.js +++ b/test/fixtures/testPlugin2.js @@ -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") -}; \ No newline at end of file + visitNode: function() { + myGlobal.jsdocPluginsTest.plugin2.visitNode = true; + } +}; diff --git a/test/specs/jsdoc/path.js b/test/specs/jsdoc/path.js index 838337e7..90952b3b 100644 --- a/test/specs/jsdoc/path.js +++ b/test/specs/jsdoc/path.js @@ -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 + }); }); diff --git a/test/specs/plugins/plugins.js b/test/specs/plugins/plugins.js index 4df84396..a989be1e 100644 --- a/test/specs/plugins/plugins.js +++ b/test/specs/plugins/plugins.js @@ -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(); }); -}); \ No newline at end of file +});