/**
@overview
@author Rafał Wrzeszcz <rafal.wrzeszcz@wrzasq.pl>
@license Apache License 2.0 - See file 'LICENSE.md' in this project.
*/
/**
@module jsdoc/tutorial/resolver
*/
'use strict';
var env = require('jsdoc/env');
var fs = require('jsdoc/fs');
var logger = require('jsdoc/util/logger');
var path = require('path');
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;
/** 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) {
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, l = names.length; i < l; ++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 (hasOwnProp.call(conf, name)) {
logger.warn('Metadata for the tutorial %s is defined more than once. Only the first definition will be used.', name );
} else {
conf[name] = meta;
}
} else {
// keys are tutorial names, values are `Tutorial` instances
names = Object.keys(meta);
for (i = 0, l = names.length; i < l; ++i) {
addTutorialConf(names[i], meta[names[i]]);
}
}
}
/**
* Add a tutorial.
* @param {module:jsdoc/tutorial.Tutorial} current - Tutorial to add.
*/
exports.addTutorial = function(current) {
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 {
// 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 = function(filepath) {
var content;
var current;
var files = fs.ls(filepath, env.opts.recurse ? 10 : undefined);
var name;
var match;
var type;
// tutorials handling
files.forEach(function(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':
var meta = JSON.parse(content);
addTutorialConf(name, meta);
// don't add this as a tutorial
return;
// how can it be? check `finder' regexp
default:
// not a file we want to work with
return;
}
current = new tutorial.Tutorial(name, content, type);
exports.addTutorial(current);
}
});
};
/** Resolves hierarchical structure.
*/
exports.resolve = function() {
var item;
var current;
Object.keys(conf).forEach(function(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(function(child) {
var childTutorial = exports.root.getByName(child);
if (!childTutorial) {
logger.error('Missing child tutorial: %s', child);
}
else {
childTutorial.setParent(current);
}
});
}
});
};