akasharender
Version:
Rendering support for generating static HTML websites or EPUB eBooks
1,210 lines • 169 kB
JavaScript
/**
*
* 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