/*global env: true */ const hasOwnProp = Object.prototype.hasOwnProperty; // Work around an issue with hasOwnProperty in JSDoc's templateHelper.js. //TODO Fix in JSDoc. Object.prototype.hasOwnProperty = function(property) { return property in this; }; const template = require('jsdoc/lib/jsdoc/template'); const fs = require('jsdoc/lib/jsdoc/fs'); const path = require('jsdoc/lib/jsdoc/path'); const taffy = require('taffydb').taffy; const handle = require('jsdoc/lib/jsdoc/util/error').handle; const helper = require('jsdoc/lib/jsdoc/util/templateHelper'); const _ = require('underscore'); const htmlsafe = helper.htmlsafe; const linkto = helper.linkto; const resolveAuthorLinks = helper.resolveAuthorLinks; const outdir = env.opts.destination; // Work around an issue with hasOwnProperty in JSDoc's templateHelper.js. //TODO Fix in JSDoc. Object.prototype.hasOwnProperty = hasOwnProp; let view; let data; 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); } function hashToLink(doclet, hash) { if (!/^(#.+)/.test(hash)) { return hash; } let url = helper.createLink(doclet); url = url.replace(/(#.+|$)/, hash); return '' + hash + ''; } function needsSignature(doclet) { let needsSig = false; // function and class definitions always get a signature if (doclet.kind === 'function' || doclet.kind === 'class') { needsSig = true; } else if (doclet.kind === 'typedef' && doclet.type && doclet.type.names && doclet.type.names.length) { // typedefs that contain functions get a signature, too for (let i = 0, l = doclet.type.names.length; i < l; i++) { if (doclet.type.names[i].toLowerCase() === 'function') { needsSig = true; break; } } } return needsSig; } function addSignatureParams(f) { const params = helper.getSignatureParams(f, 'optional'); f.signature = (f.signature || '') + '(' + params.join(', ') + ')'; } function addSignatureReturns(f) { const returnTypes = helper.getSignatureReturns(f); f.signature = '' + (f.signature || '') + ''; if (returnTypes.length) { f.signature += '' + (returnTypes.length ? '{' + returnTypes.join('|') + '}' : '') + ''; } } function addSignatureTypes(f) { const types = helper.getSignatureTypes(f); f.signature = (f.signature || '') + '' + (types.length ? ' :' + types.join('|') : '') + ' '; } function shortenPaths(files, commonPrefix) { // always use forward slashes const regexp = new RegExp('\\\\', 'g'); Object.keys(files).forEach(function(file) { files[file].shortened = files[file].resolved.replace(commonPrefix, '') .replace(regexp, '/'); }); return files; } function resolveSourcePath(filepath) { return path.resolve(process.cwd(), filepath); } function getPathFromDoclet(doclet) { if (!doclet.meta) { return; } const filepath = doclet.meta.path && doclet.meta.path !== 'null' ? doclet.meta.path + '/' + doclet.meta.filename.split(/[\/\\]/).pop() : doclet.meta.filename; return filepath; } function generate(title, docs, filename, resolveLinks) { resolveLinks = resolveLinks === false ? false : true; const docData = { filename: filename, title: title, docs: docs, packageInfo: (find({kind: 'package'}) || []) [0] }; const outpath = path.join(outdir, filename); let html = view.render('container.tmpl', docData); if (resolveLinks) { html = helper.resolveLinks(html); // turn {@link foo} into foo } fs.writeFileSync(outpath, html, 'utf8'); } function generateSourceFiles(sourceFiles) { Object.keys(sourceFiles).forEach(function(file) { let source; // links are keyed to the shortened path in each doclet's `meta.filename` property const sourceOutfile = helper.getUniqueFilename(sourceFiles[file].shortened); helper.registerLink(sourceFiles[file].shortened, sourceOutfile); try { source = { kind: 'source', code: helper.htmlsafe(fs.readFileSync(sourceFiles[file].resolved, 'utf8')) }; } catch (e) { handle(e); } generate('Source: ' + sourceFiles[file].shortened, [source], sourceOutfile, false); }); } /** * Look for classes or functions with the same name as modules (which indicates that the module * exports only that class or function), then attach the classes or functions to the `module` * property of the appropriate module doclets. The name of each class or function is also updated * for display purposes. This function mutates the original arrays. * * @private * @param {Array} doclets - The array of classes and functions to * check. * @param {Array} modules - The array of module doclets to search. */ function attachModuleSymbols(doclets, modules) { const symbols = {}; // build a lookup table doclets.forEach(function(symbol) { symbols[symbol.longname] = symbol; }); modules.forEach(function(module) { if (symbols[module.longname]) { module.module = symbols[module.longname]; module.module.name = module.module.name.replace('module:', 'require("') + '")'; } }); } function getPrettyName(longname) { return longname .split('~')[0] .replace('module:', ''); } /** * Create the navigation sidebar. * @param {object} members The members that will be used to create the sidebar. * @param {Array} members.classes Classes. * @param {Array} members.externals Externals. * @param {Array} members.globals Globals. * @param {Array} members.mixins Mixins. * @param {Array} members.modules Modules. * @param {Array} members.namespaces Namespaces. * @param {Array} members.tutorials Tutorials. * @param {Array} members.events Events. * @return {string} The HTML for the navigation sidebar. */ function buildNav(members) { const nav = []; // merge namespaces and classes, then sort const merged = members.modules.concat(members.classes); merged.sort(function(a, b) { const prettyNameA = getPrettyName(a.longname).toLowerCase(); const prettyNameB = getPrettyName(b.longname).toLowerCase(); if (prettyNameA > prettyNameB) { return 1; } if (prettyNameA < prettyNameB) { return -1; } return 0; }); _.each(merged, function(v) { // exclude interfaces from sidebar if (v.interface !== true && v.kind === 'class') { nav.push({ type: 'class', longname: v.longname, prettyname: getPrettyName(v.longname), name: v.name, module: find({ kind: 'module', longname: v.memberof })[0], members: find({ kind: 'member', memberof: v.longname }), methods: find({ kind: 'function', memberof: v.longname }), typedefs: find({ kind: 'typedef', memberof: v.longname }), fires: v.fires, events: find({ kind: 'event', memberof: v.longname }) }); } else if (v.kind == 'module') { const classes = find({ kind: 'class', memberof: v.longname }); const members = find({ kind: 'member', memberof: v.longname }); const methods = find({ kind: 'function', memberof: v.longname }); const typedefs = find({ kind: 'typedef', memberof: v.longname }); const events = find({ kind: 'event', memberof: v.longname }); // only add modules that have more to show than just a single class if (!classes.length || classes.length - 1 + members.length + methods.length + typedefs.length + events.length > 0) { nav.push({ type: 'module', longname: v.longname, prettyname: getPrettyName(v.longname), name: v.name, members: members, methods: methods, typedefs: typedefs, fires: v.fires, events: events }); } } }); return nav; } /** * @param {Object} taffyData See . * @param {Object} opts Options. * @param {Object} tutorials Tutorials. */ exports.publish = function(taffyData, opts, tutorials) { data = taffyData; const conf = env.conf.templates || {}; conf['default'] = conf['default'] || {}; const templatePath = opts.template; view = new template.Template(templatePath + '/tmpl'); // claim some special filenames in advance, so the All-Powerful Overseer of Filename Uniqueness // doesn't try to hand them out later const indexUrl = helper.getUniqueFilename('index'); // don't call registerLink() on this one! 'index' is also a valid longname const globalUrl = helper.getUniqueFilename('global'); helper.registerLink('global', globalUrl); // set up templating view.layout = 'layout.tmpl'; // set up tutorials for helper helper.setTutorials(tutorials); data = helper.prune(data); data.sort('longname, version, since'); helper.addEventListeners(data); let sourceFiles = {}; const sourceFilePaths = []; data().each(function(doclet) { doclet.attribs = ''; if (doclet.examples) { doclet.examples = doclet.examples.map(function(example) { let caption, code; if (example.match(/^\s*([\s\S]+?)<\/caption>(\s*[\n\r])([\s\S]+)$/i)) { caption = RegExp.$1; code = RegExp.$3; } return { caption: caption || '', code: code || example }; }); } if (doclet.see) { doclet.see.forEach(function(seeItem, i) { doclet.see[i] = hashToLink(doclet, seeItem); }); } // build a list of source files let sourcePath; let resolvedSourcePath; if (doclet.meta) { sourcePath = getPathFromDoclet(doclet); resolvedSourcePath = resolveSourcePath(sourcePath); sourceFiles[sourcePath] = { resolved: resolvedSourcePath, shortened: null }; sourceFilePaths.push(resolvedSourcePath); } }); fs.mkPath(outdir); // copy the template's static files to outdir const fromDir = path.join(templatePath, 'static'); const staticFiles = fs.ls(fromDir, 3); staticFiles.forEach(function(fileName) { const toDir = fs.toDir(fileName.replace(fromDir, outdir)); fs.mkPath(toDir); fs.copyFileSync(fileName, toDir); }); // copy user-specified static files to outdir let staticFilePaths; let staticFileFilter; let staticFileScanner; if (conf['default'].staticFiles) { staticFilePaths = conf['default'].staticFiles.paths || []; staticFileFilter = new (require('jsdoc/lib/jsdoc/src/filter')).Filter(conf['default'].staticFiles); staticFileScanner = new (require('jsdoc/lib/jsdoc/src/scanner')).Scanner(); staticFilePaths.forEach(function(filePath) { const extraStaticFiles = staticFileScanner.scan([filePath], 10, staticFileFilter); extraStaticFiles.forEach(function(fileName) { const sourcePath = fs.statSync(filePath).isDirectory() ? filePath : path.dirname(filePath); const toDir = fs.toDir(fileName.replace(sourcePath, outdir)); fs.mkPath(toDir); fs.copyFileSync(fileName, toDir); }); }); } if (sourceFilePaths.length) { sourceFiles = shortenPaths(sourceFiles, path.commonPrefix(sourceFilePaths)); } data().each(function(doclet) { const url = helper.createLink(doclet); helper.registerLink(doclet.longname, url); // replace the filename with a shortened version of the full path let docletPath; if (doclet.meta) { docletPath = getPathFromDoclet(doclet); docletPath = sourceFiles[docletPath].shortened; if (docletPath) { doclet.meta.filename = docletPath; } } }); data().each(function(doclet) { const url = helper.longnameToUrl[doclet.longname]; if (url.indexOf('#') > -1) { doclet.id = helper.longnameToUrl[doclet.longname].split(/#/).pop(); } else { doclet.id = doclet.name; } if (needsSignature(doclet)) { addSignatureParams(doclet); addSignatureReturns(doclet); } }); // do this after the urls have all been generated data().each(function(doclet) { doclet.ancestors = getAncestorLinks(doclet); if (doclet.kind === 'member') { addSignatureTypes(doclet); } if (doclet.kind === 'constant') { addSignatureTypes(doclet); doclet.kind = 'member'; } }); const members = helper.getMembers(data); members.tutorials = tutorials.children; // add template helpers view.find = find; view.linkto = linkto; view.resolveAuthorLinks = resolveAuthorLinks; view.tutoriallink = tutoriallink; view.htmlsafe = htmlsafe; view.members = members; //@davidshimjs: To make navigation for customizing // once for all view.nav = buildNav(members); attachModuleSymbols(find({kind: ['class', 'function'], longname: {left: 'module:'}}), members.modules); // only output pretty-printed source files if requested; do this before generating any other // pages, so the other pages can link to the source files if (conf['default'].outputSourceFiles) { generateSourceFiles(sourceFiles); } if (members.globals.length) { generate('Global', [{kind: 'globalobj'}], globalUrl); } // index page displays information from package.json and lists files const files = find({kind: 'file'}); generate('Index', [{kind: 'mainpage', readme: opts.readme, longname: (opts.mainpagetitle) ? opts.mainpagetitle : 'Main Page'}].concat(files), indexUrl); // set up the lists that we'll use to generate pages const classes = taffy(members.classes); const modules = taffy(members.modules); const namespaces = taffy(members.namespaces); const mixins = taffy(members.mixins); const externals = taffy(members.externals); for (const longname in helper.longnameToUrl) { if (hasOwnProp.call(helper.longnameToUrl, longname)) { const myClasses = helper.find(classes, {longname: longname}); if (myClasses.length) { generate('Class: ' + myClasses[0].name, myClasses, helper.longnameToUrl[longname]); } const myModules = helper.find(modules, {longname: longname}); if (myModules.length) { generate('Module: ' + myModules[0].name, myModules, helper.longnameToUrl[longname]); } const myNamespaces = helper.find(namespaces, {longname: longname}); if (myNamespaces.length) { generate('Namespace: ' + myNamespaces[0].name, myNamespaces, helper.longnameToUrl[longname]); } const myMixins = helper.find(mixins, {longname: longname}); if (myMixins.length) { generate('Mixin: ' + myMixins[0].name, myMixins, helper.longnameToUrl[longname]); } const myExternals = helper.find(externals, {longname: longname}); if (myExternals.length) { generate('External: ' + myExternals[0].name, myExternals, 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 }; let html = view.render('tutorial.tmpl', tutorialData); // yes, you can use {@link} in tutorials too! html = helper.resolveLinks(html); // turn {@link foo} into foo const tutorialPath = path.join(outdir, filename); fs.writeFileSync(tutorialPath, html, 'utf8'); } // tutorials can have only one parent so there is no risk for loops function saveChildren(node) { node.children.forEach(function(child) { generateTutorial('Tutorial: ' + child.title, child, helper.tutorialToUrl(child.name)); saveChildren(child); }); } saveChildren(tutorials); };