Source: \publish.js
/** @module publish */
const path = require('path');
const ejs = require('ejs');
const fs = require('fs-extra-promise');
const slugify = require('slugify');
const bluebird = require('bluebird');
const LinkHelper = require('./src/helpers/link');
const helpers = {
link: null,
};
/**
* A custom logger.
* @todo replace console calls with Function.prototype when options.debug === false
* @type {Object}
*/
const logger = {
/* eslint-disable no-console */
log: console.log.bind(console),
info: console.info.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
/* eslint-enable no-console */
};
function copyFile(src, dest) {
return fs.readFileAsync(src)
.then(content => fs.outputFile(dest, content));
}
function renderTemplate(relpath, data, options) {
return new Promise((resolve, reject) => {
data.helpers = helpers;
ejs.renderFile(path.resolve(__dirname, relpath), data, options, (err, html) => {
if (err) {
logger.error(err);
return reject(err);
}
resolve(html);
});
});
}
function createRecordPage(outpath, page, wrapper) {
return renderTemplate('tpl/page/record.ejs', page)
.then(html => wrapper(html, page.record.longname))
.then(html => fs.outputFileAsync(path.resolve(outpath, page.recordUrl), html));
}
/**
* Generate documentation output.
*
* @param {TAFFY} data - A TaffyDB collection representing
* all the symbols documented in your code.
* @param {object} opts - An object with options information.
*/
exports.publish = function(data, opts) {
const conf = opts.configure ? JSON.parse(fs.readFileSync(path.resolve(process.cwd(), opts.configure), 'utf-8')) : null;
const pkg = {
name: conf ? conf.title : 'documentation',
};
const tplConf = conf && conf.template ? conf.template : {};
helpers.link = new LinkHelper(tplConf);
const outpath = path.resolve(process.cwd(), opts.destination || 'docs');
// @todo handle multiple entries
const srcpath = path.resolve(process.cwd(), opts._[0]);
const menuData = {};
const pages = [];
// store list of classes declared with "unique identifier" generated from name,
// filename and lineno.
const declaredClasses = new Set();
// @todo move this thing in a "outputTaffy" debug template
// fs.outputFileAsync('./taffydata.json', data().stringify()).then(() => logger.log('output done'));
data({ name: 'AccessibilityManager' }).each(record => console.log(record));
// find modules members
data({ kind: 'module' }).each((module) => {
module.$functions = [];
module.$typedefs = [];
module.$constants = [];
module.$classes = [];
module.$other = [];
data({ memberof: module.longname, undocumented: { '!is': true } }).each(moduleMember => {
moduleMember.skip = true;
switch (moduleMember.kind) {
case 'function':
module.$functions.push(moduleMember);
break;
case 'typedef':
module.$typedefs.push(moduleMember);
break;
case 'constant':
module.$constants.push(moduleMember);
break;
case 'classes':
module.$classes.push(moduleMember);
break;
default:
module.$other.push(moduleMember);
break;
}
});
});
// defines members arrays
data({ kind: 'class' }).each((record) => {
record.$methods = [];
record.$attributes = [];
record.$staticmethods = [];
record.$staticproperties = [];
record.$augments = [];
record.$augmentedBy = [];
});
// associate classes with members
data({ kind: 'class' }).each((record) => {
if (record.description) {
const copy = JSON.parse(JSON.stringify(record));
copy.name = 'constructor';
copy.classConstructor = true;
copy.slug = 'constructor';
record.$methods.push(copy);
}
// find members
data({
memberof: record.longname,
}).each((memberRecord) => {
if (memberRecord.undocumented) {
return;
}
memberRecord.skip = true;
memberRecord.slug = slugify(memberRecord.longname);
if (memberRecord.meta.code.type === 'MethodDefinition') {
record.$methods.push(memberRecord);
} else {
record.$attributes.push(memberRecord);
}
});
// find agmented
if (record.augments) {
record.augments.forEach(typeName => {
const superclassRecord = data({ longname: typeName }).first();
if (superclassRecord && superclassRecord.$augmentedBy !== undefined) {
record.$augments.push(superclassRecord);
superclassRecord.$augmentedBy.push({ name: record.name });
} else {
record.$augments.push({ name: typeName });
}
});
}
});
logger.log('================');
let sourceFiles = [];
data().each((record) => {
if (!record.meta) {
if (record.kind === 'package') {
sourceFiles = record.files;
}
return;
}
if (record.undocumented) return;
const { name, kind, scope } = record;
const { filename, path: filepath, lineno } = record.meta;
console.log(filepath, srcpath);
const relpath = filepath !== srcpath ? filepath.replace(srcpath, '').slice(1, filepath.length - srcpath.length) : '';
const fullpath = path.join(relpath, filename);
const modulepath = relpath;
const menupath = tplConf.referencePaths !== 'folder' ? fullpath.replace('.js', '') : modulepath;
// prefix with menu object keys with `_` to avoid conflict with native
// object methods. e.g. if `menupath === "toString"`
const menukey = `_${menupath}`;
const filepath2 = path.join(relpath, filename);
record.filepath = filepath2;
logger.log(name, '(', fullpath, '-', lineno, ')', kind, scope);
if (kind === 'class' || kind === 'function') {
// check this is not a duplicate record
const classUniqueKey = `${record.londname}:${fullpath}:${lineno}`;
if (declaredClasses.has(classUniqueKey)) {
logger.warn('Dropping duplicate record ', record.longname);
return;
} else {
declaredClasses.add(classUniqueKey);
}
}
if (!menuData[menukey]) {
menuData[menukey] = [];
}
let klassRecord = null;
if (record.meta.code.type === 'MethodDefinition') {
klassRecord = data({ name: record.memberof, kind: 'class' }).first();
if (name === 'applyBlueprint') {
logger.log(klassRecord);
}
if (klassRecord) {
record.skip = true;
} else {
// logger.warn('member will not show up in documentation');
}
} else if (record.scope === 'instance' && record.memberof) {
// attribute
klassRecord = data({ name: record.memberof, kind: 'class' }).first();
if (klassRecord) {
record.skip = true;
} else {
// logger.warn('member attribute will not show up in documentation');
}
}
if (record.kind !== 'member' && !record.skip) {
recordUrl = slugify(`${menupath}-${name}.html`);
if (recordUrl.startsWith('-')) {
recordUrl = recordUrl.slice(1, recordUrl.length - 1);
}
helpers.link.registerType(name, recordUrl);
menuData[menukey].push({
name: name,
kind: kind,
record: record,
scope: scope,
recordUrl: recordUrl,
});
pages.push({
name: name,
kind: kind,
record: record,
tplConf: tplConf,
recordUrl: recordUrl,
});
record.url = recordUrl;
}
});
// build record links
data({ kind: 'typedef', skip: true }).each((record) => {
const moduleRecord = data({ longname: record.memberof }).first();
if (moduleRecord) {
const url = `${moduleRecord.url}#${record.name}`;
helpers.link.registerType(record.name, url);
helpers.link.registerType(record.longname.replace(/^module\:/, ''), url);
record.url = url;
console.log('REGISTER', record.name);
console.log('REGISTER', record.longname);
}
});
let _menuhtml = null;
const menuWrapper = (html, pageTitle) => renderTemplate('tpl/index.ejs', {
menu: _menuhtml,
title: `${pageTitle || 'Doclet'} - ${pkg.name}`,
content: html,
tplConf: tplConf,
});
renderTemplate('tpl/menu/menu.ejs', { menuData: menuData })
.then(html => {
_menuhtml = html;
return html;
})
// index page
.then(html => renderTemplate('tpl/index.ejs', {
menu: html,
title: `Home - ${pkg.name}`,
content: `${opts.readme}
`,
tplConf: tplConf,
}))
.then(html => fs.outputFileAsync(path.resolve(outpath, 'index.html'), html))
.then(() => bluebird.map(pages, page => createRecordPage(outpath, page, menuWrapper)))
.then(copyFile(path.resolve(__dirname, 'tpl/style.css'), path.resolve(outpath, 'style.css')))
.then(() => bluebird.map(sourceFiles, filepath => {
const relpath = filepath.replace(srcpath, '');
return fs.readFileAsync(filepath, 'utf-8')
.then(source => renderTemplate('tpl/page/source.ejs', {
source: source,
path: filepath.replace(srcpath, ''),
})).then(html => renderTemplate('tpl/index.ejs', {
menu: _menuhtml,
title: `source: ${relpath} - ${pkg.name}`,
content: html,
tplConf: tplConf,
}))
.then(html =>
fs.outputFileAsync(path.resolve(outpath, `source-${slugify(relpath)}.html`), html)
);
}, { concurrency: 5 }));
};