'use strict';
// TODO : ESM remove the following lines...
const TemplateOpts = require('./template-options');
const Director = require('./director');
// TODO : ESM uncomment the following lines...
// TODO : import * as TemplateOpts from './template-options.mjs';
// TODO : import * as Director from './director.mjs';
const FUNC_NAME_REGEXP = /[^\da-zA-Z]/g, FUNC_MAX_LOG = 500;
/**
* A locally sandboxed/isolated environment within a single VM instance
*/
class Sandbox {
// TODO : ESM use... export class Sandbox {
/**
* Compiles a locally sandboxed `async` template rendering function
* @param {String} [name] The name given to the template (omit to generate via {@link Sandbox.guid})
* @param {String} [template] The template content that will be used by the renderer. Omit to load the template from cache.
* @param {TemplateOpts} [compileOpts] The {@link TemplateOpts}
* @param {Object} [namers] One or more functions responsible for formatting template names into a full path name
* that is consumable by `read`/`write`.
* Each function accepts the following arguments:
* 1. _{Object}_ `namers` An object that contains a property for each available naming function.
* 1. _{String}_ `partialName` The name of the partial that will be converted into a name suitable for a read operation.
* 1. _{(TemplateOpts | Function(name:String):*)}_ `optional` Either the {@link TemplateOpts} or a function that takes a single name
* argument and returns the option value.
* 1. _{URLSearchParams}_ `[params]` The URLSearchParams that should be used in the converted name.
* 1. _{Object}_ `storage` The storage object that can contain metadata used by naming operations.
* 1. _{Boolean}_ `forContent` The flag indicating if the converted name is being used to capture partials.
* 1. _{String}_ `extension` The file extension override for the converted name (omit to use the default extension set in the options).
* 1. _{Boolean}_ `forContext` The flag indicating if the converted name is being used to capture context.
* @param {Function} [namers.namer] The default naming function that will be used.
* @param {Function} [namers.namerSuper] The naming function to use when a `operations[].read` function throws an error. The next
* reader called in the `operations[]` list will use the name generated by this reader.
* @param {Object[]} [operations] One or more functions and/or objects that will handle render-time read/write operations.
* @param {Function} [operations[].read] The reader is an `async function` responsible for reading partial template content/modules/etc
* during render-time when a partial template cannot be found within `includes`. When `options.cacheRawTemplates` is _truthy_ an
* attempt will be made to add any missing/read partials into `storage.data` in order to prevent unnecessary template partial
* reads for repeated includes. Read functions should not reference any external scope other than the global object space.
* The following arguments will be passed:
* 1. _{String}_ `name` The name of the partial that will be read. The read function may be invoked without a _name_ parameter when
* the intent is to capture all partials in a single read opteration that will be included.
* 1. _{String}_ `path` The path to the partial that will be read. The read function may be invoked without a _path_ parameter when
* the intent is to capture all partials in a single read opteration that will be included.
* 1. _{String}_ `ext` The path file extension to the partial that will be read. The read function may be invoked without an _ext_
* parameter when the intent is to capture all partials in a single read opteration that will be included.
* 1. _{Boolean}_ `forContent` The flag indicating that the read is for content. Otherwise, the read is for rendering functions.
* 1. _{(TemplateOpts | Function(name:String):*)}_ `optional` Either the {@link TemplateOpts} or a function that takes a single name
* argument and returns the option value.
* 1. _{URLSearchParams}_ `[params]` The URLSearchParams that should be used during the read
* 1. _{Object}_ `storage` The storage object that can contain metadata for read operations and should contain a __data__ object
* that stores each of the read paratial template content/metadata.
* 1. _{Function}_ `[formatter]` The function that will format reads/writes during include discovery (if any). The formatting function
* takes 1 or 2 arguments with the first being the content that will be formatted and the second being `options.readFormatOptions` for
* reads and `options.writeFormatOptions` for writes.
* The returned result should be a valid string.
* 1. _{Boolean}_ `[close]` A flag indicating whether or not any resources used during the read should be closed/cleaned up after the
* read completes. Closure may be dependent upon the policy set on the options.
* 1. _{Object}_ `[log]` A logger that can contain functions for each of the following: `error`/`warn`/`info`/`debug`.
*
* Read functions can return the partial template content and/or it can be set on the `storage.data`.
* Returning `true` will stop any further rendering from processing resulting in the rendering function returning a _blank_ string.
* @param {Function} [operations[].write] The write function that will be used for writting newly discovered template sources.
* Accepts the same arguments as `operations[].read` and all _scoped_ functions will be available. Can return a __rendering__
* function that will prevent further iteration of any subsequent `operations[].write` invocations.
* @param {Function} [operations[].finish] An `async function` that can perform cleanup tasks for a reader. Arguments passed are
* `storage`, `optional` and `log` as described for `operations[].read`.
* __after rendering has completed__. Arguments passed are `storage`, `optional` and `log` as described for `operations[].read`. All
* functions defined within `operations[].scopes` will be available by name.
* @param {Function[]} [operations[].scopes] Zero or more functions that will be in scope when the read function is called.
* Scoped functions can assit with complex read/write that can benefit from separate supporting functions. For example, `[myFunc(){}]`
* could be referenced like `async function myReader(){ myFunc(); ... }`.
* @param {Director} [director] The {@link Director} that will be for extracting {@link Director.directives}
* @param {Object} [store] The private storage set during compilation that will be passed during naming/reads. Contents should only
* contain __valid JSON__ properties that can be serialized/deserialized.
* @param {Object} [store.data] The cached partials that can be included in the template with the name of the template as a
* property of the object
* @param {String} [store.data[].name] The name of the partial template
* @param {String} [store.data[].content] The partial template content
* @param {Object} [store.data[].params] The parameters that will be added to scope when the template is parsed
* @param {Object} [log] The log flags that will determine what output will be sent to the `console` (if any) during rendering
* @param {Boolean} [log.debug] `true` to output `console.debug` level log
* @param {Boolean} [log.info] `true` to output `console.info` level log
* @param {Boolean} [log.warn] `true` to output `console.warn` level log
* @param {Boolean} [log.error] `true` to output `console.error` level log
* @returns {Function} The rendering `async function` that returns a template result string based upon the provided context.
* The following arguments apply:
* 1. _{Object}_ `context` The context JSON that can be used as data during rendering
* 1. _{TemplateOpts}_ `[renderOptions]` The rendering options that will superceed any __compile-time__ options
* 1. _{Function}_ `[readFormatter]` The function that will format read partials during include discovery (if any). The
* formatting function takes 1 or 2 arguments with the first being the content that will be formatted and the second being
* the `options.readFormatOptions`. The returned result should be a valid string.
* 1. _{Function}_ `[writeFormatter]` The function that will format written sources during include discovery (if any). The
* formatting function takes 1 or 2 arguments with the first being the content that will be formatted and the second being
* the `options.writeFormatOptions`. The returned result should be a valid string.
* 1. _{Object}_ `[sharedStore]` An object used for _in-memory_ storage space that can be shared between rendering functions.
* This ensures that updated data within a renderer execution will be retained between rendering calls from the same renderer
* or different renderers that are passed the same _shared store_.
*/
static compile(name, template, compileOpts, namers, operations, director, store, log) {
const copts = compileOpts instanceof TemplateOpts ? compileOpts : new TemplateOpts(compileOpts);
const named = (name && name.replace(FUNC_NAME_REGEXP, '_')) || `template_${Sandbox.guid(null, false)}`;
const directives = codedDirectives(director);
const code = coded(named, template, copts, directives, FUNC_NAME_REGEXP, namers, operations, {
debug: log && !!log.debug, info: log && !!log.info, warn: log && !!log.warn,
error: log && !!log.error
}, null, store);
return Sandbox.deserialzeBlock(code, named, true);
}
/**
* Deserialzes a function string within a locally sandboxed environment (only global variables are accessible)
* @param {String} functionString The function string to deserialize
* @returns {Function|null} The deserialized function
*/
static deserialzeFunction(functionString) {
if (functionString && typeof functionString === 'string') {
try {
return (new Function(`return ${functionString}`))();
} catch (err) {
err.message += ` <- Unable to deserialize function string: `
+ (functionString.length < FUNC_MAX_LOG ? functionString : `${functionString.substring(0, FUNC_MAX_LOG)}...`);
throw err;
}
}
}
/**
* Deserialzes a code block iwthin a locally sandboxed environment (only global variables are accessible)
* @param {(String | Function)} block The code block to deserialize
* @param {String} [name] A name that will be given to the function
* @param {Boolean} [isAsync] `true` when the function is `async`
* @returns {(Function | undefined)} The deserialized function
*/
static deserialzeBlock(block, name, isAsync) {
const type = typeof block;
if (block && (type === 'string' || type === 'function')) {
if (type === 'function') { // convert function into iife formatted function block
block = `return (${block.toString()})()`;
}
const named = name && name.length ? ` ${name.replace(FUNC_NAME_REGEXP, '_')}` : '';
// pass require (when present) since it may be hidden from the block scope
const rqr = typeof require !== 'undefined' ? require : undefined;
const rqrd = rqr ? `const require=arguments[0];` : '';
const code = `${rqrd}return ${isAsync ? 'async ' : ''}function${named}(){ ${block}; }`;
try {
//const func = new Function(code);
//if (named) Object.defineProperty(rtn.func, 'name', { value: named });
//return named ? { [named](...args) { return func(...args); } }[named] : func;
// could also use ES2017 new AsyncFunction(...)
return (new Function(code))(rqr);
} catch (err) {
err.message += ` <- Unable to deserialize code on name: "${name || ''}" for:\n`
+ (code.length < FUNC_MAX_LOG ? code : `${code.substring(0, FUNC_MAX_LOG)}...`);
throw err;
}
}
}
/**
* Serialzes a function
* @param {Function} func The function to serialize
* @returns {String|null} The serialized function
*/
static serialzeFunction(func) {
return func && typeof func === 'function' ? func.toString() : null;
}
/**
* Generates a GUID or formats an existing `value`
* @param {String} [value] when present, will format the value by add any missing hyphens (if `hyphenate=true`)
* instead of generating a new value
* @param {Boolean} [hyphenate=true] true to include hyphens in generated result
* @returns {String} the generated GUID
*/
static guid(value, hyphenate = true) {
const hyp = hyphenate ? '-' : '';
if (value) return hyphenate ? value.replace(/(.{8})-?(.{4})-?(.{4})-?(.{4})-?(.{12})/gi, `$1${hyp}$2${hyp}$3${hyp}$4${hyp}$5`) : value;
return `xxxxxxxx${hyp}xxxx${hyp}4xxx${hyp}yxxx${hyp}xxxxxxxxxxxx`.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* @returns {Object} The global object. Typically, `window` when ran within a browser or `global` when ran on a server
*/
static get global() {
return (function globalize(){ return this; })();
}
}
// TODO : ESM remove the following lines...
module.exports = Sandbox;
/**
* Manufactures directives/functions that will be available to template lierals during rendering
* @private
* @param {Director} [director] The {@link Director} that will be for extracting {@link Director.directives}
* @returns {Object} The `{ names:String, code:String }` where `names` is a string representation of the directive
* names (in array format) and `code` represents the commulative coded functions
*/
function codedDirectives(director) {
if (!(director instanceof Director)) {
throw new Error(`Expected ${Director.name}, but found ${director}`);
}
const directives = director.directives;
const rtn = { names: '', code: '' };
for (let drv of directives) {
rtn.code += `${drv.code.toString()}`;
rtn.names += (rtn.names ? ',' : '') + drv.name;
}
rtn.names = `[${rtn.names}]`;
return rtn;
}
/**
* Generates a locally sandboxed environment compilation for template rendering
* @private
* @param {String} name A sanitized name given to the template
* @param {String} [template] The template content that will be used by the renderer. Omit to load the template from cache.
* @param {Object} includes The cached partials that can be included in the template with the name of
* the template as a property of the object
* @param {String} includes[].name The name of the partial template
* @param {String} includes[].content The partial template content
* @param {TemplateOpts} compileOpts The {@link TemplateOpts}
* @param {Object} directives A return object from {@link codedDirectives}
* @param {RegExp} nameRegExp The regular expression that will replace invalid characters when naming rendering functions
* @param {Object} [namers] One or more functions responsible for formatting template names into a full path name
* that is consumable by `read`/`write`.
* Each function accepts the following arguments:
* 1. _{Object}_ `namers` An object that contains a property for each available naming function.
* 1. _{String}_ `partialName` The name of the partial that will be converted into a name suitable for a read operation.
* 1. _{String}_ `path` The full name/path for the partial returned from a __namer__ function.
* 1. _{(TemplateOpts | Function(name:String):*)}_ `optional` Either the {@link TemplateOpts} or a function that takes a single name
* argument and returns the option value.
* 1. _{URLSearchParams}_ `[params]` The URLSearchParams that should be used in the converted name.
* 1. _{Object}_ `storage` The storage object that can contain metadata used by naming operations.
* 1. _{Boolean}_ `forContent` The flag indicating if the converted name is being used to capture partials.
* 1. _{String}_ `extension` The file extension override for the converted name (omit to use the default extension set in the options).
* 1. _{Boolean}_ `forContext` The flag indicating if the converted name is being used to capture context.
* @param {Function} [namers.namer] The default naming function that will be used.
* @param {Function} [namers.namerSuper] The naming function to use when a `operations[].read` function throws an error. The next
* reader called in the `operations[]` list will use the name generated by this reader.
* @param {Object[]} [operations] One or more functions and/or objects that will handle render-time read/write operations.
* @param {Function} [operations[].read] The reader is an `async function` responsible for reading partial template content/modules/etc
* during render-time when a partial template cannot be found within `includes`. When `options.cacheRawTemplates` is _truthy_ an
* attempt will be made to add any missing/read partials into `storage.data` in order to prevent unnecessary template partial
* reads for repeated includes. Read functions should not reference any external scope other than the global object space.
* The following arguments will be passed:
* 1. _{String}_ `name` The name of the partial that will be read. The read function may be invoked without a _name_ parameter when
* the intent is to capture all partials in a single read opteration that will be included.
* 1. _{String}_ `path` The path to the partial that will be read. The read function may be invoked without a _path_ parameter when
* the intent is to capture all partials in a single read opteration that will be included.
* 1. _{String}_ `ext` The path file extension to the partial that will be read. The read function may be invoked without an _ext_
* parameter when the intent is to capture all partials in a single read opteration that will be included.
* 1. _{Boolean}_ `forContent` The flag indicating that the read is for content. Otherwise, the read is for rendering functions.
* 1. _{(TemplateOpts | Function(name:String):*)}_ `optional` Either the {@link TemplateOpts} or a function that takes a single name
* argument and returns the option value.
* 1. _{URLSearchParams}_ `[params]` The URLSearchParams that should be used during the read
* 1. _{Object}_ `storage` The storage object that can contain metadata for read operations and should contain a __data__ object
* that stores each of the read paratial template content/metadata.
* 1. _{Function}_ `[formatter]` The function that will format reads/writes during include discovery (if any). The formatting function
* takes 1 or 2 arguments with the first being the content that will be formatted and the second being `options.readFormatOptions` for
* reads and `options.writeFormatOptions` for writes.
* The returned result should be a valid string.
* 1. _{Boolean}_ `[close]` A flag indicating whether or not any resources used during the read should be closed/cleaned up after the
* read completes. Closure may be dependent upon the policy set on the options.
* 1. _{Object}_ `[log]` A logger that can contain functions for each of the following: `error`/`warn`/`info`/`debug`.
*
* Read functions can return the partial template content and/or it can be set on the `storage.data`.
* Returning `true` will stop any further rendering from processing resulting in the rendering function returning a _blank_ string.
* @param {Function} [operations[].write] The write function that will be used for writting newly discovered template sources.
* Accepts the same arguments as `operations[].read` and all _scoped_ functions will be available. Can return a __rendering__
* function that will prevent further iteration of any subsequent `operations[].write` invocations.
* @param {Function} [operations[].finish] An `async function` that can perform cleanup tasks for a reader. Arguments passed are
* `storage`, `optional` and `log` as described for `operations[].read`.
* __after rendering has completed__. Arguments passed are `storage`, `optional` and `log` as described for `operations[].read`. All
* functions defined within `operations[].scopes` will be available by name.
* @param {Function[]} [operations[].scopes] Zero or more functions that will be in scope when the read function is called.
* Scoped functions can assit with complex reads/writes that can benefit from separate supporting functions. For example, `[myFunc(){}]`
* could be referenced like `async function myReader(){ myFunc(); ... }`.
* @param {Object} [log] The log flags that will determine what output will be sent to the `console` (if any) during rendering
* @param {Boolean} [log.debug] `true` to output `console.debug` level log
* @param {Boolean} [log.info] `true` to output `console.info` level log
* @param {Boolean} [log.warn] `true` to output `console.warn` level log
* @param {Boolean} [log.error] `true` to output `console.error` level log
* @param {Object} [params] The parameters that will be available via the `options.includesParametersName`
* alias within the scope of the included template
* @param {Object} [store] The private storage that is passed during naming/reads. Contents should only contain __valid JSON__ properties that
* can be serialized/deserialized.
* @param {Object} [storeMeta] The __private__ metadata pertaining to the storage being used
* @param {Boolean} [storeMeta.hasNoNameInit] `true` when the templates have been initialized meetting the criteria described in {@link _readHandler}
* @param {Boolean} [storeMeta.finished] `true` when all of the available `operations[].finish` functions have completed execution
* @param {Object} [metadata] The metadata that describes the template being coded (exposed to the tempalte scope)
* @param {String} [metadata.name] The name of the template that the current template is being coded for
* @param {Object} [metadata.parent] The metadata of the template parent to the current one being coded
* @returns {String} A coded rendering representation to be used in a function body by a template
* engine. Assumes `arguments[0]` is a `context` object, `arguments[1]` is the rendering options object and `arguments[2]` is a function that
* will format written sources during include discovery (if any). The formatting function takes 1 or 2 arguments with the first being the content
* that will be formatted and the second being `options.readFormatOptions` for reads or `options.writeFormatOptions` for writes.
*/
function coded(name, template, compileOpts, directives, nameRegExp, namers, operations, log, params, store, storeMeta, metadata) {
const varName = typeof compileOpts.varName === 'string' ? compileOpts.varName : '';
const tmpl = typeof template === 'string' ? template : `\${ await include\`${optional('defaultTemplateName', compileOpts, {})}\` }`;
const strFn = (key, val) => val instanceof RegExp ? { class: RegExp.name, source: val.source, flags: val.flags } : val;
const meta = `const metadata=Object.freeze(${JSON.stringify({ name, parent: metadata })});`;
const debug = optional('debugger', compileOpts, {}) ? 'debugger;' : '';
const debugx = log && log.debug ? typeof log.debug === 'function' ? console.debug === log.debug ? 'console.debug' : log.debug.toString() : 'console.debug' : false;
const infox = log && log.info ? typeof log.info === 'function' ? console.info === log.info ? 'console.info' : log.info.toString() : 'console.info' : false;
const warnx = log && log.warn ? typeof log.warn === 'function' ? console.warn === log.warn ? 'console.warn' : log.warn.toString() : 'console.warn' : false;
const errorx = log && log.error ? typeof log.error === 'function' ? console.error === log.error ? 'console.error' : log.error.toString() : 'console.error' : false;
const logx = `const log={debug:${debugx},info:${infox},warn:${warnx},error:${errorx}};`;
let nmrs = typeof namers === 'object' ? namers : {}, namerAdded, nmro, namex = 'const namers=Object.freeze({';
for (let nmr in nmrs) {
if (!nmrs.hasOwnProperty(nmr)) continue;
if (typeof nmrs[nmr] === 'function') {
nmro = `${nmr}:${nmrs[nmr].toString()}`;
namex += `${namerAdded ? ',' : ''}${nmro}`;
namerAdded = true;
} else throw new Error(`Unable to add naming function: ${nmrs[nmr]}`);
}
namex += '});';
let ops = Array.isArray(operations) ? operations : [operations], opAdded, opx = 'const operations=Object.freeze([';
const addOp = (op, idx) => {
const rdf = typeof op.read === 'function' ? op.read : null;
const wrf = typeof op.write === 'function' ? op.write : function noopWrite() { return Promise.resolve(); };
const fsh = typeof op.finish === 'function' ? op.finish : null;
if (!rdf) throw new Error(`"read" operation function at index ${idx} not present for "${op.read}"`);
const scp = op && Array.isArray(op.scopes) && op.scopes.length ? op.scopes : null;
let opo = '', opScoped = '';
if (scp) {
const opNames = [{ name: 'read', func: rdf, num: 1 }, { name: 'write', func: wrf, num: 2 }];
if (fsh) opNames.push({ name: 'finish', func: fsh, num: 3 });
for (let sfunc of scp) {
if (typeof sfunc === 'function' && sfunc.name) {
opScoped += `let ${sfunc.name}=${sfunc.toString()};`;
opScoped += `{let fn=${sfunc.name};for(let sfn of thiz.scopes){if(sfn.name === '${sfunc.name}'){fn=null;break;}}if(fn){thiz.scopes.push(${sfunc.name});}}`
}
}
opo += `scopedOp:function(type,thiz,args){${opScoped} if(typeof type === 'undefined'){return;} `;
opo += `const _op=type === 1 ? ${rdf.toString()} : type === 2 ? ${wrf.toString()} : ${fsh ? fsh.toString() : '()=>null'};`;
opo += `return args && args.length ? _op.apply(undefined, args) : _op;}`;
for (let ono of opNames) {
opo += `,get ${ono.name}(){const thiz=this;const ${ono.name}=function ${ono.func.name || ''}(){`;
opo += `return thiz.scopedOp(${ono.num},thiz,arguments);};`;
opo += `${ono.name}.toString=() => ${ono.name}().toString();`;
opo += `return ${ono.name};}`;
}
opo += `,scopes:[]`;
} else {
opo = `read:${rdf.toString()}`;
opo += `,write:${wrf.toString()}`;
if (fsh) opo += `,finish:${fsh.toString()}`;
}
opx += `${opAdded ? ',' : ''}Object.freeze({${opo}})`;
opAdded = true;
};
let opi = -1;
for (let op of ops) addOp(op, ++opi);
opx += ']);';
const nmreg = `const nameRegExp=/${nameRegExp.source}/${nameRegExp.flags || ''};`;
const coptx = `const compileOpts=${JSON.stringify(compileOpts, strFn)};`;
const roptx = `const renderOpts=typeof arguments[1] === 'object' ? arguments[1] : {};`;
const frmtx = `const readFormatter=typeof arguments[2] === 'function' ? arguments[2] : null;const writeFormatter=typeof arguments[3] === 'function' ? arguments[3] : null;`;
const optsx = `const optional=${optional.toString()};`;
const storex = `const store=${store ? JSON.stringify(store) : '{}'};const storeDta=store.data;store.data=arguments[4] || storeDta || {};`;
const storedx = `for (let nm in (storeDta !== store.data && store.data)){if(store.data.hasOwnProperty(nm))continue;store.data[nm]=storeDta.data[nm];};`;
const storemx = `const storeMeta=${storeMeta ? JSON.stringify(storeMeta) : '{}'};`
const varx = `const varName='${varName}';`;
const dirsx = `const directives=${JSON.stringify(directives)};`;
const sandx = `const renderContent=${renderContent.toString()};const renderRead=${renderRead.toString()};const coded=${coded.toString()};`;
const inclx = `_readHandler=${_readHandler.toString()};include=${include.toString()};if (await _readHandler()) { return ''; };`;
// read/load the primary template when not passed into coded and/or context JSON when not passed into the renderer
const tempx = `ctx=typeof arguments[0] === 'object' ? arguments[0] : null;`;
const itx = `${tempx}{ const cnm=(!ctx || typeof ctx !== 'object') && optional('defaultContextName');ctx=cnm ? await renderRead(cnm, true) : ctx; } `;
const ctx = `const ${varName}=ctx;const context=${varName};ctx=undefined;`;
const prm = `const ${optional('includesParametersName', compileOpts, {}) || 'params'}=${JSON.stringify(params)};`;
// privately scoped parameters are isolated in a separate code block to restrict access to just the include
// function and other private values
const incl = `let include,ctx,_readHandler; { ${logx}${namex}${opx} { ${nmreg}${coptx}${roptx}${frmtx}${optsx}${storex}${storedx}${storemx}${varx}${dirsx}${sandx}${inclx}${itx} } }`;
const fnshx = 'await _readHandler(true);';
const srcurl = optional('useSourceURL', compileOpts, {}) ? `\n//# sourceURL=${name}.js` : '';
// the context object is contained in a separate code block in order to isolate it from access within directives
return `${meta}${directives.code}; { ${incl}${ctx}${prm}${debug}const result=\`${tmpl}\`;${fnshx}return result; }${srcurl}\n`;
}
/**
* Runs through all of the operations and runs any async `finish` tasks
* __Assumes the following variables are within scope:__
* - `metadata` - The metadata for exectution
* - `compileOpts` - The compile options
* - `renderOpts` - The rendering options
* - `readFormatter` - The rendering formatting function that will be used when reading partial template content
* - `writeFormatter` - The rendering formatting function that will be used when writting compiled sources
* - `optional` - The {@link optional} function
* - `store` - Miscellaneous storage container
* - `storeMeta` - The storage metadata
* - `operations` - One or more async functions that will read partial templates and write included/compiled rendering functions
* - `log` - The log functions
* @private
* @ignore
* @param {Boolean} [isFinish] `true` when running any `reader.finish` tasks or any other value when calling `reader.read` without a __name__
* argument on each reader. Once all read operations have been invoked without a __name__ argument, `storeMeta.hasNoNameInit` will be
* set to __true__.
* @returns {Boolean} `true` when a `reader.read` returns `true` indicating that the rendering should not continue. `false` for all
* other scenarios.
*/
async function _readHandler(isFinish) {
if (storeMeta.stopped) return;
if (isFinish) {
if (!metadata.name || storeMeta.finished) return;
if (metadata.name !== optional('defaultTemplateName')) return;
}
for (let op of operations) {
if (isFinish) {
if (op.finish) await op.finish(store, optional, log);
} else {
if (!storeMeta.hasNoNameInit) {
try {
if ((await op.read(null, null, null, true, optional, null, store, readFormatter, true, log)) === true) {
return (storeMeta.stopped = true);
}
} catch (err) {
err.message += ` <- Failed to initiate reader "${op.read.name}"`;
throw err;
}
}
}
}
if (isFinish) {
storeMeta.finished = true;
} else storeMeta.hasNoNameInit = true; // should only int 1x
return false;
}
/**
* Returns either the `renderOpts` or the `compileOpts` option value (in that order of precedence). Also converts `RegExp` constructs
* into regular expressions. For example, `{ class: 'RegExp', source: "a|b", flags: "i" }` will be converted into `/a|b/i`.
* __Assumes the following variables are within scope:__
* - `compileOpts` - The compile options
* - `renderOpts` - The rendering options
* @private
* @ignore
* @param {String} name The name of the option to check for
* @param {Object} [copts] Compile options override of an in-scope `compileOpts`
* @param {Object} [ropts] Render options override of an in-scope `renderOpts`
* @returns {*} The value of the option
*/
function optional(name, copts, ropts) {
copts = copts || compileOpts;
ropts = ropts || renderOpts;
const opts = ropts.hasOwnProperty(name) ? ropts : copts;
if (opts[name] && opts[name].class === RegExp.name) {
opts[name] = new RegExp(opts[name].source, opts[name].flags);
}
return opts[name];
}
/**
* Compiles and renders template content at render-time
* __Assumes the following variables are within scope:__
* - `metadata` - The metadata for exectution
* - `compileOpts` - The compile options
* - `renderOpts` - The rendering options
* - `readFormatter` - The rendering formatting function that will be used when reading partial template content
* - `writeFormatter` - The rendering formatting function that will be used when writting compiled sources
* - `optional` - The {@link optional} function
* - `store` - Miscellaneous storage container
* - `storeMeta` - The storage metadata
* - `context` - The context object passed into the rendering function
* - `directives` - The directive/helper functions used in the template (from {@link Director.directives})
* - `operations` - One or more async functions that will read partial templates and write included/compiled rendering functions
* - `namers` - An object that contains one or more functions that will compose names that will be passed into read/write operations
* - `nameRegExp` - The regular expression that will replace invalid characters when naming rendering functions
* - `coded` - The {@link coded} function
* - `log` - The log functions
* @private
* @ignore
* @param {String} name The template name
* @param {String} path The complete path name
* @param {String} [ext] The path extension
* @param {String} content The raw, uncompiled template content
* @param {Object} [params] The parameters that will be available via the `options.includesParametersName`
* alias within the scope of the included template
* @param {Boolean} [fromCache] `true` when the content is coming from cache
* @returns {String} The rendered template content
*/
async function renderContent(name, path, ext, content, params, fromCache) {
if (!content) content = optional('defaultPartialContent');
// new Function restricts access to closures (other than globals)
const block = coded(path, content, compileOpts, directives, nameRegExp, namers, operations, log, params, store, storeMeta, metadata);
// pass require (when present) since it may be hidden from the block scope
const rqr = typeof require !== 'undefined' ? require : undefined;
// optionally "require" can be passed when available
const rqrd = rqr ? `const require=arguments[0];` : '';
const named = path.replace(nameRegExp, '_');
let renderer = new Function(`${rqrd}return async function ${named}(){ ${block}; }`)(rqr);
if (!fromCache && ext) {
const policy = optional('renderTimePolicy');
if (policy.includes('write')) {
const isClosePolicy = policy.includes('close'), srcPath = path.replace(new RegExp(`.${ext}$`), '.js');
let error, nerr, wcnt = 0, writeRenderer;
for (let op of operations) {
wcnt++;
if (!op.write) continue;
try {
writeRenderer = await op.write(name, srcPath, 'js', false, optional, params, store, renderer, writeFormatter, isClosePolicy, log);
if (typeof writeRenderer === 'function') {
renderer = writeRenderer;
break;
}
} catch (err) {
nerr = new Error(err.constructor.name !== 'Error' ? `${err.constructor.name || 'SandboxError'}:` : '');
nerr.code = err.code;
nerr.stack = `${err.message}${error && error.message ? ` <- ${error.message}` : ''}`
+ (wcnt >= operations.length ? ` <- Unable to write content via "${op.write.name}" for included template "${name}"`
+ ` using policy "${policy}" (render-time read)`
: '') + `\nWRITER #${wcnt} of ${operations.length}: ${op.write.name}\n${err.stack}${error ? `\n${error.stack}` : ''}`;
error = nerr;
}
}
if (error) throw error;
}
}
return renderer(context, renderOpts, readFormatter, writeFormatter);
}
/**
* Reads a template __content__ or a template __context__ at render-time
* __Assumes the following variables are within scope:__
* - `metadata` - The metadata for exectution
* - `compileOpts` - The compile options
* - `renderOpts` - The rendering options
* - `readFormatter` - The rendering formatting function that will be used when reading partial template content
* - `writeFormatter` - The rendering formatting function that will be used when writting compiled sources
* - `optional` - The {@link optional} function
* - `store` - Miscellaneous storage container
* - `storeMeta` - The storage metadata
* - `context` - The context object passed into the rendering function
* - `directives` - The directive/helper functions used in the template (from {@link Director.directives})
* - `operations` - One or more async functions that will read partial templates and write included/compiled rendering functions
* - `namers` - An object that contains one or more functions that will compose names that will be passed into read/write operations
* - `nameRegExp` - The regular expression that will replace invalid characters when naming rendering functions
* - `coded` - The {@link coded} function
* - `log` - The log functions
* - `renderContent` - The {@link renderContent} function
* @private
* @param {String} name The template or context name
* @param {Boolean} [forContext] `true` to read a template __context__ instead of the default read for a template
* __content__
* @param {URLSearchParams} [sprms] The key/value parameters to pass into the `operations[].read`
* @param {Object} [iprms] The parameters that will be available via the `options.includesParametersName`
* alias within the scope of the included template
* @param {Boolean} [fromExpression] `true` when the read is from an interpolated expression
* @returns {(String | Object)} Either the template __content__ or the JSON __context__
*/
async function renderRead(name, forContext, sprms, iprms, fromExpression) {
let rtn, path;
try {
path = await namers.namer(namers, name, optional, sprms, store, true, null, forContext);
} catch (err) {
const error = new Error(`${err.message} <- Unable to extract a name for con${forContext ? 'text' : 'tent'} via`
+ ` NAMER: "${namers.namer.name}" for include template @ "${path || name}" (render-time read)`);
error.stack = err.stack;
throw error;
}
let ext = path && path.match(/^.*?\.([^/.]+)$/), fromCache;
ext = ext && ext[1];
if (store.data[path] && store.data[path].hasOwnProperty('content')) {
if (log && log.info) log.info(`RENDER: ✔️ Found registered "${path}" in memory during pre-read`);
rtn = store.data[path].content;
fromCache = true;
} else if (operations.length && optional('renderTimePolicy') !== 'none') {
let error, nerr, rcnt = 0, fnames, policy = optional('renderTimePolicy'), close = policy.includes('close'), rtnType;
for (let op of operations) {
try {
rcnt++;
rtn = await op.read(name, path, ext ? ext : undefined, true, optional, sprms, store, readFormatter, close, log);
rtnType = typeof rtn;
if (rtnType === 'string') break;
else if (rtnType === 'object' && rtn.hasOwnProperty('content')) {
rtnType = rtn.content && typeof rtn.content;
if (rtnType === 'string' || rtnType === 'object') {
rtn = rtn.content;
break;
}
} else if (store.data[path]) {
rtnType = store.data[path].content && typeof store.data[path].content;
if (rtnType === 'string' || rtnType === 'object') {
rtn = store.data[path].content;
break;
}
}
if (rcnt >= operations.length) throw new Error(`Exhausted all ${operations.length} available readers`);
throw new Error(`Reader "${op.read.name}" returned "${typeof rtn}" rather than an expected content "string"`);
} catch (err) {
if (fnames) fnames.push(path);
else fnames = [path];
if (typeof namers.namerSuper === 'function') {
try {
path = await namers.namerSuper(namers, name, optional, sprms, store, true, null, forContext, path);
if (store.data[path] && typeof store.data[path].content === 'string') {
rtn = store.data[path].content;
break;
}
} catch (nerr) {
const error = new Error(`${nerr.message} <- Unable to extract a name for con${forContext ? 'text' : 'tent'} via`
+ ` READ ERROR NAMER: "${namers.namerSuper.name}" for include template @ "${path || name}" (render-time read)`);
error.stack = nerr.stack;
throw error;
}
}
nerr = new Error(err.constructor.name !== 'Error' ? `${err.constructor.name || 'SandboxError'}:` : '');
nerr.code = err.code;
nerr.stack = `${err.message}${error && error.message ? ` <- ${error.message}` : ''}`
+ (rcnt >= operations.length ? ` <- Unable to read content via "${op.read.name}" for included template "${name}" @`
+ ` reader names: "${fnames.join('" >> "')}"${metadata.parent ? ` under parent "${metadata.parent.name}"` : ''}`
+ ` using policy "${policy}"${err instanceof TypeError ?
'. Ensure that a proper URL/path option has been set' : ''} (render-time read)`
: '') + `\nREADER #${rcnt} of ${operations.length}: ${op.read.name}\n${err.stack}${error ? `\n${error.stack}` : ''}`;
error = nerr;
}
if (error && rcnt >= operations.length) throw error;
}
if (typeof rtn === 'string' && optional('cacheRawTemplates')) {
if (log && log.info) log.info(`RENDER: 🏧 Registering "${path}" in memory during rendering`);
store.data[path] = { name: path, shortName: name, content: rtn, renderTime: true };
}
} else if (sprms) {
const cause = operations.length ? '"options.renderTimePolicy" is set to "none"' : 'no reader function(s) have been defined';
const detail = `Read refresh required since parameters have been passed to include, but ${cause}. Parameters: ${sprms.toString()} `;
throw new Error(`Cannot include template @ ${path} (${fromExpression ? 'expression' : 'string'} literal). ${detail}`);
} else throw new Error(`Cannot find included template @ ${path} (${fromExpression ? 'expression' : 'string'} literal)`);
if (forContext) {
if (typeof rtn !== 'object') {
try {
rtn = JSON.parse(rtn);
} catch (err) {
err.message += ` <- Unable to parse JSON context @ ${path} (render-time read)`;
throw err;
}
}
} else {
try {
rtn = await renderContent(name, path, ext, rtn, iprms, fromCache);
} catch (err) {
err.message += ` <- Unable to include template @ ${path} (render-time read)`;
throw err;
}
}
return rtn;
}
/**
* Template literal tag that will include partials during the rendering phase.
* __Assumes the following variables are within scope:__
* - `metadata` - The metadata for exectution
* - `compileOpts` - The compile options
* - `renderOpts` - The rendering options
* - `readFormatter` - The rendering formatting function that will be used when reading partial template content
* - `writeFormatter` - The rendering formatting function that will be used when writting compiled sources
* - `optional` - The {@link optional} function
* - `store` - Miscellaneous storage container
* - `storeMeta` - The storage metadata
* - `varName` - The name of the context variable
* - `context` - The context object passed into the rendering function
* - `directives` - The directive/helper functions used in the template (from {@link Director.directives})
* - `operations` - One or more async functions that will read partial templates and write included/compiled rendering functions
* - `namers` - An object that contains one or more functions that will compose names that will be passed into read/write operations
* - `nameRegExp` - The regular expression that will replace invalid characters when naming rendering functions
* - `coded` - The {@link coded} function
* - `log` - The log functions
* - `renderContent` - The {@link renderContent} function
* - `renderRead` - The {@link renderRead} function
* @private
* @param {String[]} strs The string passed into template literal tag that contains the partial template name to include
* @param {Array} exps The expressions passed into template literal tag. Each expression can be one of the following:
* - `String` - A partial template name to include
* - `Object` - An object that contains properties that will be passed into the read function for each name that appear
* __at__ or __before__ the same index as the object parameter expression. Parameters are __not__ cumulative. For example,
* `name1${ { param1: 1 } }name2${ { param2: 2 } }` would imply `name1` would have `param1` while `name2` would __only__
* have `param2`, __not__ `param1`.
*/
async function include(strs, ...exps) {
let rtn = '';
for (let i = 0, ln = Math.max(strs.length, exps.length), str, exp, names, ni, sprms, iprms; i < ln; i++) {
str = strs[i] && strs[i].trim();
exp = exps[i];
if (str && exp instanceof URLSearchParams) {
if (operations.length && optional('renderTimePolicy') !== 'none') sprms = exp;
exp = null;
} else if (str && exp && typeof exp === 'object') {
iprms = exp;
exp = null;
}
if (!str && !exp && !sprms && !iprms) continue;
names = str && exp ? [str, exp] : str ? [str] : exp ? [exp] : null;
if (names) {
ni = -1;
for (let name of names) {
ni++;
rtn += await renderRead(name, false, sprms, iprms, ni > 0);
}
}
sprms = iprms = null; // parameters do not carry over from one string/expression to the next
}
return rtn;
}