mutorjs 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1615 -90
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +1349 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1326 -12
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +1479 -15
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1462 -15
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,140 +1,1665 @@
|
|
|
1
|
-
# Mutor.js
|
|
1
|
+
# Mutor.js — Official Documentation
|
|
2
2
|
|
|
3
|
-
> **
|
|
3
|
+
> **Version:** Current Stable
|
|
4
|
+
> **Author:** Onah Victor
|
|
5
|
+
> **Repository:** [github.com/allAboutJS/Mutor.js](https://github.com/allAboutJS/Mutor.js)
|
|
6
|
+
> **Language:** TypeScript (zero runtime dependencies)
|
|
7
|
+
> **License:** See repository
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
1. [Introduction](#1-introduction)
|
|
14
|
+
2. [Installation](#2-installation)
|
|
15
|
+
3. [Quick Start](#3-quick-start)
|
|
16
|
+
4. [Core Concepts](#4-core-concepts)
|
|
17
|
+
5. [Syntax Guide](#5-syntax-guide)
|
|
18
|
+
6. [Logic System](#6-logic-system)
|
|
19
|
+
7. [Expression System](#7-expression-system)
|
|
20
|
+
8. [Includes](#8-includes)
|
|
21
|
+
9. [Components](#9-components)
|
|
22
|
+
10. [Rendering APIs](#10-rendering-apis)
|
|
23
|
+
11. [Security Architecture](#11-security-architecture)
|
|
24
|
+
12. [Sandbox System](#12-sandbox-system)
|
|
25
|
+
13. [Property Access Validation](#13-property-access-validation)
|
|
26
|
+
14. [Namespace System](#14-namespace-system)
|
|
27
|
+
15. [Cache System](#15-cache-system)
|
|
28
|
+
16. [Configuration Reference](#16-configuration-reference)
|
|
29
|
+
17. [Delimiter Customization](#17-delimiter-customization)
|
|
30
|
+
18. [Whitespace Control](#18-whitespace-control)
|
|
31
|
+
19. [Escaping Rules](#19-escaping-rules)
|
|
32
|
+
20. [Error Handling](#20-error-handling)
|
|
33
|
+
21. [CLI](#21-cli)
|
|
34
|
+
22. [Performance](#22-performance)
|
|
35
|
+
23. [Environment Support](#23-environment-support)
|
|
36
|
+
24. [Browser Usage](#24-browser-usage)
|
|
37
|
+
25. [Server Usage](#25-server-usage)
|
|
38
|
+
26. [Advanced Examples](#26-advanced-examples)
|
|
39
|
+
27. [Best Practices](#27-best-practices)
|
|
40
|
+
28. [Security Best Practices](#28-security-best-practices)
|
|
41
|
+
29. [Real-World Use Cases](#29-real-world-use-cases)
|
|
42
|
+
30. [Design Tradeoffs](#30-design-tradeoffs)
|
|
43
|
+
31. [Future Roadmap](#31-future-roadmap)
|
|
44
|
+
32. [FAQ](#32-faq)
|
|
45
|
+
33. [API Reference](#33-api-reference)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 1. Introduction
|
|
50
|
+
|
|
51
|
+
Mutor.js is a secure, compiler-oriented template engine written entirely in TypeScript. It is designed for environments where template output correctness, security, and execution speed all matter simultaneously — conditions under which most template engines require a tradeoff.
|
|
52
|
+
|
|
53
|
+
Mutor.js takes a different approach. Rather than performing string interpolation at runtime or relying on `eval`-based tricks, it processes templates through a full compiler pipeline: tokenization, AST construction, semantic analysis, security validation, code generation, and finally compilation into a native JavaScript function. That compiled function is then cached and reused for every subsequent render, making cold-path overhead a one-time cost.
|
|
54
|
+
|
|
55
|
+
**Mutor.js is not:**
|
|
56
|
+
|
|
57
|
+
- A frontend framework.
|
|
58
|
+
- A JSX alternative.
|
|
59
|
+
- An SSR framework or a React-like rendering system.
|
|
60
|
+
|
|
61
|
+
It is a template engine. It accepts a template string and a context object. It returns a rendered string. That output can be HTML, plain text, XML, or any other string-based format. What you build on top of Mutor.js — a static site generator, a server-side rendering layer, an email renderer — is your concern. Mutor.js concerns itself exclusively with doing that one job securely and fast.
|
|
62
|
+
|
|
63
|
+
**Mutor.js is:**
|
|
64
|
+
|
|
65
|
+
- A zero-dependency, TypeScript-native template engine.
|
|
66
|
+
- A compiler that produces JavaScript functions, not an interpreter.
|
|
67
|
+
- A security-first system with layered compile-time and runtime protection.
|
|
68
|
+
- A configurable engine that does not assume your use case.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 2. Installation
|
|
73
|
+
|
|
74
|
+
Mutor.js is distributed as an npm package. It requires Node.js 14+ or any ES6-compatible runtime.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm install mutorjs
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
yarn add mutorjs
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pnpm add mutorjs
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Both CommonJS and ESM module formats are supported. The package ships pre-compiled TypeScript output with bundled type declarations.
|
|
89
|
+
|
|
90
|
+
### Subpath Exports
|
|
91
|
+
|
|
92
|
+
Mutor.js exposes two entry points:
|
|
93
|
+
|
|
94
|
+
| Import Path | Description |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `mutorjs` | Universal entry point. Safe for both browser and Node.js environments. Does not include file-system APIs. |
|
|
97
|
+
| `mutorjs/server` | Server-only entry point. Includes `renderFile` and file-system-dependent functionality. Do not import this in browser bundles. |
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
// Universal (browser + server)
|
|
101
|
+
import Mutor from "mutorjs";
|
|
102
|
+
|
|
103
|
+
// Server-only
|
|
104
|
+
import Mutor from "mutorjs/server";
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 3. Quick Start
|
|
110
|
+
|
|
111
|
+
### Basic Render
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
import Mutor from "mutorjs";
|
|
115
|
+
|
|
116
|
+
const mutor = new Mutor();
|
|
117
|
+
|
|
118
|
+
const output = mutor.render(
|
|
119
|
+
`<h1>{{ greeting }}, {{ user.name }}!</h1>`,
|
|
120
|
+
{ greeting: "Hello", user: { name: "Onah" } }
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
console.log(output);
|
|
124
|
+
// <h1>Hello, Onah!</h1>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Compile Once, Execute Many Times
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
import Mutor from "mutorjs";
|
|
131
|
+
|
|
132
|
+
const mutor = new Mutor();
|
|
133
|
+
|
|
134
|
+
const fn = mutor.compile(`<p>{{ item.title }}</p>`);
|
|
135
|
+
|
|
136
|
+
const items = [
|
|
137
|
+
{ title: "Alpha" },
|
|
138
|
+
{ title: "Beta" },
|
|
139
|
+
{ title: "Gamma" },
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const rendered = items.map(item => fn({ item }));
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### File Rendering (Server Only)
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import Mutor from "mutorjs/server";
|
|
149
|
+
|
|
150
|
+
const mutor = new Mutor();
|
|
151
|
+
|
|
152
|
+
const html = await mutor.renderFile("./views/index.html", {
|
|
153
|
+
title: "Dashboard",
|
|
154
|
+
user: req.user,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
res.send(html);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Component Rendering
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
import Mutor from "mutorjs";
|
|
164
|
+
|
|
165
|
+
const mutor = new Mutor();
|
|
166
|
+
|
|
167
|
+
mutor.registerComponent("card", `
|
|
168
|
+
<div class="card">
|
|
169
|
+
<h2>{{ card.title }}</h2>
|
|
170
|
+
<p>{{ card.body }}</p>
|
|
171
|
+
</div>
|
|
172
|
+
`);
|
|
173
|
+
|
|
174
|
+
const output = mutor.renderComponent("card", {
|
|
175
|
+
card: { title: "Notice", body: "This is a component." }
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 4. Core Concepts
|
|
182
|
+
|
|
183
|
+
### Templates Are Compiled, Not Interpreted
|
|
184
|
+
|
|
185
|
+
Every template processed by Mutor.js is compiled into a JavaScript function. This happens once per unique template string. On subsequent calls with the same template, the compiled function is retrieved from cache and invoked directly. There is no parsing, no AST traversal, no validation on the hot path — only a function call.
|
|
186
|
+
|
|
187
|
+
This distinction matters at scale. A template engine that interprets its AST on every render pays a traversal cost proportional to the template's complexity on every single request. Mutor.js pays that cost exactly once.
|
|
188
|
+
|
|
189
|
+
### Security Is Architectural, Not Bolt-On
|
|
190
|
+
|
|
191
|
+
Most template engines treat security as a configuration layer — a set of filters you can optionally enable. In Mutor.js, security is woven into the compilation pipeline itself. The semantic analysis phase validates all property access, identifies potentially unsafe patterns, and either rewrites them into guarded forms or rejects them outright. By the time a compiled function reaches runtime, the security decisions have already been made.
|
|
192
|
+
|
|
193
|
+
This means you cannot accidentally bypass security by misconfiguring a flag. The dangerous paths do not exist in the compiled output.
|
|
194
|
+
|
|
195
|
+
### Context Is an Isolated Scope
|
|
196
|
+
|
|
197
|
+
Templates execute inside a restricted scope. The context object you provide is the entire world available to the template. There is no access to the global object, no access to `process`, no access to `require`, no access to `window`. The sandbox is enforced at code generation time: the generated function receives only what you pass in.
|
|
198
|
+
|
|
199
|
+
### Zero Dependencies
|
|
200
|
+
|
|
201
|
+
Mutor.js has no runtime dependencies. It is self-contained TypeScript. This is a deliberate architectural constraint. Every dependency you introduce to a security-critical system is a potential attack surface. Mutor.js owns its entire stack — tokenizer, parser, AST, code generator, runtime, cache — and each of those components is auditable without pulling in third-party code.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 5. Syntax Guide
|
|
206
|
+
|
|
207
|
+
Mutor.js uses a structured tag syntax. It is not raw JavaScript embedded in HTML. The template language is a defined, bounded DSL that the compiler can reason about completely.
|
|
208
|
+
|
|
209
|
+
### Interpolation
|
|
210
|
+
|
|
211
|
+
The primary mechanism for outputting values is the interpolation block:
|
|
212
|
+
|
|
213
|
+
```html
|
|
214
|
+
{{ expression }}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The result of the expression is HTML-escaped by default and inserted into the output at that position.
|
|
218
|
+
|
|
219
|
+
```html
|
|
220
|
+
<p>{{ user.name }}</p>
|
|
221
|
+
<p>{{ product.price * 1.1 }}</p>
|
|
222
|
+
<p>{{ user.bio ?? "No bio provided." }}</p>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Whitespace Trimming
|
|
226
|
+
|
|
227
|
+
By default, Mutor.js preserves whitespace around tags exactly as written. The `~` directive instructs the engine to strip whitespace (including newlines) from the relevant side of the tag.
|
|
228
|
+
|
|
229
|
+
```html
|
|
230
|
+
{{~ expression }} <!-- trim left whitespace -->
|
|
231
|
+
{{ expression ~}} <!-- trim right whitespace -->
|
|
232
|
+
{{~ expression ~}} <!-- trim both sides -->
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
This is particularly useful when working with `for` loops and `if` blocks to prevent unwanted blank lines in output.
|
|
236
|
+
|
|
237
|
+
### Comments
|
|
238
|
+
|
|
239
|
+
Comments use the `#` prefix inside the opening delimiter:
|
|
240
|
+
|
|
241
|
+
```html
|
|
242
|
+
{{# This is a comment. It produces no output. }}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Comments are multiline by default:
|
|
246
|
+
|
|
247
|
+
```html
|
|
248
|
+
{{#
|
|
249
|
+
This is a block comment.
|
|
250
|
+
It spans multiple lines.
|
|
251
|
+
None of this appears in output.
|
|
252
|
+
}}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Escaped Opening Tag
|
|
256
|
+
|
|
257
|
+
To output the literal opening delimiter (e.g., `{{`) without triggering template parsing, prefix it with a backslash:
|
|
258
|
+
|
|
259
|
+
```html
|
|
260
|
+
\{{ this is rendered literally as {{ }}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Whether the escape character itself is preserved in the output is controlled by `keepOpeningTagEscapeDelimiter` in configuration.
|
|
264
|
+
|
|
265
|
+
### Tag Summary
|
|
266
|
+
|
|
267
|
+
| Syntax | Purpose |
|
|
268
|
+
|---|---|
|
|
269
|
+
| `{{ expr }}` | Interpolation (auto-escaped) |
|
|
270
|
+
| `{{~ expr }}` | Interpolation, trim left |
|
|
271
|
+
| `{{ expr ~}}` | Interpolation, trim right |
|
|
272
|
+
| `{{~ expr ~}}` | Interpolation, trim both |
|
|
273
|
+
| `{{# ... }}` | Comment (no output) |
|
|
274
|
+
| `\{{` | Escaped literal delimiter |
|
|
275
|
+
| `{{ if ... }}` | Conditional block open |
|
|
276
|
+
| `{{ else if ... }}` | Conditional branch |
|
|
277
|
+
| `{{ else }}` | Default branch |
|
|
278
|
+
| `{{ for x of ... }}` | Iteration (value) |
|
|
279
|
+
| `{{ for x in ... }}` | Iteration (key) |
|
|
280
|
+
| `{{ end }}` | Closes any open block |
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## 6. Logic System
|
|
285
|
+
|
|
286
|
+
Mutor.js provides structured control flow via `if`, `else if`, `else`, `for`, and `end` tags. These are not arbitrary JavaScript — they are a fixed set of recognized logic directives that the parser handles explicitly.
|
|
287
|
+
|
|
288
|
+
### Conditionals
|
|
289
|
+
|
|
290
|
+
```html
|
|
291
|
+
{{ if condition }}
|
|
292
|
+
...
|
|
293
|
+
{{ end }}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
```html
|
|
297
|
+
{{ if user.role == "admin" }}
|
|
298
|
+
<a href="/admin">Admin Panel</a>
|
|
299
|
+
{{ else if user.role == "moderator" }}
|
|
300
|
+
<a href="/moderate">Moderation Queue</a>
|
|
301
|
+
{{ else }}
|
|
302
|
+
<a href="/dashboard">Dashboard</a>
|
|
303
|
+
{{ end }}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Conditions are arbitrary expressions. Any expression that evaluates to a truthy or falsy value is valid. Comparison operators, boolean operators, nullish coalescing, optional chaining, and ternaries are all valid inside condition expressions.
|
|
307
|
+
|
|
308
|
+
```html
|
|
309
|
+
{{ if items.length > 0 && user.isActive }}
|
|
310
|
+
...
|
|
311
|
+
{{ end }}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Iteration
|
|
315
|
+
|
|
316
|
+
Mutor.js supports two forms of `for` iteration, mirroring JavaScript's `for...of` and `for...in` semantics:
|
|
317
|
+
|
|
318
|
+
**`for...of` — iterate over values:**
|
|
319
|
+
|
|
320
|
+
```html
|
|
321
|
+
{{ for item of items }}
|
|
322
|
+
<li>{{ item.name }}</li>
|
|
323
|
+
{{ end }}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**`for...in` — iterate over keys:**
|
|
327
|
+
|
|
328
|
+
```html
|
|
329
|
+
{{ for key in record }}
|
|
330
|
+
<dt>{{ key }}</dt>
|
|
331
|
+
{{ end }}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Nested loops:**
|
|
335
|
+
|
|
336
|
+
```html
|
|
337
|
+
{{ for category of categories }}
|
|
338
|
+
<section>
|
|
339
|
+
<h2>{{ category.name }}</h2>
|
|
340
|
+
{{ for product of category.products }}
|
|
341
|
+
<p>{{ product.title }} — {{ product.price }}</p>
|
|
342
|
+
{{ end }}
|
|
343
|
+
</section>
|
|
344
|
+
{{ end }}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Block Termination
|
|
348
|
+
|
|
349
|
+
Every `if` block and every `for` block must be closed with `{{ end }}`. Unclosed blocks are a compile-time error. The compiler tracks block depth and reports the location (line and column) of the unclosed block.
|
|
350
|
+
|
|
351
|
+
> **Note:** There is no `break` or `continue` directive in the current version. Complex filtering logic belongs in the context object, not the template.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 7. Expression System
|
|
356
|
+
|
|
357
|
+
Mutor.js expressions are a defined subset of JavaScript expressions, parsed by Mutor.js's own expression parser — meaning the engine controls exactly what is and is not expressible.
|
|
358
|
+
|
|
359
|
+
### Supported Expression Constructs
|
|
360
|
+
|
|
361
|
+
| Category | Examples |
|
|
362
|
+
|---|---|
|
|
363
|
+
| Arithmetic | `a + b`, `price * 1.2`, `total / count`, `n % 2`, `2 ** 8` |
|
|
364
|
+
| Comparison | `a == b`, `a != b`, `a > b`, `a >= b`, `a < b`, `a <= b` |
|
|
365
|
+
| Boolean | `a && b`, `a \|\| b`, `!flag` |
|
|
366
|
+
| Ternary | `condition ? valueA : valueB` |
|
|
367
|
+
| Nullish Coalescing | `value ?? "default"` |
|
|
368
|
+
| Optional Chaining | `user?.address?.city` |
|
|
369
|
+
| Property Access | `obj.prop`, `obj["key"]` |
|
|
370
|
+
| Array Access | `arr[0]`, `arr[index]` |
|
|
371
|
+
| Namespace Access | `Math::floor(n)`, `JSON::stringify(data)` |
|
|
372
|
+
| Bitwise | `a & b`, `a \| b`, `a ^ b`, `a >> b`, `a << b` |
|
|
373
|
+
| Function Calls | `fn(args)` — only when `allowFnCalls: true` |
|
|
374
|
+
| String Literals | `'single'`, `"double"`, `` `backtick` `` |
|
|
375
|
+
|
|
376
|
+
### Operator Precedence
|
|
377
|
+
|
|
378
|
+
Mutor.js follows standard JavaScript operator precedence:
|
|
379
|
+
|
|
380
|
+
| Precedence (high → low) | Operators |
|
|
381
|
+
|---|---|
|
|
382
|
+
| Unary | `!`, unary `-` |
|
|
383
|
+
| Exponentiation | `**` |
|
|
384
|
+
| Multiplicative | `*`, `/`, `%` |
|
|
385
|
+
| Additive | `+`, `-` |
|
|
386
|
+
| Shift | `<<`, `>>` |
|
|
387
|
+
| Relational | `<`, `>`, `<=`, `>=` |
|
|
388
|
+
| Equality | `==`, `!=` |
|
|
389
|
+
| Bitwise AND | `&` |
|
|
390
|
+
| Bitwise XOR | `^` |
|
|
391
|
+
| Bitwise OR | `\|` |
|
|
392
|
+
| Logical AND | `&&` |
|
|
393
|
+
| Logical OR | `\|\|` |
|
|
394
|
+
| Nullish Coalescing | `??` |
|
|
395
|
+
| Conditional (Ternary) | `? :` |
|
|
396
|
+
|
|
397
|
+
When in doubt, use explicit parentheses:
|
|
398
|
+
|
|
399
|
+
```html
|
|
400
|
+
{{ (a + b) * c }}
|
|
401
|
+
{{ user.age >= 18 ? "adult" : "minor" }}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Strings
|
|
405
|
+
|
|
406
|
+
String literals support single quotes, double quotes, and backticks. Backtick strings are treated as static strings — template interpolation (`${...}`) inside strings does not evaluate expressions. The backtick form exists to allow strings containing both single and double quotes conveniently.
|
|
407
|
+
|
|
408
|
+
```html
|
|
409
|
+
{{ "Hello, world" }}
|
|
410
|
+
{{ 'It\'s fine' }}
|
|
411
|
+
{{ `This has "quotes" and 'apostrophes'` }}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Function Calls
|
|
415
|
+
|
|
416
|
+
Function calls are disabled by default. This prevents templates from invoking arbitrary functions that may exist on context objects, which is the correct default for most use cases.
|
|
417
|
+
|
|
418
|
+
To enable function calls:
|
|
419
|
+
|
|
420
|
+
```js
|
|
421
|
+
const mutor = new Mutor({ allowFnCalls: true });
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
When enabled, calls like `item.name.toUpperCase()` or `Math::floor(price)` are permitted. Namespace function calls (`Math::floor`, etc.) are permitted regardless of `allowFnCalls` because they are routed through the controlled namespace system.
|
|
425
|
+
|
|
426
|
+
> **Warning:** Enabling `allowFnCalls` expands the trusted surface area of your templates. Ensure the context objects you pass contain only functions you intend to expose. Do not pass raw service objects or request objects as top-level context when `allowFnCalls` is enabled.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## 8. Includes
|
|
431
|
+
|
|
432
|
+
Includes allow a template to embed another template file at a specific location in its output. The included template is compiled and cached independently, sharing the same cache as the parent instance.
|
|
433
|
+
|
|
434
|
+
### Syntax
|
|
435
|
+
|
|
436
|
+
```html
|
|
437
|
+
{{ Mutor::include("./partials/header.html") }}
|
|
438
|
+
{{ Mutor::include("./partials/nav.html", navLinks) }}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
When no context argument is provided, the include inherits the current rendering context automatically. For direct access to the current context, use `Mutor::$$context`:
|
|
442
|
+
|
|
443
|
+
```html
|
|
444
|
+
{{ Mutor::include("./partials/footer.html", Mutor::$$context) }}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Include Resolution
|
|
448
|
+
|
|
449
|
+
Include paths are resolved relative to the file being rendered when using `renderFile`. In a browser environment, include paths are treated as names of components registered with `mutor.registerComponent`. Attempting to include a non-registered component, or a file that does not exist, throws an error.
|
|
450
|
+
|
|
451
|
+
### Circular Include Detection
|
|
452
|
+
|
|
453
|
+
Mutor.js tracks the include chain across all recursive resolutions. If a template attempts to include a file already in the current include stack, the engine throws rather than entering infinite recursion:
|
|
454
|
+
|
|
455
|
+
```
|
|
456
|
+
MutorError: Circular include detected: ./partials/a.html → ./partials/b.html → ./partials/a.html
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## 9. Components
|
|
462
|
+
|
|
463
|
+
Components are named templates registered on a Mutor instance. They are the browser-compatible alternative to file-based includes, and are also useful on the server when you want to manage templates programmatically.
|
|
464
|
+
|
|
465
|
+
### Registering a Component
|
|
466
|
+
|
|
467
|
+
```js
|
|
468
|
+
mutor.registerComponent("alert", `
|
|
469
|
+
<div class="alert alert--{{ alert.type }}">
|
|
470
|
+
<strong>{{ alert.title }}</strong>
|
|
471
|
+
<p>{{ alert.message }}</p>
|
|
472
|
+
</div>
|
|
473
|
+
`);
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Rendering a Component
|
|
477
|
+
|
|
478
|
+
```js
|
|
479
|
+
const html = mutor.renderComponent("alert", {
|
|
480
|
+
alert: {
|
|
481
|
+
type: "error",
|
|
482
|
+
title: "Validation Failed",
|
|
483
|
+
message: "Email address is required.",
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Components vs. Includes
|
|
489
|
+
|
|
490
|
+
| Feature | Components | Includes |
|
|
491
|
+
|---|---|---|
|
|
492
|
+
| Template source | In-memory string | File system |
|
|
493
|
+
| Browser-compatible | Yes | No |
|
|
494
|
+
| Circular detection | Yes | Yes |
|
|
495
|
+
| Cache | Same instance cache | Same instance cache |
|
|
496
|
+
| Context handling | Explicit argument | Explicit or `$$context` |
|
|
497
|
+
|
|
498
|
+
Components are compiled and cached on first `renderComponent` call. Subsequent calls hit the cache directly.
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## 10. Rendering APIs
|
|
503
|
+
|
|
504
|
+
### `compile(template: string): (context: object) => string`
|
|
505
|
+
|
|
506
|
+
Compiles a template string into an executable function. The resulting function accepts a context object and returns the rendered string.
|
|
507
|
+
|
|
508
|
+
```js
|
|
509
|
+
const fn = mutor.compile(`<title>{{ page.title }}</title>`);
|
|
510
|
+
const output = fn({ page: { title: "Home" } });
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Calling `compile` with the same template string twice returns the cached compiled function.
|
|
514
|
+
|
|
515
|
+
### `render(template: string, context: object): string`
|
|
516
|
+
|
|
517
|
+
Convenience wrapper around `compile`. Compiles the template if not already cached, then immediately invokes it with the provided context.
|
|
518
|
+
|
|
519
|
+
```js
|
|
520
|
+
const output = mutor.render(`<p>{{ msg }}</p>`, { msg: "Hello" });
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### `renderFile(path: string, context: object): Promise<string>`
|
|
524
|
+
|
|
525
|
+
Server-only. Reads the template file from disk, compiles it, and renders it with the given context. The compiled result is cached by resolved file path — repeated calls with the same path do not re-read the file or re-compile.
|
|
526
|
+
|
|
527
|
+
```js
|
|
528
|
+
const html = await mutor.renderFile("./views/home.html", context);
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
> This method is not available when importing from `mutorjs` (the universal entry point). Import from `mutorjs/server` to access it.
|
|
532
|
+
|
|
533
|
+
### `registerComponent(name: string, template: string): void`
|
|
534
|
+
|
|
535
|
+
Registers a named template string as a component on the current Mutor instance. Compilation is deferred to first render.
|
|
536
|
+
|
|
537
|
+
### `renderComponent(name: string, context: object): string`
|
|
538
|
+
|
|
539
|
+
Renders a previously registered component by name. Throws `MutorComponentError` if the component has not been registered.
|
|
540
|
+
|
|
541
|
+
### Instance Isolation
|
|
542
|
+
|
|
543
|
+
Each `Mutor` instance maintains its own compilation cache and component registry. Two instances do not share state. This is important in multi-tenant or per-request environments where isolation between rendering contexts is required.
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## 11. Security Architecture
|
|
548
|
+
|
|
549
|
+
Security in Mutor.js is not an optional layer. It is the reason the compilation pipeline exists in the form it does.
|
|
550
|
+
|
|
551
|
+
### Threat Model
|
|
552
|
+
|
|
553
|
+
Mutor.js is designed for environments where the template may reference user-controlled data, and in some configurations, where the template itself may originate from less-trusted sources. The primary threats are:
|
|
554
|
+
|
|
555
|
+
- **Cross-site scripting (XSS):** Injecting HTML or JavaScript through interpolated values.
|
|
556
|
+
- **Prototype pollution access:** Using `__proto__`, `constructor`, or `prototype` to escape the context object and reach dangerous properties.
|
|
557
|
+
- **Unintended function invocation:** Calling methods on context objects that were not intended to be exposed to templates.
|
|
558
|
+
- **Global scope leakage:** Accessing `process`, `window`, `global`, `require`, or other environmental globals.
|
|
559
|
+
- **Computed property injection:** Using dynamic property names to bypass static property validation.
|
|
560
|
+
|
|
561
|
+
### Defense Layers
|
|
562
|
+
|
|
563
|
+
Mutor.js employs defense in depth across three distinct phases:
|
|
564
|
+
|
|
565
|
+
**Layer 1 — Compile-time analysis**
|
|
566
|
+
|
|
567
|
+
Every property access in the template is analyzed during compilation. Static accesses (`obj.prop`, `obj["key"]`) are validated against the forbidden property list. Dynamic accesses (`obj[expression]`) are rewritten into calls to a runtime guard helper. Access to `__proto__`, `constructor`, or `prototype` is unconditionally rejected — there is no configuration override.
|
|
568
|
+
|
|
569
|
+
**Layer 2 — Code generation sandboxing**
|
|
570
|
+
|
|
571
|
+
The generated function runs inside a closed scope. It receives only the context object as its argument. It has no reference to any global, no `this` binding to the environment, and no access to `require` or `import`.
|
|
572
|
+
|
|
573
|
+
**Layer 3 — Runtime context validation**
|
|
574
|
+
|
|
575
|
+
Before the compiled function executes, Mutor.js validates the context object. Prototype chains are checked, and objects with polluted prototypes or non-standard getters and setters on built-in prototype properties are rejected. This prevents an attacker from crafting a context object that passes compile-time checks but exploits runtime behavior.
|
|
576
|
+
|
|
577
|
+
### Output Escaping
|
|
578
|
+
|
|
579
|
+
All interpolated values are HTML-escaped by default:
|
|
580
|
+
|
|
581
|
+
| Character | Escaped As |
|
|
582
|
+
|---|---|
|
|
583
|
+
| `&` | `&` |
|
|
584
|
+
| `<` | `<` |
|
|
585
|
+
| `>` | `>` |
|
|
586
|
+
| `"` | `"` |
|
|
587
|
+
| `'` | `'` |
|
|
588
|
+
|
|
589
|
+
Auto-escaping is controlled by the `autoEscape` configuration option. Setting `autoEscape: false` disables this behavior globally.
|
|
590
|
+
|
|
591
|
+
> **Warning:** Do not disable `autoEscape` unless you have a specific, justified reason and you understand the full implications. Disabled auto-escaping in combination with user-controlled context data is an XSS vector.
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## 12. Sandbox System
|
|
596
|
+
|
|
597
|
+
The sandbox ensures compiled templates cannot access anything outside the explicitly provided context.
|
|
598
|
+
|
|
599
|
+
### What the Sandbox Prevents
|
|
600
|
+
|
|
601
|
+
- Access to JavaScript globals (`window`, `process`, `global`, `require`, `globalThis`)
|
|
602
|
+
- Access to `this` in a way that reaches the outer environment
|
|
603
|
+
- Access to prototype chain escape vectors (`__proto__`, `constructor`, `prototype`)
|
|
604
|
+
|
|
605
|
+
### Function Call Sandboxing
|
|
606
|
+
|
|
607
|
+
When `allowFnCalls: false` (the default), the compiler rejects any expression that involves a function invocation on a non-namespace identifier. `user.name.toUpperCase()` would be rejected. `Math::floor(price)` is permitted because it routes through the namespace system.
|
|
608
|
+
|
|
609
|
+
When `allowFnCalls: true`, method calls on context properties are permitted. The sandbox still prevents access to globals; it only allows invocation of functions that exist on the context object you explicitly pass in.
|
|
610
|
+
|
|
611
|
+
### Forbidden Property List
|
|
612
|
+
|
|
613
|
+
The default forbidden properties are `__proto__`, `constructor`, and `prototype`. These are always enforced and cannot be removed. You can extend the list for your specific threat model:
|
|
614
|
+
|
|
615
|
+
```js
|
|
616
|
+
const mutor = new Mutor({
|
|
617
|
+
forbiddenProps: new Set(["__proto__", "constructor", "prototype", "valueOf", "toString"]),
|
|
618
|
+
});
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
> **Note:** If you add a property to both `allowedProps` and `forbiddenProps`, `allowedProps` takes precedence and the property will not be blocked.
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## 13. Property Access Validation
|
|
626
|
+
|
|
627
|
+
### Static Property Access
|
|
628
|
+
|
|
629
|
+
```js
|
|
630
|
+
user.name // static — property name known at compile time
|
|
631
|
+
user["email"] // static — property name known at compile time
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
For static access, the compiler checks the property name against the forbidden list at compile time. If the name is forbidden, compilation fails immediately with a clear error.
|
|
635
|
+
|
|
636
|
+
### Dynamic Property Access
|
|
637
|
+
|
|
638
|
+
```js
|
|
639
|
+
obj[someVariable] // dynamic — property name not known until runtime
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Dynamic access cannot be fully validated at compile time. Mutor.js handles this by rewriting all dynamic property accesses through a runtime guard that validates the resolved property name against the forbidden list before performing the access. Even if an attacker sets `someVariable = "__proto__"` at runtime, the guard catches it.
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## 14. Namespace System
|
|
647
|
+
|
|
648
|
+
The namespace system provides controlled access to JavaScript built-in utilities inside templates without exposing the full global scope.
|
|
649
|
+
|
|
650
|
+
### Syntax
|
|
651
|
+
|
|
652
|
+
```html
|
|
653
|
+
{{ Namespace::identifier }}
|
|
654
|
+
{{ Namespace::method(args) }}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
The `::` operator is Mutor.js-specific. It does not exist in standard JavaScript. It is parsed by Mutor.js and translated into safe, controlled lookups at code generation time.
|
|
658
|
+
|
|
659
|
+
### Available Namespaces
|
|
660
|
+
|
|
661
|
+
| Namespace | Accessible Members |
|
|
662
|
+
|---|---|
|
|
663
|
+
| `Math` | All `Math` static methods and properties (`floor`, `ceil`, `round`, `max`, `min`, `abs`, `sqrt`, `pow`, `PI`, etc.) |
|
|
664
|
+
| `JSON` | `stringify`, `parse` |
|
|
665
|
+
| `Object` | `keys`, `values`, `entries`, `assign`, `fromEntries` |
|
|
666
|
+
| `Array` | `isArray`, `from` |
|
|
667
|
+
| `String` | `fromCharCode` |
|
|
668
|
+
| `Number` | `isInteger`, `isFinite`, `isNaN`, `parseInt`, `parseFloat` |
|
|
669
|
+
| `Date` | `now`, `getFullYear` (static forms) |
|
|
670
|
+
| `Mutor` | `include`, `$$context` (engine directives) |
|
|
671
|
+
|
|
672
|
+
### Examples
|
|
673
|
+
|
|
674
|
+
```html
|
|
675
|
+
<p>Price: {{ Math::floor(product.price) }} USD</p>
|
|
676
|
+
<p>Keys: {{ Object::keys(record).length }}</p>
|
|
677
|
+
<p>Data: {{ JSON::stringify(debug.payload) }}</p>
|
|
678
|
+
<p>Valid: {{ Array::isArray(items) ? "yes" : "no" }}</p>
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### The `Mutor` Namespace
|
|
682
|
+
|
|
683
|
+
The `Mutor` namespace is reserved for engine directives:
|
|
684
|
+
|
|
685
|
+
- `Mutor::include(path)` — includes a template file, inheriting the current context
|
|
686
|
+
- `Mutor::include(path, context)` — includes a template file with an explicit context
|
|
687
|
+
- `Mutor::$$context` — a reference to the current rendering context object
|
|
688
|
+
|
|
689
|
+
---
|
|
690
|
+
|
|
691
|
+
## 15. Cache System
|
|
692
|
+
|
|
693
|
+
Every `Mutor` instance maintains an internal LRU (Least Recently Used) cache of compiled template functions.
|
|
694
|
+
|
|
695
|
+
### Cache Behavior
|
|
696
|
+
|
|
697
|
+
- Templates are cached by their source string (for `compile` and `render`) or by their resolved absolute file path (for `renderFile`).
|
|
698
|
+
- Cache lookups are O(1).
|
|
699
|
+
- When a template is accessed, it is promoted to the most-recently-used position.
|
|
700
|
+
- When the cache reaches its configured `maxSize`, the least-recently-used entry is evicted.
|
|
701
|
+
|
|
702
|
+
### Cache Configuration
|
|
703
|
+
|
|
704
|
+
```js
|
|
705
|
+
const mutor = new Mutor({
|
|
706
|
+
cache: {
|
|
707
|
+
active: true,
|
|
708
|
+
maxSize: 50 * 1024 * 1024, // 50MB
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
| Option | Type | Default | Description |
|
|
714
|
+
|---|---|---|---|
|
|
715
|
+
| `active` | `boolean` | `true` | Whether caching is enabled. Set to `false` to recompile on every render (useful for development). |
|
|
716
|
+
| `maxSize` | `number` | `52428800` (50MB) | Maximum cache memory in bytes. When exceeded, LRU eviction runs. |
|
|
717
|
+
|
|
718
|
+
### Disabling the Cache
|
|
719
|
+
|
|
720
|
+
```js
|
|
721
|
+
const mutor = new Mutor({
|
|
722
|
+
cache: { active: false }
|
|
723
|
+
});
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
With the cache disabled, every `render` or `renderFile` call runs the full compiler pipeline. This is useful in development for picking up template changes without restarting the process, but is not appropriate for production.
|
|
727
|
+
|
|
728
|
+
### Manual Cache Control
|
|
729
|
+
|
|
730
|
+
```js
|
|
731
|
+
// Clear the entire cache
|
|
732
|
+
mutor.reset();
|
|
733
|
+
|
|
734
|
+
// Get cache diagnostics (memory usage, entry count)
|
|
735
|
+
const diagnostics = mutor.getDiagnostics();
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
> **Note:** `reset()` is synchronous and removes all compiled functions from the instance cache. The next render of any template will trigger recompilation.
|
|
739
|
+
|
|
740
|
+
### Instance Isolation
|
|
741
|
+
|
|
742
|
+
Each instance has an independent cache with its own size limit. If you need strict per-tenant or per-request memory isolation, use separate `Mutor` instances.
|
|
743
|
+
|
|
744
|
+
---
|
|
745
|
+
|
|
746
|
+
## 16. Configuration Reference
|
|
747
|
+
|
|
748
|
+
The full configuration object with all defaults:
|
|
749
|
+
|
|
750
|
+
```ts
|
|
751
|
+
interface MutorConfig {
|
|
752
|
+
build?: {
|
|
753
|
+
include?: Set<string>; // File extensions to scan (server only)
|
|
754
|
+
exclude?: Set<string>; // Directories to exclude from scanning
|
|
755
|
+
};
|
|
756
|
+
autoEscape?: boolean; // Default: true
|
|
757
|
+
allowedProps?: Set<string>; // Explicitly whitelisted property names
|
|
758
|
+
forbiddenProps?: Set<string>; // Additional forbidden property names
|
|
759
|
+
allowFnCalls?: boolean; // Default: false
|
|
760
|
+
delimiters?: {
|
|
761
|
+
openingTag?: string; // Default: "{{"
|
|
762
|
+
closingTag?: string; // Default: "}}"
|
|
763
|
+
openingTagEscape?: string; // Default: "\\"
|
|
764
|
+
whitespaceTrim?: string; // Default: "~"
|
|
765
|
+
commentTag?: string; // Default: "#"
|
|
766
|
+
};
|
|
767
|
+
keepOpeningTagEscapeDelimiter?: boolean; // Default: false
|
|
768
|
+
cache?: {
|
|
769
|
+
active?: boolean; // Default: true
|
|
770
|
+
maxSize?: number; // Default: 50 * 1024 * 1024 (50MB)
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### Default Configuration
|
|
776
|
+
|
|
777
|
+
```js
|
|
778
|
+
const defaultConfig = {
|
|
779
|
+
build: {
|
|
780
|
+
include: new Set([".html", ".txt"]),
|
|
781
|
+
exclude: new Set(["node_modules", ".git"]),
|
|
782
|
+
},
|
|
783
|
+
autoEscape: true,
|
|
784
|
+
allowedProps: new Set(),
|
|
785
|
+
forbiddenProps: new Set(["__proto__", "constructor", "prototype"]),
|
|
786
|
+
allowFnCalls: false,
|
|
787
|
+
delimiters: {
|
|
788
|
+
openingTag: "{{",
|
|
789
|
+
closingTag: "}}",
|
|
790
|
+
openingTagEscape: "\\",
|
|
791
|
+
whitespaceTrim: "~",
|
|
792
|
+
},
|
|
793
|
+
keepOpeningTagEscapeDelimiter: false,
|
|
794
|
+
cache: {
|
|
795
|
+
active: true,
|
|
796
|
+
maxSize: 50 * 1024 * 1024,
|
|
797
|
+
},
|
|
798
|
+
};
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### `allowedProps`
|
|
802
|
+
|
|
803
|
+
A `Set<string>` of property names that are explicitly permitted regardless of other validation rules.
|
|
804
|
+
|
|
805
|
+
### `forbiddenProps`
|
|
806
|
+
|
|
807
|
+
A `Set<string>` of additional property names to block. The built-in forbidden set (`__proto__`, `constructor`, `prototype`) is always enforced and merged with any values you provide here.
|
|
808
|
+
|
|
809
|
+
### `build.include` and `build.exclude`
|
|
810
|
+
|
|
811
|
+
Used by the server-side file scanner when pre-compiling a directory of templates. `include` is a set of file extensions to process. `exclude` is a set of directory names to skip.
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
## 17. Delimiter Customization
|
|
816
|
+
|
|
817
|
+
All delimiter-related strings are fully configurable.
|
|
818
|
+
|
|
819
|
+
### Changing Delimiters
|
|
820
|
+
|
|
821
|
+
```js
|
|
822
|
+
const mutor = new Mutor({
|
|
823
|
+
delimiters: {
|
|
824
|
+
openingTag: "<%",
|
|
825
|
+
closingTag: "%>",
|
|
826
|
+
openingTagEscape: "\\",
|
|
827
|
+
whitespaceTrim: "-",
|
|
828
|
+
commandTag: "#",
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
With this configuration, templates use `<%` and `%>`:
|
|
834
|
+
|
|
835
|
+
```html
|
|
836
|
+
<p><% user.name %></p>
|
|
837
|
+
<%- if user.isAdmin -%>
|
|
838
|
+
<a href="/admin">Admin</a>
|
|
839
|
+
<%- end -%>
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### Constraints on Delimiter Choice
|
|
843
|
+
|
|
844
|
+
- Opening and closing delimiters must be distinct strings.
|
|
845
|
+
- Delimiters should not be substrings of each other (e.g., `{` and `{{` would cause scanner ambiguity).
|
|
846
|
+
- The whitespace trim character must be a single character.
|
|
847
|
+
- The escape character must be a single character.
|
|
848
|
+
|
|
849
|
+
### Delimiter Change and Cache Invalidation
|
|
850
|
+
|
|
851
|
+
If you change delimiters at runtime by creating a new `Mutor` instance with different configuration, the new instance has a separate cache. Templates compiled under one delimiter configuration are not compatible with a differently configured instance — the instance and its configuration are inseparable.
|
|
852
|
+
|
|
853
|
+
---
|
|
854
|
+
|
|
855
|
+
## 18. Whitespace Control
|
|
856
|
+
|
|
857
|
+
Mutor.js provides precise whitespace control via the `~` directive (or your configured `whitespaceTrim` character).
|
|
858
|
+
|
|
859
|
+
### Without Whitespace Trimming
|
|
860
|
+
|
|
861
|
+
```html
|
|
862
|
+
<ul>
|
|
863
|
+
{{ for item of items }}
|
|
864
|
+
<li>{{ item.name }}</li>
|
|
865
|
+
{{ end }}
|
|
866
|
+
</ul>
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
Output:
|
|
870
|
+
|
|
871
|
+
```html
|
|
872
|
+
<ul>
|
|
873
|
+
|
|
874
|
+
<li>Alpha</li>
|
|
875
|
+
|
|
876
|
+
<li>Beta</li>
|
|
877
|
+
|
|
878
|
+
</ul>
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
The blank lines come from the newlines around the `for` and `end` tags.
|
|
882
|
+
|
|
883
|
+
### With Whitespace Trimming
|
|
884
|
+
|
|
885
|
+
```html
|
|
886
|
+
<ul>
|
|
887
|
+
{{~ for item of items ~}}
|
|
888
|
+
<li>{{ item.name }}</li>
|
|
889
|
+
{{~ end ~}}
|
|
890
|
+
</ul>
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
Output:
|
|
894
|
+
|
|
895
|
+
```html
|
|
896
|
+
<ul>
|
|
897
|
+
<li>Alpha</li>
|
|
898
|
+
<li>Beta</li>
|
|
899
|
+
</ul>
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
The `~` on the opening tag trims whitespace to the left of that tag; `~` on the closing tag trims whitespace to the right. Trim directives work on interpolation tags as well as logic tags.
|
|
903
|
+
|
|
904
|
+
---
|
|
905
|
+
|
|
906
|
+
## 19. Escaping Rules
|
|
907
|
+
|
|
908
|
+
### Auto-Escape (Default)
|
|
909
|
+
|
|
910
|
+
When `autoEscape: true`, every dynamic value is HTML-escaped before insertion. A function call that returns a value like `<script>alert(1)</script>` renders as:
|
|
911
|
+
|
|
912
|
+
```
|
|
913
|
+
<script>alert(1)</script>
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
### Disabling Auto-Escape
|
|
917
|
+
|
|
918
|
+
```js
|
|
919
|
+
const mutor = new Mutor({ autoEscape: false });
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
With auto-escaping disabled, values are interpolated verbatim. This is appropriate only when generating non-HTML output (e.g., plain text) or when context data has been sanitized elsewhere.
|
|
923
|
+
|
|
924
|
+
### Escaping the Delimiter
|
|
925
|
+
|
|
926
|
+
To output the literal opening delimiter without triggering parsing:
|
|
927
|
+
|
|
928
|
+
```html
|
|
929
|
+
\{{ this is not a template tag }}
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
The `keepOpeningTagEscapeDelimiter` option controls whether the backslash itself appears in output:
|
|
933
|
+
|
|
934
|
+
| `keepOpeningTagEscapeDelimiter` | Template | Output |
|
|
935
|
+
|---|---|---|
|
|
936
|
+
| `false` (default) | `\{{ expr }}` | `{{ expr }}` |
|
|
937
|
+
| `true` | `\{{ expr }}` | `\{{ expr }}` |
|
|
938
|
+
|
|
939
|
+
---
|
|
940
|
+
|
|
941
|
+
## 20. Error Handling
|
|
942
|
+
|
|
943
|
+
Mutor.js produces typed errors with detailed source location information.
|
|
944
|
+
|
|
945
|
+
### Error Types
|
|
946
|
+
|
|
947
|
+
| Error Class | Thrown When |
|
|
948
|
+
|---|---|
|
|
949
|
+
| `MutorParseError` | The tokenizer or parser cannot process the input |
|
|
950
|
+
| `MutorSecurityError` | A forbidden property access or unsafe construct is detected |
|
|
951
|
+
| `MutorCompileError` | Code generation or compilation fails |
|
|
952
|
+
| `MutorRuntimeError` | A runtime context validation failure occurs |
|
|
953
|
+
| `MutorIncludeError` | An include file cannot be found or a circular include is detected |
|
|
954
|
+
| `MutorComponentError` | A component is rendered that has not been registered |
|
|
955
|
+
|
|
956
|
+
### Error Structure
|
|
957
|
+
|
|
958
|
+
All error types extend a base `MutorError` class:
|
|
959
|
+
|
|
960
|
+
```ts
|
|
961
|
+
class MutorError extends Error {
|
|
962
|
+
code: string; // Machine-readable error code
|
|
963
|
+
source?: string; // Original template content (truncated)
|
|
964
|
+
location?: {
|
|
965
|
+
line: number;
|
|
966
|
+
column: number;
|
|
967
|
+
offset: number;
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
### Error Messages
|
|
973
|
+
|
|
974
|
+
Because every token is position-tracked, errors reference exact source locations:
|
|
975
|
+
|
|
976
|
+
```
|
|
977
|
+
MutorSecurityError: Forbidden property access: "constructor"
|
|
978
|
+
at line 4, column 12 in template "views/user-profile.html"
|
|
979
|
+
|
|
980
|
+
MutorParseError: Unclosed block: "if" block opened at line 7, column 3 was never closed.
|
|
981
|
+
|
|
982
|
+
MutorIncludeError: Circular include detected.
|
|
983
|
+
Include chain: views/layout.html → views/partials/nav.html → views/layout.html
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
### Handling Errors
|
|
987
|
+
|
|
988
|
+
```js
|
|
989
|
+
import { MutorSecurityError, MutorParseError } from "mutorjs";
|
|
990
|
+
|
|
991
|
+
try {
|
|
992
|
+
const output = mutor.render(template, context);
|
|
993
|
+
} catch (err) {
|
|
994
|
+
if (err instanceof MutorSecurityError) {
|
|
995
|
+
// Log and reject the template — do not attempt to render
|
|
996
|
+
logger.error("Template security violation", { code: err.code, location: err.location });
|
|
997
|
+
throw err;
|
|
998
|
+
}
|
|
999
|
+
if (err instanceof MutorParseError) {
|
|
1000
|
+
// Template has a syntax error — report to developer
|
|
1001
|
+
throw err;
|
|
1002
|
+
}
|
|
1003
|
+
throw err;
|
|
1004
|
+
}
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
---
|
|
1008
|
+
|
|
1009
|
+
## 21. CLI
|
|
1010
|
+
|
|
1011
|
+
Mutor.js ships a command-line interface for compiling and rendering templates outside of JavaScript code.
|
|
1012
|
+
|
|
1013
|
+
### Installation
|
|
1014
|
+
|
|
1015
|
+
Install the package globally to use the `mutor` command directly:
|
|
1016
|
+
|
|
1017
|
+
```bash
|
|
1018
|
+
npm install -g mutorjs
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
Or use it locally via `npx`:
|
|
1022
|
+
|
|
1023
|
+
```bash
|
|
1024
|
+
npx mutor <command> <input> [options]
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
### Commands
|
|
1028
|
+
|
|
1029
|
+
#### `compile <template>`
|
|
1030
|
+
|
|
1031
|
+
Compiles a single template file to its intermediate compiled form.
|
|
1032
|
+
|
|
1033
|
+
```bash
|
|
1034
|
+
mutor compile ./views/index.html --out ./dist/index.html
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
If `--out` is omitted, the compiled output is printed to stdout.
|
|
1038
|
+
|
|
1039
|
+
#### `build <dir>`
|
|
1040
|
+
|
|
1041
|
+
Renders all templates in a directory using a JSON data source.
|
|
1042
|
+
|
|
1043
|
+
```bash
|
|
1044
|
+
mutor build ./views --data ./data.json --out ./dist
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
Both `--data` and `--out` are required for `build`.
|
|
1048
|
+
|
|
1049
|
+
#### `render <template>`
|
|
1050
|
+
|
|
1051
|
+
Compiles and immediately renders a single template using a JSON data source.
|
|
1052
|
+
|
|
1053
|
+
```bash
|
|
1054
|
+
mutor render ./views/email.html --data ./context.json --out ./dist/email.html
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
If `--out` is omitted, the rendered output is printed to stdout.
|
|
1058
|
+
|
|
1059
|
+
### Options
|
|
1060
|
+
|
|
1061
|
+
| Option | Description |
|
|
1062
|
+
|---|---|
|
|
1063
|
+
| `--out <path>` | Output file or directory. Defaults to stdout for `compile` and `render`. |
|
|
1064
|
+
| `--data <path>` | JSON file to use as the render context. Required for `build` and `render`. |
|
|
1065
|
+
| `--config <path>` | JSON config file passed to the Mutor instance. |
|
|
1066
|
+
| `--version` | Print the installed version and exit. |
|
|
1067
|
+
| `--help` | Print usage information and exit. |
|
|
1068
|
+
|
|
1069
|
+
### Exit Codes
|
|
1070
|
+
|
|
1071
|
+
| Code | Meaning |
|
|
1072
|
+
|---|---|
|
|
1073
|
+
| `0` | Success |
|
|
1074
|
+
| `1` | Runtime error (I/O failure, render failure, etc.) |
|
|
1075
|
+
| `2` | Argument error (unknown flag, missing required value, wrong file type) |
|
|
1076
|
+
|
|
1077
|
+
### Configuration via `--config`
|
|
1078
|
+
|
|
1079
|
+
You can pass any `MutorConfig`-compatible JSON file to `--config`:
|
|
1080
|
+
|
|
1081
|
+
```json
|
|
1082
|
+
{
|
|
1083
|
+
"autoEscape": true,
|
|
1084
|
+
"allowFnCalls": false,
|
|
1085
|
+
"cache": {
|
|
1086
|
+
"active": true,
|
|
1087
|
+
"maxSize": 52428800
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
```bash
|
|
1093
|
+
mutor build ./views --data ./data.json --out ./dist --config ./mutor.config.json
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
### Examples
|
|
1097
|
+
|
|
1098
|
+
```bash
|
|
1099
|
+
# Compile a template and print to stdout
|
|
1100
|
+
mutor compile ./views/home.html
|
|
1101
|
+
|
|
1102
|
+
# Compile a template to a file
|
|
1103
|
+
mutor compile ./views/home.html --out ./dist/home.html
|
|
1104
|
+
|
|
1105
|
+
# Render a template with data, write to file
|
|
1106
|
+
mutor render ./views/email.html --data ./payload.json --out ./out/email.html
|
|
1107
|
+
|
|
1108
|
+
# Build an entire views directory
|
|
1109
|
+
mutor build ./views --data ./site.json --out ./dist
|
|
1110
|
+
|
|
1111
|
+
# Build with a custom config
|
|
1112
|
+
mutor build ./views --data ./site.json --out ./dist --config ./mutor.config.json
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
---
|
|
1116
|
+
|
|
1117
|
+
## 22. Performance
|
|
1118
|
+
|
|
1119
|
+
### Compilation vs. Execution
|
|
1120
|
+
|
|
1121
|
+
Mutor.js makes a deliberate tradeoff: spend more time at compile time so that runtime is as fast as possible.
|
|
1122
|
+
|
|
1123
|
+
**Compilation cost** — The full pipeline (tokenize → parse → analyze → validate → generate → compile) is more work than engines like EJS or Eta perform. Mutor.js runs semantic analysis and security validation steps that those engines skip. This cost is paid once per unique template per process lifetime.
|
|
1124
|
+
|
|
1125
|
+
**Execution cost** — The compiled output is a native JavaScript function. Executing it is as fast as executing any other JavaScript function of equivalent complexity. There is no interpreter, no AST walker, no regex match — only a function call.
|
|
1126
|
+
|
|
1127
|
+
The compilation cost is amortized across all renders of that template. For a server process that handles thousands of requests, a single template might be compiled once and executed hundreds of thousands of times. The per-request cost is execution only.
|
|
6
1128
|
|
|
7
|
-
|
|
1129
|
+
### Benchmarks
|
|
1130
|
+
|
|
1131
|
+
The following benchmarks are indicative, based on internal measurements on a modern machine with Node.js 20:
|
|
1132
|
+
|
|
1133
|
+
**Full pipeline (compile + execute):**
|
|
1134
|
+
|
|
1135
|
+
| Engine | Relative Performance |
|
|
1136
|
+
|---|---|
|
|
1137
|
+
| Eta | 1× (baseline) |
|
|
1138
|
+
| **Mutor.js** | **~0.8–0.9×** |
|
|
1139
|
+
| EJS | ~0.4× |
|
|
1140
|
+
| Nunjucks | ~0.15× |
|
|
1141
|
+
| Handlebars | ~0.12× |
|
|
1142
|
+
|
|
1143
|
+
**Raw execution (cache-warm, pre-compiled):**
|
|
1144
|
+
|
|
1145
|
+
| Engine | Relative Performance |
|
|
1146
|
+
|---|---|
|
|
1147
|
+
| **Mutor.js** | **1× (top tier)** |
|
|
1148
|
+
| Eta | ~0.8–0.9× |
|
|
1149
|
+
| EJS | ~0.3× |
|
|
1150
|
+
| Nunjucks | ~0.08× |
|
|
1151
|
+
| Handlebars | ~0.06× |
|
|
1152
|
+
|
|
1153
|
+
Mutor.js is slightly behind Eta on first-compile benchmarks, which is expected — Eta does minimal validation. On the execution-only path (cache warm), Mutor.js leads. That is the path that matters under production load.
|
|
1154
|
+
|
|
1155
|
+
> **Note:** These benchmarks do not account for real-world I/O or middleware overhead. Profile your specific use case.
|
|
1156
|
+
|
|
1157
|
+
---
|
|
1158
|
+
|
|
1159
|
+
## 23. Environment Support
|
|
1160
|
+
|
|
1161
|
+
| Environment | Supported | Notes |
|
|
1162
|
+
|---|---|---|
|
|
1163
|
+
| Node.js 14+ | ✅ | Full support including `renderFile` |
|
|
1164
|
+
| Node.js 18+ (recommended) | ✅ | Best performance |
|
|
1165
|
+
| Browser (modern) | ✅ | Use universal entry point; `renderFile` unavailable |
|
|
1166
|
+
| Deno | ✅ (via npm compat) | `renderFile` available if Deno FS APIs are mapped |
|
|
1167
|
+
| Bun | ✅ | Full support |
|
|
1168
|
+
| ESM | ✅ | Native ESM build available |
|
|
1169
|
+
| CommonJS | ✅ | CJS build available |
|
|
1170
|
+
| TypeScript | ✅ | Type declarations shipped with package |
|
|
1171
|
+
| Minimum requirement | ES6 | Requires `const`, arrow functions, template literals, `Set`, `Map` |
|
|
1172
|
+
|
|
1173
|
+
---
|
|
1174
|
+
|
|
1175
|
+
## 24. Browser Usage
|
|
1176
|
+
|
|
1177
|
+
In browser environments, import from the universal entry point:
|
|
1178
|
+
|
|
1179
|
+
```js
|
|
1180
|
+
import Mutor from "mutorjs";
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
The universal entry point excludes all file-system APIs. The following methods are available: `compile`, `render`, `registerComponent`, `renderComponent`. The `renderFile` method is not available and will throw a `MutorError` if called.
|
|
1184
|
+
|
|
1185
|
+
### Managing Templates in the Browser
|
|
1186
|
+
|
|
1187
|
+
Since file-system access is unavailable, templates must be loaded manually and registered as components:
|
|
1188
|
+
|
|
1189
|
+
```js
|
|
1190
|
+
const templateString = await fetch("/templates/card.html").then(r => r.text());
|
|
1191
|
+
|
|
1192
|
+
mutor.registerComponent("card", templateString);
|
|
1193
|
+
|
|
1194
|
+
const html = mutor.renderComponent("card", { card: data });
|
|
1195
|
+
document.getElementById("app").innerHTML = html;
|
|
1196
|
+
```
|
|
8
1197
|
|
|
9
1198
|
---
|
|
10
1199
|
|
|
11
|
-
##
|
|
1200
|
+
## 25. Server Usage
|
|
1201
|
+
|
|
1202
|
+
Import from the server entry point:
|
|
1203
|
+
|
|
1204
|
+
```js
|
|
1205
|
+
import Mutor from "mutorjs/server";
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
The server entry point includes all functionality from the universal entry point plus `renderFile` and pre-compilation scanning via the `build` configuration.
|
|
1209
|
+
|
|
1210
|
+
### Express Integration
|
|
1211
|
+
|
|
1212
|
+
```js
|
|
1213
|
+
import express from "express";
|
|
1214
|
+
import Mutor from "mutorjs/server";
|
|
1215
|
+
|
|
1216
|
+
const app = express();
|
|
1217
|
+
const mutor = new Mutor();
|
|
1218
|
+
|
|
1219
|
+
app.get("/", async (req, res) => {
|
|
1220
|
+
const html = await mutor.renderFile("./views/home.html", {
|
|
1221
|
+
user: req.user,
|
|
1222
|
+
page: { title: "Home" },
|
|
1223
|
+
});
|
|
1224
|
+
res.send(html);
|
|
1225
|
+
});
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
### Pre-Warming the Cache
|
|
1229
|
+
|
|
1230
|
+
In production, pre-compile all templates during application startup to eliminate compilation latency on first request:
|
|
1231
|
+
|
|
1232
|
+
```js
|
|
1233
|
+
import Mutor from "mutorjs/server";
|
|
1234
|
+
|
|
1235
|
+
const mutor = new Mutor();
|
|
1236
|
+
|
|
1237
|
+
async function warmCache(viewsDir) {
|
|
1238
|
+
const files = await getHtmlFiles(viewsDir); // your file enumeration logic
|
|
1239
|
+
await Promise.all(files.map(f => mutor.renderFile(f, {})));
|
|
1240
|
+
console.log(`Warmed ${files.length} templates.`);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
await warmCache("./views");
|
|
1244
|
+
```
|
|
12
1245
|
|
|
13
|
-
|
|
14
|
-
* **Security by Design**: Completely sandboxed. Forbidden properties (like `__proto__`) are blocked by default. Arbitrary function execution is locked down.
|
|
15
|
-
* **Namespace Operator**: Access built-in JS utilities (Math, JSON, String) safely without polluting the global scope.
|
|
16
|
-
* **Precision Whitespace Control**: Clean up your output without messy regex workarounds.
|
|
17
|
-
* **Developer-Friendly Error Traces**: Because it's AST-based, errors map directly to line and column numbers.
|
|
1246
|
+
Calling `renderFile` with an empty context during startup compiles and caches every template. Subsequent production renders with real context data hit the cache.
|
|
18
1247
|
|
|
19
1248
|
---
|
|
20
1249
|
|
|
21
|
-
##
|
|
1250
|
+
## 26. Advanced Examples
|
|
1251
|
+
|
|
1252
|
+
### Dashboard Template with Nested Data
|
|
1253
|
+
|
|
1254
|
+
```html
|
|
1255
|
+
{{# Project dashboard template #}}
|
|
1256
|
+
<main class="dashboard">
|
|
1257
|
+
<header>
|
|
1258
|
+
<h1>{{ org.name }} — Dashboard</h1>
|
|
1259
|
+
<p>Logged in as {{ user.displayName }}</p>
|
|
1260
|
+
</header>
|
|
22
1261
|
|
|
23
|
-
Mutor handles complex nested logic, data formatting, and whitespace control natively, without requiring arbitrary JS execution.
|
|
24
|
-
```hbs
|
|
25
|
-
{{# Manage a project list with precision logic and security #}}
|
|
26
|
-
<section class="project-board">
|
|
27
1262
|
{{~ if projects.length > 0 ~}}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{{
|
|
34
|
-
|
|
35
|
-
|
|
1263
|
+
<section class="projects">
|
|
1264
|
+
{{ for project of projects }}
|
|
1265
|
+
<article class="project-card">
|
|
1266
|
+
<h2>{{ project.name }}</h2>
|
|
1267
|
+
<p class="budget">
|
|
1268
|
+
Budget: {{ Math::floor(project.budget) }} / {{ Math::floor(project.budgetTotal) }} USD
|
|
1269
|
+
</p>
|
|
1270
|
+
{{ if project.status == "active" }}
|
|
1271
|
+
<span class="badge badge--active">Active</span>
|
|
1272
|
+
{{ else if project.status == "review" }}
|
|
1273
|
+
<span class="badge badge--review">In Review</span>
|
|
1274
|
+
{{ else }}
|
|
1275
|
+
<span class="badge badge--inactive">Inactive</span>
|
|
1276
|
+
{{ end }}
|
|
1277
|
+
<ul class="tags">
|
|
1278
|
+
{{ for tag of project.tags }}
|
|
1279
|
+
<li class="tag">{{ tag }}</li>
|
|
36
1280
|
{{ end }}
|
|
37
|
-
</
|
|
1281
|
+
</ul>
|
|
1282
|
+
</article>
|
|
38
1283
|
{{ end }}
|
|
1284
|
+
</section>
|
|
39
1285
|
{{~ else ~}}
|
|
40
|
-
|
|
1286
|
+
<p class="empty-state">No projects found.</p>
|
|
41
1287
|
{{~ end ~}}
|
|
42
|
-
</
|
|
1288
|
+
</main>
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
### Component with Namespace Utilities
|
|
1292
|
+
|
|
1293
|
+
```js
|
|
1294
|
+
mutor.registerComponent("data-table", `
|
|
1295
|
+
<table>
|
|
1296
|
+
<thead>
|
|
1297
|
+
<tr>
|
|
1298
|
+
{{ for col of table.columns }}
|
|
1299
|
+
<th>{{ col.label }}</th>
|
|
1300
|
+
{{ end }}
|
|
1301
|
+
</tr>
|
|
1302
|
+
</thead>
|
|
1303
|
+
<tbody>
|
|
1304
|
+
{{ for row of table.rows }}
|
|
1305
|
+
<tr>
|
|
1306
|
+
{{ for col of table.columns }}
|
|
1307
|
+
<td>{{ row[col.key] ?? "—" }}</td>
|
|
1308
|
+
{{ end }}
|
|
1309
|
+
</tr>
|
|
1310
|
+
{{ end }}
|
|
1311
|
+
</tbody>
|
|
1312
|
+
<tfoot>
|
|
1313
|
+
<tr>
|
|
1314
|
+
<td colspan="{{ table.columns.length }}">
|
|
1315
|
+
{{ table.rows.length }} {{ table.rows.length == 1 ? "record" : "records" }}
|
|
1316
|
+
</td>
|
|
1317
|
+
</tr>
|
|
1318
|
+
</tfoot>
|
|
1319
|
+
</table>
|
|
1320
|
+
`);
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
### Include with Explicit Context Forwarding
|
|
1324
|
+
|
|
1325
|
+
```html
|
|
1326
|
+
{{# views/layout.html #}}
|
|
1327
|
+
<!DOCTYPE html>
|
|
1328
|
+
<html lang="en">
|
|
1329
|
+
<head>
|
|
1330
|
+
<title>{{ page.title }} — {{ site.name }}</title>
|
|
1331
|
+
{{ Mutor::include("./partials/head-meta.html", Mutor::$$context) }}
|
|
1332
|
+
</head>
|
|
1333
|
+
<body>
|
|
1334
|
+
{{ Mutor::include("./partials/nav.html", { nav: nav, user: user }) }}
|
|
1335
|
+
<main>
|
|
1336
|
+
{{ Mutor::include(page.template, Mutor::$$context) }}
|
|
1337
|
+
</main>
|
|
1338
|
+
{{ Mutor::include("./partials/footer.html", { site: site }) }}
|
|
1339
|
+
</body>
|
|
1340
|
+
</html>
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
### Pre-compilation with Custom Forbidden Properties
|
|
1344
|
+
|
|
1345
|
+
```js
|
|
1346
|
+
import Mutor from "mutorjs/server";
|
|
1347
|
+
|
|
1348
|
+
const mutor = new Mutor({
|
|
1349
|
+
forbiddenProps: new Set([
|
|
1350
|
+
"__proto__",
|
|
1351
|
+
"constructor",
|
|
1352
|
+
"prototype",
|
|
1353
|
+
"valueOf",
|
|
1354
|
+
"toString",
|
|
1355
|
+
"hasOwnProperty",
|
|
1356
|
+
]),
|
|
1357
|
+
allowFnCalls: false,
|
|
1358
|
+
cache: {
|
|
1359
|
+
active: true,
|
|
1360
|
+
maxSize: 100 * 1024 * 1024,
|
|
1361
|
+
},
|
|
1362
|
+
});
|
|
43
1363
|
```
|
|
44
1364
|
|
|
45
1365
|
---
|
|
46
1366
|
|
|
47
|
-
##
|
|
1367
|
+
## 27. Best Practices
|
|
1368
|
+
|
|
1369
|
+
**Keep templates thin.** Templates should handle layout and presentation. Business logic, data transformations, filtering, and sorting belong in the context preparation layer, not the template.
|
|
1370
|
+
|
|
1371
|
+
**Pre-warm the cache in production.** Use the startup cache warming pattern to eliminate first-request compilation latency. Templates should be compiled before traffic arrives.
|
|
1372
|
+
|
|
1373
|
+
**Use a single shared `Mutor` instance per process.** Cache efficiency is maximized when all requests share the same compiled template cache. Use separate instances only when you need genuine isolation (e.g., per-tenant configuration differences).
|
|
1374
|
+
|
|
1375
|
+
**Pass minimal context.** Only expose in the context what the template actually needs. Avoid passing entire service objects, database clients, or request objects as context.
|
|
1376
|
+
|
|
1377
|
+
**Validate context data before passing it in.** Mutor.js validates the context's prototype chain but does not validate the semantic meaning of your data. Validate user input before it enters the render context.
|
|
1378
|
+
|
|
1379
|
+
**Use `for...of` for arrays, `for...in` for objects.** This mirrors JavaScript semantics and makes templates readable to any JavaScript developer.
|
|
1380
|
+
|
|
1381
|
+
**Name includes and components descriptively.** Component names and include paths are the template's interface contract. `renderComponent("card")` is readable; `renderComponent("c1")` is not.
|
|
1382
|
+
|
|
1383
|
+
**Test your templates.** Templates are code. Render them with fixture contexts in your test suite to catch regressions in output structure.
|
|
1384
|
+
|
|
1385
|
+
---
|
|
1386
|
+
|
|
1387
|
+
## 28. Security Best Practices
|
|
1388
|
+
|
|
1389
|
+
**Never disable `autoEscape` when rendering user-controlled data.** If you must render raw HTML from a trusted source, consider using a separate Mutor instance with `autoEscape: false` specifically for that purpose, so the risk is isolated.
|
|
1390
|
+
|
|
1391
|
+
**Never pass `req`, `res`, database clients, or service objects as context.** These objects have methods and properties that, with `allowFnCalls: true`, could be invoked from a template. Prepare a clean, minimal context object explicitly.
|
|
1392
|
+
|
|
1393
|
+
**Enable `allowFnCalls` only when necessary and with a clear scope.** If you need method calls on specific types (e.g., string formatting), consider the planned custom namespace feature instead.
|
|
1394
|
+
|
|
1395
|
+
**Extend `forbiddenProps` conservatively.** Add property names you know are dangerous in your specific context. `valueOf` and `toString` are candidates in many applications.
|
|
48
1396
|
|
|
49
|
-
|
|
1397
|
+
**Treat `MutorSecurityError` as a hard stop.** If a security error is thrown during compilation, do not attempt to render the template. Log it, alert on it, and investigate.
|
|
50
1398
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
4. **Execute**: The compiled function runs within a restricted scope, safely interpolating your context.
|
|
1399
|
+
**Do not use `cache: { active: false }` in production.** Beyond the performance cost, a disabled cache means every render triggers recompilation — and recompilation triggers full security analysis on every request, which while correct, is unnecessary overhead.
|
|
1400
|
+
|
|
1401
|
+
**Audit templates that come from external sources.** If your application allows user-defined templates to be compiled, review them with the same scrutiny as user-provided code. Mutor.js's sandbox is strong, but defense in depth applies here too.
|
|
55
1402
|
|
|
56
1403
|
---
|
|
57
1404
|
|
|
58
|
-
##
|
|
1405
|
+
## 29. Real-World Use Cases
|
|
1406
|
+
|
|
1407
|
+
### Server-Side HTML Rendering (Node.js)
|
|
1408
|
+
|
|
1409
|
+
The primary use case. A web application that serves HTML pages from templates. Mutor.js compiles the view templates at startup, and each request invokes the compiled functions with a prepared context.
|
|
1410
|
+
|
|
1411
|
+
### Email Template Rendering
|
|
1412
|
+
|
|
1413
|
+
Email templates have similar requirements to HTML templates: variable interpolation, conditional sections (e.g., showing a section only if a promo code is attached), iteration over lists (e.g., order items). Mutor.js's auto-escaping is important for preventing XSS in email clients that render HTML, and the predictable output makes it easy to test email rendering in CI.
|
|
1414
|
+
|
|
1415
|
+
### Static Site Generation
|
|
1416
|
+
|
|
1417
|
+
A static site generator can use Mutor.js to compile page templates and render them with content sourced from a CMS, markdown files, or a database. The CLI's `build` command makes this straightforward for file-based workflows. The compilation cache ensures that re-builds are fast: only changed templates need recompilation.
|
|
1418
|
+
|
|
1419
|
+
### Configuration File Generation
|
|
59
1420
|
|
|
60
|
-
|
|
61
|
-
Traditional engines either block all JS helpers or give templates full access to your server's global object. Mutor’s Namespace Operator solves this elegantly. Safely tap into built-ins directly in the template:
|
|
62
|
-
* `{{ Math::floor(price) }}`
|
|
63
|
-
* `{{ JSON::stringify(data) }}`
|
|
64
|
-
* `{{ Array::isArray(items) }}`
|
|
1421
|
+
Mutor.js can render structured text formats like JSON, YAML, or NGINX configuration files, not just HTML. Disable `autoEscape` for non-HTML output and use the template language to generate configuration files with environment-specific values.
|
|
65
1422
|
|
|
66
|
-
###
|
|
67
|
-
Use the configurable whitespace directive (`~` by default) exactly where you need it:
|
|
68
|
-
* `{{~ name }}` (Trim left)
|
|
69
|
-
* `{{ name ~}}` (Trim right)
|
|
70
|
-
* `{{~ name ~}}` (Trim both sides)
|
|
1423
|
+
### Report Generation
|
|
71
1424
|
|
|
72
|
-
|
|
73
|
-
Don't like `{{` and `}}`? Change them. Mutor allows you to completely redefine the syntax delimiters (e.g., `<%=` and `%>`) via the configuration object.
|
|
1425
|
+
Server-side report generation that produces HTML output for PDF conversion. Mutor.js's deterministic, secure rendering is well-suited for environments where the template must process potentially large and complex data structures into formatted output.
|
|
74
1426
|
|
|
75
1427
|
---
|
|
76
1428
|
|
|
77
|
-
##
|
|
1429
|
+
## 30. Design Tradeoffs
|
|
78
1430
|
|
|
79
|
-
|
|
1431
|
+
### Compilation Cost for Execution Speed
|
|
80
1432
|
|
|
81
|
-
|
|
82
|
-
| :--- | :--- | :--- | :--- | :--- |
|
|
83
|
-
| **Full Pipeline (Compile + Exec)** | **2nd Overall** | 1st | Distant 3rd | Slower |
|
|
84
|
-
| **Raw Execution (Compiled)** | **Top Tier (1.2x - 5x lead)** | Competitive | Significantly Slower | Significantly Slower |
|
|
1433
|
+
Mutor.js is slower to compile than Eta because it does substantially more work during compilation. This is acceptable because the compiled output executes faster, and compilation happens once per template per process lifetime.
|
|
85
1434
|
|
|
86
|
-
|
|
1435
|
+
**Impact:** Mutor.js is not ideal for environments where templates change on every request and caching is disabled. It excels in environments with a stable set of templates and many requests.
|
|
1436
|
+
|
|
1437
|
+
### Bounded Template Language for Security
|
|
1438
|
+
|
|
1439
|
+
Mutor.js does not allow arbitrary JavaScript in templates. You cannot write a `while` loop, a `try/catch`, a variable declaration, or an IIFE. This is a hard constraint from the security architecture.
|
|
1440
|
+
|
|
1441
|
+
**Impact:** Complex logic must live in the context object, not the template. This is the correct design for large applications — templates should be thin views, not business logic containers — but it requires a different authoring mindset.
|
|
1442
|
+
|
|
1443
|
+
### Zero Dependencies for Auditability
|
|
1444
|
+
|
|
1445
|
+
Mutor.js has no third-party dependencies. Every dependency in a security-critical system is a potential attack surface. Mutor.js owns its entire stack, which makes it fully auditable and immune to supply chain attacks through compromised dependencies.
|
|
1446
|
+
|
|
1447
|
+
**Impact:** Mutor.js does not benefit from improvements in third-party parsing libraries or utility packages. Every capability must be built and maintained internally.
|
|
87
1448
|
|
|
88
1449
|
---
|
|
89
1450
|
|
|
90
|
-
##
|
|
1451
|
+
## 31. Future Roadmap
|
|
91
1452
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
1453
|
+
The following features are planned or under active development and are not available in the current stable release.
|
|
1454
|
+
|
|
1455
|
+
### Template Source Maps *(Planned)*
|
|
1456
|
+
|
|
1457
|
+
Source map generation so that runtime errors in compiled functions can be mapped back to their originating template locations — the natural next step from the position tracking already in the tokenizer.
|
|
1458
|
+
|
|
1459
|
+
### Extended Namespace Registry *(Planned)*
|
|
1460
|
+
|
|
1461
|
+
An API to register custom namespaces:
|
|
1462
|
+
|
|
1463
|
+
```js
|
|
1464
|
+
mutor.registerNamespace("Utils", {
|
|
1465
|
+
formatCurrency: (n) => `$${n.toFixed(2)}`,
|
|
1466
|
+
slugify: (s) => s.toLowerCase().replace(/\s+/g, "-"),
|
|
1467
|
+
});
|
|
1468
|
+
```
|
|
1469
|
+
|
|
1470
|
+
This would allow project-specific utilities to be exposed into templates via the controlled namespace mechanism rather than requiring `allowFnCalls: true`.
|
|
1471
|
+
|
|
1472
|
+
### Async Template Rendering *(Under Consideration)*
|
|
1473
|
+
|
|
1474
|
+
Support for templates that include asynchronous expressions — for example, inline data fetching or async namespace methods. Architecturally complex and not yet committed.
|
|
1475
|
+
|
|
1476
|
+
### Template Language Server Protocol Support *(Future)*
|
|
1477
|
+
|
|
1478
|
+
An LSP implementation for Mutor.js template syntax, enabling editor tooling: syntax highlighting, error diagnostics, completion, and hover docs.
|
|
1479
|
+
|
|
1480
|
+
### Compiler Plugin API *(Future)*
|
|
1481
|
+
|
|
1482
|
+
An API for hooking into the compilation pipeline at defined extension points (post-parse, post-analyze, post-generate), enabling custom AST transformations, custom security policies, and code generation optimizations without forking the core.
|
|
1483
|
+
|
|
1484
|
+
---
|
|
1485
|
+
|
|
1486
|
+
## 32. FAQ
|
|
1487
|
+
|
|
1488
|
+
**Q: Is Mutor.js production-ready?**
|
|
1489
|
+
|
|
1490
|
+
Yes. The core engine — rendering, compilation, caching, security, components, includes — is stable and production-ready. The CLI is also available as of the current release.
|
|
1491
|
+
|
|
1492
|
+
**Q: Can I use Mutor.js with Express/Fastify/Koa?**
|
|
1493
|
+
|
|
1494
|
+
Yes. Mutor.js is a rendering function, not a framework. Any server framework that lets you produce a string response can use it. See §25 Server Usage for an Express example.
|
|
1495
|
+
|
|
1496
|
+
**Q: Why is function calling disabled by default?**
|
|
1497
|
+
|
|
1498
|
+
Templates that invoke arbitrary functions on context objects are harder to reason about and audit than templates that only read data. Disabling function calls by default makes the common case — read-only data rendering — safe without configuration, and forces an explicit opt-in for the more powerful (and potentially more risky) capability.
|
|
1499
|
+
|
|
1500
|
+
**Q: Can I extend the namespace system with my own namespaces?**
|
|
1501
|
+
|
|
1502
|
+
Not in the current version. This is planned as a future feature. For now, all logic that requires custom functions should be called in the context preparation layer and the result passed into the context.
|
|
1503
|
+
|
|
1504
|
+
**Q: Why is template interpolation inside backtick strings not supported?**
|
|
1505
|
+
|
|
1506
|
+
Backtick strings in Mutor.js are static strings, not JavaScript template literals. Supporting `${...}` inside strings would require a nested expression parser inside the string tokenizer, significantly increasing complexity. The use case is better handled outside the string: `prefix + value + suffix` as a concatenation expression.
|
|
1507
|
+
|
|
1508
|
+
**Q: How does Mutor.js handle `null` and `undefined` in interpolation?**
|
|
1509
|
+
|
|
1510
|
+
By default, `null` and `undefined` values render as empty strings. This prevents `undefined` from appearing literally in output. Use the nullish coalescing operator (`??`) to provide explicit defaults when you need them.
|
|
1511
|
+
|
|
1512
|
+
**Q: Can two Mutor instances share a cache?**
|
|
1513
|
+
|
|
1514
|
+
No. Each instance has an isolated cache. If you want cache sharing, use a single instance.
|
|
1515
|
+
|
|
1516
|
+
**Q: Is there a way to render without escaping for a specific tag?**
|
|
1517
|
+
|
|
1518
|
+
In the current version, auto-escaping is a global configuration setting, not per-tag. To render a raw value, either use `autoEscape: false` on the instance, sanitize the value before it enters the context, or use a separate instance configured without auto-escaping.
|
|
1519
|
+
|
|
1520
|
+
---
|
|
1521
|
+
|
|
1522
|
+
## 33. API Reference
|
|
1523
|
+
|
|
1524
|
+
### `new Mutor(config?: MutorConfig)`
|
|
1525
|
+
|
|
1526
|
+
Creates a new Mutor instance with the given configuration merged over the defaults.
|
|
1527
|
+
|
|
1528
|
+
```ts
|
|
1529
|
+
const mutor = new Mutor({
|
|
99
1530
|
autoEscape: true,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
active: true,
|
|
112
|
-
maxSize: 50 * 1024 * 1024, // 50MB
|
|
113
|
-
},
|
|
114
|
-
};
|
|
1531
|
+
allowFnCalls: false,
|
|
1532
|
+
cache: { active: true, maxSize: 50 * 1024 * 1024 },
|
|
1533
|
+
});
|
|
1534
|
+
```
|
|
1535
|
+
|
|
1536
|
+
---
|
|
1537
|
+
|
|
1538
|
+
### `mutor.compile(template: string): CompiledTemplate`
|
|
1539
|
+
|
|
1540
|
+
```ts
|
|
1541
|
+
type CompiledTemplate = (context: Record<string, unknown>) => string;
|
|
115
1542
|
```
|
|
116
1543
|
|
|
1544
|
+
Compiles a template string and returns the compiled function. The compilation result is stored in the instance cache.
|
|
1545
|
+
|
|
1546
|
+
**Throws:**
|
|
1547
|
+
- `MutorParseError` — if the template contains a syntax error
|
|
1548
|
+
- `MutorSecurityError` — if the template contains a forbidden construct
|
|
1549
|
+
- `MutorCompileError` — if code generation fails
|
|
1550
|
+
|
|
117
1551
|
---
|
|
118
1552
|
|
|
119
|
-
|
|
1553
|
+
### `mutor.render(template: string, context: Record<string, unknown>): string`
|
|
120
1554
|
|
|
121
|
-
|
|
122
|
-
* You are rendering user-generated content and cannot risk XSS or prototype pollution.
|
|
123
|
-
* You need near-native execution speed for high-traffic applications.
|
|
124
|
-
* You want clean, debuggable templates that fail with exact line/column numbers.
|
|
125
|
-
* You need to pre-compile entire view directories ahead of time.
|
|
1555
|
+
Compiles (or retrieves from cache) and immediately renders a template with the given context.
|
|
126
1556
|
|
|
127
|
-
**
|
|
128
|
-
* You require the ability to write complex, arbitrary JavaScript directly inside your HTML. Mutor's sandbox will block this.
|
|
129
|
-
* You only need simple string replacement for a tiny script where initializing an AST engine is overkill.
|
|
1557
|
+
**Throws:** Same as `compile`, plus `MutorRuntimeError` for runtime context validation failures.
|
|
130
1558
|
|
|
131
1559
|
---
|
|
132
1560
|
|
|
133
|
-
|
|
1561
|
+
### `mutor.renderFile(path: string, context: Record<string, unknown>): Promise<string>`
|
|
134
1562
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
1563
|
+
*Server entry point only (`mutorjs/server`).*
|
|
1564
|
+
|
|
1565
|
+
Reads, compiles, and renders a template file. The resolved absolute path is used as the cache key.
|
|
1566
|
+
|
|
1567
|
+
**Throws:** Same as `render`, plus `MutorIncludeError` if the file cannot be read.
|
|
1568
|
+
|
|
1569
|
+
---
|
|
1570
|
+
|
|
1571
|
+
### `mutor.registerComponent(name: string, template: string): void`
|
|
1572
|
+
|
|
1573
|
+
Registers a named component template. Compilation is deferred to first render.
|
|
1574
|
+
|
|
1575
|
+
**Throws:** `MutorError` if `name` is empty or if `template` is not a string.
|
|
1576
|
+
|
|
1577
|
+
---
|
|
1578
|
+
|
|
1579
|
+
### `mutor.renderComponent(name: string, context: Record<string, unknown>): string`
|
|
1580
|
+
|
|
1581
|
+
Renders a registered component by name.
|
|
1582
|
+
|
|
1583
|
+
**Throws:** `MutorComponentError` if the component has not been registered. Same compile/runtime errors as `render` on first call.
|
|
1584
|
+
|
|
1585
|
+
---
|
|
1586
|
+
|
|
1587
|
+
### `mutor.clearCache(): void`
|
|
1588
|
+
|
|
1589
|
+
Removes all entries from the instance's compilation cache.
|
|
1590
|
+
|
|
1591
|
+
---
|
|
1592
|
+
|
|
1593
|
+
### `mutor.getCacheSize(): number`
|
|
1594
|
+
|
|
1595
|
+
Returns the estimated current memory usage of the cache in bytes.
|
|
1596
|
+
|
|
1597
|
+
---
|
|
1598
|
+
|
|
1599
|
+
### `mutor.getCacheEntryCount(): number`
|
|
1600
|
+
|
|
1601
|
+
Returns the number of compiled templates currently stored in the cache.
|
|
1602
|
+
|
|
1603
|
+
---
|
|
1604
|
+
|
|
1605
|
+
### `mutor.addConfig(config: Partial<MutorConfig>): void`
|
|
1606
|
+
|
|
1607
|
+
Merges additional configuration into the instance after construction. Useful when configuration is loaded asynchronously (e.g., from a file) after the instance is created.
|
|
1608
|
+
|
|
1609
|
+
---
|
|
1610
|
+
|
|
1611
|
+
### Error Classes
|
|
1612
|
+
|
|
1613
|
+
```ts
|
|
1614
|
+
import {
|
|
1615
|
+
MutorError,
|
|
1616
|
+
MutorParseError,
|
|
1617
|
+
MutorSecurityError,
|
|
1618
|
+
MutorCompileError,
|
|
1619
|
+
MutorRuntimeError,
|
|
1620
|
+
MutorIncludeError,
|
|
1621
|
+
MutorComponentError,
|
|
1622
|
+
} from "mutorjs";
|
|
1623
|
+
```
|
|
1624
|
+
|
|
1625
|
+
All error classes extend `MutorError extends Error` and carry:
|
|
1626
|
+
|
|
1627
|
+
| Property | Type | Description |
|
|
1628
|
+
|---|---|---|
|
|
1629
|
+
| `code` | `string` | Machine-readable error code |
|
|
1630
|
+
| `message` | `string` | Human-readable description |
|
|
1631
|
+
| `source` | `string \| undefined` | Originating template source (truncated) |
|
|
1632
|
+
| `location` | `SourceLocation \| undefined` | `{ line, column, offset }` |
|
|
1633
|
+
|
|
1634
|
+
---
|
|
1635
|
+
|
|
1636
|
+
### `MutorConfig` Type Reference
|
|
1637
|
+
|
|
1638
|
+
```ts
|
|
1639
|
+
interface MutorConfig {
|
|
1640
|
+
build?: {
|
|
1641
|
+
include?: Set<string>;
|
|
1642
|
+
exclude?: Set<string>;
|
|
1643
|
+
};
|
|
1644
|
+
autoEscape?: boolean;
|
|
1645
|
+
allowedProps?: Set<string>;
|
|
1646
|
+
forbiddenProps?: Set<string>;
|
|
1647
|
+
allowFnCalls?: boolean;
|
|
1648
|
+
delimiters?: {
|
|
1649
|
+
openingTag?: string;
|
|
1650
|
+
closingTag?: string;
|
|
1651
|
+
openingTagEscape?: string;
|
|
1652
|
+
whitespaceTrim?: string;
|
|
1653
|
+
};
|
|
1654
|
+
keepOpeningTagEscapeDelimiter?: boolean;
|
|
1655
|
+
cache?: {
|
|
1656
|
+
active?: boolean;
|
|
1657
|
+
maxSize?: number;
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
140
1660
|
```
|
|
1661
|
+
|
|
1662
|
+
---
|
|
1663
|
+
|
|
1664
|
+
*Mutor.js — engineered for correctness, security, and speed.*
|
|
1665
|
+
*Built by Onah Victor. Contributions welcome under the project's guidelines.*
|