diff --git a/lib/jsdoc/opts/args.js b/lib/jsdoc/opts/args.js index 4748680d..dc1913b2 100644 --- a/lib/jsdoc/opts/args.js +++ b/lib/jsdoc/opts/args.js @@ -80,9 +80,9 @@ 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('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'); diff --git a/lib/jsdoc/tutorial.js b/lib/jsdoc/tutorial.js index 15859c40..304b283c 100644 --- a/lib/jsdoc/tutorial.js +++ b/lib/jsdoc/tutorial.js @@ -6,6 +6,9 @@ '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. @@ -99,6 +102,35 @@ exports.Tutorial.prototype.parse = function() { } }; +/** + * @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} */ diff --git a/lib/jsdoc/tutorial/resolver.js b/lib/jsdoc/tutorial/resolver.js index 7c446e25..610405e1 100644 --- a/lib/jsdoc/tutorial/resolver.js +++ b/lib/jsdoc/tutorial/resolver.js @@ -1,4 +1,3 @@ -/*global env: true */ /** @overview @author Rafał Wrzeszcz @@ -17,9 +16,9 @@ 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; -var tutorials = {}; /** checks if `conf` is the metadata for a single tutorial. * A tutorial's metadata has a property 'title' and/or a property 'children'. @@ -31,6 +30,12 @@ function isTutorialJSON(json) { 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 @@ -49,13 +54,16 @@ function isTutorialJSON(json) { * OR an object giving the configuration for the child tutorials. */ function addTutorialConf(name, meta) { - var names, i; + 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; i < names.length; ++i) { + for (i = 0, l = names.length; i < l; ++i) { addTutorialConf(names[i], meta.children[names[i]]); } // replace with an array of names. @@ -68,51 +76,40 @@ function addTutorialConf(name, meta) { conf[name] = meta; } } else { - // it's an object of tutorials, the keys are th etutorial names. + // keys are tutorial names, values are `Tutorial` instances names = Object.keys(meta); - for (i = 0; i < names.length; ++i) { + for (i = 0, l = names.length; i < l; ++i) { addTutorialConf(names[i], meta[names[i]]); } } } -/** Adds new tutorial. - @param {tutorial.Tutorial} current - New tutorial. +/** + * Add a tutorial. + * @param {module:jsdoc/tutorial.Tutorial} current - Tutorial to add. */ exports.addTutorial = function(current) { - if (hasOwnProp.call(tutorials, current.name)) { + 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 { - tutorials[current.name] = current; - - // default temporary parent + // by default, the root tutorial is the parent current.setParent(exports.root); + + exports.root._addTutorial(current); } }; -/** Root tutorial. - @type tutorial.Tutorial +/** + * Load tutorials from the given path. + * @param {string} filepath - Tutorials directory. */ -exports.root = new tutorial.Tutorial('', ''); - -/** Additional instance method for root node. - @param {string} name - Tutorial name. - @return {tutorial.Tutorial} Tutorial instance. - */ -exports.root.getByName = function(name) { - return hasOwnProp.call(tutorials, name) && tutorials[name]; -}; - -/** Load tutorials from given path. - @param {string} _path - Tutorials directory. - */ -exports.load = function(_path) { - var match, - type, - name, - content, - current, - files = fs.ls(_path); +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) { @@ -121,7 +118,7 @@ exports.load = function(_path) { // any filetype that can apply to tutorials if (match) { name = path.basename(match[1]); - content = fs.readFileSync(file, env.opts.encoding); + content = fs.readFileSync(file, global.env.opts.encoding); switch (match[2].toLowerCase()) { // HTML type @@ -160,34 +157,36 @@ exports.load = function(_path) { /** Resolves hierarchical structure. */ exports.resolve = function() { - var item, - current; - for (var name in conf) { - if ( hasOwnProp.call(conf, name) ) { - // TODO: should we complain about this? - if (!hasOwnProp.call(tutorials, name)) { - continue; - } + var item; + var current; - item = conf[name]; - current = tutorials[name]; + Object.keys(conf).forEach(function(name) { + current = exports.root.getByName(name); - // set title - if (item.title) { - current.title = item.title; - } - - // add children - if (item.children) { - item.children.forEach(function(child) { - if (!hasOwnProp.call(tutorials, child)) { - logger.error('Missing child tutorial: %s', child); - } - else { - tutorials[child].setParent(current); - } - }); - } + // 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); + } + }); + } + }); }; diff --git a/test/specs/jsdoc/tutorial.js b/test/specs/jsdoc/tutorial.js index 740800ad..0ab8d98a 100644 --- a/test/specs/jsdoc/tutorial.js +++ b/test/specs/jsdoc/tutorial.js @@ -1,4 +1,5 @@ -/*global afterEach, beforeEach, describe, expect, it */ +'use strict'; + describe('jsdoc/tutorial', function() { var tutorial = require('jsdoc/tutorial'); @@ -24,6 +25,11 @@ describe('jsdoc/tutorial', function() { expect(typeof tutorial.Tutorial).toBe('function'); }); + it('should export a RootTutorial function', function() { + expect(tutorial.RootTutorial).toBeDefined(); + expect(typeof tutorial.RootTutorial).toBe('function'); + }); + it('should export a TYPES object', function() { expect(tutorial.TYPES).toBeDefined(); expect(typeof tutorial.TYPES).toBe('object'); @@ -239,4 +245,40 @@ describe('jsdoc/tutorial', function() { }); }); }); + + describe('RootTutorial', function() { + it('should inherit from Tutorial', function() { + var root = new tutorial.RootTutorial(); + + expect(root instanceof tutorial.Tutorial).toBe(true); + }); + + it('should have a "getByName" method', function() { + expect(tutorial.RootTutorial.prototype.getByName).toBeDefined(); + expect(typeof tutorial.RootTutorial.prototype.getByName).toBe('function'); + }); + + describe('getByName', function() { + var root; + + beforeEach(function() { + root = new tutorial.RootTutorial(); + }); + + it('can retrieve tutorials by name', function() { + var myTutorial = new tutorial.Tutorial('myTutorial', '', tutorial.TYPES.HTML); + root._addTutorial(myTutorial); + + expect(root.getByName('myTutorial')).toBe(myTutorial); + }); + + it('returns nothing for non-existent tutorials', function() { + expect(root.getByName('asdf')).toBeFalsy(); + }); + + it('uses hasOwnProperty when it checks for the tutorial', function() { + expect(root.getByName('prototype')).toBeFalsy(); + }); + }); + }); }); diff --git a/test/specs/jsdoc/tutorial/resolver.js b/test/specs/jsdoc/tutorial/resolver.js index 569b4667..d2acabed 100644 --- a/test/specs/jsdoc/tutorial/resolver.js +++ b/test/specs/jsdoc/tutorial/resolver.js @@ -1,84 +1,113 @@ -/*global beforeEach, describe, env, expect, it, spyOn */ -describe("jsdoc/tutorial/resolver", function() { +'use strict'; + +describe('jsdoc/tutorial/resolver', function() { var logger = require('jsdoc/util/logger'); var resolver = require('jsdoc/tutorial/resolver'); var tutorial = require('jsdoc/tutorial'); - it("should exist", function() { + var childNames; + var constr; + var test; + var test2; + var test3; + var test4; + var test6; + + function resetRootTutorial() { + resolver.root = new tutorial.RootTutorial(); + } + + function loadTutorials() { + resetRootTutorial(); + + resolver.load(global.env.dirname + '/test/tutorials/tutorials'); + + childNames = resolver.root.children.map(function (t) { return t.name; }); + test = resolver.root.getByName('test'); + test2 = resolver.root.getByName('test2'); + test3 = resolver.root.getByName('test3'); + test4 = resolver.root.getByName('test4'); + test6 = resolver.root.getByName('test6'); + constr = resolver.root.getByName('constructor'); + } + + it('should exist', function() { expect(resolver).toBeDefined(); expect(typeof resolver).toBe('object'); }); - it("should export a 'addTutorial' function", function() { + it('should export an "addTutorial" function', function() { expect(resolver.addTutorial).toBeDefined(); - expect(typeof resolver.addTutorial).toBe("function"); + expect(typeof resolver.addTutorial).toBe('function'); }); - it("should export a 'load' function", function() { + it('should export a "load" function', function() { expect(resolver.load).toBeDefined(); - expect(typeof resolver.load).toBe("function"); + expect(typeof resolver.load).toBe('function'); }); - it("should export a 'resolve' function", function() { + it('should export a "resolve" function', function() { expect(resolver.resolve).toBeDefined(); - expect(typeof resolver.resolve).toBe("function"); + expect(typeof resolver.resolve).toBe('function'); }); - it("should export a 'root' tutorial", function() { + it('should export a "root" tutorial', function() { expect(resolver.root).toBeDefined(); - expect(resolver.root instanceof tutorial.Tutorial).toBe(true); + expect(resolver.root instanceof tutorial.RootTutorial).toBe(true); }); - it("exported 'root' tutorial should export a 'getByName' function", function() { + it('exported "root" tutorial should export a "getByName" function', function() { expect(resolver.root.getByName).toBeDefined(); - expect(typeof resolver.root.getByName).toBe("function"); + expect(typeof resolver.root.getByName).toBe('function'); }); // note: every time we addTutorial or run the resolver, we are *adding* // to the root tutorial. + describe('addTutorial', function() { + var tute; - // addTutorial - var tute = new tutorial.Tutorial('myTutorial', '', tutorial.TYPES.HTML); - resolver.addTutorial(tute); - describe("addTutorial", function() { + beforeEach(function() { + resetRootTutorial(); - it("should add a default parent of the root tutorial", function() { + tute = new tutorial.Tutorial('myTutorial', '', tutorial.TYPES.HTML); + resolver.addTutorial(tute); + }); + + afterEach(resetRootTutorial); + + it('should add a default parent of the root tutorial', function() { expect(tute.parent).toBe(resolver.root); }); - it("should be added to the root tutorial as a child", function() { + it('should be added to the root tutorial as a child', function() { expect(resolver.root.children).toContain(tute); }); }); - // root.getByName - describe("root.getByName", function() { - it("can retrieve tutorials by name", function() { - expect(resolver.root.getByName('myTutorial')).toBe(tute); + describe('load', function() { + beforeEach(loadTutorials); + + afterEach(resetRootTutorial); + + it('does not, by default, recurse into subdirectories', function() { + expect(resolver.root.getByName('test_recursive')).toBeFalsy(); }); - it("returns nothing for non-existent tutorials", function() { - expect(resolver.root.getByName('asdf')).toBeFalsy(); + it('recurses into subdirectories when the --recurse flag is used', function() { + var recurse = global.env.opts.recurse; + var recursiveTute; + + global.env.opts.recurse = true; + loadTutorials(); + recursiveTute = resolver.root.getByName('test_recursive'); + + expect(recursiveTute).toBeDefined(); + expect(recursiveTute instanceof tutorial.Tutorial).toBe(true); + + global.env.opts.recurse = recurse; }); - it("is careful with tutorials whose names are reserved keywords in JS", function() { - expect(resolver.root.getByName('prototype')).toBeFalsy(); - }); - }); - - // load - resolver.load(env.dirname + "/test/tutorials/tutorials"); - var childNames = resolver.root.children.map(function (t) { return t.name; }), - test = resolver.root.getByName('test'), - test2 = resolver.root.getByName('test2'), - test3 = resolver.root.getByName('test3'), - test4 = resolver.root.getByName('test4'), - test6 = resolver.root.getByName('test6'), - constr = resolver.root.getByName('constructor'); - - describe("load", function() { - - it("all tutorials are added, initially as top-level tutorials", function() { + it('all tutorials are added, initially as top-level tutorials', function() { // check they were added expect(test).toBeDefined(); expect(test2).toBeDefined(); @@ -94,16 +123,16 @@ describe("jsdoc/tutorial/resolver", function() { expect(childNames).toContain('test6'); }); - it("tutorials with names equal to reserved keywords in JS still function as expected", function() { + it('tutorials with names equal to reserved keywords in JS still function as expected', function() { expect(constr instanceof tutorial.Tutorial).toBe(true); }); - it("non-tutorials are skipped", function() { + it('non-tutorials are skipped', function() { expect(resolver.root.getByName('multiple')).toBeFalsy(); expect(resolver.root.getByName('test5')).toBeFalsy(); }); - it("tutorial types are determined correctly", function() { + it('tutorial types are determined correctly', function() { // test.html, test2.markdown, test3.html, test4.md, test6.xml expect(test.type).toBe(tutorial.TYPES.HTML); expect(test2.type).toBe(tutorial.TYPES.MARKDOWN); @@ -112,7 +141,6 @@ describe("jsdoc/tutorial/resolver", function() { expect(test6.type).toBe(tutorial.TYPES.HTML); expect(constr.type).toBe(tutorial.TYPES.MARKDOWN); }); - }); // resolve @@ -123,11 +151,19 @@ describe("jsdoc/tutorial/resolver", function() { // |- test6 // |- test3 // |- test4 - describe("resolve", function() { - resolver.resolve(); - it("hierarchy is resolved properly no matter how the children property is defined", function() { + describe('resolve', function() { + beforeEach(function() { + spyOn(logger, 'error'); + spyOn(logger, 'warn'); + loadTutorials(); + resolver.resolve(); + }); + + afterEach(resetRootTutorial); + + it('hierarchy is resolved properly no matter how the children property is defined', function() { // root has child 'test' - expect(resolver.root.children.length).toBe(3); + expect(resolver.root.children.length).toBe(2); expect(resolver.root.children).toContain(test); expect(resolver.root.children).toContain(constr); expect(test.parent).toBe(resolver.root); @@ -151,50 +187,44 @@ describe("jsdoc/tutorial/resolver", function() { expect(test4.parent).toBe(test3); }); - it("tutorials without configuration files have titles matching filenames", function() { + it('tutorials without configuration files have titles matching filenames', function() { // test6.xml didn't have a metadata expect(test6.title).toBe('test6'); }); - it("tutorials with configuration files have titles as specified in configuration", function() { + it('tutorials with configuration files have titles as specified in configuration', function() { // test.json had info for just test.json - expect(test.title).toBe("Test tutorial"); + expect(test.title).toBe('Test tutorial'); }); - it("multiple tutorials can appear in a configuration file", function() { - expect(test2.title).toBe("Test 2"); - expect(test3.title).toBe("Test 3"); - expect(test4.title).toBe("Test 4"); - }); - }); - - // error reporting. - describe("Error reporting", function() { - beforeEach(function() { - spyOn(logger, 'error'); - spyOn(logger, 'warn'); + it('multiple tutorials can appear in a configuration file', function() { + expect(test2.title).toBe('Test 2'); + expect(test3.title).toBe('Test 3'); + expect(test4.title).toBe('Test 4'); }); - it("logs an error for missing tutorials", function() { - resolver.load(env.dirname + "/test/tutorials/incomplete"); + it('logs an error for missing tutorials', function() { + resolver.load(global.env.dirname + '/test/tutorials/incomplete'); resolver.resolve(); expect(logger.error).toHaveBeenCalled(); }); - it("logs a warning for duplicate-named tutorials (e.g. test.md, test.html)", function() { + it('logs a warning for duplicate-named tutorials (e.g. test.md, test.html)', function() { + var tute = new tutorial.Tutorial('myTutorial', '', tutorial.TYPES.HTML); + resolver.addTutorial(tute); resolver.addTutorial(tute); expect(logger.warn).toHaveBeenCalled(); }); - it("logs an error for tutorials defined twice in .json files", function() { - // can't have a tutorial's metadata defined twice in .json files - resolver.load(env.dirname + "/test/tutorials/duplicateDefined"); + it('allows tutorials to be defined in a .json file and redefined in another; the last one wins', function() { + resolver.load(global.env.dirname + '/test/tutorials/duplicateDefined'); resolver.resolve(); - expect(logger.error).toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); + expect(logger.warn).toHaveBeenCalled(); + expect(resolver.root.getByName('asdf').title).toBe('Conflicting title'); }); }); - }); diff --git a/test/tutorials/tutorials/recursive/test_recursive.md b/test/tutorials/tutorials/recursive/test_recursive.md new file mode 100644 index 00000000..d87c0708 --- /dev/null +++ b/test/tutorials/tutorials/recursive/test_recursive.md @@ -0,0 +1 @@ +# test_recursive.md \ No newline at end of file