From e5be860cc455c8147d5faf1d572ffb94912fb6f4 Mon Sep 17 00:00:00 2001 From: Jeff Williams Date: Sat, 6 Apr 2013 16:53:20 -0700 Subject: [PATCH] support modules that export a single non-constructor function (#384) --- lib/jsdoc/src/handlers.js | 18 +++++++-- lib/jsdoc/util/templateHelper.js | 37 +++++++++++++++--- templates/default/publish.js | 39 ++++++++++++++----- test/fixtures/moduleisfunction.js | 10 +++++ .../documentation/moduleisconstructor.js | 5 +++ test/specs/documentation/moduleisfunction.js | 23 +++++++++++ test/specs/jsdoc/util/templateHelper.js | 15 ++++++- 7 files changed, 127 insertions(+), 20 deletions(-) create mode 100644 test/fixtures/moduleisfunction.js create mode 100644 test/specs/documentation/moduleisconstructor.js create mode 100644 test/specs/documentation/moduleisfunction.js diff --git a/lib/jsdoc/src/handlers.js b/lib/jsdoc/src/handlers.js index 931b239a..f9ecfdd8 100644 --- a/lib/jsdoc/src/handlers.js +++ b/lib/jsdoc/src/handlers.js @@ -54,6 +54,7 @@ exports.attachTo = function(parser) { } }); + // TODO: for clarity, decompose into smaller functions function newSymbolDoclet(docletSrc, e) { var memberofName = null, newDoclet = getNewDoclet(parser, docletSrc, e); @@ -132,9 +133,16 @@ exports.attachTo = function(parser) { } } else { - if (currentModule) { - if (!newDoclet.scope){newDoclet.addTag( 'inner');} - if (!newDoclet.memberof && newDoclet.scope !== 'global'){newDoclet.addTag( 'memberof', currentModule);} + // add @inner and @memberof tags unless the current module exports only this symbol + if (currentModule && currentModule !== newDoclet.name) { + // add @inner unless the current module exports only this symbol + if (!newDoclet.scope) { + newDoclet.addTag('inner'); + } + + if (!newDoclet.memberof && newDoclet.scope !== 'global') { + newDoclet.addTag('memberof', currentModule); + } } } } @@ -147,7 +155,9 @@ exports.attachTo = function(parser) { //resolveProperties(newDoclet); - if (!newDoclet.memberof) { + // set the scope to global unless a) the doclet is a memberof something or b) the current + // module exports only this symbol + if (!newDoclet.memberof && currentModule !== newDoclet.name) { newDoclet.scope = 'global'; } diff --git a/lib/jsdoc/util/templateHelper.js b/lib/jsdoc/util/templateHelper.js index 87544a90..82b09250 100644 --- a/lib/jsdoc/util/templateHelper.js +++ b/lib/jsdoc/util/templateHelper.js @@ -186,6 +186,20 @@ var find = exports.find = function(data, spec) { return data(spec).get(); }; +/** + * Check whether a symbol is a function and 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 a function and is the only symbol exported by a module; + * otherwise, `false`. + */ +function isModuleFunction(doclet) { + return doclet.longname && doclet.longname === doclet.name && + doclet.longname.indexOf('module:') === 0 && doclet.kind === 'function'; +} + /** * Retrieve all of the following types of members from a set of doclets: * @@ -201,7 +215,7 @@ var find = exports.find = function(data, spec) { * `events`, and `namespaces` properties. Each property contains an array of objects. */ exports.getMembers = function(data) { - return { + var members = { classes: find( data, {kind: 'class'} ), externals: find( data, {kind: 'external'} ), events: find( data, {kind: 'event'} ), @@ -213,6 +227,17 @@ exports.getMembers = function(data) { modules: find( data, {kind: 'module'} ), namespaces: find( data, {kind: 'namespace'} ) }; + + // functions that are also modules (as in "module.exports = function() {};") are not globals + members.globals = members.globals.filter(function(doclet) { + if ( isModuleFunction(doclet) ) { + return false; + } + + return true; + }); + + return members; }; /** @@ -565,16 +590,16 @@ exports.createLink = function(doclet) { var longname; var filename; - if (containers.indexOf(doclet.kind) < 0) { + if ( containers.indexOf(doclet.kind) !== -1 || isModuleFunction(doclet) ) { + longname = doclet.longname; + url = getFilename(longname); + } + else { longname = doclet.longname; filename = getFilename(doclet.memberof || exports.globalName); url = filename + '#' + getNamespace(doclet.kind) + doclet.name; } - else { - longname = doclet.longname; - url = getFilename(longname); - } return url; }; diff --git a/templates/default/publish.js b/templates/default/publish.js index 3ea89bb7..9c80a22b 100644 --- a/templates/default/publish.js +++ b/templates/default/publish.js @@ -149,6 +149,33 @@ function generateSourceFiles(sourceFiles) { }); } +/** + * Look for classes or functions with the same name as modules (which indicates that the module + * exports only that class or function), then attach the classes or functions to the `module` + * property of the appropriate module doclets. The name of each class or function is also updated + * for display purposes. This function mutates the original arrays. + * + * @private + * @param {Array.} doclets - The array of classes and functions to + * check. + * @param {Array.} modules - The array of module doclets to search. + */ +function attachModuleSymbols(doclets, modules) { + var symbols = {}; + + // build a lookup table + doclets.forEach(function(symbol) { + symbols[symbol.longname] = symbol; + }); + + return modules.map(function(module) { + if (symbols[module.longname]) { + module.module = symbols[module.longname]; + module.module.name = module.module.name.replace('module:', 'require("') + '")'; + } + }); +} + /** * Create the navigation sidebar. * @param {object} members The members that will be used to create the sidebar. @@ -193,22 +220,14 @@ function buildNav(members) { } if (members.classes.length) { - members.classes.forEach(function(c) { - var moduleSameName = find({kind: 'module', longname: c.longname}); - if (moduleSameName.length) { - c.name = c.name.replace('module:', 'require("')+'")'; - moduleSameName[0].module = c; - } - if ( !hasOwnProp.call(seen, c.longname) ) { - hasClassList = true; classNav += '
  • '+linkto(c.longname, c.name)+'
  • '; } seen[c.longname] = true; }); - if (hasClassList) { + if (classNav !== '') { nav += '

    Classes

      '; nav += classNav; nav += '
    '; @@ -427,6 +446,8 @@ exports.publish = function(taffyData, opts, tutorials) { // once for all view.nav = buildNav(members); + attachModuleSymbols( find({ kind: ['class', 'function'], longname: {left: 'module:'} }), + members.modules ); // only output pretty-printed source files if requested; do this before generating any other // pages, so the other pages can link to the source files diff --git a/test/fixtures/moduleisfunction.js b/test/fixtures/moduleisfunction.js new file mode 100644 index 00000000..21c29bda --- /dev/null +++ b/test/fixtures/moduleisfunction.js @@ -0,0 +1,10 @@ +/** + * This is a module called foo. + * @module foo + */ + +/** + * The module exports a single function. + * @param {string} bar + */ +module.exports = function(bar) {}; diff --git a/test/specs/documentation/moduleisconstructor.js b/test/specs/documentation/moduleisconstructor.js new file mode 100644 index 00000000..6b0cbe61 --- /dev/null +++ b/test/specs/documentation/moduleisconstructor.js @@ -0,0 +1,5 @@ +/*global describe: true, expect: true, it: true, xdescribe: true */ + +xdescribe('module that exports a constructor', function() { + // TODO +}); \ No newline at end of file diff --git a/test/specs/documentation/moduleisfunction.js b/test/specs/documentation/moduleisfunction.js new file mode 100644 index 00000000..1aeb783d --- /dev/null +++ b/test/specs/documentation/moduleisfunction.js @@ -0,0 +1,23 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ + +describe('module that exports a function that is not a constructor', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/moduleisfunction.js'); + var functions = docSet.doclets.filter(function(doclet) { + return doclet.kind === 'function'; + }); + + it('should include one doclet whose kind is "function"', function() { + expect(functions.length).toBe(1); + expect(functions[0].kind).toBe('function'); + }); + + describe('function doclet', function() { + it('should not include a "scope" property', function() { + expect(functions[0].scope).not.toBeDefined(); + }); + + it('should not include a "memberof" property', function() { + expect(functions[0].memberof).not.toBeDefined(); + }); + }); +}); diff --git a/test/specs/jsdoc/util/templateHelper.js b/test/specs/jsdoc/util/templateHelper.js index 7d7b22fb..e0b23a96 100644 --- a/test/specs/jsdoc/util/templateHelper.js +++ b/test/specs/jsdoc/util/templateHelper.js @@ -379,6 +379,7 @@ describe("jsdoc/util/templateHelper", function() { {kind: 'constant'}, // global {kind: 'typedef'}, // global {kind: 'constant', memberof: 'module:one/two'}, // not global + {kind: 'function', name: 'module:foo', longname: 'module:foo'} // not global ]; var array = classes.concat(externals.concat(events.concat(mixins.concat(modules.concat(namespaces.concat(misc)))))); var data = require('taffydb').taffy(array); @@ -439,7 +440,7 @@ describe("jsdoc/util/templateHelper", function() { }); it("globals are detected", function() { - compareObjectArrays(misc.slice(0, -1), members.globals); + compareObjectArrays(misc.slice(0, -2), members.globals); }); }); @@ -1259,6 +1260,18 @@ describe("jsdoc/util/templateHelper", function() { expect(url).toEqual('_.html#"*foo"'); }); + + it('should create a url for a function that is the only symbol exported by a module.', + function() { + var mockDoclet = { + kind: 'function', + longname: 'module:bar', + name: 'module:bar' + }; + var url = helper.createLink(mockDoclet); + + expect(url).toEqual('module-bar.html'); + }); }); describe("resolveAuthorLinks", function() {