UNPKG

ecto

Version:

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

655 lines (644 loc) 19.1 kB
// src/ecto.ts import fs2 from "node:fs"; import { Writr as Writr2 } from "writr"; // src/engine-map.ts var EngineMap = class { _mappings = /* @__PURE__ */ new Map(); set(name, extensions) { const engineName = name.trim().toLowerCase(); const engineExtensions = new Array(); 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 = new Array(); 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; } }; // src/engines/markdown.ts import { Writr } from "writr"; // src/base-engine.ts var BaseEngine = class { names = new Array(); opts = void 0; engine; rootTemplatePath = void 0; __extensions = new Array(); getExtensions() { return this.__extensions; } setExtensions(extensions) { this.__extensions = new Array(); 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); } } } }; // 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); } }; // src/engines/handlebars.ts import fs from "node:fs"; import * as _ from "underscore"; import { helpers, handlebars } from "@jaredwray/fumanchu"; var Handlebars = class extends BaseEngine { partialsPath = ["partials", "includes", "templates"]; constructor(options) { super(); this.names = ["handlebars", "mustache"]; this.opts = options; this.engine = handlebars; helpers({ handlebars }, this.opts); this.setExtensions(["hbs", "hjs", "handlebars", "mustache"]); } async render(source, data) { if (this.rootTemplatePath) { this.initPartials(); } const template = this.engine.compile(source, this.opts); let result = template(data, this.opts); result = _.unescape(result); return result; } renderSync(source, data) { if (this.rootTemplatePath) { this.initPartials(); } const template = this.engine.compile(source, this.opts); let result = template(data, this.opts); result = _.unescape(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; } }; // src/engines/ejs.ts import * as ejs from "ejs"; 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); } }; // src/engines/pug.ts import * as pug from "pug"; 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; } const template = pug.compile(source, this.opts); return template(data); } renderSync(source, data) { if (this.rootTemplatePath) { this.opts ||= {}; this.opts.basedir = this.rootTemplatePath; } const template = pug.compile(source, this.opts); return template(data); } }; // src/engines/nunjucks.ts import * as nunjucks from "nunjucks"; 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); } }; // src/engines/liquid.ts import { Liquid as LiquidEngine } from "liquidjs"; 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 LiquidEngine(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 LiquidEngine(this.opts); } return this.engine.parseAndRenderSync(source, data); } }; // src/ecto.ts var Ecto = class { __mapping = new EngineMap(); __engines = new Array(); __defaultEngine = "ejs"; // Engines __ejs; __markdown; __pug; __nunjucks; __handlebars; __liquid; constructor(options) { 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 !== void 0 && 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; } } /** * 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; } /** * Async render the source with the data * @param {string} source - The source to render * @param {Record<string, unknown} data - data to render with the source * @param {string} [engineName] - The engine to use for rendering * @param {string} [rootTemplatePath] - The root path to the template if using includes / partials * @param {string} [filePathOutput] - The file path to write the output * @returns {Promise<string>} */ // eslint-disable-next-line max-params async render(source, data, engineName, rootTemplatePath, filePathOutput) { let result = ""; let renderEngineName = this.__defaultEngine; if (this.isValidEngine(engineName) && engineName !== void 0) { renderEngineName = engineName; } const renderEngine = this.getRenderEngine(renderEngineName); renderEngine.rootTemplatePath = rootTemplatePath; result = await renderEngine.render(source, data); await this.writeFile(filePathOutput, result); return result; } /** * Synchronously render the source with the data * @param {string} source - The source to render * @param {Record<string, unknown} data - data to render with the source * @param {string} [engineName] - The engine to use for rendering * @param {string} [rootTemplatePath] - The root path to the template if using includes / partials * @param {string} [filePathOutput] - The file path to write the output * @returns {string} */ // eslint-disable-next-line max-params renderSync(source, data, engineName, rootTemplatePath, filePathOutput) { let result = ""; let renderEngineName = this.__defaultEngine; if (this.isValidEngine(engineName) && engineName !== void 0) { renderEngineName = engineName; } const renderEngine = this.getRenderEngine(renderEngineName); renderEngine.rootTemplatePath = rootTemplatePath; result = renderEngine.renderSync(source, data); this.writeFileSync(filePathOutput, result); return result; } /** * Render from a file path * @param {string} filePath - The file path to the source * @param {Record<string, unknown>} data - The data to render with the source * @param {string} [rootTemplatePath] - The root path to the template if using includes / partials * @param {string} [filePathOutput] - The file path to write the output * @param {string} [engineName] - The engine to use for rendering * @returns */ // eslint-disable-next-line max-params async renderFromFile(filePath, data, rootTemplatePath, filePathOutput, engineName) { let result = ""; engineName ||= this.getEngineByFilePath(filePath); const source = await fs2.promises.readFile(filePath, "utf8"); result = await this.render(source, data, engineName, rootTemplatePath, filePathOutput); return result; } /** * Sync render from a file path * @param {string} filePath - The file path to the source * @param {Record<string, unknown>} data - The data to render with the source * @param {string} [rootTemplatePath] - The root path to the template if using includes / partials * @param {string} [filePathOutput] - The file path to write the output * @param {string} [engineName] - The engine to use for rendering * @returns {string} */ // eslint-disable-next-line max-params renderFromFileSync(filePath, data, rootTemplatePath, filePathOutput, engineName) { let result = ""; engineName ||= this.getEngineByFilePath(filePath); const source = fs2.readFileSync(filePath, "utf8"); result = this.renderSync(source, data, engineName, rootTemplatePath, filePathOutput); return result; } /** * Ensure the file path exists or create it * @param {string} path * @returns {Promise<void>} */ async ensureFilePath(path) { const pathList = path.split("/"); pathList.pop(); const directory = pathList.join("/"); if (!fs2.existsSync(directory)) { fs2.mkdirSync(directory, { recursive: true }); } } /** * Ensure the file path exists or create it synchronously * @param {string} path * @returns {void} */ ensureFilePathSync(path) { const pathList = path.split("/"); pathList.pop(); const directory = pathList.join("/"); if (!fs2.existsSync(directory)) { fs2.mkdirSync(directory, { recursive: true }); } } /** * Get the Engine By File Path * @param {string} filePath * @returns {string} - will return the engine name such as 'ejs', 'markdown', 'pug', 'nunjucks', 'handlebars', 'liquid' */ 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; } /** * Find the template without the extension. This will look in a directory for a file that starts with the template name * @param {string} path - the path to look for the template file * @param {string} templateName * @returns {Promise<string>} - the path to the template file */ async findTemplateWithoutExtension(path, templateName) { let result = ""; const files = await fs2.promises.readdir(path); for (const file of files) { if (file.startsWith(templateName + ".")) { result = path + "/" + file; break; } } return result; } /** * Syncronously find the template without the extension. This will look in a directory for a file that starts with the template name * @param {string} path - the path to look for the template file * @param {string} templateName * @returns {string} - the path to the template file */ findTemplateWithoutExtensionSync(path, templateName) { let result = ""; const files = fs2.readdirSync(path); for (const file of files) { if (file.startsWith(templateName + ".")) { result = path + "/" + file; break; } } return result; } /** * Is it a valid engine that is registered in ecto * @param engineName * @returns {boolean} */ isValidEngine(engineName) { let result = false; if (engineName !== void 0 && this.__mapping.get(engineName) !== void 0) { result = true; } return result; } /** * Register the engine mappings * @returns {void} */ registerEngineMappings() { for (const eng of this.__engines) { for (const name of eng.names) { this.__mapping.set(name, eng.getExtensions()); } } } /** * Get Render Engine by the engine name. Default is EJS * @param {string} engineName * @returns {EngineInterface} */ getRenderEngine(engineName) { let result = this.__ejs; const cleanEngineName = engineName.trim().toLowerCase(); switch (cleanEngineName) { 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; } /** * Checks if the source has front matter * @param {string} source * @returns {boolean} */ hasFrontMatter(source) { const writr = new Writr2(source); if (writr.frontMatterRaw !== "") { return true; } return false; } /** * Get the Front Matter from the source * @param {string} source * @returns {Record<string, unknown>} */ getFrontMatter(source) { const writr = new Writr2(source); return writr.frontMatter; } /** * Will set the front matter in the source and return the source * @param {string} source - The source to set the front matter * @param {Record<string, unknown>} data - The front matter data * @returns {string} - The source with the front matter */ setFrontMatter(source, data) { const writr = new Writr2(source); writr.frontMatter = data; return writr.content; } /** * Remove the Front Matter from the source * @param {string} source * @returns {string} */ removeFrontMatter(source) { const writr = new Writr2(source); return writr.body; } async writeFile(filePath, source) { if (filePath && source) { await this.ensureFilePath(filePath); await fs2.promises.writeFile(filePath, source); } } writeFileSync(filePath, source) { if (filePath && source) { this.ensureFilePathSync(filePath); fs2.writeFileSync(filePath, source); } } }; export { Ecto };