UNPKG

@webdoc/default-template

Version:
585 lines (511 loc) 16.3 kB
// @flow // $FlowFixMe const {Worker} = require("worker_threads"); const {crawl} = require("./helper/crawl"); const fs = require("fs"); const fse = require("fs-extra"); const hljs = require("highlight.js"); const os = require("os"); const path = require("path"); const {traverse} = require("@webdoc/model"); const { FlushToFile, RelationsPlugin, Sitemap, TemplateRenderer, TemplatePipeline, TemplateTagsResolver, RepositoryPlugin, } = require("@webdoc/template-library"); const {linker, prepareLinker} = require("./helper/linker"); const _ = require("lodash"); // Plugins const {indexSorterPlugin} = require("./helper/renderer-plugins/index-sorter"); const {signaturePlugin} = require("./helper/renderer-plugins/signature"); const {categoryFilterPlugin} = require("./helper/renderer-plugins/category-filter"); const {preprocessMarkupPlugin} = require("./helper/renderer-plugins/preprocess"); /*:: import type { Doc, RootDoc, SourceFile, TutorialDoc, } from "@webdoc/types"; import type {CrawlData} from './helper/crawl'; type AppBarItem = { name: string; uri: string; }; type AppBarData = { current: string; items: { [id: string]: AppBarItem }; }; */ Object.assign(linker.standaloneDocTypes, [ "ClassDoc", "EnumDoc", "FunctionDoc", "InterfaceDoc", "MixinDoc", "NSDoc", "PackageDoc", "TutorialDoc", "TypedefDoc", ]); // Static files in the "code-prettify" package that are used by the generated site const PRETTIFIER_SCRIPT_FILES = [ "lang-css.js", "prettify.js", ]; let idToDoc/*: Map<string, Doc> */; exports.publish = async function publish(options /*: PublishOptions */) { const config = options.config; const source = options.source; await prepareLinker(config); const docTree = options.documentTree; const outDir = path.normalize(options.config.opts.destination); const assetsDir = path.join(outDir, "./assets"); const index = config.template.readme ? linker.createURI("index") : null; const indexRelative = index ? index.replace(`/${linker.siteRoot}/`, "") : null; const settings = linker.createURI("settings"); const settingsRelative = settings.replace(`/${linker.siteRoot}/`, ""); const manifestNormalized = config.opts.export ? path.normalize(config.opts.export) : null; const manifest = config.opts.export && manifestNormalized.startsWith(outDir) ? path.join(linker.siteRoot, manifestNormalized.replace(outDir, "")) : null; fse.ensureDir(outDir); const crawlData = crawl(options.manifest, index); const alias = _.merge( { "bottom-banner": path.join(__dirname, "tmpl/components/bottom-banner/index.tmpl"), "explorer": path.join(__dirname, "tmpl/components/explorer/index.tmpl"), "footer": path.join(__dirname, "tmpl/components/footer/index.tmpl"), "header": path.join(__dirname, "tmpl/components/header/index.tmpl"), "tutorial": path.join(__dirname, "tmpl/tutorial.tmpl"), }, config.template.alias, ); const appBarItems = _.merge({}, config.template.appBar.items, { /* NOTE: config.template.appBar.items is the primary object so we retain the order as the user desires. */ ...(crawlData.reference && { "reference": { name: "API Reference", uri: index, }, }), ...(crawlData.tutorials && { "tutorials": { name: "Tutorials", uri: crawlData.tutorials.page || crawlData.tutorials.children[Object.keys(crawlData.tutorials.children)[0]].page, }, }), }, _.pick(config.template.appBar.items, [ "reference", "tutorials", ])); const layoutTemplate = config.template.alias.layout ? path.resolve(process.cwd(), config.template.alias.layout) : "layout.tmpl"; const renderer = new TemplateRenderer(path.join(__dirname, "tmpl"), null, docTree) .alias(alias) .setLayoutTemplate(layoutTemplate) .installPlugin("linker", linker) .installPlugin("generateIndex", indexSorterPlugin) .installPlugin("signature", signaturePlugin) .installPlugin("categoryFilter", categoryFilterPlugin) .installPlugin("relations", RelationsPlugin) .installPlugin("hljs", hljs) .installPlugin("preprocess", preprocessMarkupPlugin({ assetsDir: path.relative(outDir, assetsDir), siteRoot: config.template.siteRoot, })) .setGlobalTemplateData({ appBar: { items: appBarItems, }, manifest: {url: manifest}, pages: { settings, relative: { settings: settingsRelative, }, }, variant: config.template.variant, }); if (config.template.repository) { renderer.installPlugin("repository", RepositoryPlugin); renderer.plugins.repository.buildRepository(config.template.repository); } const pipeline = new TemplatePipeline(renderer).pipe(new TemplateTagsResolver()); if (config.template.siteDomain) { pipeline.pipe(new Sitemap( outDir, config.template.siteDomain, config.template.siteRoot)); } pipeline.pipe(new FlushToFile({skipNullFile: false})); renderer.getPlugin("relations").buildRelations(); idToDoc = new Map(); traverse(docTree, (doc) => { if (doc.type === "RootDoc") { doc.packages.forEach((pkg) => { idToDoc.set(pkg.id, pkg); }); } idToDoc.set(doc.id, doc); }); await outStaticFiles(outDir, assetsDir, config); await Promise.all([ outSource(outDir, pipeline, options.config, source, options.cmdLine.mainThread || false), outExplorerData(outDir, crawlData), outMainPage(indexRelative ? path.join(outDir, indexRelative) : null, pipeline, options.config), outPages(outDir, pipeline, options.config), outIndexes(outDir, pipeline, options.config, crawlData.index), outReference(outDir, pipeline, options.config, docTree, crawlData.reference), outTutorials(outDir, pipeline, options.config, docTree, crawlData.tutorials), ]); pipeline.close(); }; // Copy the contents of ./static to the output directory async function outStaticFiles( outDir /*: string */, assetsDir /*: string */, config /*: ConfigSchema */, ) /*: Promise<void> */ { if (config.template.variant !== "plain") { const staticDir = path.join(__dirname, "./static"); await fse.copy(staticDir, outDir); } await Promise.all([ (async () => { if (config.variant !== "plain") { // Copy the prettify script to outDir PRETTIFIER_SCRIPT_FILES.forEach((fileName) => { const toPath = path.join(outDir, "scripts", path.basename(fileName)); fse.copyFileSync( path.join(require.resolve("code-prettify"), "..", fileName), toPath, ); }); } })(), (() => { // Copy the stylesheets const stylesheets = config.template.stylesheets; const copyPromises = []; const resolved = []; for (const file of stylesheets) { const input = path.join(process.cwd(), file); const output = path.join(outDir, "imported/styles", path.parse(input).base); resolved.push(path.relative(outDir, output)); copyPromises.push(fse.copy( input, output, )); } config.template.stylesheets = resolved; return Promise.all(copyPromises); })(), (() => { const assets = typeof config.template.assets === "string" ? [config.template.assets] : config.template.assets; const copyPromises = []; for (const asset of assets) { copyPromises.push(fse.copy( path.join(process.cwd(), asset), assetsDir, )); } return Promise.all(copyPromises); })(), ]); } // Write the explorer JSON data in the output directory async function outExplorerData( outDir /*: string */, crawlData /*: CrawlData */, ) /*: Promise<void> */ { const explorerDir = path.join(outDir, "./explorer"); return fse.ensureDir(explorerDir).then(() => new Promise((resolve) => { fse.writeFile( path.join(explorerDir, "./reference.json"), JSON.stringify(crawlData.reference), "utf8", (err) => { if (err) throw err; }); if (crawlData.tutorials) { fse.writeFile( path.join(explorerDir, "./tutorials.json"), JSON.stringify(crawlData.tutorials), "utf8", (err) => { if (err) throw err; resolve(); }, ); } else { resolve(); } })); } // Render the main-page into index.tmpl (outputFile) async function outMainPage( outputFile /*: ?string */, pipeline /*: TemplatePipeline */, config /*: WebdocConfig */, )/*: Promise<void> */ { if (outputFile && config.template.readme) { const readmeFile = path.join(process.cwd(), config.template.readme); outReadme( outputFile, pipeline, config, readmeFile, ); } } async function outPages( outDir /*: string */, pipeline /*: TemplatePipeline */, config /*: WebdocConfig */, )/*: Promise<void> */ { pipeline.render("pages/settings.tmpl", { env: config, title: "Site Settings", }, { outputFile: path.join(outDir, pipeline.renderer.data.pages.relative.settings), }); } async function outSource( outDir /*: string */, pipeline /*: TemplatePipeline */, config /*: ConfigSchema */, source /*: ?$ReadOnlyArray<SourceFile> */, mainThread /*:: ?: boolean */, )/*: Promise<void> */ { if (!source || !config.template.sources) return; function renderSource(file /*: SourceFile */, raw /*: string */) { const pkgName = file.package.name || ""; const pkgRelativePath = path.relative(file.package.location || "", file.path); const outFile = path.join(pkgName, pkgRelativePath + ".html"); pipeline.render("source.tmpl", { appBar: {current: "sources"}, env: config, raw, title: path.basename(file.path), }, { outputFile: path.join(outDir, outFile), }); } const workerCount = Math.min(os.cpus().length, 1 + Math.floor(source.length / 32)); if (workerCount > 1 && !mainThread) { const workers = new Array(workerCount); const renderJobs/*: Array<{ resolve: Function, reject: Function, promise: Promise<void>, }> */ = new Array(source.length);// eslint-disable-line operator-linebreak const onMessage = function onMessage( { id, file, error, result, } /*: { id: number, file: string, error: boolean, result: ?string } */, ) { if (error || typeof result !== "string") { renderJobs[id].reject("Error in highlighting worker"); } else { const raw = result; const sourceFile = source[id]; renderSource(sourceFile, raw); renderJobs[id].resolve(); } }; const startTime = Date.now(); console.log("Creating " + workerCount + " workers for highlighting source code."); for (let i = 0; i < workers.length; i++) { workers[i] = new Worker(path.resolve(__dirname, "./helper/workers/hl.js")); workers[i].on("message", onMessage); } for (let i = 0; i < source.length; i++) { let resolveFn/*: () => void */; let rejectFn/*: () => void */; const promise = new Promise((resolve, reject) => { resolveFn = resolve; rejectFn = reject; }); renderJobs[i] = {resolve: resolveFn, reject: rejectFn, promise}; workers[i % workers.length].postMessage({ id: i, file: source[i].path, }); } await Promise.all(renderJobs.map((job) => job.promise)); await Promise.all(workers.map((worker) => worker.terminate())); console.log("Rendering sources took " + (Date.now() - startTime) + "ms time!"); } else { for (const file of source) { const raw = hljs.highlightAuto( fs.readFileSync(path.resolve(process.cwd(), file.path), "utf8"), ).value; renderSource(file, raw); } } } async function outReadme( outputFile /*: string */, pipeline /*: TemplatePipeline */, config /*: WebdocConfig */, readmeFile /*: string */, )/*: Promise<void> */ { if (!(await fse.pathExists(readmeFile))) { return; } let readme; if (readmeFile.endsWith(".md")) { const markdownRenderer = require("markdown-it")({ breaks: false, html: true, highlight: function(str, lang) { if (lang === "mermaid") { try { return "<div class=\"mermaid\">\n" + str + "\n</div>"; } catch (__) {/* noop */} } else if (lang && hljs.getLanguage(lang)) { try { return "<pre class=\"hljs\"><code>" + hljs.highlight(str, {language: lang, ignoreIllegals: true}).value + "</code></pre>"; } catch (__) {/* noop */} } return "<pre class=\"hljs\"><code>" + (str) + "</code></pre>"; }, }); const markdownSource = await fse.readFile(readmeFile, "utf8"); readme = markdownRenderer.render(markdownSource); } pipeline.render("pages/main-page.tmpl", { appBar: {current: "reference"}, document: null, readme, title: "Documentation", env: config, }, {outputFile}); } async function outIndexes( outDir /*: string */, pipeline /*: TemplatePipeline */, config /*: WebdocConfig */, index /*: Index */, )/*: Promise<void> */ { const KEY_TO_TITLE = { "classes": "Class Index", }; function outIndex(indexKey, indexList /*: Array<Doc> */) { if (indexList.length > 0) { const title = KEY_TO_TITLE[indexKey]; const url = linker.getFileSystemPath(indexList.url); pipeline.render("pages/api-index.tmpl", { appBar: {current: "reference"}, documentList: indexList, title, env: config, }, { outputFile: path.join(outDir, url), }); } } for (const [key, list] of Object.entries(index)) { outIndex(key, list); } } async function outReference( outDir /*: string */, pipeline /*: TemplatePipeline */, config /*: WebdocConfig */, docTree /*: RootDoc */, explorerData /* any */, )/*: Promise<void> */ { // Don't output if nothing's there if (!docTree.members.length) { return; } for (const [id, docRecord] of linker.documentRegistry) { let {uri: page} = docRecord; if (page.includes("#")) { continue;// skip fragments (non-standalone docs) } page = linker.processInternalURI(page, {outputRelative: true}); let doc; try { doc = idToDoc.get(id); } catch (_) { continue; } if (!doc) { continue; } if (doc.type === "PackageDoc") { const readmeFile = path.join(doc.location, "README.md"); outReadme( path.join(outDir, page), pipeline, config, readmeFile, ); } else { pipeline.render("document.tmpl", { appBar: {current: "reference"}, document: doc, explorerData, sources: config.template.sources, title: doc.name, require, env: config, }, { outputFile: path.join(outDir, page), }); } } } async function outTutorials( outDir /*: string */, pipeline /*: TemplatePipeline */, config /*: WebdocConfig */, docTree /*: RootDoc */, explorerData /* any */, )/*: Promise<void> */ { function out(parent /*: { members: any[] } */) { return function renderRecursive(tutorial /*: TutorialDoc */, i /*: number */) { const uri = linker.getURI(tutorial, true); pipeline.render("tutorial", { appBar: {current: "tutorials"}, document: tutorial, explorerData, title: tutorial.title, env: config, navigation: { next: parent && parent.members[i + 1] && parent.members[i + 1].route ? parent.members[i + 1] : null, previous: parent && parent.members[i - 1] && parent.members[i - 1].route ? parent.members[i - 1] : null, }, }, { outputFile: path.join(outDir, uri), }); if (tutorial.members.length > 0) { tutorial.members.forEach((out(tutorial) /*: any */)); } }; } docTree.tutorials.forEach((out({members: docTree.tutorials}) /*: any */)); }