UNPKG

akasharender

Version:

Rendering support for generating static HTML websites or EPUB eBooks

1,210 lines 169 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. */ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _Configuration_instances, _Configuration_renderers, _Configuration_configdir, _Configuration_cachedir, _Configuration_assetsDirs, _Configuration_layoutDirs, _Configuration_documentDirs, _Configuration_partialDirs, _Configuration_mahafuncs, _Configuration_cheerio, _Configuration_renderTo, _Configuration_scripts, _Configuration_concurrency, _Configuration_cachingTimeout, _Configuration_metadata, _Configuration_root_url, _Configuration_plugins, _Configuration_pluginData, _Configuration_verbose, _Configuration_perfDataDir, _Configuration_descriptions, _Configuration_saveDescriptionsToDB; /** * 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 { mimedefine, isDirToMount } from './cache/vfstack.js'; export { isDirToMount } from './cache/vfstack.js'; 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'; const { PerfDataStore, FilesystemPerfDataStore } = 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'; export { validTagDescription } from './types.js'; import { render, render2, renderDocument, renderDocument2 } from './render.js'; export { render, render2, renderDocument, renderDocument2, renderContent } from './render.js'; export { SitemapValidator } from './sitemap-validator.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/cache-sqlite.js'; import { sqdb } from './sqdb.js'; export { newSQ3DataStore } from './sqdb.js'; import { init } from './data.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); await init(); } export async function cacheSetup(config) { try { await filecache.setup(config, await sqdb); } 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; } export async function renderPath2(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 renderDocument2(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.docBody) partialText = found.docBody; 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 = {}; 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 = {}; 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({ 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(' ')); } } ; /** * Compute an absolute vpath from a relative path reference. * * This function resolves a relative path (like "../file.html" or "./file.html") * to an absolute vpath in the virtual filesystem, based on the vpath of the * current document. * * If the input path is already absolute (starts with '/'), it is returned * as-is after normalization. * * @param baseVpath The vpath of the document making the reference (e.g., metadata.document.path) * @param relativePath The path to resolve (can be relative or absolute) * @returns The absolute vpath in the virtual filesystem * * @example * // From document at 'hier/dir1/page.html.md' referencing '../sibling/file.html' * resolveVpath('hier/dir1/page.html.md', '../sibling/file.html') * // Returns: '/hier/sibling/file.html' * * @example * // Already absolute path * resolveVpath('hier/dir1/page.html.md', '/absolute/path.html') * // Returns: '/absolute/path.html' */ export function resolveVpath(baseVpath, relativePath) { if (!baseVpath || typeof baseVpath !== 'string') { throw new Error(`resolveVpath: baseVpath must be a non-empty string, got ${typeof baseVpath}`); } if (!relativePath || typeof relativePath !== 'string') { throw new Error(`resolveVpath: relativePath must be a non-empty string, got ${typeof relativePath}`); } // If the path is already absolute, return it normalized if (path.isAbsolute(relativePath)) { return path.normalize(relativePath); } // Get the directory of the base vpath const dir = path.dirname(baseVpath); // Join with '/' prefix to ensure we get an absolute vpath // and normalize to clean up any .. or . segments return path.normalize(path.join('/', dir, relativePath)); } ///////////////// 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' }); } ; /** * 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. */ /** * 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 { constructor(modulepath) { _Configuration_instances.add(this); _Configuration_renderers.set(this, void 0); _Configuration_configdir.set(this, void 0); _Configuration_cachedir.set(this, void 0); _Configuration_assetsDirs.set(this, void 0); _Configuration_layoutDirs.set(this, void 0); _Configuration_documentDirs.set(this, void 0); _Configuration_partialDirs.set(this, void 0); _Configuration_mahafuncs.set(this, void 0); _Configuration_cheerio.set(this, void 0); _Configuration_renderTo.set(this, void 0); _Configuration_scripts.set(this, void 0); _Configuration_concurrency.set(this, void 0); _Configuration_cachingTimeout.set(this, void 0); _Configuration_metadata.set(this, void 0); _Configuration_root_url.set(this, void 0); _Configuration_plugins.set(this, void 0); _Configuration_pluginData.set(this, void 0); _Configuration_verbose.set(this, void 0); _Configuration_perfDataDir.set(this, void 0); _Configuration_descriptions.set(this, void 0); // this[_config_renderers] = []; __classPrivateFieldSet(this, _Configuration_renderers, new Renderers.Configuration({}), "f"); __classPrivateFieldSet(this, _Configuration_mahafuncs, [], "f"); __classPrivateFieldSet(this, _Configuration_scripts, { stylesheets: [], javaScriptTop: [], javaScriptBottom: [] }, "f"); __classPrivateFieldSet(this, _Configuration_concurrency, 3, "f"); // 60 seconds, or 1 minute __classPrivateFieldSet(this, _Configuration_cachingTimeout, 60000, "f"); __classPrivateFieldSet(this, _Configuration_documentDirs, [], "f"); __classPrivateFieldSet(this, _Configuration_layoutDirs, [], "f"); __classPrivateFieldSet(this, _Configuration_partialDirs, [], "f"); __classPrivateFieldSet(this, _Configuration_assetsDirs, [], "f"); __classPrivateFieldSet(this, _Configuration_mahafuncs, [], "f"); __classPrivateFieldSet(this, _Configuration_renderTo, 'out', "f"); __classPrivateFieldSet(this, _Configuration_metadata, {}, "f"); __classPrivateFieldSet(this, _Configuration_plugins, [], "f"); __classPrivateFieldSet(this, _Configuration_pluginData, [], "f"); __classPrivateFieldSet(this, _Configuration_verbose, false, "f"); __classPrivateFieldSet(this, _Configuration_perfDataDir, undefined, "f"); /* * 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 (!__classPrivateFieldGet(this, _Configuration_cachedir, "f")) { 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 (__classPrivateFieldGet(this, _Configuration_cachedir, "f") && !fs.existsSync(__classPrivateFieldGet(this, _Configuration_cachedir, "f"))) { fs.mkdirSync(__classPrivateFieldGet(this, _Configuration_cachedir, "f"), { recursive: true }); } const assetsDirsPath = configDirPath('assets'); if (!__classPrivateFieldGet(this, _Configuration_assetsDirs, "f")) { if (fs.existsSync(assetsDirsPath) && (stat = fs.statSync(assetsDirsPath))) { if (stat.isDirectory()) { this.addAssetsDir('assets'); } } } const layoutsDirsPath = configDirPath('layouts'); if (!__classPrivateFieldGet(this, _Configuration_layoutDirs, "f")) { 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 (!__classPrivateFieldGet(this, _Configuration_documentDirs, "f")) { 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 (!__classPrivateFieldGet(this, _Configuration_renderTo, "f")) { 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 (__classPrivateFieldGet(this, _Configuration_renderTo, "f") && !fs.existsSync(__classPrivateFieldGet(this, _Configuration_renderTo, "f"))) { fs.mkdirSync(__classPrivateFieldGet(this, _Configuration_renderTo, "f"), { 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) { __classPrivateFieldSet(this, _Configuration_configdir, cfgdir, "f"); } get configDir() { return __classPrivateFieldGet(this, _Configuration_configdir, "f"); } set cacheDir(dirnm) { __classPrivateFieldSet(this, _Configuration_cachedir, dirnm, "f"); } get cacheDir() { return __classPrivateFieldGet(this, _Configuration_cachedir, "f"); } set verbose(val) { __classPrivateFieldSet(this, _Configuration_verbose, val, "f"); } get verbose() { return __classPrivateFieldGet(this, _Configuration_verbose, "f"); } set perfDataDir(storeDir) { __classPrivateFieldSet(this, _Configuration_perfDataDir, storeDir, "f"); } get perfDataDir() { return __classPrivateFieldGet(this, _Configuration_perfDataDir, "f"); } // 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 | dirToMount} dir The pathname to use or dirToMount object */ addDocumentsDir(dir) { let dirMount; 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 (!path.isAbsolute(dir.src) && this.configDir != null) { dirMount = { ...dir, src: path.join(this.configDir, dir.src) }; } else { dirMount = dir; } } if (!isDirToMount(dirMount)) { throw new Error(`addDocumentsDir - invalid dirToMount object: ${util.inspect(dirMount)}`); } __classPrivateFieldGet(this, _Configuration_documentDirs, "f").push(dirMount); return this; } get documentDirs() { return __classPrivateFieldGet(this, _Configuration_documentDirs, "f"); } /** * Look up the document directory information for a given document directory. * @param {string} dirname The document directory to search for */ documentDirInfo(dirname) { 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 | dirToMount} dir The pathname to use or dirToMount object */ addLayoutsDir(dir) { let dirMount; 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 (!path.isAbsolute(dir.src) && this.configDir != null) { dirMount = { ...dir, src: path.join(this.configDir, dir.src) }; } else { dirMount = dir; } } if (!isDirToMount(dirMount)) { throw new Error(`addLayoutsDir - invalid dirToMount object: ${util.inspect(dirMount)}`); } __classPrivateFieldGet(this, _Configuration_layoutDirs, "f").push(dirMount); __classPrivateFieldGet(this, _Configuration_renderers, "f").addLayoutDir(dirMount.src); return this; } get layoutDirs() { return __classPrivateFieldGet(this, _Configuration_layoutDirs, "f"); } /** * Add a directory to the partialDirs configurtion array * @param {string | dirToMount} dir The pathname to use or dirToMount object * @returns {Configuration} */ addPartialsDir(dir) { let dirMount; 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 (!path.isAbsolute(dir.src) && this.configDir != null) { dirMount = { ...dir, src: path.join(this.configDir, dir.src) }; } else { dirMount = dir; } } if (!isDirToMount(dirMount)) { throw new Error(`addPartialsDir - invalid dirToMount object: ${util.inspect(dirMount)}`); } __classPrivateFieldGet(this, _Configuration_partialDirs, "f").push(dirMount); __classPrivateFieldGet(this, _Configuration_renderers, "f").addPartialDir(dirMount.src); return this; } get partialsDirs() { return __classPrivateFieldGet(this, _Configuration_partialDirs, "f"); } /** * Add a directory to the assetDirs configurtion array * @param {string | dirToMount} dir The pathname to use or dirToMount object * @returns {Configuration} */ addAssetsDir(dir) { let dirMount; 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 (!path.isAbsolute(dir.src) && this.configDir != null) { dirMount = { ...dir, src: path.join(this.configDir, dir.src) }; } else { dirMount = dir; } } if (!isDirToMount(dirMount)) { throw new Error(`addAssetsDir - invalid dirToMount object: ${util.inspect(dirMount)}`); } __classPrivateFieldGet(this, _Configuration_assetsDirs, "f").push(dirMount); return this; } get assetDirs() { return __classPrivateFieldGet(this, _Configuration_assetsDirs, "f"); } /** * Add an array of Mahabhuta functions * @param {Array} mahafuncs * @returns {Configuration} */ addMahabhuta(mahafuncs) { if (typeof mahafuncs === 'undefined' || !mahafuncs) { throw new Error(`undefined mahafuncs in ${this.configDir}`); } __classPrivateFieldGet(this, _Configuration_mahafuncs, "f").push(mahafuncs); return this; } get mahafuncs() { return __classPrivateFieldGet(this, _Configuration_mahafuncs, "f"); } /** * Define the directory into which the project is rendered. * @param {string} dir The pathname to use * @returns {Configuration} */ setRenderDestination(dir) { // 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); } } __classPrivateFieldSet(this, _Configuration_renderTo, dir, "f"); return this; } /** Fetch the declared destination for rendering the project. */ get renderDestination() { return __classPrivateFieldGet(this, _Configuration_renderTo, "f"); } get renderTo() { return __classPrivateFieldGet(this, _Configuration_renderTo, "f"); } /** * 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, value) { var md = __classPrivateFieldGet(this, _Configuration_metadata, "f"); md[index] = value; return this; } get metadata() { return __classPrivateFieldGet(this, _Configuration_metadata, "f"); } /** * Add tag descriptions to the database. The purpose * is for example a tag index page can give a * description at the top of the page. * * NOTE: Potential bug - This function replaces the entire #descriptions * array rather than merging with existing descriptions. If called multiple * times, earlier descriptions will be lost. Current assumption is this * function is only called once from the configuration file. A future * enhancement would be to merge descriptions instead of replacing. * * @param tagdescs */ async addTagDescriptions(tagdescs) { if (!Array.isArray(tagdescs)) { throw new Error(`addTagDescriptions must be given an array of tag descriptions`); } for (const desc of tagdescs) { if (typeof desc.tagName !== 'string' || typeof desc.description !== 'string') { throw new Error(`Incorrect tag description ${util.inspect(desc)}`); } } // TODO: Consider merging with existing descriptions instead of replacing __classPrivateFieldSet(this, _Configuration_descriptions, tagdescs, "f"); } /** * Document the URL for a website project. * @param {string} root_url * @returns {Configuration} */ rootURL(root_url) { __classPrivateFieldSet(this, _Configuration_root_url, root_url, "f"); return this; } get root_url() { return __classPrivateFieldGet(this, _Configuration_root_url, "f"); } /** * Set how many documents to render concurrently. * @param {number} concurrency * @returns {Configuration} */ setConcurrency(concurrency) { __classPrivateFieldSet(this, _Configuration_concurrency, concurrency, "f"); return this; } get concurrency() { return __classPrivateFieldGet(this, _Configuration_concurrency, "f"); } /** * Set the time, in miliseconds, to honor * the SearchCache in the search function. * * Default is 60000 (1 minute). * * Set to 0 to disable caching. * @param timeout */ setCachingTimeout(timeout) { __classPrivateFieldSet(this, _Configuration_cachingTimeout, timeout, "f"); // console.log(`setSearchCacheTimeout ${this.#searchCacheTimeout}`); } get cachingTimeout() { // console.log(`searchCacheTimeout ${this.#searchCacheTimeout}`); return __classPrivateFieldGet(this, _Configuration_cachingTimeout, "f"); } /** * Declare JavaScript to add within the head tag of rendered pages. * @param script * @returns {Configuration} */ addHeaderJavaScript(script) { __classPrivateFieldGet(this, _Configuration_scripts, "f").javaScriptTop.push(script); return this; } get scripts() { return __classPrivateFieldGet(this, _Configuration_scripts, "f"); } /** * Declare JavaScript to add at the bottom of rendered pages. * @param script * @returns {Configuration} */ addFooterJavaScript(script) { __classPrivateFieldGet(this, _Configuration_scripts, "f").javaScriptBottom.push(script); return this; } /** * Declare a CSS Stylesheet to add within the head tag of rendered pages. * @param script * @returns {Configuration} */ addStylesheet(css) { __classPrivateFieldGet(this, _Configuration_scripts, "f").stylesheets.push(css); return this; } setMahabhutaConfig(cheerio) { __classPrivateFieldSet(this, _Configuration_cheerio, cheerio, "f"); // 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 __classPrivateFieldGet(this, _Configuration_cheerio, "f"))) { __classPrivateFieldGet(this, _Configuration_cheerio, "f")._useHtmlParser2 = true; } // console.log(this[_config_cheerio]); } get mahabhutaConfig() { return __classPrivateFieldGet(this, _Configuration_cheerio, "f"); } /** * 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(); // console.log(`copyAssets paths`, // paths.map(item => { // return { // vpath: item.vpath, // renderPath: item.renderPath, // mime: item.mime // } // }) // ) // 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, vpinfo) { // 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, vpinfo) { 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, vpinfo) { 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, 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); } } // SPECIAL TREATMENT // The tag descriptions need to be installed // in the database. It is impossible to do // that during Configuration setup in // the addTagDescriptions method. // This function is invoked after the database // is setup. await __classPrivateFieldGet(this, _Configuration_instances, "m", _Configuration_saveDescriptionsToDB).call(this); } /** * 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 "+ ty