UNPKG

akasharender

Version:

Rendering support for generating static HTML websites or EPUB eBooks

712 lines (615 loc) 27.2 kB
/** * * Copyright 2014-2025 David Herron * * This file is part of AkashaCMS (http://akashacms.com/). * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import path from 'node:path'; import { promises as fsp } from 'node:fs'; import util from 'node:util'; import * as data from './data.js'; import mahabhuta from 'mahabhuta'; import fastq from 'fastq'; import type { queueAsPromised } from "fastq"; import { Configuration } from './index.js'; import { RenderingContext } from '@akashacms/renderers'; import { DocumentsFileCache, Document } from './cache/file-cache-sqlite.js'; ////////////////////////////////////////////////////////// /** * Where renderDocument is meant for a document on disk * and indexed by a DocumentsFileCache instance, this * function is meant for documents created from in-memory * data. For instance, the tagged-content plugin generates * tag pages listing links to documents based on their tag. * These pages are instantiate out of data rather than * existing on-disk. * * Required data: * * Blank page - with frontmatter including a "layout" template reference * * File-name to use for virtual page, which also determines the rendered output file * * Metadata derived from the frontmatter and filled with other stuff including the data to render into the page, * * @param config * @param docInfo */ export async function renderVirtualDocument( config: Configuration, docInfo: { // The virtual pathname vpath: string; // The document to render as if it's // at that path document: string; } ) { const renderer = config.findRendererPath( docInfo.vpath ); const rc = renderer.parseMetadata({ fspath: docInfo.vpath, content: docInfo.document, metadata: {} }); // Add necessary items to the metadata rc.metadata.config = config; rc.metadata.partial = (fname, metadata) => { return config.akasha.partial(config, fname, metadata); }; rc.metadata.partialSync = (fname, metadata) => { return config.akasha.partialSync(config, fname, metadata); }; rc.metadata.akasha = config.akasha; rc.metadata.plugin = config.plugin; // Render the primary content let docrendered = await renderer.render(rc); // If there is a layout template, render that // template passing the rendered primary content let layoutrendered; if (rc.metadata.layout) { const layouts = config.akasha .filecache.layoutsCache; const layoutInfo = await layouts.find(rc.metadata.layout); if (!layoutInfo) { throw new Error(`No layout found in ${util.inspect(config.layoutDirs)} for ${rc.metadata.layout} in file ${docInfo.vpath}`); } // Build the metadata for the layout rendering let layoutmetadata: any = {}; for (var yprop in layoutInfo.metadata) { layoutmetadata[yprop] = layoutInfo.metadata[yprop]; } for (var yprop in rc.metadata) { if (yprop !== 'layout') { layoutmetadata[yprop] = rc.metadata[yprop]; } } // Make the first rendering available // in the metadata as "content" variable layoutmetadata.content = docrendered; const renderer = config.findRendererPath( rc.metadata.layout ); if (!renderer) { throw new Error(`No renderer for ${layoutmetadata.layout} in file ${docInfo.vpath}`);; } const layoutContext = { fspath: layoutInfo.fspath, content: layoutInfo.docContent, body: layoutInfo.docBody, metadata: layoutmetadata }; layoutrendered = await renderer.render(layoutContext); } // For HTML rendering, fun Mahabhuta functions const format = renderer.renderFormat(rc); const doMahabhuta = (format === 'HTML'); if (doMahabhuta) { const mahametadata: any = {}; for (var yprop in rc.metadata) { mahametadata[yprop] = rc.metadata[yprop]; } mahametadata.content = docrendered; if (rc.metadata.config.mahabhutaConfig) { mahabhuta.config(rc.metadata.config.mahabhutaConfig); } layoutrendered = await mahabhuta.processAsync( typeof layoutrendered === 'string' ? layoutrendered : docrendered, mahametadata, config.mahafuncs ); } // layoutrendered gets the final rendering } ////////////////////////////////////////////////////////// /** * The core part of rendering content using a renderer. * This function looks for the renderer, and if none is * found it simply returns. It then does a little setup * to the metadata object, and calls the render function * * @param config - AkashaCMS Configuration * @param rc - RenderingContext for use with Renderers * @returns */ export async function renderContent( config: Configuration, rc: RenderingContext ) // The return is a simple object // containing useful data : Promise<{ rendererName?: string, format?: string, rendered: string }> { // console.log(`renderContent `, rc); const renderer = config.findRendererPath( rc.fspath ); if (!renderer) { return { rendererName: undefined, format: undefined, rendered: rc.body }; } // Add necessary items to the metadata rc.metadata.config = config; rc.metadata.partial = (fname, metadata) => { return config.akasha.partial(config, fname, metadata); }; rc.metadata.partialSync = (fname, metadata) => { return config.akasha.partialSync(config, fname, metadata); }; rc.metadata.akasha = config.akasha; rc.metadata.plugin = config.plugin; // Render the primary content let docrendered = await renderer.render(rc); // console.log(`renderContent rendered=`, docrendered); return { rendererName: renderer.name, format: renderer.renderFormat(rc), rendered: docrendered }; } /** * Render a document, accounting for the main content, * a layout template (if any), and Mahabhuta (if the content * output is HTML). This also handles rendering other types * of content such as LESS CSS files. * * @param config * @param docInfo * @returns */ export async function renderDocument( config: Configuration, docInfo ): Promise<string> { const renderStart = new Date(); // Render the main content const rc = <RenderingContext>{ fspath: docInfo.vpath, content: docInfo.docContent, body: docInfo.docBody, metadata: docInfo.metadata }; // console.log(`renderDocument context= ${rc.fspath}`) let docFormat; // Knowing the format let docRendered; try { const result = await renderContent(config, rc); docFormat = result.format; docRendered = result.rendered; } catch (err) { console.error(`Error rendering ${docInfo.vpath} ${(err.stack ? err.stack : err)}`); throw new Error(`Error rendering ${docInfo.vpath} ${(err.stack ? err.stack : err)}`); } // console.log(`renderDocument ${docInfo.vpath} rendered=`, docRendered); await data.report(docInfo.mountPoint, docInfo.vpath, config.renderTo, "FIRST RENDER", renderStart); // Render the main content into a layout template, // if one is specified let layoutFormat; let layoutRendered; // console.log(`renderDocument layout ${docInfo?.metadata?.layout} docMetadata ${util.inspect(docInfo.docMetadata)} metadata ${util.inspect(docInfo.metadata)}`); if (docInfo?.metadata?.layout) { const layouts = config.akasha.filecache.layoutsCache; // await layouts.isReady(); let found = await layouts.find(docInfo.metadata.layout); if (!found) { throw new Error(`No layout found in ${util.inspect(config.layoutDirs)} for ${docInfo.metadata.layout} in file ${docInfo.vpath}`); } const rcLayout = <RenderingContext>{ fspath: docInfo.metadata.layout, content: found.docContent, body: found.docBody, metadata: {} }; for (var yprop in found.metadata) { rcLayout.metadata[yprop] = found.metadata[yprop]; } for (var yprop in docInfo.metadata) { if (yprop !== 'layout') { rcLayout.metadata[yprop] = docInfo.metadata[yprop]; } } rcLayout.metadata.content = docRendered; try { const result = await renderContent(config, rcLayout); layoutFormat = result.format; layoutRendered = result.rendered; } catch (e) { let ee = new Error(`Error rendering ${docInfo.vpath} with ${docInfo.metadata.layout} ${e.stack ? e.stack : e}`); console.error(ee); throw ee; } } else { layoutFormat = docFormat; layoutRendered = docRendered; } // console.log(`renderDocument ${docInfo.vpath} after layout render format ${layoutFormat} `); const renderSecondRender = new Date(); await data.report(docInfo.mountPoint, docInfo.vpath, config.renderTo, "SECOND RENDER", renderStart); // Next step is to run Mahabhuta on the rendered content // Of course, Mahabhuta is not appropriate for everything // because not everything is HTML const doMahabhuta = (layoutFormat === 'HTML'); if (doMahabhuta) { try { const mahametadata: any = {}; for (var yprop in docInfo.metadata) { mahametadata[yprop] = docInfo.metadata[yprop]; } mahametadata.content = docRendered; if (docInfo.metadata.config.mahabhutaConfig) { mahabhuta.config(docInfo.metadata.config.mahabhutaConfig); } // console.log(`mahametadata`, mahametadata); layoutRendered = await mahabhuta.processAsync( layoutRendered, mahametadata, config.mahafuncs ); // OLD docrendered = await this.maharun(layoutrendered, docdata, config.mahafuncs); } catch (e2) { let eee = new Error(`Error with Mahabhuta ${docInfo.vpath} with ${docInfo.metadata.layout} ${e2.stack ? e2.stack : e2}`); console.error(eee); throw eee; } await data.report(docInfo.mountPoint, docInfo.vpath, config.renderTo, "MAHABHUTA", renderStart); const renderDest = path.join( config.renderTo, docInfo.renderPath); await fsp.mkdir(path.dirname(renderDest), { recursive: true }); await fsp.writeFile(renderDest, layoutRendered, 'utf-8'); // console.log(`RENDERED ${renderer.name} ${docInfo.path} ==> ${renderToFpath}`); const renderEndRendered = new Date(); await data.report( docInfo.mountPoint, docInfo.vpath, config.renderTo, "RENDERED", renderStart); return `${layoutFormat} ${docInfo.vpath} ==> ${docInfo.renderPath} (${(renderEndRendered.valueOf() - renderStart.valueOf()) / 1000} seconds)\n${await data.data4file(docInfo.mountPoint, docInfo.vpath)}`; } else if (layoutFormat === 'CSS') { try { const renderToFpath = path.join( config.renderTo, docInfo.renderPath); const renderToDir = path.dirname(renderToFpath); await fsp.mkdir(renderToDir, { recursive: true }); await fsp.writeFile(renderToFpath, layoutRendered, 'utf8'); const renderEndRendered = new Date(); await data.report( docInfo.mountPoint, docInfo.vpath, config.renderTo, "RENDERED", renderStart); return `${layoutFormat} ${docInfo.vpath} ==> ${docInfo.renderPath} (${(renderEndRendered.valueOf() - renderStart.valueOf()) / 1000} seconds)\n${await data.data4file(docInfo.mountPoint, docInfo.vpath)}`; } catch(err) { console.error(`in RENDER CSS branch for ${docInfo.vpath} to ${docInfo.renderPath} error=${err.stack ? err.stack : err}`); throw new Error(`in RENDER CSS branch for ${docInfo.vpath} to ${docInfo.renderPath} error=${err.stack ? err.stack : err}`); } } else { try { const renderToFpath = path.join( config.renderTo, docInfo.renderPath); const renderToDir = path.dirname(renderToFpath); await fsp.mkdir(renderToDir, { recursive: true }); await fsp.copyFile(docInfo.fspath, renderToFpath); // console.log(`COPIED ${docInfo.path} ==> ${renderToFpath}`); const renderEndCopied = new Date(); return `COPY ${docInfo.vpath} ==> ${renderToFpath} (${(renderEndCopied.valueOf() - renderStart.valueOf()) / 1000} seconds)`; } catch(err) { console.error(`in copy branch for ${docInfo.vpath} to ${docInfo.renderPath} error=${err.stack ? err.stack : err}`); throw new Error(`in copy branch for ${docInfo.vpath} to ${docInfo.renderPath} error=${err.stack ? err.stack : err}`); } } } // export async function renderDocument2(config, docInfo) { // const renderStart = new Date(); // const renderBaseMetadata = docInfo.baseMetadata; // // console.log(`renderDocument `, docInfo); // const renderer = config.findRendererPath(docInfo.vpath); // if (renderer) { // // console.log(`ABOUT TO RENDER ${renderer.name} ${docInfo.vpath} ==> ${renderToFpath}`); // try { // // Set up required metadata values // docInfo.metadata.config = config; // docInfo.metadata.partial = (fname, metadata) => { // return config.akasha.partial(config, fname, metadata); // } // docInfo.metadata.partialSync = (fname, metadata) => { // return config.akasha.partialSync(config, fname, metadata); // } // docInfo.metadata.akasha = config.akasha; // docInfo.metadata.plugin = config.plugin; // // Render the document - output goes to "docrendered" // let docrendered; // try { // const context = { // fspath: docInfo.fspath, // content: docInfo.docContent, // body: docInfo.docBody, // metadata: docInfo.metadata // }; // // console.log({ // // title: 'before DOCUMENT renderer.render', // // fspath: docInfo.fspath, // // content: docInfo.docContent, // // body: docInfo.docBody, // // metadata: docInfo.metadata, // // info: docInfo.info // // }); // if (typeof context.content !== 'string' // || typeof context.body !== 'string') { // // console.warn(`render should fail for ${util.inspect(context)} `, docInfo); // } // docrendered = await renderer.render(context); // } catch (err) { // console.error(`Error rendering ${docInfo.vpath} ${(err.stack ? err.stack : err)}`); // throw new Error(`Error rendering ${docInfo.vpath} ${(err.stack ? err.stack : err)}`); // } // // console.log(docrendered); // await data.report(docInfo.mountPoint, docInfo.vpath, config.renderTo, // "FIRST RENDER", renderStart); // // There may be a layout, which will be in metadata.layout // // If so, get the layout file // // Construct a new metadata -- and it's "content" is // // set from docrendered // // Then render the document into "layoutrendered" // // Otherwise cpy docrendered to layoutrendered // let layoutrendered; // if (docInfo.metadata.layout) { // const layouts = config.akasha.filecache.layoutsCache; // // await layouts.isReady(); // let found = await layouts.find(docInfo.metadata.layout); // if (!found) { // throw new Error(`No layout found in ${util.inspect(config.layoutDirs)} for ${docInfo.metadata.layout} in file ${docInfo.vpath}`); // } // let layoutmetadata: any = {}; // for (var yprop in found.metadata) { // layoutmetadata[yprop] = found.metadata[yprop]; // } // for (var yprop in docInfo.metadata) { // if (yprop !== 'layout') { // layoutmetadata[yprop] = docInfo.metadata[yprop]; // } // } // layoutmetadata.content = docrendered; // const renderer = config.findRendererPath(docInfo.metadata.layout); // if (!renderer) { // throw new Error(`No renderer for ${layoutmetadata.layout} in file ${docInfo.vpath}`);; // } // const context = { // fspath: found.fspath, // content: found.docContent, // body: found.docBody, // metadata: layoutmetadata // }; // if (typeof context.content !== 'string' // || typeof context.body !== 'string') { // throw new Error(`renderDocument LAYOUT RENDERING for ${docInfo.vpath} with layout ${docInfo.metadata.layout} has no context.content or context.body to which to render the content ${util.inspect(context)} ${util.inspect(found)}`); // } // // console.log(`renderDocument `, found); // // console.log(`renderDocument `, layoutmetadata); // try { // layoutrendered // = await renderer.render(context); // } catch (e) { // let ee = new Error(`Error rendering ${docInfo.vpath} with ${docInfo.metadata.layout} ${e.stack ? e.stack : e}`); // console.error(ee); // throw ee; // } // } else { // layoutrendered = docrendered; // } // const renderSecondRender = new Date(); // await data.report(docInfo.mountPoint, docInfo.vpath, config.renderTo, // "SECOND RENDER", renderStart); // // Next step is to run Mahabhuta on the rendered content // // Of course, Mahabhuta is not appropriate for everything // // because not everything is HTML // const format = renderer.renderFormat({ fspath: docInfo.fspath }); // const doMahabhuta = (format === 'HTML') ? true :false; // if (doMahabhuta) { // try { // const mahametadata: any = {}; // for (var yprop in docInfo.metadata) { // mahametadata[yprop] = docInfo.metadata[yprop]; // } // mahametadata.content = docrendered; // if (docInfo.metadata.config.mahabhutaConfig) { // mahabhuta.config(docInfo.metadata.config.mahabhutaConfig); // } // // console.log(`mahametadata`, mahametadata); // layoutrendered = await mahabhuta.processAsync( // layoutrendered, mahametadata, config.mahafuncs // ); // // OLD docrendered = await this.maharun(layoutrendered, docdata, config.mahafuncs); // } catch (e2) { // let eee = new Error(`Error with Mahabhuta ${docInfo.vpath} with ${docInfo.metadata.layout} ${e2.stack ? e2.stack : e2}`); // console.error(eee); // throw eee; // } // } // await data.report(docInfo.mountPoint, docInfo.vpath, config.renderTo, // "MAHABHUTA", renderStart); // const renderDest = path.join(config.renderTo, docInfo.renderPath); // await fsp.mkdir(path.dirname(renderDest), { recursive: true }); // await fsp.writeFile(renderDest, layoutrendered, 'utf-8'); // // console.log(`RENDERED ${renderer.name} ${docInfo.path} ==> ${renderToFpath}`); // const renderEndRendered = new Date(); // await data.report( // docInfo.mountPoint, docInfo.vpath, // config.renderTo, // "RENDERED", renderStart); // return `${renderer.name} ${docInfo.vpath} ==> ${docInfo.renderPath} (${(renderEndRendered.valueOf() - renderStart.valueOf()) / 1000} seconds)\n${await data.data4file(docInfo.mountPoint, docInfo.vpath)}`; // } catch (err) { // console.error(`in renderer branch for ${docInfo.vpath} to ${docInfo.renderPath} error=${err.stack ? err.stack : err}`); // throw new Error(`in renderer branch for ${docInfo.vpath} to ${docInfo.renderPath} error=${err.stack ? err.stack : err}`); // } // } else { // // console.log(`COPYING ${docInfo.path} ==> ${renderToFpath}`); // try { // const renderToFpath = path.join(config.renderTo, docInfo.renderPath); // const renderToDir = path.dirname(renderToFpath); // await fsp.mkdir(renderToDir, { recursive: true }); // await fsp.copyFile(docInfo.fspath, renderToFpath); // // console.log(`COPIED ${docInfo.path} ==> ${renderToFpath}`); // const renderEndCopied = new Date(); // return `COPY ${docInfo.vpath} ==> ${renderToFpath} (${(renderEndCopied.valueOf() - renderStart.valueOf()) / 1000} seconds)`; // } catch(err) { // console.error(`in copy branch for ${docInfo.vpath} to ${docInfo.renderPath} error=${err.stack ? err.stack : err}`); // throw new Error(`in copy branch for ${docInfo.vpath} to ${docInfo.renderPath} error=${err.stack ? err.stack : err}`); // } // } // } /** * Render all the documents in a site, limiting * the number of simultaneous rendering tasks * to the number in config.concurrency. * * @param config * @returns */ export async function render(config) { const documents = <DocumentsFileCache>config.akasha.filecache.documentsCache; // await documents.isReady(); // console.log('CALLING config.hookBeforeSiteRendered'); await config.hookBeforeSiteRendered(); // 1. Gather list of files from RenderFileCache const filez = await documents.paths(); // console.log(`newerrender filez ${filez.length}`); // 2. Exclude any that we want to ignore const filez2 = [] as Array<{ config: Configuration, info: Document }>; for (let entry of filez) { let include = true; // console.log(entry); let stats; try { stats = await fsp.stat(entry.fspath); } catch (err) { stats = undefined; } if (!entry) include = false; else if (!stats || stats.isDirectory()) include = false; // This should arise using an ignore clause // else if (path.basename(entry.vpath) === '.DS_Store') include = false; // else if (path.basename(entry.vpath) === '.placeholder') include = false; if (include) { // The queue is an array of tuples containing the // config object and the path string filez2.push({ config: config, info: await documents.find(entry.vpath) }); } } // console.log(`newerrender filez2 after ignore ${filez2.length}`); // 3. Make a fastq to process using renderDocument, // pushing results to the results array // This sets up the queue processor // The concurrency setting lets us process documents // in parallel while limiting total impact. const queue: queueAsPromised<{ config: Configuration, info: Document }> = fastq.promise( // This function is invoked for each entry in the // queue. It handles rendering the queue // The queue has config objects and path strings // which is exactly what's required by // renderDocument async function renderDocumentInQueue(entry) : Promise<{ result?: any; error?: any; }> { // console.log(`renderDocumentInQueue ${entry.info.vpath}`); try { let result = await renderDocument( entry.config, entry.info ); // console.log(`DONE renderDocumentInQueue ${entry.info.vpath}`, result); return { result }; } catch (error) { // console.log(`ERROR renderDocumentInQueue ${entry.info.vpath}`, error.stack); return { error }; } }, config.concurrency); // queue.push returns a Promise that's fulfilled when // the task finishes. // Hence waitFor is an array of Promises. const waitFor = []; for (let entry of filez2) { waitFor.push(queue.push(entry)); } // This automatically waits for all those // Promises to resolve, while making the results // array contain results. const results = []; for (let result of waitFor) { results.push(await result); } // 4. Invoke hookSiteRendered try { // console.log('Invoking hookSiteRendered'); await config.hookSiteRendered(); } catch (e) { console.error(e.stack); throw new Error(`hookSiteRendered failed because ${e}`); } // 5. return results return results; };