Merge pull request #334 from mathematicalcoffee/feature-multi-tutorial-json

#332: Allow master configuration file for all tutorials
This commit is contained in:
Jeff Williams 2013-02-13 08:19:12 -08:00
commit ed56da0472
14 changed files with 293 additions and 27 deletions

View File

@ -11,20 +11,80 @@
var tutorial = require('jsdoc/tutorial'),
fs = require('jsdoc/fs'),
error = require('jsdoc/util/error'),
path = require('path'),
hasOwnProp = Object.prototype.hasOwnProperty,
conf = {},
tutorials = {},
finder = /^(.*)\.(x(?:ht)?ml|html?|md|markdown|json)$/i;
/** checks if `conf` is the metadata for a single tutorial.
* A tutorial's metadata has a property 'title' and/or a property 'children'.
* @param {object} json - the object we want to test (typically from JSON.parse)
* @returns {boolean} whether `json` could be the metadata for a tutorial.
*/
function isTutorialJSON(json) {
// if conf.title exists or conf.children exists, it is metadata for a tutorial
return (json.hasOwnProperty('title') || json.hasOwnProperty('children'));
}
/** 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
* opposed to an array of tutorial names.
*
* Recurses as necessary to ensure all tutorials are added.
*
* @param {string} name - if `meta` is a configuration for a single tutorial,
* this is that tutorial's name.
* @param {object} meta - object that contains tutorial information.
* Can either be for a single tutorial, or for multiple
* (where each key in `meta` is the tutorial name and each
* value is the information for a single tutorial).
* Additionally, a tutorial's 'children' property may
* either be an array of strings (names of the child tutorials),
* OR an object giving the configuration for the child tutorials.
*/
function addTutorialConf(name, meta) {
var names, i;
if (isTutorialJSON(meta)) {
// if the children are themselves tutorial defintions as opposed to an
// array of strings, add each child.
if (meta.hasOwnProperty('children') && !Array.isArray(meta.children)) {
names = Object.keys(meta.children);
for (i = 0; i < names.length; ++i) {
addTutorialConf(names[i], meta.children[names[i]]);
}
// replace with an array of names.
meta.children = names;
}
// check if the tutorial has already been defined...
if (conf.hasOwnProperty(name)) {
error.handle(new Error("Tutorial " + name + "'s metadata is defined multiple times, only the first will be used."));
} else {
conf[name] = meta;
}
} else {
// it's an object of tutorials, the keys are th etutorial names.
names = Object.keys(meta);
for (i = 0; i < names.length; ++i) {
addTutorialConf(names[i], meta[names[i]]);
}
}
}
/** Adds new tutorial.
@param {tutorial.Tutorial} current - New tutorial.
*/
exports.addTutorial = function(current) {
tutorials[current.name] = current;
if (tutorials.hasOwnProperty(current.name)) {
error.handle(new Error("Tutorial with name " + current.name + " exists more than once, not adding (same name, different file extensions?)"));
} else {
tutorials[current.name] = current;
// default temporary parent
current.setParent(exports.root);
// default temporary parent
current.setParent(exports.root);
}
};
/** Root tutorial.
@ -77,7 +137,8 @@ exports.load = function(_path) {
// configuration file
case 'json':
conf[name] = JSON.parse(content);
var meta = JSON.parse(content);
addTutorialConf(name, meta);
// don't add this as a tutorial
return;
@ -117,7 +178,7 @@ exports.resolve = function() {
if (item.children) {
item.children.forEach(function(child) {
if (!(child in tutorials)) {
require('jsdoc/util/error').handle( new Error("Missing child tutorial: " + child) );
error.handle( new Error("Missing child tutorial: " + child) );
}
else {
tutorials[child].setParent(current);

View File

@ -1,33 +1,214 @@
/*global afterEach: true, describe: true, env: true, expect: true, it: true */
describe("jsdoc/tutorial/resolver", function() {
/*jshint evil: true */
// TODO: more tests
var resolver = require('jsdoc/tutorial/resolver'),
tutorial = require('jsdoc/tutorial'),
lenient = !!env.opts.lenient,
log = eval(console.log);
function missingTutorial() {
resolver.load(__dirname + "/test/tutorials/incomplete");
/*jshint evil: true */
it("should exist", function() {
expect(resolver).toBeDefined();
expect(typeof resolver).toEqual('object');
});
it("should export a 'addTutorial' function", function() {
expect(resolver.addTutorial).toBeDefined();
expect(typeof resolver.addTutorial).toEqual("function");
});
it("should export a 'load' function", function() {
expect(resolver.load).toBeDefined();
expect(typeof resolver.load).toEqual("function");
});
it("should export a 'resolve' function", function() {
expect(resolver.resolve).toBeDefined();
expect(typeof resolver.resolve).toEqual("function");
});
it("should export a 'root' tutorial", function() {
expect(resolver.root).toBeDefined();
expect(resolver.root instanceof tutorial.Tutorial).toEqual(true);
});
it("exported 'root' tutorial should export a 'getByName' function", function() {
expect(resolver.root.getByName).toBeDefined();
expect(typeof resolver.root.getByName).toEqual("function");
});
// note: every time we addTutorial or run the resolver, we are *adding*
// to the root tutorial.
// addTutorial
var tute = new tutorial.Tutorial('myTutorial', '', tutorial.TYPES.HTML);
resolver.addTutorial(tute);
describe("addTutorial", function() {
it("should add a default parent of the root tutorial", function() {
expect(tute.parent).toEqual(resolver.root);
});
it("should be added to the root tutorial as a child", function() {
expect(resolver.root.children[0]).toEqual(tute);
});
});
// root.getByName
describe("root.getByName", function() {
it("can retrieve tutorials by name", function() {
expect(resolver.root.getByName('myTutorial')).toEqual(tute);
});
});
// load
resolver.load(__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');
describe("load", function() {
it("all tutorials are added, initially as top-level tutorials", function() {
// check they were added
expect(test).toBeDefined();
expect(test2).toBeDefined();
expect(test3).toBeDefined();
expect(test4).toBeDefined();
expect(test6).toBeDefined();
// check they are top-level in resolver.root
expect(childNames.indexOf('test')).not.toEqual(-1);
expect(childNames.indexOf('test2')).not.toEqual(-1);
expect(childNames.indexOf('test3')).not.toEqual(-1);
expect(childNames.indexOf('test4')).not.toEqual(-1);
expect(childNames.indexOf('test6')).not.toEqual(-1);
});
it("non-tutorials are skipped", function() {
expect(resolver.root.getByName('multple')).toBeUndefined();
expect(resolver.root.getByName('test5')).toBeUndefined();
});
it("tutorial types are determined correctly", function() {
// test.html, test2.markdown, test3.html, test4.md, test6.xml
expect(test.type).toEqual(tutorial.TYPES.HTML);
expect(test2.type).toEqual(tutorial.TYPES.MARKDOWN);
expect(test3.type).toEqual(tutorial.TYPES.HTML);
expect(test4.type).toEqual(tutorial.TYPES.MARKDOWN);
expect(test6.type).toEqual(tutorial.TYPES.HTML);
});
});
// resolve
// myTutorial
// test
// |- test2
// |- test6
// |- test3
// |- test4
describe("resolve", function() {
resolver.resolve();
}
it("hierarchy is resolved properly no matter how the children property is defined", function() {
// root has child 'test'
expect(resolver.root.children.length).toEqual(2);
expect(resolver.root.children.indexOf(test)).not.toEqual(-1);
expect(test.parent).toEqual(resolver.root);
afterEach(function() {
env.opts.lenient = lenient;
console.log = log;
// test has child 'test2'
expect(test.children.length).toEqual(1);
expect(test.children[0]).toEqual(test2);
expect(test2.parent).toEqual(test);
// test2 has children test3, test6
expect(test2.children.length).toEqual(2);
expect(test2.children.indexOf(test3)).not.toEqual(-1);
expect(test2.children.indexOf(test6)).not.toEqual(-1);
expect(test3.parent).toEqual(test2);
expect(test6.parent).toEqual(test2);
// test3 has child test4
expect(test3.children.length).toEqual(1);
expect(test3.children[0]).toEqual(test4);
expect(test4.parent).toEqual(test3);
});
it("tutorials without configuration files have titles matching filenames", function() {
// test6.xml didn't have a metadata
expect(test6.title).toEqual('test6');
});
it("tutorials with configuration files have titles matching filenames", function() {
// test.json had info for just test.json
expect(test.title).toEqual("Test tutorial");
});
it("multiple tutorials can appear in a configuration file", function() {
expect(test2.title).toEqual("Test 2");
expect(test3.title).toEqual("Test 3");
expect(test4.title).toEqual("Test 4");
});
});
it("throws an exception for missing tutorials if the lenient option is not enabled", function() {
env.opts.lenient = false;
// error reporting.
describe("Error reporting", function() {
// Tests for error reporting.
function missingTutorial() {
resolver.load(__dirname + "/test/tutorials/incomplete");
resolver.resolve();
}
function duplicateNamedTutorials() {
// can't add a tutorial if another with its name has already been added
resolver.addTutorial(tute);
}
function duplicateDefinedTutorials() {
// can't have a tutorial's metadata defined twice in .json files
resolver.load(__dirname + "/test/tutorials/duplicateDefined");
resolver.resolve();
}
expect(missingTutorial).toThrow();
afterEach(function() {
env.opts.lenient = lenient;
console.log = log;
});
it("throws an exception for missing tutorials if the lenient option is not enabled", function() {
env.opts.lenient = false;
expect(missingTutorial).toThrow();
});
it("doesn't throw an exception for missing tutorials if the lenient option is enabled", function() {
console.log = function() {};
env.opts.lenient = true;
expect(missingTutorial).not.toThrow();
});
it("throws an exception for duplicate-named tutorials (e.g. test.md, test.html) if the lenient option is not enabled", function() {
env.opts.lenient = false;
expect(duplicateNamedTutorials).toThrow();
});
it("doesn't throw an exception for duplicate-named tutorials (e.g. test.md, test.html) if the lenient option is not enabled", function() {
console.log = function() {};
env.opts.lenient = true;
expect(duplicateNamedTutorials).not.toThrow();
});
it("throws an exception for tutorials defined twice in .jsons if the lenient option is not enabled", function() {
env.opts.lenient = false;
expect(duplicateDefinedTutorials).toThrow();
});
it("doesn't throw an exception for tutorials defined twice in .jsons if the lenient option is not enabled", function() {
console.log = function() {};
env.opts.lenient = true;
expect(duplicateDefinedTutorials).not.toThrow();
});
});
it("doesn't throw an exception for missing tutorials if the lenient option is enabled", function() {
console.log = function() {};
env.opts.lenient = true;
expect(missingTutorial).not.toThrow();
});
});
});

View File

@ -0,0 +1 @@
<h1>tutorial ASDF</h1>

View File

@ -0,0 +1 @@
{"title": "Conflicting title"}

View File

@ -0,0 +1,5 @@
{
"asdf": {
"title": "Tutorial Asdf"
}
}

View File

@ -0,0 +1 @@
{"title": "missing child tutorial", "children": ["child"]}

View File

@ -1 +0,0 @@
{"title": "missing child tutorial", "children": ["test2"]}

View File

@ -0,0 +1,12 @@
{
"test2": {
"title": "Test 2",
"children": ["test3", "test6"]
},
"test3": {
"title": "Test 3",
"children": {
"test4": {"title": "Test 4"}
}
}
}

View File

@ -1 +0,0 @@
{"title": "Test 2"}

View File

@ -0,0 +1,3 @@
<h1>Test3.html</h1>
<p>{@link Test}</p>

View File

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

View File

@ -0,0 +1 @@
Should not be included as a tutorial.

View File

@ -0,0 +1 @@
<h1>test 6 - has no metadata</h1>