Source: tutorial/resolver.js

/**
    @overview
    @author Rafa&#322; 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);
                }
            });
        }
    });
};