optionally scan tutorials directory recursively (#712)

Squashed commit of the following:

commit 5be2cb3103521d2ca1a14c24d4ccd776c1f2a5d9
Author: Jeff Williams <jeffrey.l.williams@gmail.com>
Date:   Mon Nov 3 14:29:29 2014 -0800

    make tutorials respect the --recurse option; refactor modules to facilitate testing; improve tests

commit 9af57751385439ceee3fac8f698a747a745c7c2b
Merge: 5399745 97a8ab0
Author: Jeff Williams <jeffrey.l.williams@gmail.com>
Date:   Sat Nov 1 19:00:43 2014 -0700

    Merge remote-tracking branch 'koalazak/master' into 712

    Conflicts:
    	lib/jsdoc/opts/args.js

commit 97a8ab000b567c220525cc179c2f45b626236933
Author: zak <zak@ultra>
Date:   Mon Jul 21 15:28:20 2014 -0300

    Removed command-line option -U to recursive. Now is default. Added tests.

commit a79c9c9dac4eeb784e3f22b1da073c2af5b014cc
Author: zak <zak@ultra>
Date:   Thu Jul 17 13:28:38 2014 -0300

    Recurse 5 levels

commit 349d10e528d6ba797fd745e31e1e358ddcf26857
Author: koalazak <facu@Cacahuate.local>
Date:   Wed Jul 16 22:30:41 2014 -0300

    Travis CI ready ready

commit ffde2bf4bdc2bd0ba2daa20a58540e4a2dd099e8
Author: koalazak <facu@Cacahuate.local>
Date:   Wed Jul 16 22:22:56 2014 -0300

    Travis CI ready

commit 3e439151fb58d530abe294f1cc499e5fab7b8fe8
Author: koalazak <facu@Cacahuate.local>
Date:   Wed Jul 16 21:47:22 2014 -0300

    Optionally scan tutorials directory recursively

    I do not want to have a directory of tutorials. I need to have the
    tutorials distributed throughout the project. I have one in each
    "package" folder.
This commit is contained in:
Jeff Williams 2014-11-03 14:30:21 -08:00
parent 5399745a97
commit 06acc6697a
6 changed files with 244 additions and 140 deletions

View File

@ -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');

View File

@ -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}
*/

View File

@ -1,4 +1,3 @@
/*global env: true */
/**
@overview
@author Rafa&#322; Wrzeszcz <rafal.wrzeszcz@wrzasq.pl>
@ -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);
}
});
}
});
};

View File

@ -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();
});
});
});
});

View File

@ -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');
});
});
});

View File

@ -0,0 +1 @@
# test_recursive.md