ecto
Version:
Modern Template Consolidation Engine for EJS, Markdown, Pug, Nunjucks, Liquid, and Handlebars
655 lines (644 loc) • 19.1 kB
JavaScript
// 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
};