@webdoc/legacy-template
Version:
Port of the JSDoc default template to webdoc!
627 lines (497 loc) • 17.4 kB
JavaScript
// @flow
// @webdoc/legacy-template uses flow comment syntax for easier migration!
/* global Webdoc */
const _ = require("lodash");
const commonPathPrefix = require("common-path-prefix");
const fs = require("fs");
const fse = require("fs-extra");
const path = require("path");
const helper = require("./helper");
const hasOwnProp = Object.prototype.hasOwnProperty;
const {
TemplateRenderer,
TemplatePipeline,
TemplateTagsResolver, // <<TemplatePipelineElement>>
SymbolLinks,
RelationsPlugin, // <<TemplateRendererPlugin>>
RepositoryPlugin, // <<TemplateRendererPlugin>>
} = require("@webdoc/template-library");
const {
doc: findDoc,
isClass,
isInterface,
isNamespace,
isMixin,
isModule,
isExternal,
traverse,
} = require("@webdoc/model");
const fsp = require("fs").promises;
const AttributesBuilder = require("./src/AttributesBuilder").AttributesBuilder;
const SignatureBuilder = require("./src/SignatureBuilder").SignatureBuilder;
/*::
import type {TypedMembers} from './helper';
import {TemplateRenderer, TemplatePipeline} from "@webdoc/template-library";
type SourceFile = {
resolved: string,
shortened: string
};
*/
let pipeline/*: TemplatePipeline */;
let view/*: TemplateRenderer */;
// linkto will be deprecated in favor of linkTo.
TemplateRenderer.prototype.linkto = SymbolLinks.linkTo;
// TODO: @webdoc/template-library should define linkTo on TemplateRenderer instead of us!
TemplateRenderer.prototype.linkTo = SymbolLinks.linkTo;
// Mixed-in method on TemplateRenderer for resolving the link to a Doc
TemplateRenderer.prototype.resolveDocLink = function(docLink) {
if (typeof docLink === "string") {
return this.linkTo(docLink, docLink);
}
return this.linkTo(docLink.path, docLink.path);
};
// Mixed-in method on TemplateRenderer for converting strings, Docs into HTML-safe strings
TemplateRenderer.prototype.htmlsafe = (str /*: string | Doc */) /*: string */ => {
if (typeof str !== "string") {
if (str.path) {
str = str.path;
} else {
str = String(str);
}
}
return str.replace(/&/g, "&")
.replace(/</g, "<");
};
const {Log, LogLevel, tag} = require("missionlog");
const klawSync = require("klaw-sync");
let publishLog;
let docDatabase;
const lsSync = ((dir, opts = {}) => {
const depth = _.has(opts, "depth") ? opts.depth : -1;
const files = klawSync(dir, {
depthLimit: depth,
filter: ((f) => !path.basename(f.path).startsWith(".")),
nodir: true,
});
return files.map((f) => f.path);
});
let env;
const FONT_NAMES = [
"OpenSans-Bold",
"OpenSans-BoldItalic",
"OpenSans-Italic",
"OpenSans-Light",
"OpenSans-LightItalic",
"OpenSans-Regular",
];
const PRETTIFIER_CSS_FILES = [
"tomorrow.min.css",
];
const PRETTIFIER_SCRIPT_FILES = [
"lang-css.js",
"prettify.js",
];
let outDir;
function mkdirpSync(filepath) {
return fs.mkdirSync(filepath, {recursive: true});
}
function hashToLink(doclet, hash) {
let url;
if ( !/^(#.+)/.test(hash) ) {
return hash;
}
url = SymbolLinks.createLink(doclet);
url = url.replace(/(#.+|$)/, hash);
return `<a href="${url}">${hash}</a>`;
}
/*::
type Signature = string
*/
function buildMemberNav(items, itemHeading, itemsSeen, linktoFn) {
let nav = "";
if (items.length) {
let itemsNav = "";
items.forEach((item) => {
let displayName;
if ( !hasOwnProp.call(item, "path") ) {
itemsNav += `<li>${linktoFn("", item.name)}</li>`;
} else if ( !hasOwnProp.call(itemsSeen, item.path) ) {
if (Webdoc.userConfig.template.useLongnameInNav) {
displayName = item.path;
} else {
displayName = item.name;
}
publishLog.info(tag.ContentBar, "Linking " + item.path);
itemsNav += `<li>${linktoFn(item.path, displayName.replace(/\b(module|event):/g, ""))}</li>`;// eslint-disable-line max-len
itemsSeen[item.path] = true;
}
});
if (itemsNav !== "") {
nav += `<h3>${itemHeading}</h3><ul>${itemsNav}</ul>`;
}
}
return nav;
}
function linktoExternal(longName, name) {
return SymbolLinks.linkTo(longName, name.replace(/(^"|"$)/g, ""));
}
// Builds the navigation sidebar's HTML content
function buildNav(members /*: TypedMembers */) /*: string */ {
let globalNav;
let nav = "<h2><a href=\"index.tmpl\">Home</a></h2>";
const seen = {};
nav += buildMemberNav(members.tutorials, "Tutorials", seen, SymbolLinks.linkTo);
nav += buildMemberNav(members.modules, "Modules", {}, SymbolLinks.linkTo);
nav += buildMemberNav(members.externals, "Externals", seen, linktoExternal);
nav += buildMemberNav(members.namespaces, "Namespaces", seen, SymbolLinks.linkTo);
nav += buildMemberNav(members.classes, "Classes", seen, SymbolLinks.linkTo);
nav += buildMemberNav(members.interfaces, "Interfaces", seen, SymbolLinks.linkTo);
nav += buildMemberNav(members.events, "Events", seen, SymbolLinks.linkTo);
nav += buildMemberNav(members.mixins, "Mixins", seen, SymbolLinks.linkTo);
if (members.globals.length) {
globalNav = "";
members.globals.forEach(({kind, longname, name}) => {
if ( kind !== "typedef" && !hasOwnProp.call(seen, longname) ) {
globalNav += `<li>${SymbolLinks.linkTo(longname, name)}</li>`;
}
seen[longname] = true;
});
if (!globalNav) {
// turn the heading into a link so you can actually get to the global page
nav += `<h3>${SymbolLinks.linkTo("global", "Global")}</h3>`;
} else {
nav += `<h3>Global</h3><ul>${globalNav}</ul>`;
}
}
return nav;
}
function initLogger(verbose = false) {
const defaultLevel = verbose ? "INFO" : "WARN";
publishLog = new Log().init(
{
TemplateGenerator: defaultLevel,
ContentBar: defaultLevel,
Signature: defaultLevel,
},
(level, tag, msg, params) => {
tag = `[${tag}]:`;
switch (level) {
case LogLevel.ERROR:
console.error(tag, msg, ...params);
break;
case LogLevel.WARN:
console.warn(tag, msg, ...params);
break;
case LogLevel.INFO:
console.info(tag, msg, ...params);
break;
default:
console.log(tag, msg, ...params);
break;
}
});
}
exports.publish = (options /*: PublishOptions */) => {
initLogger(!!options.verbose);
docDatabase = options.docDatabase;
const opts = options.opts;
const docTree = options.doctree;
const tutorials = options.tutorials;
const userConfig = global.Webdoc.userConfig;
env = options.config;
outDir = path.normalize(env.opts.destination);
let cwd;
const sourceFilePaths /*: Set<string> */ = new Set/*: <string */();
const sourceFiles = {};
let staticFilePaths;
let staticFiles;
const conf = env.conf.templates || {};
conf.default = conf.default || {};
const templatePath = __dirname;
// Setup template-renderer
view = new TemplateRenderer(path.join(templatePath, "tmpl"), docDatabase, docTree);
view.installPlugin("relations", RelationsPlugin);
view.plugins.relations.buildRelations();
// Setup repository-plugin for linking source-locations to a remote repository
if (userConfig.template.repository) {
view.installPlugin("repository", RepositoryPlugin);
view.plugins.repository.buildRepository(userConfig.template.repository);
}
// Setup template-rendering-pipeline
pipeline = new TemplatePipeline(view);
pipeline.pipe(new TemplateTagsResolver());
// Claim these special file-names beforehand!
const indexUrl = SymbolLinks.getFileName("index");
const globalUrl = SymbolLinks.getFileName("global");
SymbolLinks.registerLink("global", globalUrl);
// set up templating
view.layout = "layout.tmpl";
tutorials.forEach((t) => SymbolLinks.createLink(t));
// Compile a list of source files
traverse(docTree, (doc /*: Doc */) => {
doc.attribs = "";
if (doc.see) {
doc.see.forEach((seeItem, i) => {
doc.see[i] = hashToLink(doc, seeItem);
});
}
// Build a list of source files
if (doc.loc) {
const sourcePath = doc.loc.fileName;
sourceFiles[sourcePath] = {
resolved: sourcePath,
shortened: null,
};
if (!sourceFilePaths.has(sourcePath)) {
sourceFilePaths.add(sourcePath);
}
}
});
fse.ensureDirSync(outDir);
// Path to @webdoc/legacy-template/static
const fromDir = path.join(templatePath, "static");
// Copy the template's static files to outDir
fse.copy(fromDir, outDir).then(() => {
// Copy the fonts used by the template to outDir
staticFiles = lsSync(path.join(require.resolve("open-sans-fonts"), "..", "open-sans"));
staticFiles.forEach((fileName) => {
const toPath = path.join(outDir, "fonts", path.basename(fileName));
if (FONT_NAMES.includes(path.parse(fileName).name)) {
mkdirpSync(path.dirname(toPath));
fs.copyFileSync(fileName, toPath);
}
});
// Copy the prettify script to outDir
PRETTIFIER_SCRIPT_FILES.forEach((fileName) => {
const toPath = path.join(outDir, "scripts", path.basename(fileName));
fs.copyFileSync(
path.join(require.resolve("code-prettify"), "..", fileName),
toPath,
);
});
// Copy the prettify CSS to outDir
PRETTIFIER_CSS_FILES.forEach((fileName) => {
const toPath = path.join(outDir, "styles", path.basename(fileName));
fs.copyFileSync(
require.resolve("color-themes-for-google-code-prettify/dist/themes/" + fileName),
toPath,
);
});
// Copy user-specified static files to outDir
if (userConfig.template.staticFiles) {
staticFilePaths =
userConfig.template.staticFiles.include ||
userConfig.template.staticFiles.paths ||
[];
cwd = process.cwd();
staticFilePaths.forEach((filePath) => {
const destPath = path.resolve(path.join(outDir, path.relative(fromDir, filePath)));
filePath = path.resolve(cwd, filePath);
fse.copy(filePath, destPath).catch((e) => {
throw e;
});
});
}
}).catch((e) => {
throw e;
});
const sourceFileNames /*: Set<string */ = new Set/*: <string > */();
if (sourceFilePaths.size) {
const commonDir = commonPathPrefix([...sourceFilePaths]);
Object.keys(sourceFiles).forEach((file) => {
sourceFiles[file].shortened = sourceFiles[file].resolved.replace(commonDir, "")
.replace(/\\/g, "/");// always use forward slashes
});
for (const filePath in sourceFiles) {
// eslint-disable-next-line no-prototype-builtins
if (sourceFiles.hasOwnProperty(filePath)) {
const sourceFile = sourceFiles[filePath];
sourceFileNames.add(sourceFile.shortened);
}
}
}
// Create a hyperlink for each document
traverse(docTree, (doc /*: Doc */) => {
if (doc.type === "RootDoc") {
return;
}
const url = SymbolLinks.createLink(doc);
SymbolLinks.registerLink(
doc.path,
url,
);
});
// Generate an ID and build signature, attributes for each document, resolve ancestors
traverse(docTree, (doc /*: Doc */) => {
if (doc.type === "RootDoc") {
return;
}
const url = SymbolLinks.pathToUrl.get(doc.path);
if (url.includes("#")) {
doc.id = url.split(/#/).pop();
} else {
doc.id = doc.name;
}
// Add signature, attributes information to the doc
if (SignatureBuilder.needsSignature(doc)) {
SignatureBuilder.appendParameters(doc);
SignatureBuilder.appendReturns(doc);
AttributesBuilder.appendAttribs(doc);
}
doc.ancestors = SymbolLinks.getAncestorLinks(doc);
if (doc.type === "PropertyDoc" || doc.type === "EnumDoc") {
SignatureBuilder.appendTypes(doc);
AttributesBuilder.appendAttribs(doc);
}
});
const members = helper.getTypedMembers(docTree);
members.tutorials = tutorials;
function tutorialLinkGen(tut) {
SymbolLinks.createLink(tut);
tut.members.forEach((c) => tutorialLinkGen(c));
}
tutorials.forEach((t) => tutorialLinkGen(t));
const outputSourceFiles = userConfig.template.outputSourceFiles;
// once for all
view.nav = buildNav(members);
// generate the pretty-printed source files first so other pages can link to them
if (outputSourceFiles) {
generateSourceFiles(sourceFiles, opts.encoding);
}
if (members.globals.length) {
generate("Global", [{type: "globals"}], globalUrl);
}
generateHomePage(indexUrl, docTree);
const docPaths = SymbolLinks.pathToUrl.keys();
let docPathEntry = docPaths.next();
let docPath = docPathEntry.value;
while (!docPathEntry.done) {
let doc;
// Sometimes docPath is undefined!
if (docPath) {
doc = findDoc(docPath, docTree);
}
if (!doc) {
if (!sourceFilePaths.has(docPath) &&
!sourceFileNames.has(docPath) &&
docPath !== "index" &&
docPath !== "global") {
console.log(docPath + " doesn't point to a doc");
}
} else {
const docUrl = SymbolLinks.pathToUrl.get(docPath);
if (isClass(doc)) {
generate(`Class: ${doc.name}`, [doc], docUrl);
} else if (isInterface(doc)) {
generate(`Interface: ${doc.name}`, [doc], docUrl);
} else if (isNamespace(doc)) {
generate(`Namespace: ${doc.name}`, [doc], docUrl);
} else if (isMixin(doc)) {
generate(`Mixin: ${doc.name}`, [doc], docUrl);
} else if (isModule(doc)) {
generate(`Module: ${doc.name}`, [doc], docUrl);
} else if (isExternal(doc)) {
generate(`External: ${doc.name}`, [doc], docUrl);
}
}
// Update docPathEntry & docPath together
docPathEntry = docPaths.next();
docPath = docPathEntry.value;
}
const generatedTutorials = new Set/*: <string> */();
// tutorials can have only one parent so there is no risk for loops
function generateTutorialRecursive({members}) {
members.forEach((child) => {
if (generatedTutorials.has(child.name)) {
return;
}
generateTutorial(`Tutorial: ${child.title}`, child, SymbolLinks.pathToUrl.get(child.path));
generateTutorialRecursive(child);
generatedTutorials.add(child.name);
});
}
generateTutorialRecursive({members: tutorials});
};
// Generate a HTML page that contains the documentation for the given docs.
function generate(title, docs, filename) {
const docData = {
env: env,
title: title,
docs: docs,
};
const outpath = path.join(outDir, filename);
const html = pipeline.render("container.tmpl", docData);
fs.writeFile(outpath, html, "utf8", (err) => {
if (!err) {
return;
}
console.error("@webdoc/<template>: Couldn't write file " + outpath);
throw err;
});
}
// Generate the home page, this loads the top-level members, packages, and README
async function generateHomePage(pagePath /*: string */, rootDoc /*: RootDoc */) /*: void */ {
const userConfig = Webdoc.userConfig;
// index page displays information from package.json and lists files
const files = docDatabase({kind: "file"}).get();
const packages = docDatabase({kind: "package"}).get();
const arr = rootDoc.members.filter((doc) =>
doc.type === "FunctionDoc" ||
doc.type === "EnumDoc" ||
doc.type === "MethodDoc" ||
doc.type === "PropertyDoc" ||
doc.type === "TypedefDoc");
const readme = userConfig.template.readme;
let readmeContent = "";
if (readme) {
const readmePath = path.join(process.cwd(), readme);
readmeContent = await fsp.readFile(readmePath, "utf8");
const markdownRenderer = require("markdown-it")({
breaks: false,
html: true,
})
.use(require("markdown-it-highlightjs"));
readmeContent = markdownRenderer.render(readmeContent);
}
generate("Home",
packages.concat(
[{
type: "mainPage",
readme: readmeContent,
path: userConfig.template.mainPage.title,
children: arr,
members: arr,
}],
).concat(files), pagePath);
}
// Generate a tutorial page
function generateTutorial(title /*: string */, tutorial /*: Tutorial */, filename /*: string */) {
const tutorialData = {
title: title,
header: tutorial.title,
content: tutorial.content,
children: tutorial.members,
};
const tutorialPath = path.join(outDir, filename);
const html = view.render("tutorial.tmpl", tutorialData);
fs.writeFileSync(tutorialPath, html, "utf8");
}
// Generate the source-file pages
function generateSourceFiles(sourceFiles, encoding = "utf8") {
Object.keys(sourceFiles).forEach((file) => {
let source;
// links are keyed to the shortened path in each doclet's `meta.shortpath` property
const sourceOutfile = SymbolLinks.getFileName(sourceFiles[file].shortened);
SymbolLinks.registerLink(sourceFiles[file].shortened, sourceOutfile);
SymbolLinks.registerLink(sourceFiles[file].resolved, sourceOutfile);
try {
source = {
type: "sourceFile",
code: helper.toHtmlSafeString( fs.readFileSync(sourceFiles[file].resolved, encoding) ),
};
} catch (e) {
publishLog.error("SourceFile", `Error while generating source file ${file}: ${e.message}`);
}
generate(`Source: ${sourceFiles[file].shortened}`, [source], sourceOutfile, false);
});
}