657 lines
20 KiB
JavaScript
657 lines
20 KiB
JavaScript
|
import * as path from 'node:path';
|
||
|
import * as fs from 'node:fs';
|
||
|
|
||
|
/**
|
||
|
* Handles storage and accessing of values
|
||
|
*
|
||
|
* In this case, we use it to store compiled template functions
|
||
|
* Indexed by their `name` or `filename`
|
||
|
*/
|
||
|
class Cacher {
|
||
|
constructor(cache) {
|
||
|
this.cache = void 0;
|
||
|
this.cache = cache;
|
||
|
}
|
||
|
define(key, val) {
|
||
|
this.cache[key] = val;
|
||
|
}
|
||
|
get(key) {
|
||
|
return this.cache[key];
|
||
|
}
|
||
|
remove(key) {
|
||
|
delete this.cache[key];
|
||
|
}
|
||
|
reset() {
|
||
|
this.cache = {};
|
||
|
}
|
||
|
load(cacheObj) {
|
||
|
this.cache = {
|
||
|
...this.cache,
|
||
|
...cacheObj
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class EtaError extends Error {
|
||
|
constructor(message) {
|
||
|
super(message);
|
||
|
this.name = "Eta Error";
|
||
|
}
|
||
|
}
|
||
|
class EtaParseError extends EtaError {
|
||
|
constructor(message) {
|
||
|
super(message);
|
||
|
this.name = "EtaParser Error";
|
||
|
}
|
||
|
}
|
||
|
class EtaRuntimeError extends EtaError {
|
||
|
constructor(message) {
|
||
|
super(message);
|
||
|
this.name = "EtaRuntime Error";
|
||
|
}
|
||
|
}
|
||
|
class EtaFileResolutionError extends EtaError {
|
||
|
constructor(message) {
|
||
|
super(message);
|
||
|
this.name = "EtaFileResolution Error";
|
||
|
}
|
||
|
}
|
||
|
class EtaNameResolutionError extends EtaError {
|
||
|
constructor(message) {
|
||
|
super(message);
|
||
|
this.name = "EtaNameResolution Error";
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Throws an EtaError with a nicely formatted error and message showing where in the template the error occurred.
|
||
|
*/
|
||
|
function ParseErr(message, str, indx) {
|
||
|
const whitespace = str.slice(0, indx).split(/\n/);
|
||
|
const lineNo = whitespace.length;
|
||
|
const colNo = whitespace[lineNo - 1].length + 1;
|
||
|
message += " at line " + lineNo + " col " + colNo + ":\n\n" + " " + str.split(/\n/)[lineNo - 1] + "\n" + " " + Array(colNo).join(" ") + "^";
|
||
|
throw new EtaParseError(message);
|
||
|
}
|
||
|
function RuntimeErr(originalError, str, lineNo, path) {
|
||
|
// code gratefully taken from https://github.com/mde/ejs and adapted
|
||
|
const lines = str.split("\n");
|
||
|
const start = Math.max(lineNo - 3, 0);
|
||
|
const end = Math.min(lines.length, lineNo + 3);
|
||
|
const filename = path;
|
||
|
// Error context
|
||
|
const context = lines.slice(start, end).map(function (line, i) {
|
||
|
const curr = i + start + 1;
|
||
|
return (curr == lineNo ? " >> " : " ") + curr + "| " + line;
|
||
|
}).join("\n");
|
||
|
const header = filename ? filename + ":" + lineNo + "\n" : "line " + lineNo + "\n";
|
||
|
const err = new EtaRuntimeError(header + context + "\n\n" + originalError.message);
|
||
|
err.name = originalError.name; // the original name (e.g. ReferenceError) may be useful
|
||
|
throw err;
|
||
|
}
|
||
|
|
||
|
/* END TYPES */
|
||
|
/* istanbul ignore next */
|
||
|
const AsyncFunction = async function () {}.constructor; // eslint-disable-line @typescript-eslint/no-empty-function
|
||
|
/**
|
||
|
* Takes a template string and returns a template function that can be called with (data, config)
|
||
|
*
|
||
|
* @param str - The template string
|
||
|
* @param config - A custom configuration object (optional)
|
||
|
*/
|
||
|
function compile(str, options) {
|
||
|
const config = this.config;
|
||
|
/* ASYNC HANDLING */
|
||
|
// code gratefully taken from https://github.com/mde/ejs and adapted
|
||
|
const ctor = options && options.async ? AsyncFunction : Function;
|
||
|
/* END ASYNC HANDLING */
|
||
|
try {
|
||
|
return new ctor(config.varName, "options", this.compileToString.call(this, str, options)); // eslint-disable-line no-new-func
|
||
|
} catch (e) {
|
||
|
if (e instanceof SyntaxError) {
|
||
|
throw new EtaParseError("Bad template syntax\n\n" + e.message + "\n" + Array(e.message.length + 1).join("=") + "\n" + this.compileToString.call(this, str, options) + "\n");
|
||
|
} else {
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* TYPES */
|
||
|
/* END TYPES */
|
||
|
/**
|
||
|
* Compiles a template string to a function string. Most often users just use `compile()`, which calls `compileToString` and creates a new function using the result
|
||
|
*/
|
||
|
function compileToString(str, options) {
|
||
|
const config = this.config;
|
||
|
const isAsync = options && options.async;
|
||
|
const compileBody = this.compileBody;
|
||
|
const buffer = this.parse.call(this, str);
|
||
|
// note: when the include function passes through options, the only parameter that matters is the filepath parameter
|
||
|
let res = `${config.functionHeader}
|
||
|
let include = (template, data) => this.render(template, data, options);
|
||
|
let includeAsync = (template, data) => this.renderAsync(template, data, options);
|
||
|
|
||
|
let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction${config.debug ? ', line: 1, templateStr: "' + str.replace(/\\|"/g, "\\$&").replace(/\r\n|\n|\r/g, "\\n") + '"' : ""}};
|
||
|
|
||
|
function layout(path, data) {
|
||
|
__eta.layout = path;
|
||
|
__eta.layoutData = data;
|
||
|
}${config.debug ? "try {" : ""}${config.useWith ? "with(" + config.varName + "||{}){" : ""}
|
||
|
|
||
|
${compileBody.call(this, buffer)}
|
||
|
if (__eta.layout) {
|
||
|
__eta.res = ${isAsync ? "await includeAsync" : "include"} (__eta.layout, {...${config.varName}, body: __eta.res, ...__eta.layoutData});
|
||
|
}
|
||
|
${config.useWith ? "}" : ""}${config.debug ? "} catch (e) { this.RuntimeErr(e, __eta.templateStr, __eta.line, options.filepath) }" : ""}
|
||
|
return __eta.res;
|
||
|
`;
|
||
|
if (config.plugins) {
|
||
|
for (let i = 0; i < config.plugins.length; i++) {
|
||
|
const plugin = config.plugins[i];
|
||
|
if (plugin.processFnString) {
|
||
|
res = plugin.processFnString(res, config);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return res;
|
||
|
}
|
||
|
/**
|
||
|
* Loops through the AST generated by `parse` and transform each item into JS calls
|
||
|
*
|
||
|
* **Example**
|
||
|
*
|
||
|
* ```js
|
||
|
* let templateAST = ['Hi ', { val: 'it.name', t: 'i' }]
|
||
|
* compileBody.call(Eta, templateAST)
|
||
|
* // => "__eta.res+='Hi '\n__eta.res+=__eta.e(it.name)\n"
|
||
|
* ```
|
||
|
*/
|
||
|
function compileBody(buff) {
|
||
|
const config = this.config;
|
||
|
let i = 0;
|
||
|
const buffLength = buff.length;
|
||
|
let returnStr = "";
|
||
|
for (i; i < buffLength; i++) {
|
||
|
const currentBlock = buff[i];
|
||
|
if (typeof currentBlock === "string") {
|
||
|
const str = currentBlock;
|
||
|
// we know string exists
|
||
|
returnStr += "__eta.res+='" + str + "'\n";
|
||
|
} else {
|
||
|
const type = currentBlock.t; // "r", "e", or "i"
|
||
|
let content = currentBlock.val || "";
|
||
|
if (config.debug) returnStr += "__eta.line=" + currentBlock.lineNo + "\n";
|
||
|
if (type === "r") {
|
||
|
// raw
|
||
|
if (config.autoFilter) {
|
||
|
content = "__eta.f(" + content + ")";
|
||
|
}
|
||
|
returnStr += "__eta.res+=" + content + "\n";
|
||
|
} else if (type === "i") {
|
||
|
// interpolate
|
||
|
if (config.autoFilter) {
|
||
|
content = "__eta.f(" + content + ")";
|
||
|
}
|
||
|
if (config.autoEscape) {
|
||
|
content = "__eta.e(" + content + ")";
|
||
|
}
|
||
|
returnStr += "__eta.res+=" + content + "\n";
|
||
|
} else if (type === "e") {
|
||
|
// execute
|
||
|
returnStr += content + "\n";
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return returnStr;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Takes a string within a template and trims it, based on the preceding tag's whitespace control and `config.autoTrim`
|
||
|
*/
|
||
|
function trimWS(str, config, wsLeft, wsRight) {
|
||
|
let leftTrim;
|
||
|
let rightTrim;
|
||
|
if (Array.isArray(config.autoTrim)) {
|
||
|
// Slightly confusing,
|
||
|
// but _}} will trim the left side of the following string
|
||
|
leftTrim = config.autoTrim[1];
|
||
|
rightTrim = config.autoTrim[0];
|
||
|
} else {
|
||
|
leftTrim = rightTrim = config.autoTrim;
|
||
|
}
|
||
|
if (wsLeft || wsLeft === false) {
|
||
|
leftTrim = wsLeft;
|
||
|
}
|
||
|
if (wsRight || wsRight === false) {
|
||
|
rightTrim = wsRight;
|
||
|
}
|
||
|
if (!rightTrim && !leftTrim) {
|
||
|
return str;
|
||
|
}
|
||
|
if (leftTrim === "slurp" && rightTrim === "slurp") {
|
||
|
return str.trim();
|
||
|
}
|
||
|
if (leftTrim === "_" || leftTrim === "slurp") {
|
||
|
// full slurp
|
||
|
str = str.trimStart();
|
||
|
} else if (leftTrim === "-" || leftTrim === "nl") {
|
||
|
// nl trim
|
||
|
str = str.replace(/^(?:\r\n|\n|\r)/, "");
|
||
|
}
|
||
|
if (rightTrim === "_" || rightTrim === "slurp") {
|
||
|
// full slurp
|
||
|
str = str.trimEnd();
|
||
|
} else if (rightTrim === "-" || rightTrim === "nl") {
|
||
|
// nl trim
|
||
|
str = str.replace(/(?:\r\n|\n|\r)$/, "");
|
||
|
}
|
||
|
return str;
|
||
|
}
|
||
|
/**
|
||
|
* A map of special HTML characters to their XML-escaped equivalents
|
||
|
*/
|
||
|
const escMap = {
|
||
|
"&": "&",
|
||
|
"<": "<",
|
||
|
">": ">",
|
||
|
'"': """,
|
||
|
"'": "'"
|
||
|
};
|
||
|
function replaceChar(s) {
|
||
|
return escMap[s];
|
||
|
}
|
||
|
/**
|
||
|
* XML-escapes an input value after converting it to a string
|
||
|
*
|
||
|
* @param str - Input value (usually a string)
|
||
|
* @returns XML-escaped string
|
||
|
*/
|
||
|
function XMLEscape(str) {
|
||
|
// To deal with XSS. Based on Escape implementations of Mustache.JS and Marko, then customized.
|
||
|
const newStr = String(str);
|
||
|
if (/[&<>"']/.test(newStr)) {
|
||
|
return newStr.replace(/[&<>"']/g, replaceChar);
|
||
|
} else {
|
||
|
return newStr;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* END TYPES */
|
||
|
/** Eta's base (global) configuration */
|
||
|
const defaultConfig = {
|
||
|
autoEscape: true,
|
||
|
autoFilter: false,
|
||
|
autoTrim: [false, "nl"],
|
||
|
cache: false,
|
||
|
cacheFilepaths: true,
|
||
|
debug: false,
|
||
|
escapeFunction: XMLEscape,
|
||
|
// default filter function (not used unless enables) just stringifies the input
|
||
|
filterFunction: val => String(val),
|
||
|
functionHeader: "",
|
||
|
parse: {
|
||
|
exec: "",
|
||
|
interpolate: "=",
|
||
|
raw: "~"
|
||
|
},
|
||
|
plugins: [],
|
||
|
rmWhitespace: false,
|
||
|
tags: ["<%", "%>"],
|
||
|
useWith: false,
|
||
|
varName: "it",
|
||
|
defaultExtension: ".eta"
|
||
|
};
|
||
|
|
||
|
/* END TYPES */
|
||
|
const templateLitReg = /`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})*}|(?!\${)[^\\`])*`/g;
|
||
|
const singleQuoteReg = /'(?:\\[\s\w"'\\`]|[^\n\r'\\])*?'/g;
|
||
|
const doubleQuoteReg = /"(?:\\[\s\w"'\\`]|[^\n\r"\\])*?"/g;
|
||
|
/** Escape special regular expression characters inside a string */
|
||
|
function escapeRegExp(string) {
|
||
|
// From MDN
|
||
|
return string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
||
|
}
|
||
|
|
||
|
function getLineNo(str, index) {
|
||
|
return str.slice(0, index).split("\n").length;
|
||
|
}
|
||
|
function parse(str) {
|
||
|
const config = this.config;
|
||
|
let buffer = [];
|
||
|
let trimLeftOfNextStr = false;
|
||
|
let lastIndex = 0;
|
||
|
const parseOptions = config.parse;
|
||
|
if (config.plugins) {
|
||
|
for (let i = 0; i < config.plugins.length; i++) {
|
||
|
const plugin = config.plugins[i];
|
||
|
if (plugin.processTemplate) {
|
||
|
str = plugin.processTemplate(str, config);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/* Adding for EJS compatibility */
|
||
|
if (config.rmWhitespace) {
|
||
|
// Code taken directly from EJS
|
||
|
// Have to use two separate replaces here as `^` and `$` operators don't
|
||
|
// work well with `\r` and empty lines don't work well with the `m` flag.
|
||
|
// Essentially, this replaces the whitespace at the beginning and end of
|
||
|
// each line and removes multiple newlines.
|
||
|
str = str.replace(/[\r\n]+/g, "\n").replace(/^\s+|\s+$/gm, "");
|
||
|
}
|
||
|
/* End rmWhitespace option */
|
||
|
templateLitReg.lastIndex = 0;
|
||
|
singleQuoteReg.lastIndex = 0;
|
||
|
doubleQuoteReg.lastIndex = 0;
|
||
|
function pushString(strng, shouldTrimRightOfString) {
|
||
|
if (strng) {
|
||
|
// if string is truthy it must be of type 'string'
|
||
|
strng = trimWS(strng, config, trimLeftOfNextStr,
|
||
|
// this will only be false on the first str, the next ones will be null or undefined
|
||
|
shouldTrimRightOfString);
|
||
|
if (strng) {
|
||
|
// replace \ with \\, ' with \'
|
||
|
// we're going to convert all CRLF to LF so it doesn't take more than one replace
|
||
|
strng = strng.replace(/\\|'/g, "\\$&").replace(/\r\n|\n|\r/g, "\\n");
|
||
|
buffer.push(strng);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
const prefixes = [parseOptions.exec, parseOptions.interpolate, parseOptions.raw].reduce(function (accumulator, prefix) {
|
||
|
if (accumulator && prefix) {
|
||
|
return accumulator + "|" + escapeRegExp(prefix);
|
||
|
} else if (prefix) {
|
||
|
// accumulator is falsy
|
||
|
return escapeRegExp(prefix);
|
||
|
} else {
|
||
|
// prefix and accumulator are both falsy
|
||
|
return accumulator;
|
||
|
}
|
||
|
}, "");
|
||
|
const parseOpenReg = new RegExp(escapeRegExp(config.tags[0]) + "(-|_)?\\s*(" + prefixes + ")?\\s*", "g");
|
||
|
const parseCloseReg = new RegExp("'|\"|`|\\/\\*|(\\s*(-|_)?" + escapeRegExp(config.tags[1]) + ")", "g");
|
||
|
let m;
|
||
|
while (m = parseOpenReg.exec(str)) {
|
||
|
const precedingString = str.slice(lastIndex, m.index);
|
||
|
lastIndex = m[0].length + m.index;
|
||
|
const wsLeft = m[1];
|
||
|
const prefix = m[2] || ""; // by default either ~, =, or empty
|
||
|
pushString(precedingString, wsLeft);
|
||
|
parseCloseReg.lastIndex = lastIndex;
|
||
|
let closeTag;
|
||
|
let currentObj = false;
|
||
|
while (closeTag = parseCloseReg.exec(str)) {
|
||
|
if (closeTag[1]) {
|
||
|
const content = str.slice(lastIndex, closeTag.index);
|
||
|
parseOpenReg.lastIndex = lastIndex = parseCloseReg.lastIndex;
|
||
|
trimLeftOfNextStr = closeTag[2];
|
||
|
const currentType = prefix === parseOptions.exec ? "e" : prefix === parseOptions.raw ? "r" : prefix === parseOptions.interpolate ? "i" : "";
|
||
|
currentObj = {
|
||
|
t: currentType,
|
||
|
val: content
|
||
|
};
|
||
|
break;
|
||
|
} else {
|
||
|
const char = closeTag[0];
|
||
|
if (char === "/*") {
|
||
|
const commentCloseInd = str.indexOf("*/", parseCloseReg.lastIndex);
|
||
|
if (commentCloseInd === -1) {
|
||
|
ParseErr("unclosed comment", str, closeTag.index);
|
||
|
}
|
||
|
parseCloseReg.lastIndex = commentCloseInd;
|
||
|
} else if (char === "'") {
|
||
|
singleQuoteReg.lastIndex = closeTag.index;
|
||
|
const singleQuoteMatch = singleQuoteReg.exec(str);
|
||
|
if (singleQuoteMatch) {
|
||
|
parseCloseReg.lastIndex = singleQuoteReg.lastIndex;
|
||
|
} else {
|
||
|
ParseErr("unclosed string", str, closeTag.index);
|
||
|
}
|
||
|
} else if (char === '"') {
|
||
|
doubleQuoteReg.lastIndex = closeTag.index;
|
||
|
const doubleQuoteMatch = doubleQuoteReg.exec(str);
|
||
|
if (doubleQuoteMatch) {
|
||
|
parseCloseReg.lastIndex = doubleQuoteReg.lastIndex;
|
||
|
} else {
|
||
|
ParseErr("unclosed string", str, closeTag.index);
|
||
|
}
|
||
|
} else if (char === "`") {
|
||
|
templateLitReg.lastIndex = closeTag.index;
|
||
|
const templateLitMatch = templateLitReg.exec(str);
|
||
|
if (templateLitMatch) {
|
||
|
parseCloseReg.lastIndex = templateLitReg.lastIndex;
|
||
|
} else {
|
||
|
ParseErr("unclosed string", str, closeTag.index);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (currentObj) {
|
||
|
if (config.debug) {
|
||
|
currentObj.lineNo = getLineNo(str, m.index);
|
||
|
}
|
||
|
buffer.push(currentObj);
|
||
|
} else {
|
||
|
ParseErr("unclosed tag", str, m.index);
|
||
|
}
|
||
|
}
|
||
|
pushString(str.slice(lastIndex, str.length), false);
|
||
|
if (config.plugins) {
|
||
|
for (let i = 0; i < config.plugins.length; i++) {
|
||
|
const plugin = config.plugins[i];
|
||
|
if (plugin.processAST) {
|
||
|
buffer = plugin.processAST(buffer, config);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return buffer;
|
||
|
}
|
||
|
|
||
|
/* END TYPES */
|
||
|
function handleCache(template, options) {
|
||
|
const templateStore = options && options.async ? this.templatesAsync : this.templatesSync;
|
||
|
if (this.resolvePath && this.readFile && !template.startsWith("@")) {
|
||
|
const templatePath = options.filepath;
|
||
|
const cachedTemplate = templateStore.get(templatePath);
|
||
|
if (this.config.cache && cachedTemplate) {
|
||
|
return cachedTemplate;
|
||
|
} else {
|
||
|
const templateString = this.readFile(templatePath);
|
||
|
const templateFn = this.compile(templateString, options);
|
||
|
if (this.config.cache) templateStore.define(templatePath, templateFn);
|
||
|
return templateFn;
|
||
|
}
|
||
|
} else {
|
||
|
const cachedTemplate = templateStore.get(template);
|
||
|
if (cachedTemplate) {
|
||
|
return cachedTemplate;
|
||
|
} else {
|
||
|
throw new EtaNameResolutionError("Failed to get template '" + template + "'");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
function render(template,
|
||
|
// template name or template function
|
||
|
data, meta) {
|
||
|
let templateFn;
|
||
|
const options = {
|
||
|
...meta,
|
||
|
async: false
|
||
|
};
|
||
|
if (typeof template === "string") {
|
||
|
if (this.resolvePath && this.readFile && !template.startsWith("@")) {
|
||
|
options.filepath = this.resolvePath(template, options);
|
||
|
}
|
||
|
templateFn = handleCache.call(this, template, options);
|
||
|
} else {
|
||
|
templateFn = template;
|
||
|
}
|
||
|
const res = templateFn.call(this, data, options);
|
||
|
return res;
|
||
|
}
|
||
|
function renderAsync(template,
|
||
|
// template name or template function
|
||
|
data, meta) {
|
||
|
let templateFn;
|
||
|
const options = {
|
||
|
...meta,
|
||
|
async: true
|
||
|
};
|
||
|
if (typeof template === "string") {
|
||
|
if (this.resolvePath && this.readFile && !template.startsWith("@")) {
|
||
|
options.filepath = this.resolvePath(template, options);
|
||
|
}
|
||
|
templateFn = handleCache.call(this, template, options);
|
||
|
} else {
|
||
|
templateFn = template;
|
||
|
}
|
||
|
const res = templateFn.call(this, data, options);
|
||
|
// Return a promise
|
||
|
return Promise.resolve(res);
|
||
|
}
|
||
|
function renderString(template, data) {
|
||
|
const templateFn = this.compile(template, {
|
||
|
async: false
|
||
|
});
|
||
|
return render.call(this, templateFn, data);
|
||
|
}
|
||
|
function renderStringAsync(template, data) {
|
||
|
const templateFn = this.compile(template, {
|
||
|
async: true
|
||
|
});
|
||
|
return renderAsync.call(this, templateFn, data);
|
||
|
}
|
||
|
|
||
|
/* END TYPES */
|
||
|
class Eta$1 {
|
||
|
constructor(customConfig) {
|
||
|
this.config = void 0;
|
||
|
this.RuntimeErr = RuntimeErr;
|
||
|
this.compile = compile;
|
||
|
this.compileToString = compileToString;
|
||
|
this.compileBody = compileBody;
|
||
|
this.parse = parse;
|
||
|
this.render = render;
|
||
|
this.renderAsync = renderAsync;
|
||
|
this.renderString = renderString;
|
||
|
this.renderStringAsync = renderStringAsync;
|
||
|
this.filepathCache = {};
|
||
|
this.templatesSync = new Cacher({});
|
||
|
this.templatesAsync = new Cacher({});
|
||
|
// resolvePath takes a relative path from the "views" directory
|
||
|
this.resolvePath = null;
|
||
|
this.readFile = null;
|
||
|
if (customConfig) {
|
||
|
this.config = {
|
||
|
...defaultConfig,
|
||
|
...customConfig
|
||
|
};
|
||
|
} else {
|
||
|
this.config = {
|
||
|
...defaultConfig
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
// METHODS
|
||
|
configure(customConfig) {
|
||
|
this.config = {
|
||
|
...this.config,
|
||
|
...customConfig
|
||
|
};
|
||
|
}
|
||
|
withConfig(customConfig) {
|
||
|
return {
|
||
|
...this,
|
||
|
config: {
|
||
|
...this.config,
|
||
|
...customConfig
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
loadTemplate(name, template,
|
||
|
// template string or template function
|
||
|
options) {
|
||
|
if (typeof template === "string") {
|
||
|
const templates = options && options.async ? this.templatesAsync : this.templatesSync;
|
||
|
templates.define(name, this.compile(template, options));
|
||
|
} else {
|
||
|
let templates = this.templatesSync;
|
||
|
if (template.constructor.name === "AsyncFunction" || options && options.async) {
|
||
|
templates = this.templatesAsync;
|
||
|
}
|
||
|
templates.define(name, template);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* END TYPES */
|
||
|
function readFile(path) {
|
||
|
let res = "";
|
||
|
try {
|
||
|
res = fs.readFileSync(path, "utf8");
|
||
|
// eslint-disable-line @typescript-eslint/no-explicit-any
|
||
|
} catch (err) {
|
||
|
if ((err == null ? void 0 : err.code) === "ENOENT") {
|
||
|
throw new EtaFileResolutionError(`Could not find template: ${path}`);
|
||
|
} else {
|
||
|
throw err;
|
||
|
}
|
||
|
}
|
||
|
return res;
|
||
|
}
|
||
|
function resolvePath(templatePath, options) {
|
||
|
let resolvedFilePath = "";
|
||
|
const views = this.config.views;
|
||
|
if (!views) {
|
||
|
throw new EtaFileResolutionError("Views directory is not defined");
|
||
|
}
|
||
|
const baseFilePath = options && options.filepath;
|
||
|
const defaultExtension = this.config.defaultExtension === undefined ? ".eta" : this.config.defaultExtension;
|
||
|
// how we index cached template paths
|
||
|
const cacheIndex = JSON.stringify({
|
||
|
filename: baseFilePath,
|
||
|
path: templatePath,
|
||
|
views: this.config.views
|
||
|
});
|
||
|
templatePath += path.extname(templatePath) ? "" : defaultExtension;
|
||
|
// if the file was included from another template
|
||
|
if (baseFilePath) {
|
||
|
// check the cache
|
||
|
if (this.config.cacheFilepaths && this.filepathCache[cacheIndex]) {
|
||
|
return this.filepathCache[cacheIndex];
|
||
|
}
|
||
|
const absolutePathTest = absolutePathRegExp.exec(templatePath);
|
||
|
if (absolutePathTest && absolutePathTest.length) {
|
||
|
const formattedPath = templatePath.replace(/^\/*|^\\*/, "");
|
||
|
resolvedFilePath = path.join(views, formattedPath);
|
||
|
} else {
|
||
|
resolvedFilePath = path.join(path.dirname(baseFilePath), templatePath);
|
||
|
}
|
||
|
} else {
|
||
|
resolvedFilePath = path.join(views, templatePath);
|
||
|
}
|
||
|
if (dirIsChild(views, resolvedFilePath)) {
|
||
|
// add resolved path to the cache
|
||
|
if (baseFilePath && this.config.cacheFilepaths) {
|
||
|
this.filepathCache[cacheIndex] = resolvedFilePath;
|
||
|
}
|
||
|
return resolvedFilePath;
|
||
|
} else {
|
||
|
throw new EtaFileResolutionError(`Template '${templatePath}' is not in the views directory`);
|
||
|
}
|
||
|
}
|
||
|
function dirIsChild(parent, dir) {
|
||
|
const relative = path.relative(parent, dir);
|
||
|
return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
||
|
}
|
||
|
const absolutePathRegExp = /^\\|^\//;
|
||
|
|
||
|
class Eta extends Eta$1 {
|
||
|
constructor(...args) {
|
||
|
super(...args);
|
||
|
this.readFile = readFile;
|
||
|
this.resolvePath = resolvePath;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export { Eta, EtaError, EtaFileResolutionError, EtaNameResolutionError, EtaParseError, EtaRuntimeError };
|
||
|
//# sourceMappingURL=eta.module.mjs.map
|