{"id":14155539,"url":"https://github.com/jaredwray/ecto","last_synced_at":"2026-03-06T00:11:21.239Z","repository":{"id":37959514,"uuid":"336075761","full_name":"jaredwray/ecto","owner":"jaredwray","description":"Modern Template Consolidation Engine for EJS, Markdown, Pug, Nunjucks, Mustache, and Handlebars","archived":false,"fork":false,"pushed_at":"2025-07-17T15:31:02.000Z","size":783,"stargazers_count":20,"open_issues_count":0,"forks_count":5,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-07-25T04:22:51.256Z","etag":null,"topics":["ejs","handlebars","javascript","markdown","mustache","nodejs","nunjucks","pug","template-engine","typescript"],"latest_commit_sha":null,"homepage":"https://ecto.org","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jaredwray.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-02-04T20:35:17.000Z","updated_at":"2025-07-17T15:30:47.000Z","dependencies_parsed_at":"2024-01-16T22:19:07.997Z","dependency_job_id":"ca926d1a-b940-4a9b-82e4-d3f68823bfb4","html_url":"https://github.com/jaredwray/ecto","commit_stats":{"total_commits":433,"total_committers":3,"mean_commits":"144.33333333333334","dds":"0.050808314087759765","last_synced_commit":"a2f8cf5efac8ce996c219f44ac5a8d83dc9d4fa3"},"previous_names":[],"tags_count":77,"template":false,"template_full_name":null,"purl":"pkg:github/jaredwray/ecto","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredwray%2Fecto","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredwray%2Fecto/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredwray%2Fecto/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredwray%2Fecto/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jaredwray","download_url":"https://codeload.github.com/jaredwray/ecto/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredwray%2Fecto/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266974537,"owners_count":24014928,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-25T02:00:09.625Z","response_time":70,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ejs","handlebars","javascript","markdown","mustache","nodejs","nunjucks","pug","template-engine","typescript"],"created_at":"2024-08-17T08:03:48.652Z","updated_at":"2026-03-06T00:11:21.226Z","avatar_url":"https://github.com/jaredwray.png","language":"TypeScript","funding_links":[],"categories":["typescript"],"sub_categories":[],"readme":"![Ecto](site/logo.svg \"Ecto\")\n\n# Modern Template Consolidation Engine for EJS, Markdown, Pug, Nunjucks, Mustache, and Handlebars\n\n[![tests](https://github.com/jaredwray/ecto/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/ecto/actions/workflows/tests.yml)\n[![Release Status](https://github.com/jaredwray/ecto/workflows/release/badge.svg)](https://github.com/jaredwray/ecto/actions)\n[![GitHub license](https://img.shields.io/github/license/jaredwray/ecto)](https://github.com/jaredwray/ecto/blob/master/LICENSE)\n[![codecov](https://codecov.io/gh/jaredwray/ecto/branch/main/graph/badge.svg?token=dpbFqSW5Kh)](https://codecov.io/gh/jaredwray/ecto)\n[![npm](https://img.shields.io/npm/dm/ecto)](https://npmjs.com/package/ecto)\n\n-----\n\nEcto is a modern template consolidation engine that enables the best template engines: EJS, Markdown, Pug, Nunjucks, Mustache, Handlebars, and Liquid. It consolidates these template engines to a single library, allowing you to use any of them with ease.\n\n# Features\n\n* Zero Config by default but all properties exposed for flexibility. Check out our easy [API.](#api)\n* Async `render` and `renderFromFile` functions for ES6 and Typescript. \n* [Render via Template File](#render-from-file) with Automatic Engine Selection. No more selecting which engine to use, engine choice is automatically decided based on the file extension.\n* [Only the Top Template Engines](#only-the-top-template-engines-and-their-extensions): EJS, Markdown, Pug, Nunjucks, Mustache, Liquid, and Handlebars.\n* FrontMatter Helpers `.hasFrontMatter`, `.getFrontMatter`, and `.removeFrontMatter` for Markdown files.\n* Maintained with Monthly Updates! \n\n# Table of Contents\n* [Getting Started](#getting-started)\n* [Only the Top Template Engines and Their Extensions](#only-the-top-template-engines-and-their-extensions)\n* [API - Methods and Parameters](#api---methods-and-parameters)\n  * [Render From String](#render-from-string)\n  * [Render From File](#render-from-file)\n  * [Default Engine](#default-engine)\n  * [Engine Mappings](#engine-mappings)\n  * [FrontMatter Helper Functions](#frontmatter-helper-functions)\n  * [Detect Template Engine](#detect-template-engine)\n* [The Template Engines We Support](#the-template-engines-we-support)\n    * [EJS](#ejs)\n    * [Markdown](#markdown)\n    * [PUG](#pug)\n    * [Nunjucks](#nunjucks)\n    * [Mustache](#mustache)\n    * [Handlebars](#handlebars)\n    * [Liquid](#liquid)\n* [FrontMatter Helper Functions](#frontmatter-helper-functions)\n* [Caching on Rendering](#caching-on-rendering)\n* [Emitting Events](#emitting-events)\n* [Hooks](#hooks)\n* [Creating Custom Engines](#creating-custom-engines)\n  * [Direct Engine Usage](#direct-engine-usage)\n  * [Creating a Custom Engine](#creating-a-custom-engine)\n  * [Integrating Custom Engine with Ecto](#integrating-custom-engine-with-ecto)\n  * [Engine Interface Reference](#engine-interface-reference)\n  * [BaseEngine Helper Methods](#baseengine-helper-methods)\n  * [Tips for Custom Engines](#tips-for-custom-engines)\n* [How to Contribute](#how-to-contribute)\n* [License](#license)\n\n# Getting Started\n\nThe [Node.js package manager documentation](https://nodejs.org/en/download/package-manager/) provides the commands needed to complete the install on Windows and other operating systems.\n\n1. Open the terminal for your project and run `npm install` to ensure all project dependencies are correctly installed.\n\n```\nnpm install ecto\n```\n\n2.  Declare and Initialize.\n\n```javascript\nimport { Ecto } from `ecto`;\n\nconst ecto = new Ecto();\n```\n\n5. Render via String for EJS ([Default Engine](#default-engine))\n\n```javascript\nconst source = \"\u003ch1\u003eHello \u003c%= firstName%\u003e \u003c%= lastName %\u003e!\u003c/h1\u003e\";\nconst data = {firstName: \"John\", lastName: \"Doe\"}\necto.render(source, data).then((output) =\u003e {\n    console.log(output);\n});\n```\n\nAfter running your program you should see the following output:\n\n```\n\u003ch1\u003eHello John Doe!\u003c/h1\u003e\n```\n\n------\n\nYou can easily set a different [defaultEngine](#default-engine), here we use Handlebars.\n\n```javascript\nimport { Ecto } from `ecto`;\nconst ecto = new Ecto({defaultEngine: \"handlebars\"});\n\nlet source = \"\u003ch1\u003eHello {{ firstName }} {{ lastName }}!\u003c/h1\u003e\";\nlet data = {firstName: \"John\", lastName: \"Doe\"};\nawait ecto.render(source, data); //returns \u003ch1\u003eHello John Doe!\u003c/h1\u003e\n```\n\nTo render from a template file, Ecto uses the `renderFromFile` function. This performs an automatic selection of the engine based on the file extension.\n\n```javascript\nimport { Ecto } from `ecto`;\nconst ecto = new Ecto();\nlet data = { firstName: \"John\", lastName: \"Doe\"};\n//async renderFromFile(filePath:string, data?:object, rootTemplatePath?:string, filePathOutput?:string, engineName?:string): Promise\u003cstring\u003e\nawait ecto.renderFromFile(\"./path/to/template.ejs\", data); // returns \u003ch1\u003eHello John Doe!\u003c/h1\u003e\n```\n\n# Only the Top Template Engines and Their Extensions\n\nWe focus on the most popular and well-maintained consolidation engines. Unfortunately other engines suffered from packages that were unsupported, making it difficult to validate them as working fully. Some engines also had limited types and lacked ease of use. \n\nOur goal is to support the top engines, handling the vast majority of use cases. Here are the top engines that we support:\n\n| Engine     | Monthly Downloads                                                                              | Extensions              |\n| ---------- | ---------------------------------------------------------------------------------------------- | ----------------------- |\n| [EJS](https://www.npmjs.com/package/ejs)        | [![npm](https://img.shields.io/npm/dm/ejs)](https://npmjs.com/package/ejs)                 | .ejs                    |\n| [Markdown](https://www.npmjs.com/package/marked)   | [![npm](https://img.shields.io/npm/dm/marked)](https://npmjs.com/package/marked) | .markdown, .md          |\n| [Pug](https://www.npmjs.com/package/pug)        | [![npm](https://img.shields.io/npm/dm/pug)](https://npmjs.com/package/pug)                 | .pug, .jade             |\n| [Nunjucks](https://www.npmjs.com/package/nunjucks)   | [![npm](https://img.shields.io/npm/dm/nunjucks)](https://npmjs.com/package/nunjucks)       | .njk                    |\n| [Mustache](https://www.npmjs.com/package/mustache)   | [![npm](https://img.shields.io/npm/dm/mustache)](https://npmjs.com/package/mustache)       | .mustache               |\n| [Handlebars](https://www.npmjs.com/package/handlebars) | [![npm](https://img.shields.io/npm/dm/handlebars)](https://npmjs.com/package/handlebars)   | .handlebars, .hbs, .hjs |\n| [Liquid](https://www.npmjs.com/package/liquidjs)     | [![npm](https://img.shields.io/npm/dm/liquidjs)](https://npmjs.com/package/liquidjs)       | .liquid                 |               |\n\n_The `Extensions` are listed above for when we [Render from File](#render-from-file)._\n\n# API - Methods and Parameters\nThe API is focused on using the main Ecto class:\n\n```javascript\nconst ecto = new Ecto();\n//ecto.\u003cAPI\u003e -- functions and parameters\n```\n\nThere are a couple of options that can be passed to the Ecto constructor:\n\n| Name             | Type   | Description                                                  |\n| ---------------- | ------ | ------------------------------------------------------------ |\n| defaultEngine    | string | This is the default engine to use if it is not set when doing rendering |\n| engineOptions    | object | The options for each engin that you can set initially. |\n\nhere is an example of setting the default engine and also options for `nunjucks`:\n\n```javascript\nconst ecto = new Ecto({defaultEngine: \"nunjucks\", engineOptions: {nunjucks: {autoescape: true}});\n```\n\nWhen looking at the API there are two main methods to make note of:\n[render](#render-from-string) (async) - Render from a string.\n[renderFromFile](#render-from-file) (async) - Renders from a file path and will auto-select what engine to use based on the file extension. It will return a `Promise\u003cstring\u003e` of the rendered output.\n\n## Render From String\n\nAs we have shown in [Getting Started -- It's that Easy!](#getting-started) You can render in only a couple of lines of code.\n\nrender and renderSync (`async`) - Render from a string. Here is the render function with all possible arguments shown:\n\n| Name             | Type   | Description                                                  |\n| ---------------- | ------ | ------------------------------------------------------------ |\n| source           | string | The markup/template source to be rendered.                   |\n| data             | object | The data to be rendered by the file.                         |\n| engineName       | string | Used to override the `Ecto.defaultEngine` parameter.         |\n| rootTemplatePath | string | The root template path that is used for `partials` and `layouts`. |\n| filePathOutput   | string | Used to specify the file path, if you want to write the rendered output to a file. |\n\nHere is the simplest example of a render function. We are also showing the required steps you need to take beforehand such as setting up Ecto.\n\n```javascript\nconst ecto = new Ecto();\n\nconst source = \"\u003ch1\u003eHello \u003c%= firstName%\u003e \u003c%= lastName %\u003e!\u003c/h1\u003e\";\nconst data = {firstName: \"John\", lastName: \"Doe\"};\nawait ecto.render(source, data); //returns \u003ch1\u003eHello John Doe!\u003c/h1\u003e\n```\n\nIf you want to do synchronous rendering, you can do so by using the `renderSync` function. This function takes the same parameters as the `render` function.\n\n```javascript\nconst ecto = new Ecto();\nconst source = \"\u003ch1\u003eHello \u003c%= firstName%\u003e \u003c%= lastName %\u003e!\u003c/h1\u003e\";\nconst data = {firstName: \"John\", lastName: \"Doe\"};\necto.renderSync(source, data); //returns \u003ch1\u003eHello John Doe!\u003c/h1\u003e\n```\n\nNow let's say your desired engine is not [EJS](https://www.npmjs.com/package/ejs), you will need to specify it explicitly. You can either set the [defaultEngine](#default-engine) parameter, or simply pass it in the `render` function. In this case with the popular engine, [Handlebars](https://www.npmjs.com/package/handlebars):\n\n```javascript\nconst ecto = new Ecto();\n\nconst source = \"\u003ch1\u003eHello {{firstName}} {{lastName}}!\u003c/h1\u003e\";\nconst data = {firstName: \"John\", lastName: \"Doe\"}\necto.render(source, data, \"handlebars\").then((output) =\u003e {\n    console.log(output);\n});\n\n```\n\nThe `render` and `renderSync` function also can handle partial files for standard engines (markdown excluded) by simply adding the `rootTemplatePath`:\n\n```javascript\nconst ecto = new Ecto();\n\nconst source = \"\u003ch1\u003eHello \u003c%= firstName%\u003e \u003c%= lastName %\u003e!\u003c/h1\u003e\u003c%- include('/relative/path/to/partial'); %\u003e\";\nconst data = {firstName: \"John\", lastName: \"Doe\"};\necto.render(source, data, undefined, \"./path/to/templates\").then((output) =\u003e {\n    console.log(output);\n});\n\n```\n\nWith `render` and `renderSync` you can also write to a file. This is accomplished by specifying the `filePathOutput` parameter as below. It will still return the output as a `string`:\n\n```javascript\nconst ecto = new Ecto();\n\nconst source = \"\u003ch1\u003eHello \u003c%= firstName%\u003e \u003c%= lastName %\u003e!\u003c/h1\u003e\";\nconst data = {firstName: \"John\", lastName: \"Doe\"};\necto.render(source, data, undefined, undefined, \"./path/to/output/file.html\").then((output) =\u003e {\n    console.log(output);\n});\n```\n\nNotice the `undefined` value passed into the `engineName` parameter. This is done because we already have the [defaultEngine](#default-engine) set to [EJS](https://www.npmjs.com/package/ejs). If you want you can easily add it in here too.\n\n-----\n\n## Render From File\n\nTo render via a template file, it is as simple as calling the `renderFromFile` or `renderFromFileSync` function with a couple of simple parameters passed in. \n\nrenderFromFile (`async`) or renderFromFileSync (`synchronous`) - Renders from a file path and will auto-select what engine to use based on the file extension. It will return a `Promise\u003cstring\u003e` of the rendered output. One of the main benefits is that it will automatically select the correct engine based on the file extension. The renderFromFile function takes the following parameters:\n\n| Name             | Type   | Description                                                  |\n| ---------------- | ------ | ------------------------------------------------------------ |\n| filePath         | string | The file that you would like to render.                      |\n| data             | object | The data to be rendered by the file.                         |\n| rootTemplatePath | string | The root template path that is used for `partials` and `layouts`. |\n| filePathOutput   | string | Used to specify the file path if you want to write the rendered output to a file. |\n| engineName       | string | Used to to override the auto-selection of the `engineName`.  |\n\nThis simple example showing the `renderFromFile` or `renderFromFileSync` function shows you the bare minimum required to execute this function successfully, we are passing in the template and it will return a `string`. \n\nOne of the main benefits is that it will automatically select the correct engine based on the file extension.\n\n```javascript\nconst ecto = new Ecto();\nconst data = { firstName: \"John\", lastName: \"Doe\"};\n\nawait ecto.renderFromFile(\"./path/to/template.ejs\", data); // returns \u003ch1\u003eHello John Doe!\u003c/h1\u003e\n```\n\nTo do this synchronously, you can use the `renderFromFileSync` function. This function takes the same parameters as the `renderFromFile` function.\n\n```javascript\nconst ecto = new Ecto();\nconst data = { firstName: \"John\", lastName: \"Doe\"};\n\nawait ecto.renderFromFileSync(\"./path/to/template.ejs\", data); // returns \u003ch1\u003eHello John Doe!\u003c/h1\u003e\n```\n\nIn this example, we are writing the output to a HTML file:\n\n```javascript\nconst ecto = new Ecto();\nconst data = { firstName: \"John\", lastName: \"Doe\"};\n\nawait ecto.renderFromFile(\"./path/to/template.ejs\", data, undefined, \"./path/to/output/yourname.html\")\n```\n\nNotice that in these examples it is using the `./path/to/template.ejs` to specify [EJS](https://www.npmjs.com/package/ejs) for the rendering. \n\nYou can override the auto-selected engine by passing in the string value of a template engine as a parameter into the `renderFromFile` function. We pass in `pug`, which states we want to render the template using the [Pug](https://www.npmjs.com/package/pug) engine.\n\n```javascript\nconst ecto = new Ecto();\nconst data = { firstName: \"John\", lastName: \"Doe\"};\n\necto.renderFromFile(\"./path/to/template.ejs\", data, undefined, \"./path/to/output/yourname.html\", \"pug\");\n```\n\n## Default Engine\n\nThis string parameter can be used to set the default template engine for an instance of the Ecto class. Ecto has been designed to support all major engines. \n\n`Ecto.defaultEngine` is set by default to EJS.If you would like to change the template engine there are several options available to users when setting the value of `defaultEngine`:\n\n## Set the engine in the arguments of the Ecto constructor\n\nOne option for setting the default engine, is to do so in the Ecto constructor:\n\n- `const ecto = new Ecto({defaultEngine: \"ejs\"});`\n- `const ecto = new Ecto({defaultEngine: \"markdown\"});`\n- `const ecto = new Ecto({defaultEngine: \"pug\"});`\n- `const ecto = new Ecto({defaultEngine: \"nunjucks\"});`\n- `const ecto = new Ecto({defaultEngine: \"mustache\"});`\n- `const ecto = new Ecto({defaultEngine: \"handlebars\"});`\n- `const ecto = new Ecto({defaultEngine: \"liquid\"});`\n\n## Set the default engine as a parameter\n\nWe can also set the default engine as a parameter, like so:\n\n```javascript\nconst ecto = new Ecto();\necto.defaultEngine = \"mustache\";\n```\n\nAlternatively, we can set the engine directly on the constructor. This would make our previous example:\n\n```javascript\nconst ecto = new Ecto({defaultEngine: \"liquid\"});\necto.defaultEngine = \"mustache\";\n```\n\n## Set the engine as a parameter on the render function\n\nYou can explicitly override the Ecto.defaultEngine parameter in the render function:\n\n```javascript\nconst ecto = new Ecto();\nconst source = \"\u003ch1\u003eHello {{firstName}} {{lastName}}!\u003c/h1\u003e\";\nconst data = {firstName: \"John\", lastName: \"Doe\"};\nawait ecto.render(source, data, \"handlebars\"); //returns \u003ch1\u003eHello John Doe!\u003c/h1\u003e\n```\n\n## Override the auto selection on renderFromFile\n\nThe `renderFromFile` function automatically decides on the template engine, based on the file extension in the file path you specify. However, you can also explicitly set the engine you would like to use. Here we set the engine to be Pug.\n\n```javascript\nconst ecto = new Ecto();\nconst data = { firstName: \"John\", lastName: \"Doe\"};\nawait ecto.renderFromFile(\"./path/to/template.ejs\", data, undefined, \"./path/to/output/yourname.html\", \"pug\"); // returns \u003ch1\u003eHello John Doe!\u003c/h1\u003e\n```\n\nTo make it easier to access and change between engines, all supported engines are provided as parameters on the `Ecto` class as `Ecto.\u003cEngineFullName\u003e`\n\n```javascript\nconst ecto = Ecto();\nconsole.log(ecto.Handlebars.name); // will return \"handlebars\"\nconsole.log(ecto.Handlebars.opts); // will return \"handlebars\" options object\n```\n\nTo access a specific engine you can do so by going to `ecto.\u003cengine_name\u003e.engine` and setting the [SafeString](https://handlebarsjs.com/api-reference/utilities.html#handlebars-safestring-string):\n\n```javascript\nconst ecto = Ecto();\necto.Handlebars.engine.SafeString(\"\u003cdiv\u003eHTML Content!\u003c/div\u003e\");\n```\n\n## Find Template Without Extension - Helper Methods\n\nWe have added in a couple of helper methods to make it easier to find a template without the extension. This is useful when you want to find a template without the extension. For example, if you have a template called `template.ejs` and you want to find it without the extension, you can use the `findTemplateWithoutExtension` for async or `findTemplateWithoutExtensionSync` function. This function takes two parameters:\n\n| Name             | Type   | Description                                                  |\n| ---------------- | ------ | ------------------------------------------------------------ |\n| filePath         | string | The file that you would like to find without the extension.  |\n| templateName     | string | The name of the template you would like to find.             |\n\n```javascript\nconst ecto = new Ecto();\nconst templateFilePath = ecto.findTemplateWithoutExtensionSync(\"./path/to/\", \"index\");\n```\n\nThis will return the full path of the template based on what extension is there such as `./path/to/index.ejs`.\n\n# Markdown Example\n\nMarkdown does not contain complexities such as data objects, or partials and layouts. To render markdown its as simple as:\n\n```javascript\nconst ecto = Ecto();\nconst source = \"# markdown rulezz!\";\nawait ecto.render(source, undefined, \"markdown\"); //should be \u003ch1 id=\"markdown-rulezz\"\u003emarkdown rulezz!\u003c/h1\u003e\n```\nRender by Markdown file:\n\n```javascript\nconst ecto = Ecto();\nawait ecto.renderByFile(\"/path/to/file.md\");\n```\n\nWe are using [Writr](https://writr.org/) which is remark with all the plugins and features you need.\n\n# Handlebars Example\n\nIn Ecto we use the [Fumanchu](https://www.npmjs.com/package/@jaredwray/fumanchu) engine to render `mustache` and `handlebars` related templates. This is because handlebars is based on mustache with just more additional features. Fumanchu includes handlebar engine with helpers.\n\n```javascript\nconst ecto = Ecto();\nconst source = \"{{year}}\";\n\necto.render(source, undefined, \"handlebars\").then((output) =\u003e {\n    console.log(output)\n});\n```\n\n\n# Engine Mappings\n\nEcto contains a mapping class containing all of the engines registered in the system. The class has been designed to allow users to edit existing engine mappings. Let us explore this class in detail.\n\nThe `Map` object has the following structure: `Map\u003cstring, Array\u003cstring\u003e\u003e `\n\n- The first element of the tuple is the name of the map, such as \"handlebars\".\n\n*   The second element of the tuple is an array of the extensions for that map, example values may include \"handlebars\", \"hbs\",\"hjs\".\n\nFrom here you can start setting and editing engine mappings.\n\n## Setting an engine mapping - set(name:string, extensions:Array\u0026lt;string\u003e): void\n\nTo set an engine mapping with extensions simply write:\n\n\n```\nmappings.set(\"handlebars\", [\"handlebars\",\"hbs\",\"hjs\"]);\n```\n\n\nThis sets the handlebars engine to accept files with the following file extensions:\n\n*   .handlebars\n*   .hbs\n*   .hjs\n\n## Delete an engine mapping - delete(name:string): void\n\nTo delete a mapping entirely you can use the `delete` method.\n\n```\nmappings.delete(\"handlebars\");\n```\n\nThis will remove this engine mapping entirely.\n\n## Delete an extension - deleteExtension(name:string, extension:string): void\n\nTo delete an extension for a particular engine mapping, you can use the `deleteExtension` method. This method takes two arguments:\n\n*   `name:string` - the name of the engine you would like to delete the extension for.\n*   `extension:string` - the extension you would like to delete.\n\nThe following code deletes two of the extensions for our `handlebars` engine mapping. \n\n```\nmappings.deleteExtension(\"handlebars\", \"hbs\",\"hjs\");\n```\n\nAfter executing this code the only accepted file extension for the Handlebars engine will be `handlebars`.\n\nOther useful methods include `get` and `getName`.\n\n## get method - get(name:string): Array\u0026lt;string\u003e | undefined\n\nThe `get` method takes one argument, `name:string `, and will return the extensions for the name you specify. If extensions are found, they are returned as an `Array\u003cstring\u003e`. If no engine mapping can be found for the name you specify, ` undefined` is returned. To use the `get` method simply write: \n\n\n```\nmappings.get(\"handlebars\")\n```\n\n\nThis will retrieve the array of extensions assigned to the Handlebars engine.\n\n## getName method - getName(extension:string): string | undefined\n\nThe `getName` method takes a single argument, `extension:string`. If a valid extension is given, this method will return the name of the engine mapping that the extension belongs to. For example:\n\n```\nmappings.getName(\"hjs\")\n```\n\nThis will return the string “handlebars”, which is the corresponding engine for this extension. If no match can be found, this method will return `undefined`.\n\nGaining an understanding of this class will provide you with more options and possibilities when using Ecto.\n\n## Detect Template Engine\n\nEcto provides a `detectEngine` method that can automatically detect the template engine from a template string by analyzing its syntax patterns. This is useful when you receive template content but don't know which engine it uses.\n\n## detectEngine(source: string): string\n\nThe `detectEngine` method analyzes the template syntax and returns the detected engine name. If no specific template syntax is found, it returns the configured default engine.\n\n| Name   | Type   | Description                                       |\n| ------ | ------ | ------------------------------------------------- |\n| source | string | The template source string to analyze            |\n\n**Returns:** The detected engine name ('ejs', 'markdown', 'pug', 'nunjucks', 'handlebars', 'liquid') or the default engine if no specific syntax is detected.\n\n### Basic Usage\n\n```javascript\nconst ecto = new Ecto();\n\n// Detect EJS templates\nconst ejsEngine = ecto.detectEngine('\u003c%= name %\u003e');\nconsole.log(ejsEngine); // 'ejs'\n\n// Detect Handlebars templates\nconst hbsEngine = ecto.detectEngine('{{name}}');\nconsole.log(hbsEngine); // 'handlebars'\n\n// Detect Markdown\nconst mdEngine = ecto.detectEngine('# Hello World\\n\\nThis is markdown');\nconsole.log(mdEngine); // 'markdown'\n\n// Detect Pug templates\nconst pugEngine = ecto.detectEngine('div.container\\n  h1 Hello');\nconsole.log(pugEngine); // 'pug'\n\n// Detect Nunjucks templates\nconst njkEngine = ecto.detectEngine('{% block content %}Hello{% endblock %}');\nconsole.log(njkEngine); // 'nunjucks'\n\n// Detect Liquid templates\nconst liquidEngine = ecto.detectEngine('{% assign name = \"John\" %}{{ name | upcase }}');\nconsole.log(liquidEngine); // 'liquid'\n\n// Returns default engine for plain text\nconst defaultEngine = ecto.detectEngine('Plain text without template syntax');\nconsole.log(defaultEngine); // 'ejs' (or whatever is set as defaultEngine)\n```\n\n### Detection Patterns\n\nThe `detectEngine` method recognizes the following syntax patterns:\n\n- **EJS**: `\u003c% %\u003e`, `\u003c%= %\u003e`, `\u003c%- %\u003e`\n- **Handlebars/Mustache**: `{{ }}`, `{{# }}`, `{{\u003e }}`\n- **Pug**: Indentation-based syntax without angle brackets\n- **Nunjucks**: `{% block %}`, `{% extends %}`, `{% include %}`\n- **Liquid**: `{% assign %}`, `{% capture %}`, pipe filters `{{ var | filter }}`\n- **Markdown**: Headers `#`, lists, code blocks, links, tables\n\nNote: For ambiguous syntax (like simple `{{ }}` which could be Handlebars, Mustache, or Liquid), the method makes intelligent decisions based on additional context clues in the template.\n\n\n# The Template Engines We Support\n\nA template engine is a tool that allows developers to write HTML markup that contains the template engine’s defined tags and syntax. These tags are used to insert variables into the final output of the template, or run some programming logic at run-time before sending the final HTML to the browser for display.\n\n## EJS\n\nEJS stands for Embedded JavaScript. It is a templating engine that allows users to generate HTML using plain JavaScript.\n\nYou define HTML pages in the EJS syntax and specify where various data will be shown on the page. Then, your application combines data with the template and \"renders\" a complete HTML page where EJS takes your data and inserts it into the web page according to how you've defined the template. For example, you could have a table of dynamic data from a database and you want EJS to generate the table of data according to your display rules. It saves you from the writing code and logic to dynamically generate HTML based on data.\n\nIt is a tool for generating web pages that can include dynamic data and can share templated pieces with other web pages. It is not a front-end framework. While EJS can be used by client-side Javascript to generate HTML on the client-side, it is typically used by your back-end to generate web pages in response to some URL request. EJS is not a client-side framework like Angular or React.\n\n## Markdown\n\nMarkdown is a lightweight markup language that you can use to add formatting elements to plaintext text documents. Markdown is now one of the world’s most popular markup languages.\n\nUsing Markdown is different from using a word and text editor. In an application like Microsoft Word, you click buttons to format words and phrases, and the changes are visible immediately. Markdown isn’t like that. When you create a Markdown-formatted file, you add Markdown syntax to the text to indicate which words and phrases should look different.\n\nThere are several reasons why people might choose Markdown instead of standard text editors.\n\n*   Markdown has a wide range of potential uses. It can be used to create websites, documents, notes, books, presentations, email messages, and technical documentation.\n*   Files containing Markdown-formatted text can be opened using virtually any application. This differs greatly from word processing applications like Microsoft Word that lock your content into a proprietary file format.\n*   You can create Markdown-formatted text on any device running any operating system.\n*   Markdown is future proof. Even if the application you’re using stops working at some point in the future, you’ll still be able to read your Markdown-formatted text using a text editing application.\n*   Markdown is widely supported. Websites like Reddit and GitHub support Markdown, along with many other desktop and web-based applications.\n\n## PUG\n\nPug.js is an HTML templating engine that takes simple Pug code, which the Pug compiler will compile into HTML code that browsers can understand. Some features and advantages of the Pug template engine are as follows:\n\n*   Pug has powerful features like conditions, loops, includes, that allows us to render HTML code based on user input or reference data. \n*   Pug supports JavaScript natively.\n*   Pug is excellent for handling dynamic, changing data. Imagine we have an email template, with certain fields to be customized depending on who you are sending the email to. Before sending the email we can compile the Pug code to HTML, using the user data to fill the gaps where the dynamic information should go. \n\n## Nunjucks\n\nNunjucks is a rich and powerful template engine for JavaScript. Nunjucks is developed by Mozilla and maintained by the Node JS Foundation. Nunjucks can be used in both Node and the browser.\n\nIn Node, Nunjucks is installed using npm. It is rich, fast, extensible, and available everywhere. It's highly optimized at just 8kb gzipped.\n\nSome of the advantages of using Nunjucks for your project are:\n\n*   It is a rich templating language with block inheritance, auto-escaping, macros, asynchronous control, and more.\n*   Nunjucks is fast, lean, and highly-performant. \n*   Easily extensible with custom filters and extensions.\n*   Available in Node and all modern web browsers, along with precompilation options.\n\n## Mustache\n\nMustache is a logic-less template syntax. It can be used for HTML, config files, source code, and more. It is often referred to as “logic-less” as there are no if statements, else clauses, or for loops. There are only tags. Tags are replaced with actual values at runtime.\n\nMustache.js is an implementation of the mustache template system in JavaScript. It is often considered the base for JavaScript templating. Since mustache supports various languages, we don’t need a separate templating system on the server side.\n\n\n```javascript\nMustache.render(\"Hello, {{name}}\", { name: \"John\" });\n// returns: Hello, John\n```\n\n\nWe see two braces around `{{ name }}`. This is Mustache syntax to show that it is a placeholder. When Mustache compiles this, it will look for the “name” property in the object we pass in, and replace `{{ name }}` with the actual value, in this case,  “John”.\n\nMustache is not actually a templating engine. Mustache is a specification for a templating language. In general, we would write templates according to the Mustache specification, and they can then be compiled by a templating engine to be rendered, eventually creating an output.\n\n## Handlebars\n\nHandlebars is a simple templating language. It uses a template and an input object to generate HTML or other text formats. Handlebars templates look like regular text with embedded Handlebars expressions. Handlebars expressions are wrapped in double curly braces, like this: `{{expression}}`. We use `@jaredwray/fumanchu` as it contains handbars and helpers.\n\n\n```javascript\nconst ecto = Ecto();\nconst source = \"{{year}}\";\n\nawait ecto.render(source, undefined, \"handlebars\"); //returns current year as a number\n```\n\n## Liquid\n\nSome refer to Liquid as a template language, while others may call it a template engine. It doesn't really matter which label you apply, in many ways both are right. It has a syntax (like traditional programming languages), has concepts such as output, logic, and loops, and it interacts with variables and data, just as you would with a language such as PHP.\n\nLiquid, like the previous template engines, creates a bridge between an HTML file and a data store. It does this by allowing us to access variables from within a template, or the Liquid file, with a simple and readable syntax.\n\nLiquid files have the extension of `.liquid`. A liquid file is a mix of standard HTML code and Liquid constructs. Its clear syntax is easy to distinguish from HTML when working with a Liquid file. This is made even easier thanks to the use of two sets of delimiters.\n\nThe double curly brace delimiters `{{ }}` denote output, and the curly brace percentage delimiters `{% %}` denote logic. You'll become very familiar with these as every Liquid construct begins with one, or the other. Another way of thinking of delimiters is as placeholders. A placeholder can be viewed as a piece of code that will ultimately be replaced by data when the compiled file is sent to the browser.\n\n# FrontMatter Helper Functions\n\nEcto has added in some helper functions for frontmatter in markdown files. Frontmatter is metadata that is at the top of a markdown file. It is used to store information about the file such as the author, date, tags, and license.\n\n* `.hasFrontMatter(source: string): boolean` - This function checks if the markdown file has frontmatter. It takes in a string and returns a boolean value.\n* `.getFrontMatter(source: string): object` - This function gets the frontmatter from the markdown file. It takes in a string and returns an object.\n* `setFrontMatter(source:string, data: Record\u003cstring, unknown\u003e)` - This function sets the front matter even if it already exists and returns the full source with the new front matter.\n* `.removeFrontMatter(source: string): string` - This function removes the frontmatter from the markdown file. It takes in a string and returns a string.\n\n# Caching on Rendering\n\nEcto has a built-in caching mechanism that is `disabled by default` that allows you to cache the rendered output of templates. This is useful for improving performance and reducing the number of times a template needs to be rendered. There are currently two caching engines available: `MemoryCache` and `FileCache`.\n* `cache` - This is the default caching engine and it uses in-memory by default but is called `async` so you can use a storage layer such as Redis or PostgreSQL. This is done using `Cacheable` which is a high performance caching library. \n* `cacheSync` - This is the synchronous version of the `cache` engine and uses `CacheableMemory`. It is useful for when you need to render a template synchronously and cache the output.\n\nHere is an example of how to use the caching engine with `render`:\n\n```javascript\nimport { Ecto } from 'ecto';\n\nconst ecto = new Ecto({ cache: true });\nconst source = \"\u003ch1\u003eHello \u003c%= firstName%\u003e \u003c%= lastName %\u003e!\u003c/h1\u003e\";\nconst data = { firstName: \"John\", lastName: \"Doe\" };\nconst result = await ecto.render(source, data);\nconsole.log(result); // \u003ch1\u003eHello John Doe!\u003c/h1\u003e\n```\n\nYou can also use the `cacheSync` engine with `renderSync`:\n\n```javascript\nimport { Ecto } from 'ecto';\nconst ecto = new Ecto({ cacheSync: true });\nconst source = \"\u003ch1\u003eHello \u003c%= firstName%\u003e \u003c%= lastName %\u003e!\u003c/h1\u003e\";\nconst data = { firstName: \"John\", lastName: \"Doe\" };\nconst result = ecto.renderSync(source, data);\nconsole.log(result); // \u003ch1\u003eHello John Doe!\u003c/h1\u003e\n```\n\nIf you want to provide your own caching configuration, you can do so by passing `Cacheable` instance to the `Ecto` constructor:\n\n```javascript\nimport { Ecto } from 'ecto';\nimport { Cacheable } from 'cacheable';\nconst cacheable = new Cacheable({\n    // Your caching configuration here\n});\nconst ecto = new Ecto({ cache: cacheable });\nconst source = \"\u003ch1\u003eHello \u003c%= firstName%\u003e \u003c%= lastName %\u003e!\u003c/h1\u003e\";\nconst data = { firstName: \"John\", lastName: \"Doe\" };\nconst result = await ecto.render(source, data);\nconsole.log(result); // \u003ch1\u003eHello John Doe!\u003c/h1\u003e\n```\n\n# Emitting Events\n\nEcto has a built-in event emitter that allows you to listen for events that occur during the rendering process. This is useful for debugging and monitoring the rendering process. The following events are emitted:\n\n- `cacheHit` - Emitted when a cached result is found and returned.\n- `cacheMiss` - Emitted when a cached result is not found and the template needs to be rendered.\n- `info` - Emitted for informational messages.\n- `warn` - Emitted for warning messages.\n- `error` - Emitted for error messages.\n\nYou can listen for these events by using the `on` method of the `Ecto` instance:\n\n```javascript\nimport { Ecto } from 'ecto';\nconst ecto = new Ecto();\necto.on('cacheHit', (data) =\u003e {\n    console.log('Cache hit:', data);\n});\necto.on('cacheMiss', (data) =\u003e {\n    console.log('Cache miss:', data);\n});\necto.on('warn', (data) =\u003e {\n    console.warn('Warning:', data);\n});\necto.on('error', (data) =\u003e {\n    console.error('Error:', data);\n});\n\nconst source = \"\u003ch1\u003eHello \u003c%= firstName%\u003e \u003c%= lastName %\u003e!\u003c/h1\u003e\";\nconst data = { firstName: \"John\", lastName: \"Doe\" };\nawait ecto.render(source, data); // \u003ch1\u003eHello John Doe!\u003c/h1\u003e\n```\n\n# Hooks\n\nEcto supports hooks that allow you to intercept and modify data during the rendering process. Unlike events (which are notifications only), hooks let you transform the source, data, or result as it flows through the render pipeline.\n\n## Available Hooks\n\n| Hook Name | Description |\n| --------- | ----------- |\n| `beforeRender` | Called before async rendering. Allows modifying source and data. |\n| `afterRender` | Called after async rendering. Allows modifying the result. |\n| `beforeRenderSync` | Called before sync rendering. Allows modifying source and data. |\n| `afterRenderSync` | Called after sync rendering. Allows modifying the result. |\n\n## Hook Context\n\nThe `beforeRender` and `beforeRenderSync` hooks receive a `RenderContext` object:\n\n```typescript\ntype RenderContext = {\n  source: string;              // Template source (modifiable)\n  data?: Record\u003cstring, unknown\u003e;  // Template data (modifiable)\n  engineName: string;          // Engine being used\n  rootTemplatePath?: string;   // Root path for partials\n  filePathOutput?: string;     // Output file path\n  cached: boolean;             // True if result came from cache\n};\n```\n\nThe `afterRender` and `afterRenderSync` hooks receive a `RenderResult` object:\n\n```typescript\ntype RenderResult = {\n  result: string;              // Rendered output (modifiable)\n  context: RenderContext;      // Original render context\n};\n```\n\n## Using Hooks\n\nRegister hooks using the `onHook` method:\n\n```javascript\nimport { Ecto, EctoEvents } from 'ecto';\n\nconst ecto = new Ecto();\n\n// Modify source before rendering\necto.onHook(EctoEvents.beforeRender, (context) =\u003e {\n  context.source = context.source.replace('{{placeholder}}', '{{replaced}}');\n});\n\n// Inject data before rendering\necto.onHook(EctoEvents.beforeRender, (context) =\u003e {\n  context.data = { ...context.data, injectedValue: 'hello' };\n});\n\n// Transform result after rendering\necto.onHook(EctoEvents.afterRender, (renderResult) =\u003e {\n  renderResult.result = renderResult.result.toUpperCase();\n});\n\n// Check if result was cached\necto.onHook(EctoEvents.afterRender, (renderResult) =\u003e {\n  if (renderResult.context.cached) {\n    console.log('Result came from cache');\n  }\n});\n\nconst output = await ecto.render('\u003c%= name %\u003e', { name: 'World' });\n```\n\n## Sync Hooks Example\n\n```javascript\nimport { Ecto, EctoEvents } from 'ecto';\n\nconst ecto = new Ecto();\n\n// Works the same for sync rendering\necto.onHook(EctoEvents.beforeRenderSync, (context) =\u003e {\n  context.data = { ...context.data, timestamp: Date.now() };\n});\n\necto.onHook(EctoEvents.afterRenderSync, (renderResult) =\u003e {\n  renderResult.result = `\u003c!-- Rendered at ${Date.now()} --\u003e\\n${renderResult.result}`;\n});\n\nconst output = ecto.renderSync('\u003c%= name %\u003e', { name: 'World' });\n```\n\n## Multiple Hooks\n\nMultiple hooks for the same event are executed in the order they are registered:\n\n```javascript\necto.onHook(EctoEvents.beforeRender, (context) =\u003e {\n  console.log('First hook');\n  context.source += ' - modified by first';\n});\n\necto.onHook(EctoEvents.beforeRender, (context) =\u003e {\n  console.log('Second hook');\n  context.source += ' - modified by second';\n});\n```\n\n# Creating Custom Engines\n\nEcto allows you to create your own custom template engines by implementing the `EngineInterface`. This is useful when you want to integrate a template engine that isn't built into Ecto or when you need custom rendering logic.\n\n## Direct Engine Usage\n\nAll built-in engines can be imported and used directly without going through the main `Ecto` class:\n\n```typescript\nimport { EJS, Handlebars, Liquid, Markdown, Nunjucks, Pug } from 'ecto';\n\n// Use any engine directly\nconst handlebars = new Handlebars();\nconst result = await handlebars.render('Hello {{name}}', { name: 'World' });\n```\n\n### Customizing Engines via Constructor\n\nThe recommended way to customize engines is through the `engineOptions` parameter in the Ecto constructor:\n\n```typescript\nimport { Ecto } from 'ecto';\n\nconst ecto = new Ecto({\n  engineOptions: {\n    handlebars: {\n      noEscape: false,\n      strict: true\n    },\n    markdown: {\n      html: true,\n      breaks: true\n    },\n    ejs: {\n      delimiter: '?'  // Use \u003c? ?\u003e instead of \u003c% %\u003e\n    }\n  }\n});\n\n// Engines are now configured with your options\nawait ecto.render('\u003c? name ?\u003e', { name: 'World' }, 'ejs');\n```\n\n### Accessing and Modifying Engines After Creation\n\nAfter creating an Ecto instance, you can access each engine directly and modify it. This is useful for adding custom helpers or changing settings dynamically:\n\n```typescript\nimport { Ecto } from 'ecto';\n\nconst ecto = new Ecto();\n\n// Access the Handlebars engine and add custom helpers\necto.handlebars.engine.registerHelper('uppercase', (text) =\u003e {\n  return text.toUpperCase();\n});\n\necto.handlebars.engine.registerHelper('formatDate', (date) =\u003e {\n  return new Date(date).toLocaleDateString();\n});\n\n// Now use the customized engine\nconst result = await ecto.render(\n  'Hello {{uppercase name}} - {{formatDate date}}',\n  { name: 'world', date: '2024-01-15' },\n  'handlebars'\n);\n// Output: Hello WORLD - 1/15/2024\n\n// Works with renderFromFile too\nawait ecto.renderFromFile('./template.hbs', { name: 'world', date: '2024-01-15' });\n```\n\n### Customizing Multiple Engines\n\nYou can customize multiple engines in the same Ecto instance:\n\n```typescript\nimport { Ecto } from 'ecto';\n\nconst ecto = new Ecto();\n\n// Customize Handlebars with helpers\necto.handlebars.engine.registerHelper('bold', (text) =\u003e {\n  return `\u003cstrong\u003e${text}\u003c/strong\u003e`;\n});\n\n// Customize EJS options\necto.ejs.opts = {\n  ...ecto.ejs.opts,\n  delimiter: '?',\n  openDelimiter: '[',\n  closeDelimiter: ']'\n};\n\n// Customize Markdown options\necto.markdown.opts = {\n  ...ecto.markdown.opts,\n  html: true,\n  breaks: true\n};\n\n// Use all customized engines\nawait ecto.render('{{bold \"Hello\"}}', {}, 'handlebars');\nawait ecto.render('[? name ?]', { name: 'World' }, 'ejs');\nawait ecto.render('# Hello\\n\\nVisit https://example.com', {}, 'markdown');\n```\n\n### Setting Partials Path for Handlebars\n\nYou can configure Handlebars to look for partials in multiple directories:\n\n```typescript\nimport { Ecto } from 'ecto';\n\nconst ecto = new Ecto();\n\n// Configure where to look for partials\necto.handlebars.partialsPath = ['partials', 'includes', 'components', 'layouts'];\necto.handlebars.rootTemplatePath = './templates';\n\n// Now partials will be loaded from multiple directories\nawait ecto.render('{{\u003e header}}Content{{\u003e footer}}', {}, 'handlebars');\n```\n\n### Using Exported Engines Standalone\n\nYou can also use the exported engine classes independently of Ecto:\n\n```typescript\nimport { Handlebars } from 'ecto';\n\n// Create a standalone engine instance\nconst handlebars = new Handlebars();\n\n// Add custom helpers\nhandlebars.engine.registerHelper('shout', (text) =\u003e {\n  return text.toUpperCase() + '!!!';\n});\n\n// Use it directly\nconst result = await handlebars.render('{{shout greeting}}', { greeting: 'hello' });\nconsole.log(result); // HELLO!!!\n```\n\nThis pattern is particularly powerful when:\n- You need consistent helpers across all templates\n- You want to configure engine-specific options globally\n- You're building a framework or application with specific template requirements\n- You need to extend engine functionality beyond default behavior\n\n## Creating a Custom Engine\n\nTo create a custom engine, you need to:\n\n1. Extend the `BaseEngine` class\n2. Implement the `EngineInterface`\n3. Define your engine's name and file extensions\n4. Implement the `render` and `renderSync` methods\n\n### Basic Custom Engine Example\n\nHere's a simple example of a custom uppercase engine:\n\n```typescript\nimport { BaseEngine, type EngineInterface } from 'ecto';\n\nclass UppercaseEngine extends BaseEngine implements EngineInterface {\n  constructor(options?: Record\u003cstring, unknown\u003e) {\n    super();\n\n    // Define the engine name(s)\n    this.names = ['uppercase', 'upper'];\n\n    // Set any options\n    this.opts = options;\n\n    // Define file extensions this engine handles\n    this.setExtensions(['upper', 'uppercase']);\n  }\n\n  async render(source: string, data?: Record\u003cstring, unknown\u003e): Promise\u003cstring\u003e {\n    // Simple rendering logic: replace variables and uppercase everything\n    let result = source;\n\n    if (data) {\n      // Replace {{variable}} with data values\n      for (const [key, value] of Object.entries(data)) {\n        const regex = new RegExp(`\\\\{\\\\{\\\\s*${key}\\\\s*\\\\}\\\\}`, 'g');\n        result = result.replace(regex, String(value));\n      }\n    }\n\n    // Convert to uppercase\n    return result.toUpperCase();\n  }\n\n  renderSync(source: string, data?: Record\u003cstring, unknown\u003e): string {\n    // Same logic for synchronous rendering\n    let result = source;\n\n    if (data) {\n      for (const [key, value] of Object.entries(data)) {\n        const regex = new RegExp(`\\\\{\\\\{\\\\s*${key}\\\\s*\\\\}\\\\}`, 'g');\n        result = result.replace(regex, String(value));\n      }\n    }\n\n    return result.toUpperCase();\n  }\n}\n\n// Use the custom engine\nconst engine = new UppercaseEngine();\nconst result = await engine.render('Hello {{name}}!', { name: 'World' });\nconsole.log(result); // Output: HELLO WORLD!\n```\n\n### Integrating Custom Engine with Ecto\n\nTo use your custom engine with the main Ecto class, you need to register it:\n\n```typescript\nimport { Ecto } from 'ecto';\n\nconst ecto = new Ecto();\n\n// Create and register your custom engine\nconst uppercaseEngine = new UppercaseEngine();\n\n// Add to the engines collection\necto.engines.set('uppercase', uppercaseEngine);\n\n// Register the file extensions\necto.mappings.set('uppercase', ['upper', 'uppercase']);\n\n// Now you can use it with render\nconst result = await ecto.render('Hello {{name}}!', { name: 'World' }, 'uppercase');\nconsole.log(result); // HELLO WORLD!\n\n// Or with renderFromFile (for .upper or .uppercase files)\nawait ecto.renderFromFile('./template.upper', { name: 'World' });\n```\n\n### Advanced Custom Engine with Template Library\n\nHere's a more advanced example that wraps an external template library:\n\n```typescript\nimport { BaseEngine, type EngineInterface } from 'ecto';\nimport Handlebars from 'handlebars'; // Example external library\n\nclass CustomHandlebarsEngine extends BaseEngine implements EngineInterface {\n  constructor(options?: Record\u003cstring, unknown\u003e) {\n    super();\n\n    this.names = ['custom-handlebars'];\n    this.opts = options;\n    this.setExtensions(['chbs']);\n\n    // Initialize the underlying engine\n    this.engine = Handlebars.create();\n\n    // Register custom helpers\n    this.engine.registerHelper('shout', (text: string) =\u003e {\n      return text.toUpperCase() + '!!!';\n    });\n\n    this.engine.registerHelper('whisper', (text: string) =\u003e {\n      return text.toLowerCase() + '...';\n    });\n  }\n\n  async render(source: string, data?: Record\u003cstring, unknown\u003e): Promise\u003cstring\u003e {\n    const template = this.engine.compile(source, this.opts);\n    return template(data);\n  }\n\n  renderSync(source: string, data?: Record\u003cstring, unknown\u003e): string {\n    const template = this.engine.compile(source, this.opts);\n    return template(data);\n  }\n}\n\n// Usage\nconst engine = new CustomHandlebarsEngine();\nconst result = await engine.render(\n  'Normal: {{name}}, Shout: {{shout name}}, Whisper: {{whisper name}}',\n  { name: 'Hello' }\n);\nconsole.log(result);\n// Output: Normal: Hello, Shout: HELLO!!!, Whisper: hello...\n```\n\n### Custom Engine with Partials Support\n\nIf your engine needs to support partials, you can handle the `rootTemplatePath`:\n\n```typescript\nimport fs from 'node:fs';\nimport { BaseEngine, type EngineInterface } from 'ecto';\n\nclass CustomEngineWithPartials extends BaseEngine implements EngineInterface {\n  private partials: Map\u003cstring, string\u003e = new Map();\n\n  constructor(options?: Record\u003cstring, unknown\u003e) {\n    super();\n    this.names = ['custom-partials'];\n    this.opts = options;\n    this.setExtensions(['cpart']);\n  }\n\n  async render(source: string, data?: Record\u003cstring, unknown\u003e): Promise\u003cstring\u003e {\n    // Load partials if rootTemplatePath is set\n    if (this.rootTemplatePath) {\n      await this.loadPartials(this.rootTemplatePath);\n    }\n\n    // Process includes: {{\u003e partialName}}\n    let result = source;\n    const includeRegex = /\\{\\{\u003e\\s*(\\w+)\\s*\\}\\}/g;\n    result = result.replace(includeRegex, (match, partialName) =\u003e {\n      return this.partials.get(partialName) || match;\n    });\n\n    // Process variables: {{variableName}}\n    if (data) {\n      for (const [key, value] of Object.entries(data)) {\n        const regex = new RegExp(`\\\\{\\\\{\\\\s*${key}\\\\s*\\\\}\\\\}`, 'g');\n        result = result.replace(regex, String(value));\n      }\n    }\n\n    return result;\n  }\n\n  renderSync(source: string, data?: Record\u003cstring, unknown\u003e): string {\n    if (this.rootTemplatePath) {\n      this.loadPartialsSync(this.rootTemplatePath);\n    }\n\n    let result = source;\n    const includeRegex = /\\{\\{\u003e\\s*(\\w+)\\s*\\}\\}/g;\n    result = result.replace(includeRegex, (match, partialName) =\u003e {\n      return this.partials.get(partialName) || match;\n    });\n\n    if (data) {\n      for (const [key, value] of Object.entries(data)) {\n        const regex = new RegExp(`\\\\{\\\\{\\\\s*${key}\\\\s*\\\\}\\\\}`, 'g');\n        result = result.replace(regex, String(value));\n      }\n    }\n\n    return result;\n  }\n\n  private async loadPartials(partialsPath: string): Promise\u003cvoid\u003e {\n    const files = await fs.promises.readdir(partialsPath);\n    for (const file of files) {\n      if (file.endsWith('.cpart')) {\n        const name = file.replace('.cpart', '');\n        const content = await fs.promises.readFile(\n          `${partialsPath}/${file}`,\n          'utf-8'\n        );\n        this.partials.set(name, content);\n      }\n    }\n  }\n\n  private loadPartialsSync(partialsPath: string): void {\n    const files = fs.readdirSync(partialsPath);\n    for (const file of files) {\n      if (file.endsWith('.cpart')) {\n        const name = file.replace('.cpart', '');\n        const content = fs.readFileSync(`${partialsPath}/${file}`, 'utf-8');\n        this.partials.set(name, content);\n      }\n    }\n  }\n}\n\n// Usage\nconst engine = new CustomEngineWithPartials();\nengine.rootTemplatePath = './templates/partials';\nconst result = await engine.render('{{\u003e header}}\\nHello {{name}}!\\n{{\u003e footer}}', {\n  name: 'World'\n});\n```\n\n## Engine Interface Reference\n\nWhen creating a custom engine, implement these required methods:\n\n```typescript\ninterface EngineInterface {\n  // Engine name(s) - can support multiple aliases\n  names: string[];\n\n  // The underlying template engine (if wrapping a library)\n  engine: any;\n\n  // Optional configuration\n  opts?: Record\u003cstring, unknown\u003e;\n\n  // Root path for templates (used for partials/includes)\n  rootTemplatePath?: string;\n\n  // Async rendering method\n  render(source: string, data?: Record\u003cstring, unknown\u003e): Promise\u003cstring\u003e;\n\n  // Synchronous rendering method\n  renderSync(source: string, data?: Record\u003cstring, unknown\u003e): string;\n}\n```\n\n## BaseEngine Helper Methods\n\nThe `BaseEngine` class provides useful helper methods:\n\n- `getExtensions()`: Get registered file extensions\n- `setExtensions(extensions: string[])`: Set file extensions this engine handles\n- `deleteExtension(name: string)`: Remove a file extension\n\n## Tips for Custom Engines\n\n1. **Error Handling**: Always wrap your rendering logic in try-catch blocks\n2. **Options**: Make your engine configurable through the constructor options\n3. **Performance**: Consider caching compiled templates if applicable\n4. **Type Safety**: Use TypeScript for better development experience\n5. **Testing**: Write comprehensive tests for your custom engine\n6. **Documentation**: Document your engine's syntax and features\n\n# How to Contribute\n\nTo contribute please ready our [CONTRIBUTING.md](CONTRIBUTING.md) guide. Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.\n\n# License\n\n[MIT License - Copyright (c) Jared Wray](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjaredwray%2Fecto","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjaredwray%2Fecto","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjaredwray%2Fecto/lists"}