crossbrowdy
Version:
A Multimedia JavaScript framework to create real cross-platform and hybrid game engines, games, emulators, multimedia libraries and apps.
897 lines (746 loc) • 25.4 kB
JavaScript
/**
* @module template/publish
* @type {*}
*/
/*global env: true */
var template = require('jsdoc/template'),
doop = require('jsdoc/util/doop'),
fs = require('jsdoc/fs'),
_ = require('underscore'),
path = require('jsdoc/path'),
taffy = require('taffydb').taffy,
handle = require('jsdoc/util/error').handle,
helper = require('jsdoc/util/templateHelper'),
moment = require("moment"),
htmlsafe = helper.htmlsafe,
sanitizeHtml = require('sanitize-html'),
linkto = helper.linkto,
resolveAuthorLinks = helper.resolveAuthorLinks,
scopeToPunc = helper.scopeToPunc,
hasOwnProp = Object.prototype.hasOwnProperty,
conf = env.conf.templates || {},
data,
view,
outdir = env.opts.destination,
searchEnabled = conf.search !== false;
var globalUrl = helper.getUniqueFilename('global');
var indexUrl = helper.getUniqueFilename('index');
var navOptions = {
includeDate: conf.includeDate !== false,
logoFile: conf.logoFile,
systemName: conf.systemName || "Documentation",
navType: conf.navType || "vertical",
footer: conf.footer || "",
copyright: conf.copyright || "",
theme: conf.theme || "simplex",
syntaxTheme: conf.syntaxTheme || "default",
linenums: conf.linenums,
collapseSymbols: conf.collapseSymbols || false,
inverseNav: conf.inverseNav,
outputSourceFiles: conf.outputSourceFiles === true,
sourceRootPath: conf.sourceRootPath,
disablePackagePath: conf.disablePackagePath,
outputSourcePath: conf.outputSourcePath,
dateFormat: conf.dateFormat,
analytics: conf.analytics || null,
methodHeadingReturns: conf.methodHeadingReturns === true,
sort: conf.sort,
search: searchEnabled
};
var searchableDocuments = {};
var navigationMaster = {
index: {
title: navOptions.systemName,
link: indexUrl,
members: []
},
namespace: {
title: "Namespaces",
link: helper.getUniqueFilename("namespaces.list"),
members: []
},
module: {
title: "Modules",
link: helper.getUniqueFilename("modules.list"),
members: []
},
class: {
title: "Classes",
link: helper.getUniqueFilename('classes.list'),
members: []
},
mixin: {
title: "Mixins",
link: helper.getUniqueFilename("mixins.list"),
members: []
},
event: {
title: "Events",
link: helper.getUniqueFilename("events.list"),
members: []
},
interface: {
title: "Interfaces",
link: helper.getUniqueFilename("interfaces.list"),
members: []
},
tutorial: {
title: "Tutorials",
link: helper.getUniqueFilename("tutorials.list"),
members: []
},
global: {
title: "Global",
link: globalUrl,
members: []
},
external: {
title: "Externals",
link: helper.getUniqueFilename("externals.list"),
members: []
}
};
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;
}
var url = helper.createLink(doclet);
url = url.replace(/(#.+|$)/, hash);
return '<a href="' + url + '">' + hash + '</a>';
}
function needsSignature(doclet) {
var needsSig = false;
// function and class definitions always get a signature
if (doclet.kind === 'function' || doclet.kind === 'class') {
needsSig = true;
}
// typedefs that contain functions get a signature, too
else if (doclet.kind === 'typedef' && doclet.type && doclet.type.names &&
doclet.type.names.length) {
for (var 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) {
var optionalClass = 'optional';
var params = helper.getSignatureParams(f, optionalClass);
f.signature = (f.signature || '') + '(';
for (var i = 0, l = params.length; i < l; i++) {
var element = params[i];
var seperator = (i > 0) ? ', ' : '';
if (!new RegExp("class=[\"|']"+optionalClass+"[\"|']").test(element)) {
f.signature += seperator + element;
} else {
var regExp = new RegExp("<span class=[\"|']"+optionalClass+"[\"|']>(.*?)<\\/span>", "i");
f.signature += element.replace(regExp, " $`["+seperator+"$1$']");
}
}
f.signature += ')';
}
function addSignatureReturns(f) {
if (navOptions.methodHeadingReturns) {
var returnTypes = helper.getSignatureReturns(f);
f.signature = '<span class="signature">' + (f.signature || '') + '</span>' + '<span class="type-signature">' + (returnTypes.length ? ' → {' + returnTypes.join('|') + '}' : '') + '</span>';
}
else {
f.signature = f.signature || '';
}
}
function addSignatureTypes(f) {
var types = helper.getSignatureTypes(f);
f.signature = (f.signature || '') + '<span class="type-signature">' + (types.length ? ' :' + types.join('|') : '') + '</span>';
}
function addAttribs(f) {
var attribs = helper.getAttribs(f);
f.attribs = '<span class="type-signature">' + htmlsafe(attribs.length ? '<' + attribs.join(', ') + '> ' : '') + '</span>';
}
function shortenPaths(files, commonPrefix) {
// // always use forward slashes
// var regexp = new RegExp( '\\\\', 'g' );
//
// var prefix = commonPrefix.toLowerCase().replace( regexp, "/" );
//
// Object.keys( files ).forEach( function ( file ) {
// files[file].shortened = files[file]
// .resolved
// .toLowerCase()
// .replace( regexp, '/' )
// .replace( prefix, '' );
// } );
Object.keys(files).forEach(function(file) {
files[file].shortened = files[file].resolved.replace(commonPrefix, '')
// always use forward slashes
.replace(/\\/g, '/');
});
return files;
}
function getPathFromDoclet(doclet) {
if (!doclet.meta) {
return;
}
return path.normalize(doclet.meta.path && doclet.meta.path !== 'null' ?
doclet.meta.path + '/' + doclet.meta.filename :
doclet.meta.filename);
}
function searchData(html) {
var startOfContent = html.indexOf("<div class=\"container\">");
if (startOfContent > 0) {
var startOfSecondContent = html.indexOf("<div class=\"container\">", startOfContent + 2);
if (startOfSecondContent > 0) {
startOfContent = startOfSecondContent;
}
html = html.slice(startOfContent);
}
var endOfContent = html.indexOf("<span class=\"copyright\">");
if (endOfContent > 0) {
html = html.substring(0, endOfContent);
}
var stripped = sanitizeHtml(html, {allowedTags: [], allowedAttributes: []});
stripped = stripped.replace(/\s+/g, ' ');
return stripped;
}
function generate(docType, title, docs, filename, resolveLinks) {
resolveLinks = resolveLinks === false ? false : true;
var docData = {
title: title,
docs: docs,
docType: docType
};
var outpath = path.join(outdir, filename),
html = view.render('container.tmpl', docData);
if (resolveLinks) {
html = helper.resolveLinks(html); // turn {@link foo} into <a href="foodoc.html">foo</a>
}
if (searchEnabled) {
searchableDocuments[filename] = {
"id": filename,
"title": title,
"body": searchData(html)
};
}
fs.writeFileSync(outpath, html, 'utf8');
}
function generateSourceFiles(sourceFiles) {
Object.keys(sourceFiles).forEach(function(file) {
var source;
// links are keyed to the shortened path in each doclet's `meta.shortpath` property
var 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', '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.<module:jsdoc/doclet.Doclet>} doclets - The array of classes and functions to
* check.
* @param {Array.<module:jsdoc/doclet.Doclet>} modules - The array of module doclets to search.
*/
function attachModuleSymbols(doclets, modules) {
var symbols = {};
// build a lookup table
doclets.forEach(function(symbol) {
symbols[symbol.longname] = symbols[symbol.longname] || [];
symbols[symbol.longname].push(symbol);
});
return modules.map(function(module) {
if (symbols[module.longname]) {
module.modules = symbols[module.longname]
// Only show symbols that have a description. Make an exception for classes, because
// we want to show the constructor-signature heading no matter what.
.filter(function(symbol) {
return symbol.description || symbol.kind === 'class';
})
.map(function(symbol) {
symbol = doop(symbol);
if (symbol.kind === 'class' || symbol.kind === 'function') {
symbol.name = symbol.name.replace('module:', '(require("') + '"))';
}
return symbol;
});
}
});
}
/**
* Create the navigation sidebar.
* @param {object} members The members that will be used to create the sidebar.
* @param {array<object>} members.classes
* @param {array<object>} members.externals
* @param {array<object>} members.globals
* @param {array<object>} members.mixins
* @param {array<object>} members.interfaces
* @param {array<object>} members.modules
* @param {array<object>} members.namespaces
* @param {array<object>} members.tutorials
* @param {array<object>} members.events
* @return {string} The HTML for the navigation sidebar.
*/
function buildNav(members) {
var seen = {};
var nav = navigationMaster;
if (members.modules.length) {
members.modules.forEach(function(m) {
if (!hasOwnProp.call(seen, m.longname)) {
nav.module.members.push(linkto(m.longname, m.longname.replace("module:", "")));
}
seen[m.longname] = true;
});
}
if (members.externals.length) {
members.externals.forEach(function(e) {
if (!hasOwnProp.call(seen, e.longname)) {
nav.external.members.push(linkto(e.longname, e.name.replace(/(^"|"$)/g, '')));
}
seen[e.longname] = true;
});
}
if (members.classes.length) {
members.classes.forEach(function(c) {
if (!hasOwnProp.call(seen, c.longname)) {
nav.class.members.push(linkto(c.longname, c.longname.replace("module:", "")));
}
seen[c.longname] = true;
});
}
if (members.events.length) {
members.events.forEach(function(e) {
if (!hasOwnProp.call(seen, e.longname)) {
nav.event.members.push(linkto(e.longname, e.longname.replace("module:", "")));
}
seen[e.longname] = true;
});
}
if (members.namespaces.length) {
members.namespaces.forEach(function(n) {
if (!hasOwnProp.call(seen, n.longname)) {
nav.namespace.members.push(linkto(n.longname, n.longname.replace("module:", "")));
}
seen[n.longname] = true;
});
}
if (members.mixins.length) {
members.mixins.forEach(function(m) {
if (!hasOwnProp.call(seen, m.longname)) {
nav.mixin.members.push(linkto(m.longname, m.longname.replace("module:", "")));
}
seen[m.longname] = true;
});
}
if (members.interfaces && members.interfaces.length) {
members.interfaces.forEach(function(m) {
if (!hasOwnProp.call(seen, m.longname)) {
nav.interface.members.push(linkto(m.longname, m.longname.replace("module:", "")));
}
seen[m.longname] = true;
});
}
if (members.tutorials.length) {
members.tutorials.forEach(function(t) {
nav.tutorial.members.push(tutoriallink(t.name));
});
}
if (members.globals.length) {
members.globals.forEach(function(g) {
if (g.kind !== 'typedef' && !hasOwnProp.call(seen, g.longname)) {
nav.global.members.push(linkto(g.longname, g.longname.replace("module:", "")));
}
seen[g.longname] = true;
});
// even if there are no links, provide a link to the global page.
if (nav.global.members.length === 0) {
nav.global.members.push(linkto("global", "Global"));
}
}
var topLevelNav = [];
_.each(nav, function(entry, name) {
if (entry.members.length > 0 && name !== "index") {
topLevelNav.push({
title: entry.title,
link: entry.link,
members: entry.members
});
}
});
nav.topLevelNav = topLevelNav;
}
/**
@param {TAFFY} taffyData See <http://taffydb.com/>.
@param {object} opts
@param {Tutorial} tutorials
*/
exports.publish = function(taffyData, opts, tutorials) {
data = taffyData;
conf['default'] = conf['default'] || {};
var 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
// var indexUrl = helper.getUniqueFilename( 'index' );
// don't call registerLink() on this one! 'index' is also a valid longname
// var globalUrl = helper.getUniqueFilename( 'global' );
helper.registerLink('global', globalUrl);
// set up templating
// set up templating
view.layout = conf['default'].layoutFile ?
path.getResourcePath(path.dirname(conf['default'].layoutFile),
path.basename(conf['default'].layoutFile) ) : 'layout.tmpl';
// set up tutorials for helper
helper.setTutorials(tutorials);
data = helper.prune(data);
var sortOption = navOptions.sort === undefined ? opts.sort : navOptions.sort;
sortOption = sortOption === undefined ? true : sortOption;
sortOption = sortOption === true ? 'longname, version, since' : sortOption;
if (sortOption) {
data.sort(sortOption);
}
helper.addEventListeners(data);
var sourceFiles = {};
var sourceFilePaths = [];
data().each(function(doclet) {
doclet.attribs = '';
if (doclet.examples) {
doclet.examples = doclet.examples.map(function(example) {
var caption, lang;
// allow using a markdown parser on the examples captions (surrounded by useless HTML p tags)
if (example.match(/^\s*(<p>)?<caption>([\s\S]+?)<\/caption>(\s*)([\s\S]+?)(<\/p>)?$/i)) {
caption = RegExp.$2;
example = RegExp.$4 + (RegExp.$1 ? '' : RegExp.$5);
}
var lang = /{@lang (.*?)}/.exec(example);
if (lang && lang[1]) {
example = example.replace(lang[0], "");
lang = lang[1];
} else {
lang = null;
}
return {
caption: caption || '',
code: example,
lang: lang || "javascript"
};
});
}
if (doclet.see) {
doclet.see.forEach(function(seeItem, i) {
doclet.see[i] = hashToLink(doclet, seeItem);
});
}
// build a list of source files
var sourcePath;
if (doclet.meta) {
sourcePath = getPathFromDoclet(doclet);
sourceFiles[sourcePath] = {
resolved: sourcePath,
shortened: null
};
//Check to see if the array of source file paths already contains
// the source path, if not then add it
if (sourceFilePaths.indexOf(sourcePath) === -1) {
sourceFilePaths.push(sourcePath)
}
}
});
// update outdir if necessary, then create outdir
var packageInfo = (find({
kind: 'package'
}) || [])[0];
if (navOptions.disablePackagePath !== true && packageInfo && packageInfo.name) {
if (packageInfo.version) {
outdir = path.join(outdir, packageInfo.name, packageInfo.version);
} else {
outdir = path.join(outdir, packageInfo.name);
}
}
fs.mkPath(outdir);
// copy the template's static files to outdir
var fromDir = path.join( templatePath, 'static' );
var staticFiles = fs.ls( fromDir, 3 );
staticFiles.forEach( function ( fileName ) {
var toFile = fileName.replace( fromDir, outdir );
var toDir = fs.toDir( toFile );
fs.mkPath( toDir );
fs.copyFileSync( fileName, '', toFile );
} );
// copy user-specified static files to outdir
var staticFilePaths;
var staticFileFilter;
var staticFileScanner;
if (conf.default.staticFiles) {
// The canonical property name is `include`. We accept `paths` for backwards compatibility
// with a bug in JSDoc 3.2.x.
staticFilePaths = conf.default.staticFiles.include ||
conf.default.staticFiles.paths ||
[];
staticFileFilter = new (require('jsdoc/src/filter')).Filter(conf.default.staticFiles);
staticFileScanner = new (require('jsdoc/src/scanner')).Scanner();
staticFilePaths.forEach(function(filePath) {
var extraStaticFiles = staticFileScanner.scan([filePath], 10, staticFileFilter);
extraStaticFiles.forEach(function(fileName) {
var sourcePath = fs.toDir(filePath);
var toDir = fs.toDir( fileName.replace(sourcePath, outdir) );
fs.mkPath(toDir);
fs.copyFileSync(fileName, toDir);
});
});
}
if (sourceFilePaths.length) {
var payload = navOptions.sourceRootPath;
if (!payload) {
payload = path.commonPrefix(sourceFilePaths);
}
sourceFiles = shortenPaths(sourceFiles, payload);
}
data().each(function(doclet) {
var url = helper.createLink(doclet);
helper.registerLink(doclet.longname, url);
// add a shortened version of the full path
var docletPath;
if (doclet.meta) {
docletPath = getPathFromDoclet(doclet);
if (!_.isEmpty(sourceFiles[docletPath])) {
docletPath = sourceFiles[docletPath].shortened;
if (docletPath) {
doclet.meta.shortpath = docletPath;
}
}
}
});
data().each(function(doclet) {
var 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);
addAttribs(doclet);
}
});
// do this after the urls have all been generated
data().each(function(doclet) {
doclet.ancestors = getAncestorLinks(doclet);
if (doclet.kind === 'member') {
addSignatureTypes(doclet);
addAttribs(doclet);
}
if (doclet.kind === 'constant') {
addSignatureTypes(doclet);
addAttribs(doclet);
doclet.kind = 'member';
}
});
var 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.moment = moment;
// once for all
buildNav(members);
view.nav = navigationMaster;
view.navOptions = navOptions;
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 (navOptions.outputSourceFiles) {
generateSourceFiles(sourceFiles);
}
if (members.globals.length) {
generate('global', 'Global', [{
kind: 'globalobj'
}], globalUrl);
}
// some browsers can't make the dropdown work
if (view.nav.module && view.nav.module.members.length) {
generate('module', view.nav.module.title, [{
kind: 'sectionIndex',
contents: view.nav.module
}], navigationMaster.module.link);
}
if (view.nav.class && view.nav.class.members.length) {
generate('class', view.nav.class.title, [{
kind: 'sectionIndex',
contents: view.nav.class
}], navigationMaster.class.link);
}
if (view.nav.namespace && view.nav.namespace.members.length) {
generate('namespace', view.nav.namespace.title, [{
kind: 'sectionIndex',
contents: view.nav.namespace
}], navigationMaster.namespace.link);
}
if (view.nav.mixin && view.nav.mixin.members.length) {
generate('mixin', view.nav.mixin.title, [{
kind: 'sectionIndex',
contents: view.nav.mixin
}], navigationMaster.mixin.link);
}
if (view.nav.interface && view.nav.interface.members.length) {
generate('interface', view.nav.interface.title, [{
kind: 'sectionIndex',
contents: view.nav.interface
}], navigationMaster.interface.link);
}
if (view.nav.external && view.nav.external.members.length) {
generate('external', view.nav.external.title, [{
kind: 'sectionIndex',
contents: view.nav.external
}], navigationMaster.external.link);
}
if (view.nav.tutorial && view.nav.tutorial.members.length) {
generate('tutorial', view.nav.tutorial.title, [{
kind: 'sectionIndex',
contents: view.nav.tutorial
}], navigationMaster.tutorial.link);
}
// index page displays information from package.json and lists files
var files = find({
kind: 'file'
}),
packages = find({
kind: 'package'
});
generate('index', 'Index',
packages.concat(
[{
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
var classes = taffy(members.classes);
var modules = taffy(members.modules);
var namespaces = taffy(members.namespaces);
var mixins = taffy(members.mixins);
var interfaces = taffy(members.interfaces);
var externals = taffy(members.externals);
for (var longname in helper.longnameToUrl) {
if (hasOwnProp.call(helper.longnameToUrl, longname)) {
var myClasses = helper.find(classes, {
longname: longname
});
if (myClasses.length) {
generate('class', 'Class: ' + myClasses[0].name, myClasses, helper.longnameToUrl[longname]);
}
var myModules = helper.find(modules, {
longname: longname
});
if (myModules.length) {
generate('module', 'Module: ' + myModules[0].name, myModules, helper.longnameToUrl[longname]);
}
var myNamespaces = helper.find(namespaces, {
longname: longname
});
if (myNamespaces.length) {
generate('namespace', 'Namespace: ' + myNamespaces[0].name, myNamespaces, helper.longnameToUrl[longname]);
}
var myMixins = helper.find(mixins, {
longname: longname
});
if (myMixins.length) {
generate('mixin', 'Mixin: ' + myMixins[0].name, myMixins, helper.longnameToUrl[longname]);
}
var myInterfaces = helper.find(interfaces, {
longname: longname
});
if (myInterfaces.length) {
generate('interface', 'Interface: ' + myInterfaces[0].name, myInterfaces, helper.longnameToUrl[longname]);
}
var myExternals = helper.find(externals, {
longname: longname
});
if (myExternals.length) {
generate('external', 'External: ' + myExternals[0].name, myExternals, helper.longnameToUrl[longname]);
}
}
}
// TODO: move the tutorial functions to templateHelper.js
function generateTutorial(title, tutorial, filename) {
var tutorialData = {
title: title,
header: tutorial.title,
content: tutorial.parse(),
children: tutorial.children,
docs: null
};
var tutorialPath = path.join(outdir, filename),
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>
if (searchEnabled) {
searchableDocuments[filename] = {
"id": filename,
"title": title,
"body": searchData(html)
};
}
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);
});
}
function generateQuickTextSearch(templatePath, searchableDocuments, navOptions) {
var data = {
searchableDocuments: JSON.stringify(searchableDocuments),
navOptions: navOptions
};
var tmplString = fs.readFileSync(templatePath + "/quicksearch.tmpl").toString(),
tmpl = _.template(tmplString);
var html = tmpl(data),
outpath = path.join(outdir, "quicksearch.html");
fs.writeFileSync(outpath, html, "utf8");
}
saveChildren(tutorials);
if (searchEnabled) {
generateQuickTextSearch(templatePath + '/tmpl', searchableDocuments, navOptions);
}
};
;