UNPKG

ecto

Version:

Modern Template Consolidation Engine for EJS, Markdown, Pug, Nunjucks, Liquid, and Handlebars

864 lines (863 loc) 29.8 kB
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 };