mutorjs 1.5.9 → 2.0.0
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/README.md +532 -1336
- package/dist/cli.cjs +16 -16
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +11 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -31
- package/dist/index.d.ts +22 -31
- package/dist/index.js +12 -12
- package/dist/index.js.map +1 -1
- package/dist/mutor.global.js +11 -11
- package/dist/mutor.global.js.map +1 -1
- package/dist/server.cjs +12 -12
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +23 -31
- package/dist/server.d.ts +23 -31
- package/dist/server.js +12 -12
- package/dist/server.js.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,1665 +1,861 @@
|
|
|
1
|
-
# Mutor.js
|
|
2
|
-
|
|
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
|
|
8
|
-
|
|
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
|
-
```
|
|
1
|
+
# Mutor.js
|
|
87
2
|
|
|
88
|
-
|
|
3
|
+
Mutor.js is a small, fast templating engine for people who want templates to be expressive without turning them into a second application runtime.
|
|
89
4
|
|
|
90
|
-
|
|
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
|
-
```
|
|
5
|
+
It gives you interpolation, conditionals, loops, partials, file rendering, async values, escaping, and a guarded expression system. It does not ship layout inheritance on purpose. In Mutor, complex pages are built from partials/components and the right context.
|
|
106
6
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
## 3. Quick Start
|
|
110
|
-
|
|
111
|
-
### Basic Render
|
|
112
|
-
|
|
113
|
-
```js
|
|
7
|
+
```ts
|
|
114
8
|
import Mutor from "mutorjs";
|
|
115
9
|
|
|
116
10
|
const mutor = new Mutor();
|
|
117
11
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
);
|
|
12
|
+
const html = mutor.render("Hello, {{ user.name }}.", {
|
|
13
|
+
user: { name: "Ada" },
|
|
14
|
+
});
|
|
122
15
|
|
|
123
|
-
console.log(
|
|
124
|
-
// <h1>Hello, Onah!</h1>
|
|
16
|
+
console.log(html); // Hello, Ada.
|
|
125
17
|
```
|
|
126
18
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
```js
|
|
130
|
-
import Mutor from "mutorjs";
|
|
131
|
-
|
|
132
|
-
const mutor = new Mutor();
|
|
133
|
-
|
|
134
|
-
const fn = mutor.compile(`<p>{{ item.title }}</p>`);
|
|
19
|
+
## Install
|
|
135
20
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
{ title: "Beta" },
|
|
139
|
-
{ title: "Gamma" },
|
|
140
|
-
];
|
|
141
|
-
|
|
142
|
-
const rendered = items.map(item => fn({ item }));
|
|
21
|
+
```sh
|
|
22
|
+
npm install mutorjs
|
|
143
23
|
```
|
|
144
24
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
```
|
|
148
|
-
import Mutor from "mutorjs/server";
|
|
25
|
+
```sh
|
|
26
|
+
yarn add mutorjs
|
|
27
|
+
```
|
|
149
28
|
|
|
150
|
-
|
|
29
|
+
```sh
|
|
30
|
+
pnpm add mutorjs
|
|
31
|
+
```
|
|
151
32
|
|
|
152
|
-
|
|
153
|
-
title: "Dashboard",
|
|
154
|
-
user: req.user,
|
|
155
|
-
});
|
|
33
|
+
## Why Mutor
|
|
156
34
|
|
|
157
|
-
|
|
158
|
-
|
|
35
|
+
- Small template language with familiar JavaScript-like expressions.
|
|
36
|
+
- HTML escaping is on by default.
|
|
37
|
+
- Dangerous properties such as `constructor`, `prototype`, and `__proto__` are blocked.
|
|
38
|
+
- Function calls are restricted by default.
|
|
39
|
+
- Partials/components are first-class.
|
|
40
|
+
- Server rendering supports file includes and directory builds.
|
|
41
|
+
- Async values work through `Mutor::await`.
|
|
42
|
+
- Cache entries can be inspected, reset, or invalidated.
|
|
159
43
|
|
|
160
|
-
|
|
44
|
+
## Quick Start
|
|
161
45
|
|
|
162
|
-
```
|
|
46
|
+
```ts
|
|
163
47
|
import Mutor from "mutorjs";
|
|
164
48
|
|
|
165
49
|
const mutor = new Mutor();
|
|
166
50
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
</
|
|
172
|
-
|
|
51
|
+
const template = `
|
|
52
|
+
{{ if user.admin }}
|
|
53
|
+
<strong>{{ user.name }}</strong>
|
|
54
|
+
{{ else }}
|
|
55
|
+
<span>{{ user.name }}</span>
|
|
56
|
+
{{ endif }}
|
|
57
|
+
`;
|
|
173
58
|
|
|
174
|
-
const
|
|
175
|
-
|
|
59
|
+
const html = mutor.render(template, {
|
|
60
|
+
user: { name: "Grace", admin: true },
|
|
176
61
|
});
|
|
177
62
|
```
|
|
178
63
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
## 4. Core Concepts
|
|
64
|
+
By default, strings are escaped before they are written:
|
|
182
65
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
66
|
+
```ts
|
|
67
|
+
mutor.render("{{ value }}", {
|
|
68
|
+
value: "<script>alert('nope')</script>",
|
|
69
|
+
});
|
|
70
|
+
// <script>alert('nope')</script>
|
|
71
|
+
```
|
|
188
72
|
|
|
189
|
-
|
|
73
|
+
Disable escaping only when you know the output is already safe:
|
|
190
74
|
|
|
191
|
-
|
|
75
|
+
```ts
|
|
76
|
+
const mutor = new Mutor({ autoEscape: false });
|
|
77
|
+
```
|
|
192
78
|
|
|
193
|
-
|
|
79
|
+
## Template Syntax
|
|
194
80
|
|
|
195
|
-
|
|
81
|
+
Mutor expressions live inside `{{ ... }}`.
|
|
196
82
|
|
|
197
|
-
|
|
83
|
+
```html
|
|
84
|
+
<h1>{{ title }}</h1>
|
|
85
|
+
```
|
|
198
86
|
|
|
199
|
-
###
|
|
87
|
+
### Comments
|
|
200
88
|
|
|
201
|
-
|
|
89
|
+
Comments are removed from the rendered output.
|
|
202
90
|
|
|
203
|
-
|
|
91
|
+
```html
|
|
92
|
+
{{# This will not render }}
|
|
93
|
+
```
|
|
204
94
|
|
|
205
|
-
|
|
95
|
+
### Whitespace Control
|
|
206
96
|
|
|
207
|
-
|
|
97
|
+
Use `~` next to a tag to trim whitespace on that side.
|
|
208
98
|
|
|
209
|
-
|
|
99
|
+
```html
|
|
100
|
+
Hello {{~ name ~}} !
|
|
101
|
+
```
|
|
210
102
|
|
|
211
|
-
|
|
103
|
+
With `name = "Ada"`, that renders:
|
|
212
104
|
|
|
213
105
|
```html
|
|
214
|
-
|
|
106
|
+
HelloAda!
|
|
215
107
|
```
|
|
216
108
|
|
|
217
|
-
|
|
109
|
+
### Escaped Tags
|
|
110
|
+
|
|
111
|
+
Prefix an opening tag with the escape delimiter when you want the tag to appear as text.
|
|
218
112
|
|
|
219
113
|
```html
|
|
220
|
-
|
|
221
|
-
<p>{{ product.price * 1.1 }}</p>
|
|
222
|
-
<p>{{ user.bio ?? "No bio provided." }}</p>
|
|
114
|
+
\{{ name }}
|
|
223
115
|
```
|
|
224
116
|
|
|
225
|
-
|
|
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.
|
|
117
|
+
That renders:
|
|
228
118
|
|
|
229
119
|
```html
|
|
230
|
-
{{
|
|
231
|
-
{{ expression ~}} <!-- trim right whitespace -->
|
|
232
|
-
{{~ expression ~}} <!-- trim both sides -->
|
|
120
|
+
{{ name }}
|
|
233
121
|
```
|
|
234
122
|
|
|
235
|
-
|
|
123
|
+
If `preserveEscapeDelimiter` is enabled, the escape delimiter is kept too.
|
|
236
124
|
|
|
237
|
-
|
|
125
|
+
## Values And Literals
|
|
238
126
|
|
|
239
|
-
|
|
127
|
+
Mutor supports simple literals:
|
|
240
128
|
|
|
241
129
|
```html
|
|
242
|
-
{{
|
|
130
|
+
{{ "hello" }}
|
|
131
|
+
{{ 'hello' }}
|
|
132
|
+
{{ `hello` }}
|
|
133
|
+
{{ 42 }}
|
|
134
|
+
{{ 3.14 }}
|
|
135
|
+
{{ 1e-3 }}
|
|
136
|
+
{{ true }}
|
|
137
|
+
{{ false }}
|
|
138
|
+
{{ null }}
|
|
139
|
+
{{ undefined }}
|
|
243
140
|
```
|
|
244
141
|
|
|
245
|
-
|
|
142
|
+
Mutor does not allow JavaScript object literals, array literals, function literals, arrow functions, or constructors inside templates:
|
|
246
143
|
|
|
247
144
|
```html
|
|
248
|
-
{{
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}}
|
|
145
|
+
{{ { name: "Ada" } }} <!-- not allowed -->
|
|
146
|
+
{{ [1, 2, 3] }} <!-- not allowed -->
|
|
147
|
+
{{ function() {} }} <!-- not allowed -->
|
|
148
|
+
{{ () => {} }} <!-- not allowed -->
|
|
149
|
+
{{ new User() }} <!-- not allowed -->
|
|
253
150
|
```
|
|
254
151
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
To output the literal opening delimiter (e.g., `{{`) without triggering template parsing, prefix it with a backslash:
|
|
152
|
+
When you need an array or object value in a template expression, pass it in the context or create it from JSON:
|
|
258
153
|
|
|
259
154
|
```html
|
|
260
|
-
|
|
155
|
+
{{ JSON::parse("[1,2,3]")[0] }}
|
|
156
|
+
{{ JSON::parse('{"name":"Ada"}').name }}
|
|
261
157
|
```
|
|
262
158
|
|
|
263
|
-
|
|
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 |
|
|
159
|
+
Passing values through context is usually cleaner:
|
|
281
160
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
161
|
+
```ts
|
|
162
|
+
mutor.render("{{ user.name }}", {
|
|
163
|
+
user: { name: "Ada" },
|
|
164
|
+
});
|
|
165
|
+
```
|
|
285
166
|
|
|
286
|
-
|
|
167
|
+
## Expressions
|
|
287
168
|
|
|
288
|
-
|
|
169
|
+
Mutor expressions are intentionally familiar:
|
|
289
170
|
|
|
290
171
|
```html
|
|
291
|
-
{{
|
|
292
|
-
|
|
293
|
-
{{
|
|
294
|
-
|
|
172
|
+
{{ user.name }}
|
|
173
|
+
{{ user?.profile?.name }}
|
|
174
|
+
{{ user["name"] }}
|
|
175
|
+
{{ count + 1 }}
|
|
176
|
+
{{ price * quantity }}
|
|
177
|
+
{{ score >= 80 }}
|
|
178
|
+
{{ admin && active }}
|
|
179
|
+
{{ name ?? "Anonymous" }}
|
|
180
|
+
{{ admin ? "Admin" : "Member" }}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Supported expression pieces include:
|
|
184
|
+
|
|
185
|
+
- property access with `.` and `[]`
|
|
186
|
+
- optional chaining with `?.`
|
|
187
|
+
- arithmetic operators such as `+`, `-`, `*`, `/`, `%`, `**`
|
|
188
|
+
- comparison operators such as `>`, `<`, `>=`, `<=`
|
|
189
|
+
- equality operators `==` and `!=`
|
|
190
|
+
- logical operators `&&`, `||`, `??`
|
|
191
|
+
- bitwise operators `&`, `|`, `^`, `>>`, `<<`
|
|
192
|
+
- unary operators `!`, `+`, `-`
|
|
193
|
+
- ternaries with `condition ? yes : no`
|
|
194
|
+
- grouping with parentheses
|
|
195
|
+
|
|
196
|
+
## Conditionals
|
|
295
197
|
|
|
296
198
|
```html
|
|
297
|
-
{{ if user.
|
|
298
|
-
<
|
|
299
|
-
{{ else if user.
|
|
300
|
-
<
|
|
199
|
+
{{ if user.admin }}
|
|
200
|
+
<strong>{{ user.name }}</strong>
|
|
201
|
+
{{ else if user.active }}
|
|
202
|
+
<span>{{ user.name }}</span>
|
|
301
203
|
{{ else }}
|
|
302
|
-
<
|
|
303
|
-
{{
|
|
204
|
+
<em>Inactive user</em>
|
|
205
|
+
{{ endif }}
|
|
304
206
|
```
|
|
305
207
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
```html
|
|
309
|
-
{{ if items.length > 0 && user.isActive }}
|
|
310
|
-
...
|
|
311
|
-
{{ end }}
|
|
312
|
-
```
|
|
208
|
+
## Loops
|
|
313
209
|
|
|
314
|
-
|
|
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:**
|
|
210
|
+
Use `of` for arrays and iterable values:
|
|
319
211
|
|
|
320
212
|
```html
|
|
321
213
|
{{ for item of items }}
|
|
322
|
-
<li>{{ item
|
|
323
|
-
{{
|
|
214
|
+
<li>{{ item }}</li>
|
|
215
|
+
{{ endfor }}
|
|
324
216
|
```
|
|
325
217
|
|
|
326
|
-
|
|
218
|
+
You can add an optional second binding for the index:
|
|
327
219
|
|
|
328
220
|
```html
|
|
329
|
-
{{ for
|
|
330
|
-
<
|
|
331
|
-
{{
|
|
221
|
+
{{ for item, index of items }}
|
|
222
|
+
<li>{{ index }}: {{ item }}</li>
|
|
223
|
+
{{ endfor }}
|
|
332
224
|
```
|
|
333
225
|
|
|
334
|
-
|
|
226
|
+
Use `in` for object keys:
|
|
335
227
|
|
|
336
228
|
```html
|
|
337
|
-
{{ for
|
|
338
|
-
<
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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:
|
|
229
|
+
{{ for key in user }}
|
|
230
|
+
<p>{{ key }}</p>
|
|
231
|
+
{{ endfor }}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
You can add an optional second binding for the value:
|
|
398
235
|
|
|
399
236
|
```html
|
|
400
|
-
{{
|
|
401
|
-
{{
|
|
237
|
+
{{ for key, value in user }}
|
|
238
|
+
<p>{{ key }} = {{ value }}</p>
|
|
239
|
+
{{ endfor }}
|
|
402
240
|
```
|
|
403
241
|
|
|
404
|
-
|
|
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.
|
|
242
|
+
`break` and `continue` are available inside loops:
|
|
407
243
|
|
|
408
244
|
```html
|
|
409
|
-
{{
|
|
410
|
-
{{
|
|
411
|
-
{{
|
|
245
|
+
{{ for item of items }}
|
|
246
|
+
{{ if item.hidden }}{{ continue }}{{ endif }}
|
|
247
|
+
{{ item.name }}
|
|
248
|
+
{{ if item.last }}{{ break }}{{ endif }}
|
|
249
|
+
{{ endfor }}
|
|
412
250
|
```
|
|
413
251
|
|
|
414
|
-
|
|
252
|
+
## Partials And Composition
|
|
415
253
|
|
|
416
|
-
|
|
254
|
+
Mutor does not have layouts. That is a design choice.
|
|
417
255
|
|
|
418
|
-
|
|
256
|
+
Instead, build pages from partials/components and pass the context they need. This keeps the core smaller and makes composition explicit.
|
|
419
257
|
|
|
420
|
-
```
|
|
421
|
-
|
|
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.
|
|
258
|
+
```ts
|
|
259
|
+
import Mutor from "mutorjs";
|
|
425
260
|
|
|
426
|
-
|
|
261
|
+
const mutor = new Mutor({ autoEscape: false });
|
|
427
262
|
|
|
428
|
-
|
|
263
|
+
mutor.registerComponent(
|
|
264
|
+
"shell",
|
|
265
|
+
`
|
|
266
|
+
<!doctype html>
|
|
267
|
+
<html>
|
|
268
|
+
<head><title>{{ title }}</title></head>
|
|
269
|
+
<body>
|
|
270
|
+
{{ Mutor::include("nav") }}
|
|
271
|
+
<main>{{ content }}</main>
|
|
272
|
+
</body>
|
|
273
|
+
</html>
|
|
274
|
+
`,
|
|
275
|
+
);
|
|
429
276
|
|
|
430
|
-
|
|
277
|
+
mutor.registerComponent(
|
|
278
|
+
"nav",
|
|
279
|
+
`
|
|
280
|
+
<nav>
|
|
281
|
+
{{ for item of nav }}
|
|
282
|
+
<a href="{{ item.href }}">{{ item.label }}</a>
|
|
283
|
+
{{ endfor }}
|
|
284
|
+
</nav>
|
|
285
|
+
`,
|
|
286
|
+
);
|
|
431
287
|
|
|
432
|
-
|
|
288
|
+
const page = mutor.render('{{ Mutor::include("shell") }}', {
|
|
289
|
+
title: "Dashboard",
|
|
290
|
+
content: "<h1>Welcome</h1>",
|
|
291
|
+
nav: [
|
|
292
|
+
{ href: "/", label: "Home" },
|
|
293
|
+
{ href: "/settings", label: "Settings" },
|
|
294
|
+
],
|
|
295
|
+
});
|
|
296
|
+
```
|
|
433
297
|
|
|
434
|
-
|
|
298
|
+
If no context is passed to an include, it inherits the parent context:
|
|
435
299
|
|
|
436
300
|
```html
|
|
437
|
-
{{ Mutor::include("
|
|
438
|
-
{{ Mutor::include("./partials/nav.html", navLinks) }}
|
|
301
|
+
{{ Mutor::include("profile-card") }}
|
|
439
302
|
```
|
|
440
303
|
|
|
441
|
-
|
|
304
|
+
Pass a different context when the partial should render against a smaller or different value:
|
|
442
305
|
|
|
443
306
|
```html
|
|
444
|
-
{{ Mutor::include("
|
|
307
|
+
{{ Mutor::include("profile-card", user) }}
|
|
445
308
|
```
|
|
446
309
|
|
|
447
|
-
|
|
310
|
+
Inside any template or partial, the current context is available as `Mutor::$$context`:
|
|
448
311
|
|
|
449
|
-
|
|
312
|
+
```html
|
|
313
|
+
<pre>{{ JSON::stringify(Mutor::$$context, 2) }}</pre>
|
|
314
|
+
```
|
|
450
315
|
|
|
451
|
-
|
|
316
|
+
That is useful for generic components that render the value they were given:
|
|
452
317
|
|
|
453
|
-
|
|
318
|
+
```ts
|
|
319
|
+
mutor.registerComponent("badge", `<span>{{ Mutor::$$context }}</span>`);
|
|
454
320
|
|
|
455
|
-
|
|
456
|
-
|
|
321
|
+
mutor.render('{{ Mutor::include("badge", "New") }}', {});
|
|
322
|
+
// <span>New</span>
|
|
457
323
|
```
|
|
458
324
|
|
|
459
|
-
|
|
325
|
+
## Server Rendering
|
|
460
326
|
|
|
461
|
-
|
|
327
|
+
Use the server entry when templates live on disk.
|
|
462
328
|
|
|
463
|
-
|
|
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
|
-
```
|
|
329
|
+
```ts
|
|
330
|
+
import Mutor from "mutorjs/server";
|
|
475
331
|
|
|
476
|
-
|
|
332
|
+
const mutor = new Mutor({
|
|
333
|
+
rootDir: "./views",
|
|
334
|
+
});
|
|
477
335
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
alert: {
|
|
481
|
-
type: "error",
|
|
482
|
-
title: "Validation Failed",
|
|
483
|
-
message: "Email address is required.",
|
|
484
|
-
}
|
|
336
|
+
const html = mutor.renderFile("./pages/home.html", {
|
|
337
|
+
title: "Home",
|
|
485
338
|
});
|
|
486
339
|
```
|
|
487
340
|
|
|
488
|
-
|
|
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
|
-
---
|
|
341
|
+
Server includes resolve relative to the file currently being rendered:
|
|
501
342
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
Compiles a template string into an executable function. The resulting function accepts a context object and returns the rendered string.
|
|
343
|
+
```html
|
|
344
|
+
<!-- pages/home.html -->
|
|
345
|
+
{{ Mutor::include("../partials/header.html") }}
|
|
507
346
|
|
|
508
|
-
|
|
509
|
-
const fn = mutor.compile(`<title>{{ page.title }}</title>`);
|
|
510
|
-
const output = fn({ page: { title: "Home" } });
|
|
347
|
+
<h1>{{ title }}</h1>
|
|
511
348
|
```
|
|
512
349
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
### `render(template: string, context: object): string`
|
|
350
|
+
Use the `@/` alias to resolve from `rootDir`:
|
|
516
351
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
```js
|
|
520
|
-
const output = mutor.render(`<p>{{ msg }}</p>`, { msg: "Hello" });
|
|
352
|
+
```html
|
|
353
|
+
{{ Mutor::include("@/partials/header.html") }}
|
|
521
354
|
```
|
|
522
355
|
|
|
523
|
-
###
|
|
356
|
+
### Build A Directory
|
|
524
357
|
|
|
525
|
-
|
|
358
|
+
`buildDir` renders matching template files and copies everything else.
|
|
526
359
|
|
|
527
|
-
```
|
|
528
|
-
|
|
360
|
+
```ts
|
|
361
|
+
await mutor.buildDir("./site", "./dist", {
|
|
362
|
+
title: "Mutor Site",
|
|
363
|
+
});
|
|
529
364
|
```
|
|
530
365
|
|
|
531
|
-
|
|
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.
|
|
366
|
+
By default, `.html` and `.txt` files are rendered. `node_modules` and `.git` are skipped.
|
|
544
367
|
|
|
545
|
-
|
|
368
|
+
### Compile A Directory
|
|
546
369
|
|
|
547
|
-
|
|
370
|
+
`compileDir` precompiles matching files into the cache.
|
|
548
371
|
|
|
549
|
-
|
|
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
|
-
});
|
|
372
|
+
```ts
|
|
373
|
+
await mutor.compileDir("./views");
|
|
619
374
|
```
|
|
620
375
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
---
|
|
376
|
+
After that, `renderFile` can use cached compiled templates.
|
|
624
377
|
|
|
625
|
-
##
|
|
378
|
+
## Async Templates
|
|
626
379
|
|
|
627
|
-
|
|
380
|
+
Use `Mutor::await` when a value may be a promise.
|
|
628
381
|
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
user["email"] // static — property name known at compile time
|
|
382
|
+
```html
|
|
383
|
+
Hello, {{ (Mutor::await(userPromise)).name }}
|
|
632
384
|
```
|
|
633
385
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
### Dynamic Property Access
|
|
386
|
+
Use the async render methods for templates that use `Mutor::await`:
|
|
637
387
|
|
|
638
|
-
```
|
|
639
|
-
|
|
388
|
+
```ts
|
|
389
|
+
const html = await mutor.renderAsync(
|
|
390
|
+
"Hello, {{ Mutor::await(namePromise) }}",
|
|
391
|
+
{
|
|
392
|
+
namePromise: Promise.resolve("Ada"),
|
|
393
|
+
},
|
|
394
|
+
);
|
|
640
395
|
```
|
|
641
396
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
---
|
|
397
|
+
Server and component APIs also have async forms:
|
|
645
398
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
### Syntax
|
|
651
|
-
|
|
652
|
-
```html
|
|
653
|
-
{{ Namespace::identifier }}
|
|
654
|
-
{{ Namespace::method(args) }}
|
|
399
|
+
```ts
|
|
400
|
+
await mutor.renderFileAsync("./page.html", context);
|
|
401
|
+
await mutor.renderAsyncComponent("card", context);
|
|
655
402
|
```
|
|
656
403
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
### Available Namespaces
|
|
404
|
+
Good to know: `Mutor::await` makes the compiled template async. Prefer the async APIs for templates that use it.
|
|
660
405
|
|
|
661
|
-
|
|
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) |
|
|
406
|
+
## Namespaces
|
|
671
407
|
|
|
672
|
-
|
|
408
|
+
Namespaces are trusted helper groups available from templates. Namespace calls are allowed even when normal function calls are disabled.
|
|
673
409
|
|
|
674
410
|
```html
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
411
|
+
{{ Math::max(10, 20) }}
|
|
412
|
+
{{ Array::range(1, 3) }}
|
|
413
|
+
{{ Object::keys(user) }}
|
|
414
|
+
{{ JSON::stringify(user) }}
|
|
415
|
+
{{ String::capitalize(name) }}
|
|
416
|
+
{{ Date::iso() }}
|
|
417
|
+
{{ URL::encode(query) }}
|
|
679
418
|
```
|
|
680
419
|
|
|
681
|
-
|
|
420
|
+
Useful built-ins include:
|
|
682
421
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
422
|
+
| Namespace | Examples |
|
|
423
|
+
| --- | --- |
|
|
424
|
+
| `JSON` | `stringify`, `parse` |
|
|
425
|
+
| `Object` | `keys`, `values`, `entries`, `hasOwn`, `fromEntries`, `pick`, `omit` |
|
|
426
|
+
| `Array` | `isArray`, `from`, `of`, `unique`, `compact`, `chunk`, `range` |
|
|
427
|
+
| `Number` | `isFinite`, `isNaN`, `isInteger`, `parseInt`, `parseFloat`, `clamp`, `toFixed`, `random` |
|
|
428
|
+
| `String` | `fromCharCode`, `capitalize` |
|
|
429
|
+
| `Math` | `abs`, `floor`, `ceil`, `round`, `sqrt`, `pow`, `max`, `min`, `PI` |
|
|
430
|
+
| `Date` | `now`, `parse`, `new`, `iso`, `timestamp` |
|
|
431
|
+
| `Boolean` | `valueOf` |
|
|
432
|
+
| `URL` | `encode`, `decode` |
|
|
433
|
+
| `Mutor` | `include`, `await`, `$$context` |
|
|
434
|
+
|
|
435
|
+
## Security Model
|
|
436
|
+
|
|
437
|
+
Mutor is designed to keep templates useful without handing them the whole JavaScript runtime.
|
|
438
|
+
|
|
439
|
+
By default:
|
|
440
|
+
|
|
441
|
+
- HTML strings are escaped.
|
|
442
|
+
- Function calls from context values are disabled.
|
|
443
|
+
- Namespace calls are allowed.
|
|
444
|
+
- Dangerous property names are blocked.
|
|
445
|
+
- Computed property access is validated.
|
|
446
|
+
- Template expressions are parsed by Mutor, not executed as arbitrary JavaScript source.
|
|
447
|
+
|
|
448
|
+
Blocked properties include:
|
|
449
|
+
|
|
450
|
+
```txt
|
|
451
|
+
__proto__
|
|
452
|
+
constructor
|
|
453
|
+
prototype
|
|
454
|
+
__defineGetter__
|
|
455
|
+
__defineSetter__
|
|
456
|
+
__lookupGetter__
|
|
457
|
+
__lookupSetter__
|
|
458
|
+
caller
|
|
459
|
+
callee
|
|
460
|
+
arguments
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
You can allow or forbid additional properties:
|
|
703
464
|
|
|
704
|
-
```
|
|
465
|
+
```ts
|
|
705
466
|
const mutor = new Mutor({
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
maxSize: 50 * 1024 * 1024, // 50MB
|
|
709
|
-
}
|
|
467
|
+
allowedProps: new Set(["constructor"]),
|
|
468
|
+
forbiddenProps: new Set(["passwordHash"]),
|
|
710
469
|
});
|
|
711
470
|
```
|
|
712
471
|
|
|
713
|
-
|
|
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. |
|
|
472
|
+
Use `allowFnCalls` deliberately:
|
|
717
473
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
```js
|
|
474
|
+
```ts
|
|
721
475
|
const mutor = new Mutor({
|
|
722
|
-
|
|
476
|
+
allowFnCalls: true,
|
|
723
477
|
});
|
|
724
478
|
```
|
|
725
479
|
|
|
726
|
-
With
|
|
727
|
-
|
|
728
|
-
### Manual Cache Control
|
|
480
|
+
With `allowFnCalls: false`, this is blocked:
|
|
729
481
|
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
mutor.reset();
|
|
733
|
-
|
|
734
|
-
// Get cache diagnostics (memory usage, entry count)
|
|
735
|
-
const diagnostics = mutor.getDiagnostics();
|
|
482
|
+
```html
|
|
483
|
+
{{ user.deleteAccount() }}
|
|
736
484
|
```
|
|
737
485
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
### Instance Isolation
|
|
486
|
+
Mutor is a template engine, not a complete sandbox for hostile code. If users can write templates, keep the default restrictions unless you have a reason to loosen them.
|
|
741
487
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
---
|
|
745
|
-
|
|
746
|
-
## 16. Configuration Reference
|
|
747
|
-
|
|
748
|
-
The full configuration object with all defaults:
|
|
488
|
+
## Configuration
|
|
749
489
|
|
|
750
490
|
```ts
|
|
751
|
-
|
|
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
|
-
},
|
|
491
|
+
const mutor = new Mutor({
|
|
783
492
|
autoEscape: true,
|
|
784
|
-
allowedProps: new Set(),
|
|
785
|
-
forbiddenProps: new Set(["__proto__", "constructor", "prototype"]),
|
|
786
493
|
allowFnCalls: false,
|
|
494
|
+
preserveEscapeDelimiter: false,
|
|
495
|
+
debugRuntimeErrors: false,
|
|
496
|
+
rootDir: "./views",
|
|
497
|
+
cache: {
|
|
498
|
+
active: true,
|
|
499
|
+
maxSize: 50 * 1024 * 1024,
|
|
500
|
+
},
|
|
787
501
|
delimiters: {
|
|
788
502
|
openingTag: "{{",
|
|
789
503
|
closingTag: "}}",
|
|
790
504
|
openingTagEscape: "\\",
|
|
791
505
|
whitespaceTrim: "~",
|
|
506
|
+
commentTag: "#",
|
|
792
507
|
},
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
maxSize: 50 * 1024 * 1024,
|
|
508
|
+
build: {
|
|
509
|
+
include: new Set([".html", ".txt"]),
|
|
510
|
+
exclude: new Set(["node_modules", ".git"]),
|
|
797
511
|
},
|
|
798
|
-
|
|
512
|
+
onIncludeFail: "throw",
|
|
513
|
+
});
|
|
799
514
|
```
|
|
800
515
|
|
|
801
|
-
### `
|
|
802
|
-
|
|
803
|
-
A `Set<string>` of property names that are explicitly permitted regardless of other validation rules.
|
|
804
|
-
|
|
805
|
-
### `forbiddenProps`
|
|
516
|
+
### `autoEscape`
|
|
806
517
|
|
|
807
|
-
|
|
518
|
+
Escapes HTML-sensitive characters in strings. Enabled by default.
|
|
808
519
|
|
|
809
|
-
### `
|
|
520
|
+
### `allowFnCalls`
|
|
810
521
|
|
|
811
|
-
|
|
522
|
+
Allows templates to call functions from context values. Disabled by default.
|
|
812
523
|
|
|
813
|
-
|
|
524
|
+
Namespace calls such as `Math::max(1, 2)` are still allowed.
|
|
814
525
|
|
|
815
|
-
|
|
526
|
+
### `delimiters`
|
|
816
527
|
|
|
817
|
-
|
|
528
|
+
Customize tags and control markers.
|
|
818
529
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
```js
|
|
530
|
+
```ts
|
|
822
531
|
const mutor = new Mutor({
|
|
823
532
|
delimiters: {
|
|
824
|
-
openingTag: "
|
|
825
|
-
closingTag: "
|
|
826
|
-
|
|
827
|
-
whitespaceTrim: "-",
|
|
828
|
-
commandTag: "#",
|
|
829
|
-
}
|
|
533
|
+
openingTag: "{%",
|
|
534
|
+
closingTag: "%}",
|
|
535
|
+
},
|
|
830
536
|
});
|
|
831
537
|
```
|
|
832
538
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
```html
|
|
836
|
-
<p><% user.name %></p>
|
|
837
|
-
<%- if user.isAdmin -%>
|
|
838
|
-
<a href="/admin">Admin</a>
|
|
839
|
-
<%- end -%>
|
|
840
|
-
```
|
|
539
|
+
### `preserveEscapeDelimiter`
|
|
841
540
|
|
|
842
|
-
|
|
541
|
+
Controls whether escaped opening tags keep their escape marker.
|
|
843
542
|
|
|
844
|
-
|
|
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.
|
|
543
|
+
### `rootDir`
|
|
848
544
|
|
|
849
|
-
|
|
545
|
+
Used by the server renderer for `@/` includes.
|
|
850
546
|
|
|
851
|
-
|
|
547
|
+
### `cache`
|
|
852
548
|
|
|
853
|
-
|
|
549
|
+
Controls compiled template caching.
|
|
854
550
|
|
|
855
|
-
|
|
551
|
+
```ts
|
|
552
|
+
const mutor = new Mutor({
|
|
553
|
+
cache: {
|
|
554
|
+
active: true,
|
|
555
|
+
maxSize: 10 * 1024 * 1024,
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
```
|
|
856
559
|
|
|
857
|
-
|
|
560
|
+
### `build`
|
|
858
561
|
|
|
859
|
-
|
|
562
|
+
Controls which files `buildDir` and `compileDir` process.
|
|
860
563
|
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
{
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
564
|
+
```ts
|
|
565
|
+
const mutor = new Mutor({
|
|
566
|
+
build: {
|
|
567
|
+
include: new Set([".html", ".md"]),
|
|
568
|
+
exclude: new Set(["node_modules", ".git", "drafts"]),
|
|
569
|
+
},
|
|
570
|
+
});
|
|
867
571
|
```
|
|
868
572
|
|
|
869
|
-
|
|
573
|
+
### `onIncludeFail`
|
|
870
574
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
</ul>
|
|
575
|
+
Controls what happens when an include fails.
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
const mutor = new Mutor({
|
|
579
|
+
onIncludeFail: "throw", // "throw" | "ignore" | "ignoreLog"
|
|
580
|
+
});
|
|
879
581
|
```
|
|
880
582
|
|
|
881
|
-
|
|
583
|
+
### `onIncludeError`
|
|
882
584
|
|
|
883
|
-
|
|
585
|
+
Return fallback content for failed includes.
|
|
884
586
|
|
|
885
|
-
```
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
587
|
+
```ts
|
|
588
|
+
const mutor = new Mutor({
|
|
589
|
+
onIncludeFail: "ignore",
|
|
590
|
+
onIncludeError(meta, err) {
|
|
591
|
+
return `<!-- include failed: ${meta.path} -->`;
|
|
592
|
+
},
|
|
593
|
+
});
|
|
891
594
|
```
|
|
892
595
|
|
|
893
|
-
|
|
596
|
+
### `debugRuntimeErrors`
|
|
894
597
|
|
|
895
|
-
|
|
896
|
-
<ul>
|
|
897
|
-
<li>Alpha</li>
|
|
898
|
-
<li>Beta</li>
|
|
899
|
-
</ul>
|
|
900
|
-
```
|
|
598
|
+
Wraps runtime failures with template source context.
|
|
901
599
|
|
|
902
|
-
|
|
600
|
+
```ts
|
|
601
|
+
const mutor = new Mutor({
|
|
602
|
+
debugRuntimeErrors: true,
|
|
603
|
+
allowFnCalls: true,
|
|
604
|
+
});
|
|
605
|
+
```
|
|
903
606
|
|
|
904
|
-
|
|
607
|
+
This is helpful during development because errors point back to the template line and column.
|
|
905
608
|
|
|
906
|
-
##
|
|
609
|
+
## Cache
|
|
907
610
|
|
|
908
|
-
|
|
611
|
+
Mutor caches compiled templates by identifier or absolute file path.
|
|
909
612
|
|
|
910
|
-
|
|
613
|
+
For registered components:
|
|
911
614
|
|
|
912
|
-
```
|
|
913
|
-
|
|
615
|
+
```ts
|
|
616
|
+
mutor.registerComponent("card", "<article>{{ title }}</article>");
|
|
617
|
+
mutor.invalidateCacheEntry("card");
|
|
914
618
|
```
|
|
915
619
|
|
|
916
|
-
|
|
620
|
+
For server files:
|
|
917
621
|
|
|
918
|
-
```
|
|
919
|
-
|
|
622
|
+
```ts
|
|
623
|
+
mutor.renderFile("./views/page.html", context);
|
|
624
|
+
mutor.invalidateCacheEntry("./views/page.html");
|
|
920
625
|
```
|
|
921
626
|
|
|
922
|
-
|
|
627
|
+
The next render recompiles the template.
|
|
923
628
|
|
|
924
|
-
|
|
629
|
+
Inspect cache usage:
|
|
925
630
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
```html
|
|
929
|
-
\{{ this is not a template tag }}
|
|
631
|
+
```ts
|
|
632
|
+
mutor.getDiagnostics();
|
|
930
633
|
```
|
|
931
634
|
|
|
932
|
-
|
|
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:
|
|
635
|
+
Example result:
|
|
959
636
|
|
|
960
637
|
```ts
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
638
|
+
{
|
|
639
|
+
bytesUsed: 1200,
|
|
640
|
+
bytesMax: 52428800,
|
|
641
|
+
readableUsed: "0.00 MB",
|
|
642
|
+
readableMax: "50.00 MB",
|
|
643
|
+
totalEntries: 2,
|
|
644
|
+
percentFull: 0,
|
|
645
|
+
avgTemplateSize: 600
|
|
969
646
|
}
|
|
970
647
|
```
|
|
971
648
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
Because every token is position-tracked, errors reference exact source locations:
|
|
649
|
+
Clear all cache entries and restore default config:
|
|
975
650
|
|
|
651
|
+
```ts
|
|
652
|
+
mutor.reset();
|
|
976
653
|
```
|
|
977
|
-
MutorSecurityError: Forbidden property access: "constructor"
|
|
978
|
-
at line 4, column 12 in template "views/user-profile.html"
|
|
979
654
|
|
|
980
|
-
|
|
655
|
+
Good to know: Mutor does not watch files. If a template file changes while cache is active, call `invalidateCacheEntry`, call `reset`, or disable cache in development.
|
|
981
656
|
|
|
982
|
-
|
|
983
|
-
Include chain: views/layout.html → views/partials/nav.html → views/layout.html
|
|
984
|
-
```
|
|
657
|
+
## API
|
|
985
658
|
|
|
986
|
-
###
|
|
659
|
+
### Core API
|
|
987
660
|
|
|
988
|
-
|
|
989
|
-
import { MutorSecurityError, MutorParseError } from "mutorjs";
|
|
661
|
+
Import from `mutorjs`:
|
|
990
662
|
|
|
991
|
-
|
|
992
|
-
|
|
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
|
-
}
|
|
663
|
+
```ts
|
|
664
|
+
import Mutor from "mutorjs";
|
|
1005
665
|
```
|
|
1006
666
|
|
|
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
|
-
```
|
|
667
|
+
#### `render(template, context)`
|
|
1020
668
|
|
|
1021
|
-
|
|
669
|
+
Renders a template string.
|
|
1022
670
|
|
|
1023
|
-
```
|
|
1024
|
-
|
|
671
|
+
```ts
|
|
672
|
+
mutor.render("Hello {{ name }}", { name: "Ada" });
|
|
1025
673
|
```
|
|
1026
674
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
#### `compile <template>`
|
|
675
|
+
#### `renderAsync(template, context)`
|
|
1030
676
|
|
|
1031
|
-
|
|
677
|
+
Renders a template string through a promise. Use this when the template uses `Mutor::await`.
|
|
1032
678
|
|
|
1033
|
-
```
|
|
1034
|
-
mutor
|
|
679
|
+
```ts
|
|
680
|
+
await mutor.renderAsync("{{ Mutor::await(value) }}", {
|
|
681
|
+
value: Promise.resolve("done"),
|
|
682
|
+
});
|
|
1035
683
|
```
|
|
1036
684
|
|
|
1037
|
-
|
|
685
|
+
#### `compile(template)`
|
|
1038
686
|
|
|
1039
|
-
|
|
687
|
+
Compiles a template and returns a reusable function.
|
|
1040
688
|
|
|
1041
|
-
|
|
689
|
+
```ts
|
|
690
|
+
const renderGreeting = mutor.compile("Hello {{ name }}");
|
|
1042
691
|
|
|
1043
|
-
|
|
1044
|
-
|
|
692
|
+
renderGreeting({ name: "Ada" });
|
|
693
|
+
renderGreeting({ name: "Grace" });
|
|
1045
694
|
```
|
|
1046
695
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
#### `render <template>`
|
|
696
|
+
#### `registerComponent(identifier, template)`
|
|
1050
697
|
|
|
1051
|
-
|
|
698
|
+
Registers a reusable component/partial.
|
|
1052
699
|
|
|
1053
|
-
```
|
|
1054
|
-
mutor
|
|
700
|
+
```ts
|
|
701
|
+
mutor.registerComponent("button", "<button>{{ label }}</button>");
|
|
1055
702
|
```
|
|
1056
703
|
|
|
1057
|
-
|
|
704
|
+
#### `renderComponent(identifier, context)`
|
|
1058
705
|
|
|
1059
|
-
|
|
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
|
-
```
|
|
706
|
+
Renders a registered component.
|
|
1091
707
|
|
|
1092
|
-
```
|
|
1093
|
-
mutor
|
|
708
|
+
```ts
|
|
709
|
+
mutor.renderComponent("button", { label: "Save" });
|
|
1094
710
|
```
|
|
1095
711
|
|
|
1096
|
-
|
|
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
|
|
712
|
+
#### `renderAsyncComponent(identifier, context)`
|
|
1107
713
|
|
|
1108
|
-
|
|
1109
|
-
mutor build ./views --data ./site.json --out ./dist
|
|
714
|
+
Async component rendering.
|
|
1110
715
|
|
|
1111
|
-
|
|
1112
|
-
mutor
|
|
716
|
+
```ts
|
|
717
|
+
await mutor.renderAsyncComponent("button", context);
|
|
1113
718
|
```
|
|
1114
719
|
|
|
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.
|
|
1128
|
-
|
|
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.
|
|
720
|
+
#### `invalidateCacheEntry(identifier)`
|
|
1156
721
|
|
|
1157
|
-
|
|
722
|
+
Removes a cached component entry.
|
|
1158
723
|
|
|
1159
|
-
|
|
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";
|
|
724
|
+
```ts
|
|
725
|
+
mutor.invalidateCacheEntry("button");
|
|
1181
726
|
```
|
|
1182
727
|
|
|
1183
|
-
|
|
728
|
+
#### `addConfig(config)`
|
|
1184
729
|
|
|
1185
|
-
|
|
730
|
+
Updates the engine config.
|
|
1186
731
|
|
|
1187
|
-
|
|
732
|
+
```ts
|
|
733
|
+
mutor.addConfig({ autoEscape: false });
|
|
734
|
+
```
|
|
1188
735
|
|
|
1189
|
-
|
|
1190
|
-
const templateString = await fetch("/templates/card.html").then(r => r.text());
|
|
736
|
+
#### `restoreDefaultConfig()`
|
|
1191
737
|
|
|
1192
|
-
|
|
738
|
+
Restores the default config.
|
|
1193
739
|
|
|
1194
|
-
|
|
1195
|
-
|
|
740
|
+
```ts
|
|
741
|
+
mutor.restoreDefaultConfig();
|
|
1196
742
|
```
|
|
1197
743
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
## 25. Server Usage
|
|
744
|
+
#### `getDiagnostics()`
|
|
1201
745
|
|
|
1202
|
-
|
|
746
|
+
Returns cache diagnostics.
|
|
1203
747
|
|
|
1204
|
-
```
|
|
1205
|
-
|
|
748
|
+
```ts
|
|
749
|
+
mutor.getDiagnostics();
|
|
1206
750
|
```
|
|
1207
751
|
|
|
1208
|
-
|
|
752
|
+
#### `reset()`
|
|
1209
753
|
|
|
1210
|
-
|
|
754
|
+
Restores default config and clears cached templates.
|
|
1211
755
|
|
|
1212
|
-
```
|
|
1213
|
-
|
|
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
|
-
});
|
|
756
|
+
```ts
|
|
757
|
+
mutor.reset();
|
|
1226
758
|
```
|
|
1227
759
|
|
|
1228
|
-
###
|
|
760
|
+
### Server API
|
|
1229
761
|
|
|
1230
|
-
|
|
762
|
+
Import from `mutorjs/server`:
|
|
1231
763
|
|
|
1232
|
-
```
|
|
764
|
+
```ts
|
|
1233
765
|
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
766
|
```
|
|
1245
767
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
---
|
|
1249
|
-
|
|
1250
|
-
## 26. Advanced Examples
|
|
768
|
+
#### `renderFile(path, context)`
|
|
1251
769
|
|
|
1252
|
-
|
|
770
|
+
Renders a template file.
|
|
1253
771
|
|
|
1254
|
-
```
|
|
1255
|
-
|
|
1256
|
-
<main class="dashboard">
|
|
1257
|
-
<header>
|
|
1258
|
-
<h1>{{ org.name }} — Dashboard</h1>
|
|
1259
|
-
<p>Logged in as {{ user.displayName }}</p>
|
|
1260
|
-
</header>
|
|
1261
|
-
|
|
1262
|
-
{{~ if projects.length > 0 ~}}
|
|
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>
|
|
1280
|
-
{{ end }}
|
|
1281
|
-
</ul>
|
|
1282
|
-
</article>
|
|
1283
|
-
{{ end }}
|
|
1284
|
-
</section>
|
|
1285
|
-
{{~ else ~}}
|
|
1286
|
-
<p class="empty-state">No projects found.</p>
|
|
1287
|
-
{{~ end ~}}
|
|
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>
|
|
772
|
+
```ts
|
|
773
|
+
mutor.renderFile("./views/home.html", context);
|
|
1341
774
|
```
|
|
1342
775
|
|
|
1343
|
-
|
|
776
|
+
#### `renderFileAsync(path, context)`
|
|
1344
777
|
|
|
1345
|
-
|
|
1346
|
-
import Mutor from "mutorjs/server";
|
|
778
|
+
Async file rendering.
|
|
1347
779
|
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
});
|
|
780
|
+
```ts
|
|
781
|
+
await mutor.renderFileAsync("./views/home.html", context);
|
|
1363
782
|
```
|
|
1364
783
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
## 27. Best Practices
|
|
784
|
+
#### `buildDir(src, destination, context)`
|
|
1368
785
|
|
|
1369
|
-
|
|
786
|
+
Renders a directory into another directory.
|
|
1370
787
|
|
|
1371
|
-
|
|
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.
|
|
1396
|
-
|
|
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.
|
|
1398
|
-
|
|
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.
|
|
1402
|
-
|
|
1403
|
-
---
|
|
1404
|
-
|
|
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
|
|
1420
|
-
|
|
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.
|
|
1422
|
-
|
|
1423
|
-
### Report Generation
|
|
1424
|
-
|
|
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.
|
|
1426
|
-
|
|
1427
|
-
---
|
|
1428
|
-
|
|
1429
|
-
## 30. Design Tradeoffs
|
|
1430
|
-
|
|
1431
|
-
### Compilation Cost for Execution Speed
|
|
1432
|
-
|
|
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.
|
|
1434
|
-
|
|
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.
|
|
1448
|
-
|
|
1449
|
-
---
|
|
1450
|
-
|
|
1451
|
-
## 31. Future Roadmap
|
|
1452
|
-
|
|
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
|
-
});
|
|
788
|
+
```ts
|
|
789
|
+
await mutor.buildDir("./site", "./dist", context);
|
|
1468
790
|
```
|
|
1469
791
|
|
|
1470
|
-
|
|
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?**
|
|
792
|
+
#### `compileDir(src)`
|
|
1489
793
|
|
|
1490
|
-
|
|
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.
|
|
794
|
+
Precompiles matching files in a directory into the cache.
|
|
1527
795
|
|
|
1528
796
|
```ts
|
|
1529
|
-
|
|
1530
|
-
autoEscape: true,
|
|
1531
|
-
allowFnCalls: false,
|
|
1532
|
-
cache: { active: true, maxSize: 50 * 1024 * 1024 },
|
|
1533
|
-
});
|
|
797
|
+
await mutor.compileDir("./views");
|
|
1534
798
|
```
|
|
1535
799
|
|
|
1536
|
-
|
|
800
|
+
#### `invalidateCacheEntry(path)`
|
|
1537
801
|
|
|
1538
|
-
|
|
802
|
+
Removes a cached file entry. The server renderer resolves the path before removing it.
|
|
1539
803
|
|
|
1540
804
|
```ts
|
|
1541
|
-
|
|
805
|
+
mutor.invalidateCacheEntry("./views/home.html");
|
|
1542
806
|
```
|
|
1543
807
|
|
|
1544
|
-
|
|
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
|
-
|
|
1551
|
-
---
|
|
1552
|
-
|
|
1553
|
-
### `mutor.render(template: string, context: Record<string, unknown>): string`
|
|
1554
|
-
|
|
1555
|
-
Compiles (or retrieves from cache) and immediately renders a template with the given context.
|
|
1556
|
-
|
|
1557
|
-
**Throws:** Same as `compile`, plus `MutorRuntimeError` for runtime context validation failures.
|
|
1558
|
-
|
|
1559
|
-
---
|
|
1560
|
-
|
|
1561
|
-
### `mutor.renderFile(path: string, context: Record<string, unknown>): Promise<string>`
|
|
808
|
+
## CLI
|
|
1562
809
|
|
|
1563
|
-
|
|
810
|
+
Mutor ships with a small CLI.
|
|
1564
811
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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
|
-
---
|
|
812
|
+
```sh
|
|
813
|
+
mutor <command> <input> [options]
|
|
814
|
+
```
|
|
1610
815
|
|
|
1611
|
-
###
|
|
816
|
+
### Render A File
|
|
1612
817
|
|
|
1613
|
-
```
|
|
1614
|
-
|
|
1615
|
-
MutorError,
|
|
1616
|
-
MutorParseError,
|
|
1617
|
-
MutorSecurityError,
|
|
1618
|
-
MutorCompileError,
|
|
1619
|
-
MutorRuntimeError,
|
|
1620
|
-
MutorIncludeError,
|
|
1621
|
-
MutorComponentError,
|
|
1622
|
-
} from "mutorjs";
|
|
818
|
+
```sh
|
|
819
|
+
mutor render ./views/home.html --data ./data.json --out ./dist/home.html
|
|
1623
820
|
```
|
|
1624
821
|
|
|
1625
|
-
|
|
822
|
+
Without `--out`, the result is printed to stdout.
|
|
1626
823
|
|
|
1627
|
-
|
|
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 }` |
|
|
824
|
+
### Build A Directory
|
|
1633
825
|
|
|
1634
|
-
|
|
826
|
+
```sh
|
|
827
|
+
mutor build ./site --data ./data.json --out ./dist
|
|
828
|
+
```
|
|
1635
829
|
|
|
1636
|
-
###
|
|
830
|
+
### Compile A Template
|
|
1637
831
|
|
|
1638
|
-
```
|
|
1639
|
-
|
|
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
|
-
}
|
|
832
|
+
```sh
|
|
833
|
+
mutor compile ./views/home.html --out ./compiled.txt
|
|
1660
834
|
```
|
|
1661
835
|
|
|
1662
|
-
|
|
836
|
+
### Options
|
|
1663
837
|
|
|
1664
|
-
|
|
1665
|
-
|
|
838
|
+
| Option | Meaning |
|
|
839
|
+
| --- | --- |
|
|
840
|
+
| `--data <path>` | JSON data file used as render context |
|
|
841
|
+
| `--out <path>` | Output file or directory |
|
|
842
|
+
| `--config <path>` | JSON config file |
|
|
843
|
+
| `--version` | Print the installed version |
|
|
844
|
+
| `--help` | Show CLI help |
|
|
845
|
+
|
|
846
|
+
## Good To Know
|
|
847
|
+
|
|
848
|
+
- Mutor has no layout inheritance. Compose pages from partials/components and context.
|
|
849
|
+
- Includes inherit their parent context when no context is passed.
|
|
850
|
+
- The current context is available as `Mutor::$$context`.
|
|
851
|
+
- Use async render methods when templates use `Mutor::await`.
|
|
852
|
+
- File cache does not watch the filesystem.
|
|
853
|
+
- Function calls from context values are disabled by default.
|
|
854
|
+
- Array/object/function/class literals are not part of the template language.
|
|
855
|
+
- Use `JSON::parse(...)` or pass data through context when you need arrays or objects.
|
|
856
|
+
- Auto-escaping only changes strings. Non-string values are returned as they are.
|
|
857
|
+
- Missing includes can throw, return empty output, log, or use `onIncludeError`, depending on config.
|
|
858
|
+
|
|
859
|
+
## License
|
|
860
|
+
|
|
861
|
+
ISC
|