ecto
Version:
Modern Template Consolidation Engine for EJS, Markdown, Pug, Nunjucks, Liquid, and Handlebars
864 lines (863 loc) • 29.8 kB
JavaScript
import fs from "node:fs";
import path from "node:path";
import { Cacheable, CacheableMemory } from "cacheable";
import { Hookified } from "hookified";
import { Writr } from "writr";
import ejs from "ejs";
import { fumanchu } from "@jaredwray/fumanchu";
import { decode } from "ent";
import { Liquid as Liquid$1 } from "liquidjs";
import * as nunjucks from "nunjucks";
import * as pug from "pug";
//#region src/engine-map.ts
var EngineMap = class {
_mappings = /* @__PURE__ */ new Map();
set(name, extensions) {
const engineName = name.trim().toLowerCase();
const engineExtensions = [];
if (extensions.length > 0 && name !== "") {
for (let extension of extensions) {
extension = extension.trim().toLowerCase();
if (!engineExtensions.includes(extension)) engineExtensions.push(extension);
}
this._mappings.set(engineName, engineExtensions);
}
}
delete(name) {
this._mappings.delete(name.trim().toLowerCase());
}
deleteExtension(name, extension) {
const engineName = name.trim().toLowerCase();
const extensions = this._mappings.get(engineName);
if (extensions) {
const engineExtensions = [];
for (const extension_ of extensions) if (extension_ !== extension.trim().toLowerCase()) engineExtensions.push(extension_);
this._mappings.set(engineName, engineExtensions);
}
}
get(name) {
return this._mappings.get(name.trim().toLowerCase());
}
getName(extension) {
let engineName;
for (const [name, extensions] of this._mappings) if (extensions.includes(extension.trim().toLowerCase())) engineName = name;
return engineName;
}
};
//#endregion
//#region src/base-engine.ts
var BaseEngine = class {
names = [];
opts = void 0;
engine;
rootTemplatePath = void 0;
_extensions = [];
getExtensions() {
return this._extensions;
}
setExtensions(extensions) {
this._extensions = [];
for (const extension of extensions) {
const newExtension = extension.trim().toLowerCase();
if (!this._extensions.includes(newExtension)) this._extensions.push(newExtension);
}
}
deleteExtension(name) {
for (const [index, extension] of this._extensions.entries()) if (name.toLowerCase().trim() === extension) this._extensions.splice(index, 1);
}
};
//#endregion
//#region src/engines/ejs.ts
var EJS = class extends BaseEngine {
constructor(options) {
super();
this.names = ["ejs"];
if (options) this.opts = options;
this.setExtensions(["ejs"]);
}
async render(source, data) {
this.engine ??= ejs;
this.opts ??= {};
if (this.rootTemplatePath) this.opts.root = this.rootTemplatePath;
return ejs.render(source, data, this.opts);
}
renderSync(source, data) {
this.engine ??= ejs;
this.opts ??= {};
if (this.rootTemplatePath) this.opts.root = this.rootTemplatePath;
return ejs.render(source, data, this.opts);
}
};
//#endregion
//#region src/engines/handlebars.ts
var Handlebars = class extends BaseEngine {
partialsPath = [
"partials",
"includes",
"templates"
];
constructor(options) {
super();
this.names = ["handlebars", "mustache"];
this.opts = options;
this.engine = fumanchu();
this.setExtensions([
"hbs",
"hjs",
"handlebars",
"mustache"
]);
}
async render(source, data) {
if (this.rootTemplatePath) this.initPartials();
let result = this.engine.compile(source, this.opts)(data, this.opts);
result = decode(result);
return result;
}
renderSync(source, data) {
if (this.rootTemplatePath) this.initPartials();
let result = this.engine.compile(source, this.opts)(data, this.opts);
result = decode(result);
return result;
}
initPartials() {
for (const path of this.partialsPath) {
const fullPath = `${this.rootTemplatePath}/${path}`;
this.registerPartials(fullPath);
}
}
registerPartials(partialsPath) {
let result = false;
if (fs.existsSync(partialsPath)) {
const partials = fs.readdirSync(partialsPath, {
recursive: true,
encoding: "utf8"
});
for (const p of partials) if (fs.statSync(`${partialsPath}/${p}`).isDirectory()) {
const directoryPartials = fs.readdirSync(`${partialsPath}/${p}`, {
recursive: true,
encoding: "utf8"
});
for (const dp of directoryPartials) {
const source = fs.readFileSync(`${partialsPath}/${p}/${dp}`).toString();
const name = `${p}/${dp.split(".")[0]}`;
this.engine.registerPartial(name, this.engine.compile(source));
}
} else {
const source = fs.readFileSync(`${partialsPath}/${p}`, "utf8");
const name = p.split(".")[0];
this.engine.registerPartial(name, this.engine.compile(source));
}
result = true;
}
return result;
}
};
//#endregion
//#region src/engines/liquid.ts
var Liquid = class extends BaseEngine {
constructor(options) {
super();
this.names = ["liquid"];
if (options) this.opts = options;
this.setExtensions(["liquid"]);
}
async render(source, data) {
if (this.rootTemplatePath) {
this.opts ??= {};
this.opts.root = this.rootTemplatePath;
}
if (!this.engine) this.engine = new Liquid$1(this.opts);
return this.engine.parseAndRender(source, data);
}
renderSync(source, data) {
if (this.rootTemplatePath) {
this.opts ??= {};
this.opts.root = this.rootTemplatePath;
}
if (!this.engine) this.engine = new Liquid$1(this.opts);
return this.engine.parseAndRenderSync(source, data);
}
};
//#endregion
//#region src/engines/markdown.ts
var Markdown = class extends BaseEngine {
constructor(options) {
super();
this.names = ["markdown"];
if (options) this.opts = options;
this.engine = new Writr(options);
this.setExtensions(["md", "markdown"]);
}
async render(source, data) {
this.engine.content = source;
return this.engine.render(data);
}
renderSync(source, data) {
this.engine.content = source;
return this.engine.renderSync(data);
}
};
//#endregion
//#region src/engines/nunjucks.ts
var Nunjucks = class extends BaseEngine {
constructor(options) {
super();
this.names = ["nunjucks"];
this.engine = nunjucks;
this.opts = { autoescape: true };
if (options) this.opts = options;
this.setExtensions(["njk"]);
}
async render(source, data) {
if (this.rootTemplatePath) nunjucks.configure(this.rootTemplatePath, this.opts);
else nunjucks.configure(this.opts);
data ??= {};
return nunjucks.renderString(source, data);
}
renderSync(source, data) {
if (this.rootTemplatePath) nunjucks.configure(this.rootTemplatePath, this.opts);
else nunjucks.configure(this.opts);
data ??= {};
return nunjucks.renderString(source, data);
}
};
//#endregion
//#region src/engines/pug.ts
var Pug = class extends BaseEngine {
constructor(options) {
super();
this.names = ["pug"];
this.engine = pug;
if (options) this.opts = options;
this.setExtensions(["pug", "jade"]);
}
async render(source, data) {
if (this.rootTemplatePath) {
this.opts ??= {};
this.opts.basedir = this.rootTemplatePath;
}
return pug.compile(source, this.opts)(data);
}
renderSync(source, data) {
if (this.rootTemplatePath) {
this.opts ??= {};
this.opts.basedir = this.rootTemplatePath;
}
return pug.compile(source, this.opts)(data);
}
};
//#endregion
//#region src/ecto.ts
let EctoEvents = /* @__PURE__ */ function(EctoEvents) {
EctoEvents["cacheHit"] = "cacheHit";
EctoEvents["cacheMiss"] = "cacheMiss";
EctoEvents["warn"] = "warn";
EctoEvents["error"] = "error";
EctoEvents["beforeRender"] = "beforeRender";
EctoEvents["afterRender"] = "afterRender";
EctoEvents["beforeRenderSync"] = "beforeRenderSync";
EctoEvents["afterRenderSync"] = "afterRenderSync";
return EctoEvents;
}({});
var Ecto = class extends Hookified {
_mapping = new EngineMap();
_engines = [];
_cache;
_cacheSync;
_defaultEngine = "ejs";
_ejs;
_markdown;
_pug;
_nunjucks;
_handlebars;
_liquid;
/**
* Ecto constructor
* @param {EctoOptions} [options] - The options for the ecto engine
*/
constructor(options) {
super();
this._ejs = new EJS(options?.engineOptions?.ejs);
this._markdown = new Markdown(options?.engineOptions?.markdown);
this._pug = new Pug(options?.engineOptions?.pug);
this._nunjucks = new Nunjucks(options?.engineOptions?.nunjucks);
this._handlebars = new Handlebars(options?.engineOptions?.handlebars);
this._liquid = new Liquid(options?.engineOptions?.liquid);
this._engines.push(this._ejs, this._markdown, this._pug, this._nunjucks, this._handlebars, this._liquid);
this.registerEngineMappings();
if (options?.cache === true) this._cache = new Cacheable();
else if (options?.cache instanceof Cacheable) this._cache = options.cache;
if (options?.cacheSync === true) this._cacheSync = new CacheableMemory();
else if (options?.cacheSync instanceof CacheableMemory) this._cacheSync = options.cacheSync;
if (options?.defaultEngine && this.isValidEngine(options.defaultEngine)) this._defaultEngine = options.defaultEngine;
}
/**
* Get the default engine
* @returns {string} - the engine name such as 'ejs', 'markdown', 'pug', 'nunjucks', 'handlebars', 'liquid'
*/
get defaultEngine() {
return this._defaultEngine;
}
/**
* Set the default engine
* @param {string} value the engine name such as 'ejs', 'markdown', 'pug', 'nunjucks', 'handlebars', 'liquid'
*/
set defaultEngine(value) {
value = value.toLowerCase().trim();
if (this.isValidEngine(value)) this._defaultEngine = value;
else this.emit("warn", `Invalid engine name: ${value}. Defaulting to ${this._defaultEngine}.`);
}
/**
* Get the cacheable instance
* @returns {Cacheable | undefined} - The cacheable instance or undefined if caching is disabled
*/
get cache() {
return this._cache;
}
/**
* Set the cacheable instance
* @param {Cacheable | undefined} value - The cacheable instance to set. If set to undefined, caching will be disabled.
*/
set cache(value) {
this._cache = value;
}
/**
* Get the cacheable memory instance
* @returns {CacheableMemory | undefined} - The cacheable memory instance or undefined if caching is disabled
*/
get cacheSync() {
return this._cacheSync;
}
/**
* Set the cacheable memory instance
* @param {CacheableMemory | undefined} value - The cacheable memory instance to set. If set to undefined, caching will be disabled.
*/
set cacheSync(value) {
this._cacheSync = value;
}
/**
* Get the Engine Mappings. This is used to map file extensions to engines
* @returns {EngineMap}
*/
get mappings() {
return this._mapping;
}
/**
* Get the EJS Engine
* @returns {EJS}
*/
get ejs() {
return this._ejs;
}
/**
* Get the Markdown Engine
* @returns {Markdown}
*/
get markdown() {
return this._markdown;
}
/**
* Get the Pug Engine
* @returns {Pug}
*/
get pug() {
return this._pug;
}
/**
* Get the Nunjucks Engine
* @returns {Nunjucks}
*/
get nunjucks() {
return this._nunjucks;
}
/**
* Get the Handlebars Engine
* @returns {Handlebars}
*/
get handlebars() {
return this._handlebars;
}
/**
* Get the Liquid Engine
* @returns {Liquid}
*/
get liquid() {
return this._liquid;
}
/**
* Asynchronously render a template source with data using the specified engine
* @param {string} source - The template source string to render
* @param {Record<string, unknown>} [data] - Data object to pass to the template engine
* @param {string} [engineName] - Name of the engine to use (e.g., 'ejs', 'pug'). Defaults to defaultEngine
* @param {string} [rootTemplatePath] - Root directory path for template includes/partials resolution
* @param {string} [filePathOutput] - Optional file path to write the rendered output to
* @returns {Promise<string>} The rendered template output as a string
* @example
* const result = await ecto.render('<%= name %>', { name: 'World' }, 'ejs');
*/
async render(source, data, engineName, rootTemplatePath, filePathOutput) {
try {
let renderEngineName = this._defaultEngine;
if (this.isValidEngine(engineName) && engineName !== void 0) renderEngineName = engineName;
const cacheKey = `${renderEngineName}-${source}-${JSON.stringify(data)}`;
if (this._cache) {
const cachedResult = await this._cache.get(cacheKey);
if (cachedResult) {
this.emit("cacheHit", `Cache hit for key: ${cacheKey}`);
const context = {
source,
data,
engineName: renderEngineName,
rootTemplatePath,
filePathOutput,
cached: true
};
await this.hook("beforeRender", context);
const renderResult = {
result: cachedResult,
context
};
await this.hook("afterRender", renderResult);
await this.writeFile(filePathOutput, renderResult.result);
return renderResult.result;
}
this.emit("cacheMiss", `Cache miss for key: ${cacheKey}`);
}
const context = {
source,
data,
engineName: renderEngineName,
rootTemplatePath,
filePathOutput,
cached: false
};
await this.hook("beforeRender", context);
const renderEngine = this.getRenderEngine(renderEngineName);
renderEngine.rootTemplatePath = context.rootTemplatePath;
let result = await renderEngine.render(context.source, context.data);
const renderResult = {
result,
context
};
await this.hook("afterRender", renderResult);
result = renderResult.result;
if (this._cache) await this._cache.set(cacheKey, result);
await this.writeFile(filePathOutput, result);
return result;
} catch (error) {
/* v8 ignore next -- @preserve */
this.emit("error", error);
/* v8 ignore next -- @preserve */
return "";
}
}
/**
* Synchronously render a template source with data using the specified engine
* @param {string} source - The template source string to render
* @param {Record<string, unknown>} [data] - Data object to pass to the template engine
* @param {string} [engineName] - Name of the engine to use (e.g., 'ejs', 'pug'). Defaults to defaultEngine
* @param {string} [rootTemplatePath] - Root directory path for template includes/partials resolution
* @param {string} [filePathOutput] - Optional file path to write the rendered output to
* @returns {string} The rendered template output as a string
* @example
* const result = ecto.renderSync('<%= name %>', { name: 'World' }, 'ejs');
*/
renderSync(source, data, engineName, rootTemplatePath, filePathOutput) {
try {
let renderEngineName = this._defaultEngine;
if (this.isValidEngine(engineName) && engineName !== void 0) renderEngineName = engineName;
const cacheKey = `${renderEngineName}-${source}-${JSON.stringify(data)}`;
if (this._cacheSync) {
const cachedResult = this._cacheSync.get(cacheKey);
if (cachedResult) {
this.emit("cacheHit", `Cache hit for key: ${cacheKey}`);
const context = {
source,
data,
engineName: renderEngineName,
rootTemplatePath,
filePathOutput,
cached: true
};
this.hookSync("beforeRenderSync", context);
const renderResult = {
result: cachedResult,
context
};
this.hookSync("afterRenderSync", renderResult);
this.writeFileSync(filePathOutput, renderResult.result);
return renderResult.result;
}
this.emit("cacheMiss", `Cache miss for key: ${cacheKey}`);
}
const context = {
source,
data,
engineName: renderEngineName,
rootTemplatePath,
filePathOutput,
cached: false
};
this.hookSync("beforeRenderSync", context);
const renderEngine = this.getRenderEngine(renderEngineName);
renderEngine.rootTemplatePath = context.rootTemplatePath;
let result = renderEngine.renderSync(context.source, context.data);
const renderResult = {
result,
context
};
this.hookSync("afterRenderSync", renderResult);
result = renderResult.result;
if (this._cacheSync) this._cacheSync.set(cacheKey, result);
this.writeFileSync(filePathOutput, result);
return result;
} catch (error) {
this.emit("error", error);
return "";
}
}
/**
* Asynchronously render a template from a file path
* @param {string} filePath - Path to the template file to render
* @param {Record<string, unknown>} [data] - Data object to pass to the template engine
* @param {string} [rootTemplatePath] - Root directory for template includes. Defaults to file's directory
* @param {string} [filePathOutput] - Optional file path to write the rendered output to
* @param {string} [engineName] - Engine to use. If not specified, determined from file extension
* @returns {Promise<string>} The rendered template output as a string
* @example
* const result = await ecto.renderFromFile('./templates/index.ejs', { title: 'Home' });
*/
async renderFromFile(filePath, data, rootTemplatePath, filePathOutput, engineName) {
let result = "";
engineName ??= this.getEngineByFilePath(filePath);
const templateRootPath = rootTemplatePath ?? path.dirname(filePath);
const source = await fs.promises.readFile(filePath, "utf8");
result = await this.render(source, data, engineName, templateRootPath, filePathOutput);
return result;
}
/**
* Synchronously render a template from a file path
* @param {string} filePath - Path to the template file to render
* @param {Record<string, unknown>} [data] - Data object to pass to the template engine
* @param {string} [rootTemplatePath] - Root directory for template includes. Defaults to file's directory
* @param {string} [filePathOutput] - Optional file path to write the rendered output to
* @param {string} [engineName] - Engine to use. If not specified, determined from file extension
* @returns {string} The rendered template output as a string
* @example
* const result = ecto.renderFromFileSync('./templates/index.ejs', { title: 'Home' });
*/
renderFromFileSync(filePath, data, rootTemplatePath, filePathOutput, engineName) {
let result = "";
engineName ??= this.getEngineByFilePath(filePath);
const templateRootPath = rootTemplatePath ?? path.dirname(filePath);
const source = fs.readFileSync(filePath, "utf8");
result = this.renderSync(source, data, engineName, templateRootPath, filePathOutput);
return result;
}
/**
* Asynchronously ensure that the directory path for a file exists, creating it if necessary
* @param {string} path - The full file path (directories will be extracted from this)
* @returns {Promise<void>}
* @example
* await ecto.ensureFilePath('/path/to/file.txt');
*/
async ensureFilePath(path) {
const pathList = path.split("/");
pathList.pop();
const directory = pathList.join("/");
if (!fs.existsSync(directory)) fs.mkdirSync(directory, { recursive: true });
}
/**
* Synchronously ensure that the directory path for a file exists, creating it if necessary
* @param {string} path - The full file path (directories will be extracted from this)
* @returns {void}
* @example
* ecto.ensureFilePathSync('/path/to/file.txt');
*/
ensureFilePathSync(path) {
const pathList = path.split("/");
pathList.pop();
const directory = pathList.join("/");
if (!fs.existsSync(directory)) fs.mkdirSync(directory, { recursive: true });
}
/**
* Determine the appropriate template engine based on a file's extension
* @param {string} filePath - The file path to analyze
* @returns {string} The engine name (e.g., 'ejs', 'markdown', 'pug', 'nunjucks', 'handlebars', 'liquid')
* @example
* const engine = ecto.getEngineByFilePath('template.ejs'); // Returns 'ejs'
*/
getEngineByFilePath(filePath) {
let result = this._defaultEngine;
if (filePath !== void 0) {
const extension = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".") + 1) : "";
const engExtension = this._mapping.getName(extension);
if (engExtension !== void 0) result = engExtension;
}
return result;
}
/**
* Asynchronously find a template file in a directory by name, regardless of extension
* @param {string} path - Directory path to search in
* @param {string} templateName - Template name without extension
* @returns {Promise<string>} Full path to the found template file, or empty string if not found
* @example
* const templatePath = await ecto.findTemplateWithoutExtension('./templates', 'index');
*/
async findTemplateWithoutExtension(path, templateName) {
let result = "";
const files = await fs.promises.readdir(path);
for (const file of files) if (file.startsWith(`${templateName}.`)) {
result = `${path}/${file}`;
break;
}
return result;
}
/**
* Synchronously find a template file in a directory by name, regardless of extension
* @param {string} path - Directory path to search in
* @param {string} templateName - Template name without extension
* @returns {string} Full path to the found template file, or empty string if not found
* @example
* const templatePath = ecto.findTemplateWithoutExtensionSync('./templates', 'index');
*/
findTemplateWithoutExtensionSync(path, templateName) {
let result = "";
const files = fs.readdirSync(path);
for (const file of files) if (file.startsWith(`${templateName}.`)) {
result = `${path}/${file}`;
break;
}
return result;
}
/**
* Check if the given engine name is valid and registered in Ecto
* @param {string} [engineName] - The engine name to validate
* @returns {boolean} True if the engine is valid and registered, false otherwise
* @example
* const isValid = ecto.isValidEngine('ejs'); // Returns true
*/
isValidEngine(engineName) {
let result = false;
if (engineName !== void 0 && this._mapping.get(engineName) !== void 0) result = true;
return result;
}
/**
* Detect the template engine from a template string by analyzing its syntax
* @param {string} source - The template source string to analyze
* @returns {string} The detected engine name ('ejs', 'markdown', 'pug', 'nunjucks', 'handlebars', 'liquid') or the default engine
* @example
* const engine = ecto.detectEngine('<%= name %>'); // Returns 'ejs'
* const engine2 = ecto.detectEngine('{{name}}'); // Returns 'handlebars' or 'liquid'
* const engine3 = ecto.detectEngine('# Heading'); // Returns 'markdown'
* const engine4 = ecto.detectEngine('plain text'); // Returns defaultEngine (e.g., 'ejs')
*/
detectEngine(source) {
if (!source || typeof source !== "string") return this._defaultEngine;
if (source.includes("<%")) {
if (source.includes("<%=") || source.includes("<%-") || source.includes("%>")) return "ejs";
}
if (source.includes("{%")) {
for (const keyword of [
"liquid",
"assign",
"capture",
"endcapture",
"case",
"when",
"unless",
"endunless",
"tablerow",
"endtablerow",
"increment",
"decrement"
]) if (source.includes(`{% ${keyword}`) || source.includes(`{%${keyword}`)) return "liquid";
if (source.includes("{{") && source.includes("|") && source.includes("}}")) {
const nunjucksSpecific = [
"block",
"extends",
"macro",
"import",
"call"
];
let hasNunjucksKeyword = false;
for (const keyword of nunjucksSpecific) if (source.includes(`{% ${keyword}`) || source.includes(`{%${keyword}`)) {
hasNunjucksKeyword = true;
break;
}
if (!hasNunjucksKeyword) return "liquid";
}
}
if (source.includes("{%")) {
for (const keyword of [
"block",
"extends",
"include",
"import",
"for",
"if",
"elif",
"else",
"endif",
"endfor",
"set",
"macro",
"endmacro",
"call"
]) if (source.includes(`{% ${keyword}`) || source.includes(`{%${keyword}`)) return "nunjucks";
}
if (source.includes("{{")) {
if (source.includes(" | ") && source.includes("}}")) {
if (source.includes("{% assign ") || source.includes("{% capture ") || source.includes("{% unless ") || source.includes("{% increment ") || source.includes("{% decrement ") || source.includes("{% tablerow ") || source.includes("{% case ") || source.includes("{% when ") || source.includes(" | upcase") || source.includes(" | downcase") || source.includes(" | capitalize") || source.includes(" | minus:") || source.includes(" | plus:") || source.includes(" | money") || source.includes(" | date:")) return "liquid";
}
if (source.includes("{{#") || source.includes("{{/") || source.includes("{{>") || source.includes("{{!--")) return "handlebars";
/* v8 ignore next -- @preserve */
if (source.includes("}}") && !source.includes("{%")) return "handlebars";
}
const lines = source.split("\n");
let markdownIndicators = 0;
let pugIndicators = 0;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith("# ") || trimmed.startsWith("## ") || trimmed.startsWith("### ") || trimmed.startsWith("#### ") || trimmed.startsWith("##### ") || trimmed.startsWith("###### ")) markdownIndicators++;
if (trimmed.startsWith("- ") || trimmed.startsWith("* ") || trimmed.startsWith("+ ")) markdownIndicators++;
const firstChar = trimmed.charAt(0);
if (firstChar >= "0" && firstChar <= "9") {
const dotIndex = trimmed.indexOf(".");
if (dotIndex > 0 && dotIndex < 4 && trimmed.charAt(dotIndex + 1) === " ") markdownIndicators++;
}
if (trimmed.startsWith("> ")) markdownIndicators++;
if (trimmed.startsWith("```")) markdownIndicators++;
if (!trimmed.startsWith("# ") && !trimmed.startsWith("- ") && !trimmed.startsWith("* ") && !trimmed.startsWith("+ ") && !trimmed.startsWith("> ")) {
if (trimmed.startsWith("doctype") || trimmed.startsWith("html") || trimmed.startsWith("head") || trimmed.startsWith("body") || trimmed.startsWith("div") || trimmed.startsWith("p") || trimmed.startsWith("h") && trimmed.length > 1 && trimmed.charAt(1) >= "1" && trimmed.charAt(1) <= "6") pugIndicators++;
if (trimmed.includes("(") && trimmed.includes(")") || trimmed.indexOf(".") === 0 || trimmed.indexOf("#") === 0 && !trimmed.includes(" ")) pugIndicators++;
}
}
/* v8 ignore next -- @preserve */
if (source.includes("](") && (source.includes("[") || source.includes("!["))) markdownIndicators++;
if (source.includes("|") && source.includes("---")) markdownIndicators++;
if (markdownIndicators > 0) {
/* v8 ignore next -- @preserve */
if (!source.includes("<%") && !source.includes("{{") && !source.includes("{%")) return "markdown";
}
const hasHtmlOpenTag = source.includes("<") && source.includes(">");
const hasHtmlCloseTag = source.includes("</");
if (pugIndicators > 0 && !hasHtmlOpenTag && !hasHtmlCloseTag && !source.includes("<%") && !source.includes("{{") && !source.includes("{%")) return "pug";
return this._defaultEngine;
}
/**
* Register all engine mappings between engine names and file extensions
* @returns {void}
* @private
*/
registerEngineMappings() {
for (const eng of this._engines) for (const name of eng.names) this._mapping.set(name, eng.getExtensions());
}
/**
* Get the render engine instance by name
* @param {string} engineName - The name of the engine to retrieve
* @returns {EngineInterface} The engine instance (defaults to EJS if not found)
* @example
* const engine = ecto.getRenderEngine('pug');
*/
getRenderEngine(engineName) {
let result = this._ejs;
switch (engineName.trim().toLowerCase()) {
case "markdown":
result = this._markdown;
break;
case "pug":
result = this._pug;
break;
case "nunjucks":
result = this._nunjucks;
break;
case "mustache":
result = this._handlebars;
break;
case "handlebars":
result = this._handlebars;
break;
case "liquid":
result = this._liquid;
break;
default:
result = this._ejs;
break;
}
return result;
}
/**
* Check if the source content contains front matter (YAML metadata)
* @param {string} source - The source content to check
* @returns {boolean} True if front matter is present, false otherwise
* @example
* const hasFM = ecto.hasFrontMatter('---\ntitle: Test\n---\nContent');
*/
hasFrontMatter(source) {
if (new Writr(source).frontMatterRaw !== "") return true;
return false;
}
/**
* Extract front matter data from the source content
* @param {string} source - The source content containing front matter
* @returns {Record<string, unknown>} Parsed front matter as an object
* @example
* const data = ecto.getFrontMatter('---\ntitle: Test\n---\nContent');
*/
getFrontMatter(source) {
return new Writr(source).frontMatter;
}
/**
* Set or replace front matter in the source content
* @param {string} source - The source content
* @param {Record<string, unknown>} data - The front matter data to set
* @returns {string} The source content with updated front matter
* @example
* const updated = ecto.setFrontMatter('Content', { title: 'New Title' });
*/
setFrontMatter(source, data) {
const writr = new Writr(source);
writr.frontMatter = data;
return writr.content;
}
/**
* Remove front matter from the source content, returning only the body
* @param {string} source - The source content with front matter
* @returns {string} The source content without front matter
* @example
* const body = ecto.removeFrontMatter('---\ntitle: Test\n---\nContent');
*/
removeFrontMatter(source) {
return new Writr(source).body;
}
/**
* Write content to a file asynchronously, creating directories if needed
* @private
* @param {string} [filePath] - The path to write the file to
* @param {string} [source] - The content to write to the file
* @returns {Promise<void>}
*/
async writeFile(filePath, source) {
if (filePath && source) {
await this.ensureFilePath(filePath);
await fs.promises.writeFile(filePath, source);
}
}
/**
* Write content to a file synchronously, creating directories if needed
* @private
* @param {string} [filePath] - The path to write the file to
* @param {string} [source] - The content to write to the file
* @returns {void}
*/
writeFileSync(filePath, source) {
if (filePath && source) {
this.ensureFilePathSync(filePath);
fs.writeFileSync(filePath, source);
}
}
};
//#endregion
export { EJS, Ecto, EctoEvents, Handlebars, Liquid, Markdown, Nunjucks, Pug };