feature(jsdoc): temporarily remove tutorial support

To be replaced by more flexible support for additional content.

BREAKING CHANGE: Removed support for tutorials.
This commit is contained in:
Jeff Williams 2020-05-25 11:06:23 -07:00
parent 3d7e3307a6
commit 65e2226eb0
33 changed files with 6 additions and 1316 deletions

View File

@ -96,12 +96,6 @@ module.exports = {
boolean: true,
description: 'Run all tests and exit.'
},
tutorials: {
alias: 'u',
description: 'The directory to search for tutorials.',
normalize: true,
requiresArg: true
},
verbose: {
boolean: true,
description: 'Log detailed information to the console.'

View File

@ -343,8 +343,6 @@ module.exports = (() => {
return Promise.resolve();
}
else {
cli.resolveTutorials();
return cli.generateDocs();
}
};
@ -355,21 +353,9 @@ module.exports = (() => {
return cli;
};
cli.resolveTutorials = () => {
const resolver = require('jsdoc/tutorial/resolver');
if (env.opts.tutorials) {
resolver.load(env.opts.tutorials);
resolver.resolve();
}
return cli;
};
cli.generateDocs = () => {
let message;
const path = require('path');
const resolver = require('jsdoc/tutorial/resolver');
const taffy = require('taffydb').taffy;
let template;
@ -392,8 +378,7 @@ module.exports = (() => {
log.info('Generating output files...');
publishPromise = template.publish(
taffy(props.docs),
env.opts,
resolver.root
env.opts
);
return Promise.resolve(publishPromise);

View File

@ -491,14 +491,6 @@ const DOCLET_SCHEMA = exports.DOCLET_SCHEMA = {
type: STRING
}
},
// extended tutorials
tutorials: {
type: ARRAY,
minItems: 1,
items: {
type: STRING
}
},
// what type is the value that this doc is associated with, like `number`
type: TYPE_PROPERTY_SCHEMA,
undocumented: {

View File

@ -760,13 +760,6 @@ let baseTags = exports.baseTags = {
},
synonyms: ['exception']
},
tutorial: {
mustHaveValue: true,
onTagged(doclet, {value}) {
doclet.tutorials = doclet.tutorials || [];
doclet.tutorials.push(value);
}
},
type: {
mustHaveValue: true,
mustNotHaveDescription: true,

View File

@ -1,153 +0,0 @@
/**
* @module jsdoc/tutorial
*/
const markdown = require('jsdoc/util/markdown');
const hasOwnProp = Object.prototype.hasOwnProperty;
/**
* Removes child tutorial from the parent. Does *not* unset child.parent though.
*
* @param {Tutorial} parent - parent tutorial.
* @param {Tutorial} child - Old child.
* @private
*/
function removeChild({children}, child) {
const index = children.indexOf(child);
if (index !== -1) {
children.splice(index, 1);
}
}
/**
* Adds a child to the parent tutorial. Does *not* set child.parent though.
*
* @param {Tutorial} parent - parent tutorial.
* @param {Tutorial} child - New child.
* @private
*/
function addChild({children}, child) {
children.push(child);
}
/**
* Represents a single JSDoc tutorial.
*/
class Tutorial {
/**
* @param {string} name - Tutorial name.
* @param {string} content - Text content.
* @param {number} type - Source formating.
*/
constructor(name, content, type) {
this.title = this.name = this.longname = name;
this.content = content;
this.type = type;
// default values
this.parent = null;
this.children = [];
}
/**
* Moves children from current parent to different one.
*
* @param {?Tutorial} parent - New parent. If null, the tutorial has no parent.
*/
setParent(parent) {
// removes node from old parent
if (this.parent) {
removeChild(this.parent, this);
}
this.parent = parent;
if (parent) {
addChild(parent, this);
}
}
/* eslint-disable class-methods-use-this */
/**
* Removes children from current node.
*
* @param {Tutorial} child - Old child.
*/
removeChild(child) {
child.setParent(null);
}
/* eslint-enable class-methods-use-this */
/**
* Adds new children to current node.
*
* @param {Tutorial} child - New child.
*/
addChild(child) {
child.setParent(this);
}
/**
* Prepares source.
*
* @return {string} HTML source.
*/
parse() {
switch (this.type) {
// nothing to do
case exports.TYPES.HTML:
return this.content;
// markdown
case exports.TYPES.MARKDOWN:
return markdown.getParser()(this.content);
// uhm... should we react somehow?
// if not then this case can be merged with TYPES.HTML
default:
return this.content;
}
}
}
exports.Tutorial = Tutorial;
/**
* Represents the root tutorial.
* @extends {module:jsdoc/tutorial.Tutorial}
*/
class RootTutorial extends Tutorial {
constructor() {
super('', '', null);
this._tutorials = {};
}
/**
* Retrieve a tutorial by name.
* @param {string} name - Tutorial name.
* @return {module:jsdoc/tutorial.Tutorial} Tutorial instance.
*/
getByName(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.
*/
_addTutorial(child) {
this._tutorials[child.name] = child;
}
}
exports.RootTutorial = RootTutorial;
/**
* Tutorial source types.
*
* @enum {number}
*/
exports.TYPES = {
HTML: 1,
MARKDOWN: 2
};

View File

@ -1,192 +0,0 @@
/**
* @module jsdoc/tutorial/resolver
*/
const env = require('jsdoc/env');
const fs = require('fs');
const { log } = require('@jsdoc/util');
const { lsSync } = require('@jsdoc/util').fs;
const path = require('path');
const stripBom = require('strip-bom');
const tutorial = require('jsdoc/tutorial');
const hasOwnProp = Object.prototype.hasOwnProperty;
// TODO: make this an instance member of `RootTutorial`?
const conf = {};
const 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 (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 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) {
let 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 (let childName of names) {
addTutorialConf(childName, meta.children[childName]);
}
// replace with an array of names.
meta.children = names;
}
// check if the tutorial has already been defined...
if (hasOwnProp.call(conf, name)) {
log.warn(
`Metadata for the tutorial ${name} is defined more than once. ` +
'Only the first definition will be used.'
);
} else {
conf[name] = meta;
}
} else {
// keys are tutorial names, values are `Tutorial` instances
names = Object.keys(meta);
for (let tutorialName of names) {
addTutorialConf(tutorialName, meta[tutorialName]);
}
}
}
/**
* Add a tutorial.
* @param {module:jsdoc/tutorial.Tutorial} current - Tutorial to add.
*/
exports.addTutorial = current => {
if (exports.root.getByName(current.name)) {
log.warn(
`The tutorial ${current.name} is defined more than once. ` +
'Only the first definition will be used.'
);
} else {
// by default, the root tutorial is the parent
current.setParent(exports.root);
exports.root._addTutorial(current);
}
};
/**
* Load tutorials from the given path.
* @param {string} filepath - Tutorials directory.
*/
exports.load = filepath => {
let content;
let current;
const files = lsSync(filepath, {
depth: env.opts.recurse ? env.conf.recurseDepth : 0
});
let name;
let match;
let type;
// tutorials handling
files.forEach(file => {
match = file.match(finder);
// any filetype that can apply to tutorials
if (match) {
name = path.basename(match[1]);
content = fs.readFileSync(file, env.opts.encoding);
switch (match[2].toLowerCase()) {
// HTML type
case 'xml':
case 'xhtml':
case 'html':
case 'htm':
type = tutorial.TYPES.HTML;
break;
// Markdown typs
case 'md':
case 'markdown':
type = tutorial.TYPES.MARKDOWN;
break;
// configuration file
case 'json':
addTutorialConf(name, JSON.parse(stripBom(content)));
// don't add this as a tutorial
return;
// how can it be? check `finder' regexp
// not a file we want to work with
default:
return;
}
current = new tutorial.Tutorial(name, content, type);
exports.addTutorial(current);
}
});
};
/**
* Resolves hierarchical structure.
*/
exports.resolve = () => {
let item;
let current;
Object.keys(conf).forEach(name => {
current = exports.root.getByName(name);
// 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(child => {
const childTutorial = exports.root.getByName(child);
if (!childTutorial) {
log.error(`Missing child tutorial: ${child}`);
}
else {
childTutorial.setParent(current);
}
});
}
});
};

View File

@ -18,15 +18,6 @@ const ids = {};
// each container gets its own html file
const containers = ['class', 'module', 'external', 'namespace', 'mixin', 'interface'];
let tutorials;
/** Sets tutorials map.
@param {jsdoc.tutorial.Tutorial} root - Root tutorial node.
*/
exports.setTutorials = root => {
tutorials = root;
};
exports.globalName = SCOPE.NAMES.GLOBAL;
exports.fileExtension = '.html';
exports.SCOPE_TO_PUNC = SCOPE_TO_PUNC;
@ -40,12 +31,6 @@ const linkMap = {
longnameToId: {}
};
// two-way lookup
const tutorialLinkMap = {
nameToUrl: {},
urlToName: {}
};
const longnameToUrl = exports.longnameToUrl = linkMap.longnameToUrl;
const longnameToId = exports.longnameToId = linkMap.longnameToId;
@ -429,80 +414,6 @@ function splitLinkText(text) {
};
}
const tutorialToUrl = exports.tutorialToUrl = tutorial => {
let fileUrl;
const node = tutorials.getByName(tutorial);
// no such tutorial
if (!node) {
log.error( new Error(`No such tutorial: ${tutorial}`) );
return null;
}
// define the URL if necessary
if (!hasOwnProp.call(tutorialLinkMap.nameToUrl, node.name)) {
fileUrl = `tutorial-${getUniqueFilename(node.name)}`;
tutorialLinkMap.nameToUrl[node.name] = fileUrl;
tutorialLinkMap.urlToName[fileUrl] = node.name;
}
return tutorialLinkMap.nameToUrl[node.name];
};
/**
* Retrieve a link to a tutorial, or the name of the tutorial if the tutorial is missing. If the
* `missingOpts` parameter is supplied, the names of missing tutorials will be prefixed by the
* specified text and wrapped in the specified HTML tag and CSS class.
*
* @function
* @todo Deprecate missingOpts once we have a better error-reporting mechanism.
* @param {string} tutorial The name of the tutorial.
* @param {string} content The link text to use.
* @param {object} [missingOpts] Options for displaying the name of a missing tutorial.
* @param {string} missingOpts.classname The CSS class to wrap around the tutorial name.
* @param {string} missingOpts.prefix The prefix to add to the tutorial name.
* @param {string} missingOpts.tag The tag to wrap around the tutorial name.
* @return {string} An HTML link to the tutorial, or the name of the tutorial with the specified
* options.
*/
const toTutorial = exports.toTutorial = (tutorial, content, missingOpts) => {
let classname;
let link;
let node;
let tag;
if (!tutorial) {
log.error(new Error('Missing required parameter: tutorial'));
return null;
}
node = tutorials.getByName(tutorial);
// no such tutorial
if (!node) {
missingOpts = missingOpts || {};
tag = missingOpts.tag;
classname = missingOpts.classname;
link = tutorial;
if (missingOpts.prefix) {
link = missingOpts.prefix + link;
}
if (tag) {
link = `<${tag}${classname ? (` class="${classname}">`) : '>'}${link}`;
link += `</${tag}>`;
}
return link;
}
content = content || node.title;
return `<a href="${tutorialToUrl(tutorial)}">${content}</a>`;
};
function shouldShortenLongname() {
if (env.conf && env.conf.templates && env.conf.templates.useShortNamesInLinks) {
return true;
@ -512,9 +423,9 @@ function shouldShortenLongname() {
}
/**
* Find `{@link ...}` and `{@tutorial ...}` inline tags and turn them into HTML links.
* Find `{@link ...}` inline tags and turn them into HTML links.
*
* @param {string} str - The string to search for `{@link ...}` and `{@tutorial ...}` tags.
* @param {string} str - The string to search for `{@link ...}` tags.
* @return {string} The linkified text.
*/
exports.resolveLinks = str => {
@ -565,19 +476,10 @@ exports.resolveLinks = str => {
}) );
}
function processTutorial(string, {completeTag, text}) {
const leading = extractLeadingText(string, completeTag);
string = leading.string;
return string.replace( completeTag, toTutorial(text, leading.leadingText) );
}
replacers = {
link: processLink,
linkcode: processLink,
linkplain: processLink,
tutorial: processTutorial
linkplain: processLink
};
return inline.replaceInlineTags(str, replacers).newString;

View File

@ -43,14 +43,6 @@ function find(spec) {
return helper.find(data, spec);
}
function tutoriallink(tutorial) {
return helper.toTutorial(tutorial, null, {
tag: 'em',
classname: 'disabled',
prefix: 'Tutorial: '
});
}
function getAncestorLinks(doclet) {
return helper.getAncestorLinks(data, doclet);
}
@ -353,10 +345,6 @@ function buildMemberNav(items, itemHeading, itemsSeen, linktoFn) {
return nav;
}
function linktoTutorial(longName, name) {
return tutoriallink(name);
}
function linktoExternal(longName, name) {
return linkto(longName, name.replace(/(^"|"$)/g, ''));
}
@ -370,7 +358,6 @@ function linktoExternal(longName, name) {
* @param {array<object>} members.mixins
* @param {array<object>} members.modules
* @param {array<object>} members.namespaces
* @param {array<object>} members.tutorials
* @param {array<object>} members.events
* @param {array<object>} members.interfaces
* @return {string} The HTML for the navigation sidebar.
@ -379,7 +366,6 @@ function buildNav(members) {
let globalNav;
let nav = '<h2><a href="index.html">Home</a></h2>';
const seen = {};
const seenTutorials = {};
nav += buildMemberNav(members.modules, 'Modules', {}, linkto);
nav += buildMemberNav(members.externals, 'Externals', seen, linktoExternal);
@ -388,7 +374,6 @@ function buildNav(members) {
nav += buildMemberNav(members.interfaces, 'Interfaces', seen, linkto);
nav += buildMemberNav(members.events, 'Events', seen, linkto);
nav += buildMemberNav(members.mixins, 'Mixins', seen, linkto);
nav += buildMemberNav(members.tutorials, 'Tutorials', seenTutorials, linktoTutorial);
if (members.globals.length) {
globalNav = '';
@ -421,9 +406,8 @@ function sourceToDestination(parentDir, sourcePath, destDir) {
/**
@param {TAFFY} taffyData See <http://taffydb.com/>.
@param {object} opts
@param {Tutorial} tutorials
*/
exports.publish = (taffyData, opts, tutorials) => {
exports.publish = (taffyData, opts) => {
let classes;
let conf;
let cwd;
@ -469,9 +453,6 @@ exports.publish = (taffyData, opts, tutorials) => {
path.resolve(conf.default.layoutFile) :
'layout.tmpl';
// set up tutorials for helper
helper.setTutorials(tutorials);
data = helper.prune(data);
data.sort('longname, version, since');
helper.addEventListeners(data);
@ -656,7 +637,6 @@ exports.publish = (taffyData, opts, tutorials) => {
});
members = helper.getMembers(data);
members.tutorials = tutorials.children;
// output pretty-printed source files by default
outputSourceFiles = conf.default && conf.default.outputSourceFiles !== false;
@ -665,7 +645,6 @@ exports.publish = (taffyData, opts, tutorials) => {
view.find = find;
view.linkto = linkto;
view.resolveAuthorLinks = resolveAuthorLinks;
view.tutoriallink = tutoriallink;
view.htmlsafe = htmlsafe;
view.outputSourceFiles = outputSourceFiles;
@ -733,31 +712,4 @@ exports.publish = (taffyData, opts, tutorials) => {
generate(`Interface: ${myInterfaces[0].name}`, myInterfaces, helper.longnameToUrl[longname]);
}
});
// TODO: move the tutorial functions to templateHelper.js
function generateTutorial(title, tutorial, filename) {
const tutorialData = {
title: title,
header: tutorial.title,
content: tutorial.parse(),
children: tutorial.children
};
const tutorialPath = path.join(outdir, filename);
let html = view.render('tutorial.tmpl', tutorialData);
// yes, you can use {@link} in tutorials too!
html = helper.resolveLinks(html); // turn {@link foo} into <a href="foodoc.html">foo</a>
fs.writeFileSync(tutorialPath, html, 'utf8');
}
// tutorials can have only one parent so there is no risk for loops
function saveChildren({children}) {
children.forEach(child => {
generateTutorial(`Tutorial: ${child.title}`, child, helper.tutorialToUrl(child.name));
saveChildren(child);
});
}
saveChildren(tutorials);
};

View File

@ -114,15 +114,6 @@ if (data.defaultvalue && (data.defaultvaluetype === 'object' || data.defaultvalu
</li></ul></dd>
<?js } ?>
<?js if (data.tutorials && tutorials.length) {?>
<dt class="tag-tutorial">Tutorials:</dt>
<dd class="tag-tutorial">
<ul><?js tutorials.forEach(function(t) { ?>
<li><?js= self.tutoriallink(t) ?></li>
<?js }); ?></ul>
</dd>
<?js } ?>
<?js if (data.see && see.length) {?>
<dt class="tag-see">See:</dt>
<dd class="tag-see">

View File

@ -1,19 +0,0 @@
<section>
<header>
<?js if (children.length > 0) { ?>
<ul><?js
var self = this;
children.forEach(function(t) { ?>
<li><?js= self.tutoriallink(t.name) ?></li>
<?js }); ?></ul>
<?js } ?>
<h2><?js= header ?></h2>
</header>
<article>
<?js= content ?>
</article>
</section>

View File

@ -2,6 +2,5 @@
/**
@param {TAFFY} taffyData See <http://taffydb.com/>.
@param {object} opts
@param {Tutorial} tutorials
*/
exports.publish = (taffyData, opts, tutorials) => {};
exports.publish = (taffyData, opts) => {};

View File

@ -1 +0,0 @@
{}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
/**
* Test {@tutorial test2} {@tutorial test3}
*
* @class
* @tutorial test
*/
function Test() {}

View File

@ -1 +0,0 @@
This tutorial has a tricksy name to make sure we are not loading Array.constructor or Object.constructor.

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
# test2.markdown

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
/** Some documentation.
* @tutorial tute1
* @tutorial tute2
*/
var x;

View File

@ -1,276 +0,0 @@
describe('jsdoc/tutorial', () => {
const env = require('jsdoc/env');
const tutorial = require('jsdoc/tutorial');
const name = 'tuteID';
const content = 'Tutorial content blah blah blah & <';
const tute = new tutorial.Tutorial(name, content, tutorial.TYPES.NOTAVALUE);
const par = new tutorial.Tutorial('parent',
"# This is the parent tutorial's <em>content & stuff</em> A_B X_Y",
tutorial.TYPES.MARKDOWN);
const par2 = new tutorial.Tutorial('parent2', '<h2>This is the second parent tutorial</h2>',
tutorial.TYPES.HTML);
const markdownEntities = new tutorial.Tutorial('markdown-entities',
'<pre>This Markdown tutorial contains HTML entities: &amp; &lt; &gt;</pre>',
tutorial.TYPES.MARKDOWN);
it('module should exist', () => {
expect(tutorial).toBeObject();
});
it('should export a Tutorial function', () => {
expect(tutorial.Tutorial).toBeFunction();
});
it('should export a RootTutorial function', () => {
expect(tutorial.RootTutorial).toBeFunction();
});
it('should export a TYPES object', () => {
expect(tutorial.TYPES).toBeObject();
});
describe('tutorial.TYPES', () => {
it('should have a HTML property', () => {
expect(tutorial.TYPES.HTML).toBeNumber();
});
it('should have a MARKDOWN property', () => {
expect(tutorial.TYPES.MARKDOWN).toBeNumber();
});
});
describe('Tutorial', () => {
it('should have a "setParent" method', () => {
expect(tutorial.Tutorial.prototype.setParent).toBeFunction();
});
it('should have a "removeChild" method', () => {
expect(tutorial.Tutorial.prototype.removeChild).toBeFunction();
});
it('should have an "addChild" method', () => {
expect(tutorial.Tutorial.prototype.addChild).toBeFunction();
});
it('should have a "parse" method', () => {
expect(tutorial.Tutorial.prototype.parse).toBeFunction();
});
it('should have a "name" property', () => {
expect(tute.name).toBe(name);
});
it('should have a "longname" property', () => {
expect(tute.longname).toBe(name);
});
it("should have a 'title' property, by default set to to the tutorial's name", () => {
expect(tute.title).toBe(name);
// Testing of overriding a tutorial's title in its JSON file is
// covered in tutorial/resolver.js tests.
});
it("should have a 'content' property set to the tutorial's content", () => {
expect(tute.content).toBe(content);
});
it("should have a 'type' property set to the tutorial's type", () => {
expect(par.type).toBe(tutorial.TYPES.MARKDOWN);
});
it("should have a 'parent' property, initially null", () => {
expect(tute.parent).toBeNull();
});
it("should have a 'children' property, an empty array", () => {
expect(tute.children).toBeEmptyArray();
});
describe('setParent', () => {
it("adding a parent sets the child's 'parent' property", () => {
tute.setParent(par);
expect(tute.parent).toBe(par);
});
it("adding a parent adds the child to the parent's 'children' property", () => {
expect(par.children).toContain(tute);
});
it('re-parenting removes the child from the previous parent', () => {
tute.setParent(par2);
expect(tute.parent).toBe(par2);
expect(par2.children).toContain(tute);
expect(par.children).not.toContain(tute);
});
it("calling setParent with a null parent unsets the child's parent and removes the child from its previous parent", () => {
expect(par2.children).toContain(tute);
tute.setParent(null);
expect(tute.parent).toBeNull();
expect(par2.children).not.toContain(tute);
});
});
describe('addChild', () => {
it("adding a child tutorial adds the child to the parent's 'children' property", () => {
tute.setParent(null);
const n = par.children.length;
par.addChild(tute);
expect(par.children).toBeArrayOfSize(n + 1);
expect(par.children).toContain(tute);
});
it("adding a child tutorial sets the child's parent to to the parent tutorial", () => {
expect(tute.parent).toBe(par);
});
it('adding a child tutorial removes the child from its old parent', () => {
// tue is currently owned by par; we reparent it to par2
expect(tute.parent).toBe(par);
par2.addChild(tute);
expect(tute.parent).toBe(par2);
expect(par.children).not.toContain(tute);
expect(par2.children).toContain(tute);
});
});
describe('removeChild', () => {
function removeChild() {
par2.removeChild(par);
}
it('removing a tutorial that is not a child silently passes', () => {
const n = par2.children.length;
expect(removeChild).not.toThrow();
expect(par2.children).toBeArrayOfSize(n);
});
it("removing a child removes the child from the parent's 'children' property", () => {
tute.setParent(par2);
expect(par2.children.length).toBe(1);
par2.removeChild(tute);
expect(par2.children).not.toContain(tute);
expect(par2.children).toBeEmptyArray();
});
it("removing a child unsets the child's 'parent' property", () => {
expect(tute.parent).toBeNull();
});
});
describe('various inheritance tests with addChild, setParent and removeChild', () => {
it('parenting and unparenting via addChild, setParent and removeChild makes sure inheritance is set accordingly', () => {
// unparent everything.
tute.setParent(null);
par.setParent(null);
par2.setParent(null);
// let tute belong to par
tute.setParent(par);
expect(tute.parent).toBe(par);
expect(par2.children).toBeEmptyArray();
expect(par.children).toBeArrayOfSize(1);
expect(par.children[0]).toBe(tute);
// addChild tute to par2. its parent should now be par2, and
// it can't be the child of two parents
par2.addChild(tute);
expect(tute.parent).toBe(par2);
expect(par.children).toBeEmptyArray();
expect(par2.children).toBeArrayOfSize(1);
expect(par2.children[0]).toBe(tute);
// removeChild tute from par2. tute should now be unparented.
par2.removeChild(tute);
expect(tute.parent).toBe(null);
expect(par.children).toBeEmptyArray();
expect(par2.children).toBeEmptyArray();
});
});
describe('parse', () => {
const markdownConfig = env.conf.markdown;
function setMarkdownConfig(config) {
env.conf.markdown = config;
}
beforeEach(() => {
setMarkdownConfig({parser: 'marked'});
});
afterEach(() => {
env.conf.markdown = markdownConfig;
});
it('Tutorials with HTML type return content as-is', () => {
expect(par2.parse()).toBe('<h2>This is the second parent tutorial</h2>');
});
it('Tutorials with MARKDOWN type go through the markdown parser, respecting configuration options', () => {
expect(par.parse()).toBe("<h1>This is the parent tutorial's <em>content &amp; stuff</em> A_B X_Y</h1>");
});
it('Tutorials with MARKDOWN type preserve &amp;/&lt;/&gt; entities', () => {
expect(markdownEntities.parse())
.toBe('<pre>This Markdown tutorial contains HTML entities: &amp; &lt; &gt;</pre>');
});
it('Tutorials with unrecognised type are returned as-is', () => {
expect(tute.parse()).toBe(content);
});
});
});
describe('RootTutorial', () => {
it('should inherit from Tutorial', () => {
const root = new tutorial.RootTutorial();
expect(root instanceof tutorial.Tutorial).toBe(true);
});
it('should have a "getByName" method', () => {
expect(tutorial.RootTutorial.prototype.getByName).toBeFunction();
});
describe('getByName', () => {
let root;
beforeEach(() => {
root = new tutorial.RootTutorial();
});
it('can retrieve tutorials by name', () => {
const myTutorial = new tutorial.Tutorial('myTutorial', '', tutorial.TYPES.HTML);
root._addTutorial(myTutorial);
expect(root.getByName('myTutorial')).toBe(myTutorial);
});
it('returns nothing for non-existent tutorials', () => {
expect(root.getByName('asdf')).toBeFalse();
});
it('uses hasOwnProperty when it checks for the tutorial', () => {
expect(root.getByName('prototype')).toBeFalse();
});
});
});
});

View File

@ -1,242 +0,0 @@
describe('jsdoc/tutorial/resolver', () => {
const env = require('jsdoc/env');
const resolver = require('jsdoc/tutorial/resolver');
const tutorial = require('jsdoc/tutorial');
let childNames;
let constr;
let test;
let test2;
let test3;
let test4;
let test6;
function resetRootTutorial() {
resolver.root = new tutorial.RootTutorial();
}
function loadTutorials() {
resetRootTutorial();
resolver.load(`${env.dirname}/test/fixtures/tutorials/tutorials`);
childNames = resolver.root.children.map(({name}) => 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', () => {
expect(resolver).toBeObject();
});
it('should export an "addTutorial" function', () => {
expect(resolver.addTutorial).toBeFunction();
});
it('should export a "load" function', () => {
expect(resolver.load).toBeFunction();
});
it('should export a "resolve" function', () => {
expect(resolver.resolve).toBeFunction();
});
it('should export a "root" tutorial', () => {
expect(resolver.root).toBeObject();
expect(resolver.root instanceof tutorial.RootTutorial).toBe(true);
});
it('exported "root" tutorial should export a "getByName" function', () => {
expect(resolver.root.getByName).toBeFunction();
});
// note: every time we addTutorial or run the resolver, we are *adding*
// to the root tutorial.
describe('addTutorial', () => {
let tute;
beforeEach(() => {
resetRootTutorial();
tute = new tutorial.Tutorial('myTutorial', '', tutorial.TYPES.HTML);
resolver.addTutorial(tute);
});
afterEach(resetRootTutorial);
it('should add a default parent of the root tutorial', () => {
expect(tute.parent).toBe(resolver.root);
});
it('should be added to the root tutorial as a child', () => {
expect(resolver.root.children).toContain(tute);
});
});
describe('load', () => {
const recurse = env.opts.recurse;
beforeEach(loadTutorials);
afterEach(() => {
resetRootTutorial();
env.opts.recurse = recurse;
});
it('does not, by default, recurse into subdirectories', () => {
expect(resolver.root.getByName('test_recursive')).toBeFalsy();
});
it('recurses into subdirectories when the --recurse flag is used', () => {
let recursiveTute;
env.opts.recurse = true;
loadTutorials();
recursiveTute = resolver.root.getByName('test_recursive');
expect(recursiveTute).toBeObject();
expect(recursiveTute instanceof tutorial.Tutorial).toBe(true);
});
it('all tutorials are added, initially as top-level tutorials', () => {
// check they were added
expect(test).toBeObject();
expect(test2).toBeObject();
expect(test3).toBeObject();
expect(test4).toBeObject();
expect(test6).toBeObject();
expect(constr).toBeObject();
// check they are top-level in resolver.root
expect(childNames).toContain('test');
expect(childNames).toContain('test2');
expect(childNames).toContain('test3');
expect(childNames).toContain('test4');
expect(childNames).toContain('test6');
});
it('tutorials with names equal to reserved keywords in JS still function as expected', () => {
expect(constr instanceof tutorial.Tutorial).toBe(true);
});
it('non-tutorials are skipped', () => {
expect(resolver.root.getByName('multiple')).toBeFalse();
expect(resolver.root.getByName('test5')).toBeFalse();
});
it('tutorial types are determined correctly', () => {
// test.html, test2.markdown, test3.html, test4.md, test6.xml
expect(test.type).toBe(tutorial.TYPES.HTML);
expect(test2.type).toBe(tutorial.TYPES.MARKDOWN);
expect(test3.type).toBe(tutorial.TYPES.HTML);
expect(test4.type).toBe(tutorial.TYPES.MARKDOWN);
expect(test6.type).toBe(tutorial.TYPES.HTML);
expect(constr.type).toBe(tutorial.TYPES.MARKDOWN);
});
it('JSON files with a leading BOM are handled correctly', () => {
resetRootTutorial();
function loadBomTutorials() {
resolver.load(`${env.dirname}/test/fixtures/tutorials/bom`);
}
expect(loadBomTutorials).not.toThrow();
});
});
// resolve
// myTutorial
// constructor
// test
// |- test2
// |- test6
// |- test3
// |- test4
describe('resolve', () => {
beforeEach(() => {
loadTutorials();
resolver.resolve();
});
afterEach(resetRootTutorial);
it('hierarchy is resolved properly no matter how the children property is defined', () => {
// root has child 'test'
expect(resolver.root.children).toBeArrayOfSize(2);
expect(resolver.root.children).toContain(test);
expect(resolver.root.children).toContain(constr);
expect(test.parent).toBe(resolver.root);
expect(constr.parent).toBe(resolver.root);
// test has child 'test2'
expect(test.children).toBeArrayOfSize(1);
expect(test.children).toContain(test2);
expect(test2.parent).toBe(test);
// test2 has children test3, test6
expect(test2.children).toBeArrayOfSize(2);
expect(test2.children).toContain(test3);
expect(test2.children).toContain(test6);
expect(test3.parent).toBe(test2);
expect(test6.parent).toBe(test2);
// test3 has child test4
expect(test3.children).toBeArrayOfSize(1);
expect(test3.children).toContain(test4);
expect(test4.parent).toBe(test3);
});
it('tutorials without configuration files have titles matching filenames', () => {
// test6.xml didn't have a metadata
expect(test6.title).toBe('test6');
});
it('tutorials with configuration files have titles as specified in configuration', () => {
// test.json had info for just test.json
expect(test.title).toBe('Test tutorial');
});
it('multiple tutorials can appear in a configuration file', () => {
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 load() {
resolver.load(`${env.dirname}/test/fixtures/tutorials/incomplete`);
resolver.resolve();
}
expect(jsdoc.didLog(load, 'error')).toBeTrue();
});
it('logs a warning for duplicate-named tutorials (e.g. test.md, test.html)', () => {
function load() {
const tute = new tutorial.Tutorial('myTutorial', '', tutorial.TYPES.HTML);
resolver.addTutorial(tute);
resolver.addTutorial(tute);
}
expect(jsdoc.didLog(load, 'warn')).toBeTrue();
});
it('allows tutorials to be defined in one .json file and redefined in another', () => {
function load() {
resolver.load(`${env.dirname}/test/fixtures/tutorials/duplicateDefined`);
resolver.resolve();
}
expect(jsdoc.didLog(load, 'warn')).toBeTrue();
// we don't check to see which one wins; it depends on the order in which the JS engine
// iterates over object keys
expect(resolver.root.getByName('asdf')).toBeObject();
});
});
});

View File

@ -8,7 +8,6 @@ describe("jsdoc/util/templateHelper", () => {
const doclet = require('jsdoc/doclet');
const env = require('jsdoc/env');
const helper = require('jsdoc/util/templateHelper');
const resolver = require('jsdoc/tutorial/resolver');
const { taffy } = require('taffydb');
helper.registerLink('test', 'path/to/test.html');
@ -17,10 +16,6 @@ describe("jsdoc/util/templateHelper", () => {
expect(helper).toBeObject();
});
it("should export a 'setTutorials' function", () => {
expect(helper.setTutorials).toBeFunction();
});
it("should export a 'globalName' property", () => {
expect(helper.globalName).toBeString();
});
@ -97,14 +92,6 @@ describe("jsdoc/util/templateHelper", () => {
expect(helper.registerLink).toBeFunction();
});
it("should export a 'tutorialToUrl' function", () => {
expect(helper.tutorialToUrl).toBeFunction();
});
it("should export a 'toTutorial' function", () => {
expect(helper.toTutorial).toBeFunction();
});
it("should export a 'resolveLinks' function", () => {
expect(helper.resolveLinks).toBeFunction();
});
@ -121,25 +108,6 @@ describe("jsdoc/util/templateHelper", () => {
expect(helper.longnamesToTree).toBeFunction();
});
describe("setTutorials", () => {
// used in tutorialToUrl, toTutorial.
it("setting tutorials to null causes all tutorial lookups to fail", () => {
// bit of a dodgy test but the best I can manage. setTutorials doesn't do much.
helper.setTutorials(null);
// should throw error: no 'getByName' in tutorials.
expect(() => helper.tutorialToUrl('asdf')).toThrow();
});
it("setting tutorials to the root tutorial object lets lookups work", () => {
helper.setTutorials(resolver.root);
spyOn(resolver.root, 'getByName');
helper.tutorialToUrl('asdf');
expect(resolver.root.getByName).toHaveBeenCalled();
});
});
describe("globalName", () => {
it("should equal 'global'", () => {
expect(helper.globalName).toBe('global');
@ -1278,150 +1246,6 @@ describe("jsdoc/util/templateHelper", () => {
});
});
describe("tutorialToUrl", () => {
beforeEach(() => {
helper.setTutorials(resolver.root);
});
afterEach(() => {
helper.setTutorials(null);
});
it('logs an error if the tutorial is missing', () => {
function toUrl() {
helper.tutorialToUrl('be-a-perfect-person-in-just-three-days');
}
expect(jsdoc.didLog(toUrl, 'error')).toBeTrue();
});
it("logs an error if the tutorial's name is a reserved JS keyword and it doesn't exist", () => {
function toUrl() {
helper.tutorialToUrl('prototype');
}
expect(jsdoc.didLog(toUrl, 'error')).toBeTrue();
});
it("creates links to tutorials if they exist", () => {
let url;
// load the tutorials we already have for the tutorials tests
resolver.load(`${env.dirname}/test/fixtures/tutorials/tutorials`);
resolver.resolve();
url = helper.tutorialToUrl('test');
expect(url).toBe('tutorial-test.html');
});
it("creates links for tutorials where the name is a reserved JS keyword", () => {
const url = helper.tutorialToUrl('constructor');
expect(url).toBe('tutorial-constructor.html');
});
it("returns the same link if called multiple times on the same tutorial", () => {
expect(helper.tutorialToUrl('test2')).toBe(helper.tutorialToUrl('test2'));
});
});
describe("toTutorial", () => {
beforeEach(() => {
helper.setTutorials(resolver.root);
});
afterEach(() => {
helper.setTutorials(null);
});
it('logs an error if the first param is missing', () => {
function toTutorial() {
helper.toTutorial();
}
expect(jsdoc.didLog(toTutorial, 'error')).toBeTrue();
});
// missing tutorials
it("returns the tutorial name if it's missing and no missingOpts is provided", () => {
let link;
helper.setTutorials(resolver.root);
link = helper.toTutorial('qwerty');
expect(link).toBe('qwerty');
});
it("returns the tutorial name wrapped in missingOpts.tag if provided and the tutorial is missing", () => {
const link = helper.toTutorial('qwerty', 'lkjklqwerty', {tag: 'span'});
expect(link).toBe('<span>qwerty</span>');
});
it("returns the tutorial name wrapped in missingOpts.tag with class missingOpts.classname if provided and the tutorial is missing", () => {
let link = helper.toTutorial('qwerty', 'lkjklqwerty', {classname: 'missing'});
expect(link).toBe('qwerty');
link = helper.toTutorial('qwerty', 'lkjklqwerty', {
tag: 'span',
classname: 'missing'
});
expect(link).toBe('<span class="missing">qwerty</span>');
});
it("prefixes the tutorial name with missingOpts.prefix if provided and the tutorial is missing", () => {
let link = helper.toTutorial('qwerty', 'lkjklqwerty', {
tag: 'span',
classname: 'missing',
prefix: 'TODO-'
});
expect(link).toBe('<span class="missing">TODO-qwerty</span>');
link = helper.toTutorial('qwerty', 'lkjklqwerty', {prefix: 'TODO-'});
expect(link).toBe('TODO-qwerty');
link = helper.toTutorial('qwerty', 'lkjklqwerty', {
prefix: 'TODO-',
classname: 'missing'
});
expect(link).toBe('TODO-qwerty');
});
// now we do non-missing tutorials.
it("returns a link to the tutorial if not missing", () => {
let link;
// load the tutorials we already have for the tutorials tests
resolver.load(`${env.dirname}/test/fixtures/tutorials/tutorials`);
resolver.resolve();
link = helper.toTutorial('constructor', 'The Constructor tutorial');
expect(link).toBe(`<a href="${helper.tutorialToUrl('constructor')}">The Constructor tutorial</a>`);
});
it("uses the tutorial's title for the link text if no content parameter is provided", () => {
const link = helper.toTutorial('test');
expect(link).toBe(`<a href="${helper.tutorialToUrl('test')}">Test tutorial</a>`);
});
it("does not apply any of missingOpts if the tutorial was found", () => {
const link = helper.toTutorial('test', '', {
tag: 'span',
classname: 'missing',
prefix: 'TODO-'
});
expect(link).toBe(`<a href="${helper.tutorialToUrl('test')}">Test tutorial</a>`);
});
});
describe("resolveLinks", () => {
let conf;

View File

@ -1,11 +0,0 @@
describe('@tutorial tag', () => {
// these are tests for the block usage, not the inline usage. see util/templateHelper for that.
const docSet = jsdoc.getDocSetFromFile('test/fixtures/tutorialtag.js');
const doc = docSet.getByLongname('x')[0];
it("adds the listed tutorials to a 'tutorials' array on the doclet", () => {
expect(doc.tutorials).toBeArrayOfSize(2);
expect(doc.tutorials).toContain('tute1');
expect(doc.tutorials).toContain('tute2');
});
});