UNPKG

akasharender

Version:

Rendering support for generating static HTML websites or EPUB eBooks

1,349 lines (1,199 loc) 45.9 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. */ /** * AkashaRender * @module akasharender */ import util from 'node:util'; import { promises as fsp } from 'node:fs'; import fs from 'node:fs'; import path from 'node:path'; // const oembetter = require('oembetter')(); import RSS from 'rss'; import fastq from 'fastq'; import { DirsWatcher, VPathData, dirToWatch, mimedefine } from '@akashacms/stacked-dirs'; import * as Renderers from '@akashacms/renderers'; export * as Renderers from '@akashacms/renderers'; import { Renderer } from '@akashacms/renderers'; export { Renderer } from '@akashacms/renderers'; import * as mahabhuta from 'mahabhuta'; export * as mahabhuta from 'mahabhuta'; import * as cheerio from 'cheerio'; import mahaPartial from 'mahabhuta/maha/partial.js'; export * from './mahafuncs.js'; import * as relative from 'relative'; export * as relative from 'relative'; import { Plugin } from './Plugin.js'; export { Plugin } from './Plugin.js'; import { render, renderDocument, renderContent } from './render.js'; export { render, renderDocument, renderContent } from './render.js'; const __filename = import.meta.filename; const __dirname = import.meta.dirname; // For use in Configure.prepare import { BuiltInPlugin } from './built-in.js'; import * as filecache from './cache/file-cache-sqlite.js'; export { newSQ3DataStore } from './sqdb.js'; // There doesn't seem to be an official MIME type registered // for AsciiDoctor // per: https://asciidoctor.org/docs/faq/ // per: https://github.com/asciidoctor/asciidoctor/issues/2502 // // As of November 6, 2022, the AsciiDoctor FAQ said they are // in the process of registering a MIME type for `text/asciidoc`. // The MIME type we supply has been updated. // // This also seems to be true for the other file types. We've made up // some MIME types to go with each. // // The MIME package had previously been installed with AkashaRender. // But, it seems to not be used, and instead we compute the MIME type // for files in Stacked Directories. // // The required task is to register some MIME types with the // MIME package. It isn't appropriate to do this in // the Stacked Directories package. Instead that's left // for code which uses Stacked Directories to determine which // (if any) added MIME types are required. Ergo, AkashaRender // needs to register the MIME types it is interested in. // That's what is happening here. // // There's a thought that this should be handled in the Renderer // implementations. But it's not certain that's correct. // // Now that the Renderers are in `@akashacms/renderers` should // these definitions move to that package? mimedefine({'text/asciidoc': [ 'adoc', 'asciidoc' ]}); mimedefine({'text/x-markdoc': [ 'markdoc' ]}); mimedefine({'text/x-ejs': [ 'ejs']}); mimedefine({'text/x-nunjucks': [ 'njk' ]}); mimedefine({'text/x-handlebars': [ 'handlebars' ]}); mimedefine({'text/x-liquid': [ 'liquid' ]}); mimedefine({'text/x-tempura': [ 'tempura' ]}); /** * Performs setup of things so that AkashaRender can function. * The correct initialization of AkashaRender is to * 1. Generate the Configuration object * 2. Call config.prepare * 3. Call akasharender.setup * * This function ensures all objects that initialize asynchronously * are correctly setup. * * @param {*} config */ export async function setup(config) { config.renderers.partialFunc = (fname, metadata) => { // console.log(`calling partial ${fname}`); return partial(config, fname, metadata); }; config.renderers.partialSyncFunc = (fname, metadata) => { // console.log(`calling partialSync ${fname}`); return partialSync(config, fname, metadata); } await cacheSetup(config); await fileCachesReady(config); } export async function cacheSetup(config) { try { await filecache.setup(config); } catch (err) { console.error(`INITIALIZATION FAILURE COULD NOT INITIALIZE CACHE `, err); process.exit(1); } } export async function closeCaches() { try { await filecache.closeFileCaches(); } catch (err) { console.error(`INITIALIZATION FAILURE COULD NOT CLOSE CACHES `, err); process.exit(1); } } export async function fileCachesReady(config) { try { await Promise.all([ filecache.documentsCache.isReady(), filecache.assetsCache.isReady(), filecache.layoutsCache.isReady(), filecache.partialsCache.isReady() ]); } catch (err) { console.error(`INITIALIZATION FAILURE COULD NOT INITIALIZE CACHE SYSTEM `, err); process.exit(1); } } export async function renderPath(config, path2r) { const documents = filecache.documentsCache; let found; let count = 0; while (count < 20) { /* What's happening is this might be called from cli.js * in render-document, and we might be asked to render the * last document that will be ADD'd to the FileCache. * * In such a case <code>isReady</code> might return <code>true</code> * but not all files will have been ADD'd to the FileCache. * In that case <code>documents.find</code> returns * <code>undefined</code> * * What this does is try up to 20 times to load the document, * sleeping for 100 milliseconds each time. * * The cleaner alternative would be to wait for not only * the <code>ready</code> from the <code>documents</code> FileCache, * but also for all the initial ADD events to be handled. But * that second condition seems difficult to detect reliably. */ found = await documents.find(path2r); if (found) break; else { await new Promise((resolve, reject) => { setTimeout(() => { resolve(undefined); }, 100); }); count++; } } // console.log(`renderPath ${path2r}`, found); if (!found) { throw new Error(`Did not find document for ${path2r}`); } let result = await renderDocument(config, found); return result; } /** * Reads a file from the rendering directory. It is primarily to be * used in test cases, where we'll run a build then read the individual * files to make sure they've rendered correctly. * * @param {*} config * @param {*} fpath * @returns */ export async function readRenderedFile(config, fpath) { let html = await fsp.readFile(path.join(config.renderDestination, fpath), 'utf8'); let $ = config.mahabhutaConfig ? cheerio.load(html, config.mahabhutaConfig) : cheerio.load(html); return { html, $ }; } /** * Renders a partial template using the supplied metadata. This version * allows for asynchronous execution, and every bit of code it * executes is allowed to be async. * * @param {*} config AkashaRender Configuration object * @param {*} fname Path within the filecache.partials cache * @param {*} metadata Object containing metadata * @returns Promise that resolves to a string containing the rendered stuff */ export async function partial(config, fname, metadata) { if (!fname || typeof fname !== 'string') { throw new Error(`partial fname not a string ${util.inspect(fname)}`); } // console.log(`partial ${fname}`); const found = await filecache.partialsCache.find(fname); if (!found) { throw new Error(`No partial found for ${fname} in ${util.inspect(config.partialsDirs)}`); } // console.log(`partial ${fname} ==> ${found.vpath} ${found.fspath}`); const renderer = config.findRendererPath(found.vpath); if (renderer) { // console.log(`partial about to render ${util.inspect(found.vpath)}`); let partialText; if (found.docBody) partialText = found.docBody; else if (found.docContent) partialText = found.docContent; else partialText = await fsp.readFile(found.fspath, 'utf8'); // Some renderers (Nunjuks) require that metadata.config // point to the config object. This block of code // duplicates the metadata object, then sets the // config field in the duplicate, passing that to the partial. let mdata: any = {}; let prop; for (prop in metadata) { mdata[prop] = metadata[prop]; } mdata.config = config; mdata.partialSync = partialSync.bind(renderer, config); mdata.partial = partial.bind(renderer, config); // console.log(`partial-funcs render ${renderer.name} ${found.vpath}`); return renderer.render({ fspath: found.fspath, content: partialText, metadata: mdata // partialText, mdata, found }); } else if (found.vpath.endsWith('.html') || found.vpath.endsWith('.xhtml')) { // console.log(`partial reading file ${found.vpath}`); return fsp.readFile(found.fspath, 'utf8'); } else { throw new Error(`renderPartial no Renderer found for ${fname} - ${found.vpath}`); } } /** * Renders a partial template using the supplied metadata. This version * allows for synchronous execution, and every bit of code it * executes is synchronous functions. * * @param {*} config AkashaRender Configuration object * @param {*} fname Path within the filecache.partials cache * @param {*} metadata Object containing metadata * @returns String containing the rendered stuff */ export function partialSync(config, fname, metadata) { if (!fname || typeof fname !== 'string') { throw new Error(`partialSync fname not a string ${util.inspect(fname)}`); } const found = filecache.partialsCache.findSync(fname); if (!found) { throw new Error(`No partial found for ${fname} in ${util.inspect(config.partialsDirs)}`); } var renderer = config.findRendererPath(found.vpath); if (renderer) { // Some renderers (Nunjuks) require that metadata.config // point to the config object. This block of code // duplicates the metadata object, then sets the // config field in the duplicate, passing that to the partial. let mdata: any = {}; let prop; for (prop in metadata) { mdata[prop] = metadata[prop]; } mdata.config = config; // In this context, partialSync is directly available // as a function that we can directly use. // console.log(`partialSync `, partialSync); mdata.partialSync = partialSync.bind(renderer, config); // for findSync, the "found" object is VPathData which // does not have docBody nor docContent. Therefore we // must read this content let partialText = fs.readFileSync(found.fspath, 'utf-8'); // if (found.docBody) partialText = found.docBody; // else if (found.docContent) partialText = found.docContent; // else partialText = fs.readFileSync(found.fspath, 'utf8'); // console.log(`partial-funcs renderSync ${renderer.name} ${found.vpath}`); return renderer.renderSync(<Renderers.RenderingContext>{ fspath: found.fspath, content: partialText, metadata: mdata // partialText, mdata, found }); } else if (found.vpath.endsWith('.html') || found.vpath.endsWith('.xhtml')) { return fs.readFileSync(found.fspath, 'utf8'); } else { throw new Error(`renderPartial no Renderer found for ${fname} - ${found.vpath}`); } } /** * Starting from a virtual path in the documents, searches upwards to * the root of the documents file-space, finding files that * render to "index.html". The "index.html" files are index files, * as the name suggests. * * @param {*} config * @param {*} fname * @returns */ export async function indexChain(config, fname) { // This used to be a full function here, but has moved // into the FileCache class. Requiring a `config` option // is for backwards compatibility with the former API. const documents = filecache.documentsCache; return documents.indexChain(fname); } /** * Manipulate the rel= attributes on a link returned from Mahabhuta. * * @params {$link} The link to manipulate * @params {attr} The attribute name * @params {doattr} Boolean flag whether to set (true) or remove (false) the attribute * */ export function linkRelSetAttr($link, attr, doattr) { let linkrel = $link.attr('rel'); let rels = linkrel ? linkrel.split(' ') : []; let hasattr = rels.indexOf(attr) >= 0; if (!hasattr && doattr) { rels.unshift(attr); $link.attr('rel', rels.join(' ')); } else if (hasattr && !doattr) { rels.splice(rels.indexOf(attr)); $link.attr('rel', rels.join(' ')); } }; ///////////////// RSS Feed Generation export async function generateRSS(config, configrss, feedData, items, renderTo) { // Supposedly it's required to use hasOwnProperty // http://stackoverflow.com/questions/728360/how-do-i-correctly-clone-a-javascript-object#728694 // // But, in our case that resulted in an empty object // console.log('configrss '+ util.inspect(configrss)); // Construct initial rss object var rss = {}; for (let key in configrss.rss) { //if (configrss.hasOwnProperty(key)) { rss[key] = configrss.rss[key]; //} } // console.log('rss '+ util.inspect(rss)); // console.log('feedData '+ util.inspect(feedData)); // Then fill in from feedData for (let key in feedData) { //if (feedData.hasOwnProperty(key)) { rss[key] = feedData[key]; //} } // console.log('generateRSS rss '+ util.inspect(rss)); var rssfeed = new RSS(rss); items.forEach(function(item) { rssfeed.item(item); }); var xml = rssfeed.xml(); var renderOut = path.join(config.renderDestination, renderTo); await fsp.mkdir(path.dirname(renderOut), { recursive: true }) await fsp.writeFile(renderOut, xml, { encoding: 'utf8' }); }; // For oEmbed, Consider making an external plugin // https://www.npmjs.com/package/oembed-all // https://www.npmjs.com/package/embedable // https://www.npmjs.com/package/media-parser // https://www.npmjs.com/package/oembetter /** * The AkashaRender project configuration object. * One instantiates a Configuration object, then fills it * with settings and plugins. * * @see module:Configuration */ // const _config_pluginData = Symbol('pluginData'); // const _config_assetsDirs = Symbol('assetsDirs'); // const _config_documentDirs = Symbol('documentDirs'); // const _config_layoutDirs = Symbol('layoutDirs'); // const _config_partialDirs = Symbol('partialDirs'); // const _config_mahafuncs = Symbol('mahafuncs'); // const _config_renderTo = Symbol('renderTo'); // const _config_metadata = Symbol('metadata'); // const _config_root_url = Symbol('root_url'); // const _config_scripts = Symbol('scripts'); // const _config_plugins = Symbol('plugins'); // const _config_cheerio = Symbol('cheerio'); // const _config_configdir = Symbol('configdir'); // const _config_cachedir = Symbol('cachedir'); // const _config_concurrency = Symbol('concurrency'); // const _config_renderers = Symbol('renderers'); /** * Data type describing items in the * javaScriptTop and javaScriptBottom arrays. * The fields correspond to the attributes * of the <script> tag which can be used * either in the top or bottom of * an HTML file. */ export type javaScriptItem = { href?: string, script?: string, lang?: string }; export type stylesheetItem = { href?: string, media?: string }; /** * Defines the structure for directory * mount specification in the Configuration. * * The simple 'string' form says to mount * the named fspath on the root of the * virtual filespace. * * The object form allows us to mount * an fspath into a different location * in the virtual filespace, to ignore * files based on GLOB patterns, and to * include metadata for every file in * a directory tree. * * In the file-cache module, this is * converted to the dirToWatch structure * used by StackedDirs. */ export type dirToMount = string | { /** * The fspath to mount */ src: string, /** * The virtual filespace * location */ dest: string, /** * Array of GLOB patterns * of files to ignore */ ignore?: string[], /** * An object containing * metadata that's to * apply to every file */ baseMetadata?: any }; /** * Configuration of an AkashaRender project, including the input directories, * output directory, plugins, and various settings. * * USAGE: * * const akasha = require('akasharender'); * const config = new akasha.Configuration(); */ export class Configuration { #renderers: Renderers.Configuration; #configdir: string; #cachedir: string; #assetsDirs?: dirToMount[]; #layoutDirs?: dirToMount[]; #documentDirs?: dirToMount[]; #partialDirs?: dirToMount[]; #mahafuncs; #cheerio?: cheerio.CheerioOptions; #renderTo: string; #scripts?: { stylesheets?: stylesheetItem[], javaScriptTop?: javaScriptItem[], javaScriptBottom?: javaScriptItem[] }; #concurrency: number; #metadata: any; #root_url: string; #plugins; #pluginData; constructor(modulepath) { // this[_config_renderers] = []; this.#renderers = new Renderers.Configuration({ }); this.#mahafuncs = []; this.#scripts = { stylesheets: [], javaScriptTop: [], javaScriptBottom: [] }; this.#concurrency = 3; this.#documentDirs = []; this.#layoutDirs = []; this.#partialDirs = []; this.#assetsDirs = []; this.#mahafuncs = []; this.#renderTo = 'out'; this.#metadata = {} as any; this.#plugins = []; this.#pluginData = []; /* * Is this the best place for this? It is necessary to * call this function somewhere. The nature of this function * is that it can be called multiple times with no impact. * By being located here, it will always be called by the * time any Configuration is generated. */ // This is executed in @akashacms/renderers // this[_config_renderers].registerBuiltInRenderers(); // Provide a mechanism to easily specify configDir // The path in configDir must be the path of the configuration file. // There doesn't appear to be a way to determine that from here. // // For example module.parent.filename in this case points // to akasharender/index.js because that's the module which // loaded this module. // // One could imagine a different initialization pattern. Instead // of akasharender requiring Configuration.js, that file could be // required by the configuration file. In such a case // module.parent.filename WOULD indicate the filename for the // configuration file, and would be a source of setting // the configDir value. if (typeof modulepath !== 'undefined' && modulepath !== null) { this.configDir = path.dirname(modulepath); } // Very carefully add the <partial> support from Mahabhuta as the // very first thing so that it executes before anything else. let config = this; this.addMahabhuta(mahaPartial.mahabhutaArray({ renderPartial: function(fname, metadata) { return partial(config, fname, metadata); } })); } /** * Initialize default configuration values for anything which has not * already been configured. Some built-in defaults have been decided * ahead of time. For each configuration setting, if nothing has been * declared, then the default is substituted. * * It is expected this function will be called last in the config file. * * This function installs the `built-in` plugin. It needs to be last on * the plugin chain so that its stylesheets and partials and whatnot * can be overridden by other plugins. * * @returns {Configuration} */ prepare() { const CONFIG = this; const configDirPath = function(dirnm) { let configPath = dirnm; if (typeof CONFIG.configDir !== 'undefined' && CONFIG.configDir != null) { configPath = path.join(CONFIG.configDir, dirnm); } return configPath; } let stat; const cacheDirsPath = configDirPath('cache'); if (!this.#cachedir) { if (fs.existsSync(cacheDirsPath) && (stat = fs.statSync(cacheDirsPath))) { if (stat.isDirectory()) { this.cacheDir = 'cache'; } else { throw new Error("'cache' is not a directory"); } } else { fs.mkdirSync(cacheDirsPath, { recursive: true }); this.cacheDir = 'cache'; } } else if (this.#cachedir && !fs.existsSync(this.#cachedir)) { fs.mkdirSync(this.#cachedir, { recursive: true }); } const assetsDirsPath = configDirPath('assets'); if (!this.#assetsDirs) { if (fs.existsSync(assetsDirsPath) && (stat = fs.statSync(assetsDirsPath))) { if (stat.isDirectory()) { this.addAssetsDir('assets'); } } } const layoutsDirsPath = configDirPath('layouts'); if (!this.#layoutDirs) { if (fs.existsSync(layoutsDirsPath) && (stat = fs.statSync(layoutsDirsPath))) { if (stat.isDirectory()) { this.addLayoutsDir('layouts'); } } } const partialDirsPath = configDirPath('partials'); if (!mahaPartial.configuration.partialDirs) { if (fs.existsSync(partialDirsPath) && (stat = fs.statSync(partialDirsPath))) { if (stat.isDirectory()) { this.addPartialsDir('partials'); } } } const documentDirsPath = configDirPath('documents'); if (!this.#documentDirs) { if (fs.existsSync(documentDirsPath) && (stat = fs.statSync(documentDirsPath))) { if (stat.isDirectory()) { this.addDocumentsDir('documents'); } else { throw new Error("'documents' is not a directory"); } } else { throw new Error("No 'documentDirs' setting, and no 'documents' directory"); } } const renderToPath = configDirPath('out'); if (!this.#renderTo) { if (fs.existsSync(renderToPath) && (stat = fs.statSync(renderToPath))) { if (stat.isDirectory()) { this.setRenderDestination('out'); } else { throw new Error("'out' is not a directory"); } } else { fs.mkdirSync(renderToPath, { recursive: true }); this.setRenderDestination('out'); } } else if (this.#renderTo && !fs.existsSync(this.#renderTo)) { fs.mkdirSync(this.#renderTo, { recursive: true }); } // The akashacms-builtin plugin needs to be last on the chain so that // its partials etc can be easily overridden. This is the most convenient // place to declare that plugin. // // Normally we'd do require('./built-in.js'). // But, in this context that doesn't work. // What we did is to import the // BuiltInPlugin class earlier so that // it can be used here. this.use(BuiltInPlugin, { // built-in options if any // Do not need this here any longer because it is handled // in the constructor. // Set up the Mahabhuta partial tag so it renders through AkashaRender // renderPartial: function(fname, metadata) { // return render.partial(config, fname, metadata); // } }); return this; } /** * Record the configuration directory so that we can correctly interpolate * the pathnames we're provided. */ set configDir(cfgdir: string) { this.#configdir = cfgdir; } get configDir() { return this.#configdir; } set cacheDir(dirnm: string) { this.#cachedir = dirnm; } get cacheDir() { return this.#cachedir; } // set akasha(_akasha) { this[_config_akasha] = _akasha; } get akasha() { return module_exports; } async documentsCache() { return filecache.documentsCache; } async assetsCache() { return filecache.assetsCache; } async layoutsCache() { return filecache.layoutsCache; } async partialsCache() { return filecache.partialsCache; } /** * Add a directory to the documentDirs configuration array * @param {string} dir The pathname to use */ addDocumentsDir(dir: dirToMount) { // If we have a configDir, and it's a relative directory, make it // relative to the configDir let dirMount: dirToMount; if (typeof dir === 'string') { if (!path.isAbsolute(dir) && this.configDir != null) { dirMount = { src: path.join(this.configDir, dir), dest: '/' }; } else { dirMount = { src: dir, dest: '/' }; } } else if (typeof dir === 'object') { if (!path.isAbsolute(dir.src) && this.configDir != null) { dir.src = path.join(this.configDir, dir.src); dirMount = dir; } else { dirMount = dir; } } else { throw new Error(`addDocumentsDir - directory to mount of wrong type ${util.inspect(dir)}`); } this.#documentDirs.push(dirMount); // console.log(`addDocumentsDir ${util.inspect(dir)} ==> ${util.inspect(this[_config_documentDirs])}`); return this; } get documentDirs() { return this.#documentDirs; } /** * Look up the document directory information for a given document directory. * @param {string} dirname The document directory to search for */ documentDirInfo(dirname: string) { for (var docDir of this.documentDirs) { if (typeof docDir === 'object') { if (docDir.src === dirname) { return docDir; } } else if (docDir === dirname) { return docDir; } } } /** * Add a directory to the layoutDirs configurtion array * @param {string} dir The pathname to use */ addLayoutsDir(dir: dirToMount) { // If we have a configDir, and it's a relative directory, make it // relative to the configDir let dirMount: dirToMount; if (typeof dir === 'string') { if (!path.isAbsolute(dir) && this.configDir != null) { dirMount = { src: path.join(this.configDir, dir), dest: '/' }; } else { dirMount = { src: dir, dest: '/' }; } } else if (typeof dir === 'object') { if (!path.isAbsolute(dir.src) && this.configDir != null) { dir.src = path.join(this.configDir, dir.src); dirMount = dir; } else { dirMount = dir; } } else { throw new Error(`addLayoutsDir - directory to mount of wrong type ${util.inspect(dir)}`); } this.#layoutDirs.push(dirMount); // console.log(`AkashaRender Configuration addLayoutsDir ${util.inspect(dir)} ${util.inspect(dirMount)} layoutDirs ${util.inspect(this.#layoutDirs)} Renderers layoutDirs ${util.inspect(this.#renderers.layoutDirs)}`); this.#renderers.addLayoutDir(dirMount.src); // console.log(`AkashaRender Configuration addLayoutsDir ${util.inspect(dir)} layoutDirs ${util.inspect(this.#layoutDirs)} Renderers layoutDirs ${util.inspect(this.#renderers.layoutDirs)}`); return this; } get layoutDirs() { return this.#layoutDirs; } /** * Add a directory to the partialDirs configurtion array * @param {string} dir The pathname to use * @returns {Configuration} */ addPartialsDir(dir: dirToMount) { // If we have a configDir, and it's a relative directory, make it // relative to the configDir let dirMount: dirToMount; if (typeof dir === 'string') { if (!path.isAbsolute(dir) && this.configDir != null) { dirMount = { src: path.join(this.configDir, dir), dest: '/' }; } else { dirMount = { src: dir, dest: '/' }; } } else if (typeof dir === 'object') { if (!path.isAbsolute(dir.src) && this.configDir != null) { dir.src = path.join(this.configDir, dir.src); dirMount = dir; } else { dirMount = dir; } } else { throw new Error(`addPartialsDir - directory to mount of wrong type ${util.inspect(dir)}`); } // console.log(`addPartialsDir `, dir); this.#partialDirs.push(dirMount); this.#renderers.addPartialDir(dirMount.src); return this; } get partialsDirs() { return this.#partialDirs; } /** * Add a directory to the assetDirs configurtion array * @param {string} dir The pathname to use * @returns {Configuration} */ addAssetsDir(dir: dirToMount) { // If we have a configDir, and it's a relative directory, make it // relative to the configDir let dirMount: dirToMount; if (typeof dir === 'string') { if (!path.isAbsolute(dir) && this.configDir != null) { dirMount = { src: path.join(this.configDir, dir), dest: '/' }; } else { dirMount = { src: dir, dest: '/' }; } } else if (typeof dir === 'object') { if (!path.isAbsolute(dir.src) && this.configDir != null) { dir.src = path.join(this.configDir, dir.src); dirMount = dir; } else { dirMount = dir; } } else { throw new Error(`addAssetsDir - directory to mount of wrong type ${util.inspect(dir)}`); } this.#assetsDirs.push(dirMount); return this; } get assetDirs() { return this.#assetsDirs; } /** * Add an array of Mahabhuta functions * @param {Array} mahafuncs * @returns {Configuration} */ addMahabhuta(mahafuncs: mahabhuta.MahafuncArray | mahabhuta.MahafuncType) { if (typeof mahafuncs === 'undefined' || !mahafuncs) { throw new Error(`undefined mahafuncs in ${this.configDir}`); } this.#mahafuncs.push(mahafuncs); return this; } get mahafuncs() { return this.#mahafuncs; } /** * Define the directory into which the project is rendered. * @param {string} dir The pathname to use * @returns {Configuration} */ setRenderDestination(dir: string) { // If we have a configDir, and it's a relative directory, make it // relative to the configDir if (this.configDir != null) { if (typeof dir === 'string' && !path.isAbsolute(dir)) { dir = path.join(this.configDir, dir); } } this.#renderTo = dir; return this; } /** Fetch the declared destination for rendering the project. */ get renderDestination() { return this.#renderTo; } get renderTo() { return this.#renderTo; } /** * Add a value to the project metadata. The metadata is combined with * the document metadata and used during rendering. * @param {string} index The key to store the value. * @param value The value to store in the metadata. * @returns {Configuration} */ addMetadata(index: string, value: any) { var md = this.#metadata; md[index] = value; return this; } get metadata() { return this.#metadata; } /** * Document the URL for a website project. * @param {string} root_url * @returns {Configuration} */ rootURL(root_url: string) { this.#root_url = root_url; return this; } get root_url() { return this.#root_url; } /** * Set how many documents to render concurrently. * @param {number} concurrency * @returns {Configuration} */ setConcurrency(concurrency: number) { this.#concurrency = concurrency; return this; } get concurrency() { return this.#concurrency; } /** * Declare JavaScript to add within the head tag of rendered pages. * @param script * @returns {Configuration} */ addHeaderJavaScript(script: javaScriptItem) { this.#scripts.javaScriptTop.push(script); return this; } get scripts() { return this.#scripts; } /** * Declare JavaScript to add at the bottom of rendered pages. * @param script * @returns {Configuration} */ addFooterJavaScript(script: javaScriptItem) { this.#scripts.javaScriptBottom.push(script); return this; } /** * Declare a CSS Stylesheet to add within the head tag of rendered pages. * @param script * @returns {Configuration} */ addStylesheet(css: stylesheetItem) { this.#scripts.stylesheets.push(css); return this; } setMahabhutaConfig(cheerio?: cheerio.CheerioOptions) { this.#cheerio = cheerio; // For cheerio 1.0.0-rc.10 we need to use this setting. // If the configuration has set this, we must not // override their setting. But, generally, for correct // operation and handling of Mahabhuta tags, we need // this setting to be <code>true</code> if (!('_useHtmlParser2' in this.#cheerio)) { (this.#cheerio as any)._useHtmlParser2 = true; } // console.log(this[_config_cheerio]); } get mahabhutaConfig() { return this.#cheerio; } /** * Copy the contents of all directories in assetDirs to the render destination. */ async copyAssets() { // console.log('copyAssets START'); const config = this; const assets = filecache.assetsCache; // await assets.isReady(); // Fetch the list of all assets files const paths = await assets.paths(); // The work task is to copy each file const queue = fastq.promise(async function(item) { try { // console.log(`copyAssets ${config.renderTo} ${item.renderPath}`); let destFN = path.join(config.renderTo, item.renderPath); // Make sure the destination directory exists await fsp.mkdir(path.dirname(destFN), { recursive: true }); // Copy from the absolute pathname, to the computed // location within the destination directory // console.log(`copyAssets ${item.fspath} ==> ${destFN}`); await fsp.cp(item.fspath, destFN, { force: true, preserveTimestamps: true }); return "ok"; } catch (err) { throw new Error(`copyAssets FAIL to copy ${item.fspath} ${item.vpath} ${item.renderPath} ${config.renderTo} because ${err.stack}`); } }, 10); // Push the list of asset files into the queue // Because queue.push returns Promise's we end up with // an array of Promise objects const waitFor = []; for (let entry of paths) { waitFor.push(queue.push(entry)); } // This waits for all Promise's to finish // But if there were no Promise's, no need to wait if (waitFor.length > 0) await Promise.all(waitFor); // There are no results in this case to care about // const results = []; // for (let result of waitFor) { // results.push(await result); // } } /** * Call the beforeSiteRendered function of any plugin which has that function. */ async hookBeforeSiteRendered() { // console.log('hookBeforeSiteRendered'); const config = this; for (let plugin of config.plugins) { if (typeof plugin.beforeSiteRendered !== 'undefined') { // console.log(`CALLING plugin ${plugin.name} beforeSiteRendered`); await plugin.beforeSiteRendered(config); } } } /** * Call the onSiteRendered function of any plugin which has that function. */ async hookSiteRendered() { // console.log('hookSiteRendered'); const config = this; for (let plugin of config.plugins) { if (typeof plugin.onSiteRendered !== 'undefined') { // console.log(`CALLING plugin ${plugin.name} onSiteRendered`); await plugin.onSiteRendered(config); } } } async hookFileAdded(collection: string, vpinfo: VPathData) { // console.log(`hookFileAdded ${collection} ${vpinfo.vpath}`); const config = this; for (let plugin of config.plugins) { if (typeof plugin.onFileAdded !== 'undefined') { // console.log(`CALLING plugin ${plugin.name} onFileAdded`); await plugin.onFileAdded(config, collection, vpinfo); } } } async hookFileChanged(collection: string, vpinfo: VPathData) { const config = this; for (let plugin of config.plugins) { if (typeof plugin.onFileChanged !== 'undefined') { // console.log(`CALLING plugin ${plugin.name} onFileChanged`); await plugin.onFileChanged(config, collection, vpinfo); } } } async hookFileUnlinked(collection: string, vpinfo: VPathData) { const config = this; for (let plugin of config.plugins) { if (typeof plugin.onFileUnlinked !== 'undefined') { // console.log(`CALLING plugin ${plugin.name} onFileUnlinked`); await plugin.onFileUnlinked(config, collection, vpinfo); } } } async hookFileCacheSetup(collectionnm: string, collection) { const config = this; for (let plugin of config.plugins) { if (typeof plugin.onFileCacheSetup !== 'undefined') { await plugin.onFileCacheSetup(config, collectionnm, collection); } } } async hookPluginCacheSetup() { const config = this; for (let plugin of config.plugins) { if (typeof plugin.onPluginCacheSetup !== 'undefined') { await plugin.onPluginCacheSetup(config); } } } /** * use - go through plugins array, adding each to the plugins array in * the config file, then calling the config function of each plugin. * @param PluginObj The plugin name or object to add * @returns {Configuration} */ use(PluginObj, options) { // console.log("Configuration #1 use PluginObj "+ typeof PluginObj +" "+ util.inspect(PluginObj)); if (typeof PluginObj === 'string') { // This is going to fail because // require doesn't work in this context // Further, this context does not // support async functions, so we // cannot do import. PluginObj = require(PluginObj); } if (!PluginObj || PluginObj instanceof Plugin) { throw new Error("No plugin supplied"); } // console.log("Configuration #2 use PluginObj "+ typeof PluginObj +" "+ util.inspect(PluginObj)); var plugin = new PluginObj(); plugin.akasha = this.akasha; this.#plugins.push(plugin); if (!options) options = {}; plugin.configure(this, options); return this; } get plugins() { return this.#plugins; } /** * Iterate over the installed plugins, calling the function passed in `iterator` * for each plugin, then calling the function passed in `final`. * * @param iterator The function to call for each plugin. Signature: `function(plugin, next)` The `next` parameter is a function used to indicate error -- `next(err)` -- or success -- next() * @param final The function to call after all iterator calls have been made. Signature: `function(err)` */ eachPlugin(iterator, final) { throw new Error("eachPlugin deprecated"); /* async.eachSeries(this.plugins, function(plugin, next) { iterator(plugin, next); }, final); */ } /** * Look for a plugin, returning its module reference. * @param {string} name * @returns {Plugin} */ plugin(name: string) { // console.log('config.plugin: '+ util.inspect(this._plugins)); if (! this.plugins) { return undefined; } for (var pluginKey in this.plugins) { var plugin = this.plugins[pluginKey]; if (plugin.name === name) return plugin; } console.log(`WARNING: Did not find plugin ${name}`); return undefined; } /** * Retrieve the pluginData object for the named plugin. * @param {string} name * @returns {Object} */ pluginData(name: string) { var pluginDataArray = this.#pluginData; if (!(name in pluginDataArray)) { pluginDataArray[name] = {}; } return pluginDataArray[name]; } askPluginsLegitLocalHref(href) { for (var plugin of this.plugins) { if (typeof plugin.isLegitLocalHref !== 'undefined') { if (plugin.isLegitLocalHref(this, href)) { return true; } } } return false; } registerRenderer(renderer: Renderer) { if (!(renderer instanceof Renderer)) { console.error('Not A Renderer '+ util.inspect(renderer)); throw new Error(`Not a Renderer ${util.inspect(renderer)}`); } if (!this.findRendererName(renderer.name)) { // renderer.akasha = this.akasha; // renderer.config = this; // console.log(`registerRenderer `, renderer); this.#renderers.registerRenderer(renderer); } } /** * Allow an application to override one of the built-in renderers * that are initialized below. The inspiration is epubtools that * must write HTML files with an .xhtml extension. Therefore it * can subclass EJSRenderer etc with implementations that force the * file name to be .xhtml. We're not checking if the renderer name * is already there in case epubtools must use the same renderer name. */ registerOverrideRenderer(renderer: Renderer) { if (!(renderer instanceof Renderer)) { console.error('Not A Renderer '+ util.inspect(renderer)); throw new Error('Not a Renderer'); } // renderer.akasha = this.akasha; // renderer.config = this; this.#renderers.registerOverrideRenderer(renderer); } findRendererName(name: string): Renderer { return this.#renderers.findRendererName(name); } findRendererPath(_path: string): Renderer { return this.#renderers.findRendererPath(_path); } get renderers() { return this.#renderers; } /** * Find a Renderer by its extension. */ findRenderer(name: string) { return this.findRendererName(name); } } const module_exports = { Renderers, Renderer: Renderers.Renderer, mahabhuta, filecache, setup, cacheSetup, closeCaches, fileCachesReady, Plugin, render, renderDocument, renderPath, readRenderedFile, partial, partialSync, indexChain, relative, linkRelSetAttr, generateRSS, Configuration } as any; export default module_exports;