abdk-mustache-js 1.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +298 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
- package/src/index.ts +298 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [1.0.0] — 2026-04-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `compile(template)` — compiles a Mustache template string to a native JS
|
|
15
|
+
function
|
|
16
|
+
- `renderCompiled(template, view, partials?, escape?)` — renders a
|
|
17
|
+
pre-compiled template synchronously
|
|
18
|
+
- `render(template, view, partials?, escape?)` — compiles and renders in one
|
|
19
|
+
call; string partials compiled lazily
|
|
20
|
+
- `renderCompiledAsync(template, view, partials?, escape?)` — async render
|
|
21
|
+
with automatic resolution of Promise-returning view functions and async
|
|
22
|
+
partial loaders
|
|
23
|
+
- `renderAsync(template, view, partials?, escape?)` — async convenience
|
|
24
|
+
wrapper accepting template strings and `string | (() => Promise<string>)`
|
|
25
|
+
partials
|
|
26
|
+
- Full Mustache syntax: variables, escaped / unescaped, sections, inverted
|
|
27
|
+
sections, comments, partials, dynamic partials, set-delimiter tags
|
|
28
|
+
- Template inheritance extension: `{{< parent}}` / `{{$ block}}`
|
|
29
|
+
- Built-in HTML escape function; custom escape function supported on all APIs
|
|
30
|
+
- Zero runtime dependencies
|
|
31
|
+
- TypeScript declarations included
|
|
32
|
+
|
|
33
|
+
[1.0.0]: https://github.com/abdk-consulting/abdk-mustache-js/releases/tag/v1.0.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ABDK Consulting
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# abdk-mustache-js
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/abdk-mustache-js)
|
|
4
|
+
[](https://github.com/abdk-consulting/abdk-mustache-js/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
A fast, spec-compliant [Mustache](https://mustache.github.io/) template engine
|
|
8
|
+
for JavaScript / TypeScript that **compiles templates into native JavaScript
|
|
9
|
+
functions** for high-throughput rendering.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Compiled templates** — each template is compiled once to a plain JS
|
|
16
|
+
function; subsequent renders are pure function calls with no parsing overhead
|
|
17
|
+
- **Full Mustache syntax** — variables, sections, inverted sections, comments,
|
|
18
|
+
partials, dynamic partials, and set-delimiter tags
|
|
19
|
+
- **Template inheritance** — `{{< parent}}` / `{{$ block}}` for layout
|
|
20
|
+
composition (extends the base Mustache spec)
|
|
21
|
+
- **Async rendering** — `renderAsync` / `renderCompiledAsync` resolve async
|
|
22
|
+
and Promise-returning view values automatically, with lazy async partial
|
|
23
|
+
loading
|
|
24
|
+
- **TypeScript-first** — full type declarations included, zero runtime
|
|
25
|
+
dependencies
|
|
26
|
+
- **No extra dependencies** — only the Node.js built-in `node:test` runner is
|
|
27
|
+
used for tests
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install abdk-mustache-js
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Quick start
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { render } from "abdk-mustache-js";
|
|
45
|
+
|
|
46
|
+
const html = render("Hello, {{name}}!", { name: "World" });
|
|
47
|
+
// → "Hello, World!"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Pre-compile a template
|
|
51
|
+
|
|
52
|
+
Compile once, render many times:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { compile, renderCompiled } from "abdk-mustache-js";
|
|
56
|
+
|
|
57
|
+
const tmpl = compile("Hello, {{name}}!");
|
|
58
|
+
|
|
59
|
+
console.log(renderCompiled(tmpl, { name: "Alice" })); // Hello, Alice!
|
|
60
|
+
console.log(renderCompiled(tmpl, { name: "Bob" })); // Hello, Bob!
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Sections
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
render("{{#show}}visible{{/show}}", { show: true }); // "visible"
|
|
67
|
+
render("{{#show}}visible{{/show}}", { show: false }); // ""
|
|
68
|
+
render("{{^empty}}fallback{{/empty}}", { empty: [] }); // "fallback"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Arrays are iterated automatically:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
render(
|
|
75
|
+
"{{#items}}- {{name}}\n{{/items}}",
|
|
76
|
+
{ items: [{ name: "One" }, { name: "Two" }] }
|
|
77
|
+
);
|
|
78
|
+
// "- One\n- Two\n"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Partials
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { render } from "abdk-mustache-js";
|
|
85
|
+
|
|
86
|
+
const output = render(
|
|
87
|
+
"{{> header}}Content{{> footer}}",
|
|
88
|
+
{ title: "Home", year: 2026 },
|
|
89
|
+
{
|
|
90
|
+
header: "<h1>{{title}}</h1>\n",
|
|
91
|
+
footer: "<footer>{{year}}</footer>",
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Template inheritance (blocks & parents)
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { compile, renderCompiled } from "abdk-mustache-js";
|
|
100
|
+
|
|
101
|
+
const layout = compile(`
|
|
102
|
+
<html>
|
|
103
|
+
<head><title>{{$title}}Default Title{{/title}}</title></head>
|
|
104
|
+
<body>{{$body}}{{/body}}</body>
|
|
105
|
+
</html>
|
|
106
|
+
`);
|
|
107
|
+
|
|
108
|
+
const page = compile(`
|
|
109
|
+
{{< layout}}
|
|
110
|
+
{{$title}}My Page{{/title}}
|
|
111
|
+
{{$body}}<p>Hello, {{name}}!</p>{{/body}}
|
|
112
|
+
{{/layout}}
|
|
113
|
+
`);
|
|
114
|
+
|
|
115
|
+
renderCompiled(page, { name: "Alice" }, { layout });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Unescaped output
|
|
119
|
+
|
|
120
|
+
Use triple mustaches `{{{…}}}` or `{{& …}}` to skip HTML escaping:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
render("{{{html}}}", { html: "<b>bold</b>" }); // "<b>bold</b>"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Custom escape function
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
render("{{val}}", { val: "x" }, {}, s => s); // disable escaping entirely
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Async rendering
|
|
133
|
+
|
|
134
|
+
`renderAsync` and `renderCompiledAsync` resolve any view property that is an
|
|
135
|
+
async / Promise-returning function. Partials can also be loaded asynchronously.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { renderAsync } from "abdk-mustache-js";
|
|
139
|
+
|
|
140
|
+
const html = await renderAsync(
|
|
141
|
+
"Hello, {{name}}! You have {{count}} messages.",
|
|
142
|
+
{
|
|
143
|
+
name: async () => fetchUserName(),
|
|
144
|
+
count: async () => fetchMessageCount(),
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Async partials (loaded on demand):
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { renderAsync } from "abdk-mustache-js";
|
|
153
|
+
|
|
154
|
+
const html = await renderAsync(
|
|
155
|
+
"{{> header}}{{body}}",
|
|
156
|
+
{ body: "Content" },
|
|
157
|
+
{
|
|
158
|
+
// string partial — compiled eagerly
|
|
159
|
+
header: "<h1>ABDK</h1>",
|
|
160
|
+
// async partial — compiled lazily, result cached for subsequent iterations
|
|
161
|
+
nav: async () => fetchNavTemplate(),
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## API
|
|
169
|
+
|
|
170
|
+
### `compile(template: string): CompiledTemplate`
|
|
171
|
+
|
|
172
|
+
Parses the template string and returns a compiled function. Throws a
|
|
173
|
+
descriptive `Error` if the template is syntactically invalid (unmatched tags,
|
|
174
|
+
unclosed sections, etc.).
|
|
175
|
+
|
|
176
|
+
### `renderCompiled(template, view, partials?, escape?): string`
|
|
177
|
+
|
|
178
|
+
Renders a pre-compiled template synchronously.
|
|
179
|
+
|
|
180
|
+
| Parameter | Type | Default |
|
|
181
|
+
|-----------|------|---------|
|
|
182
|
+
| `template` | `CompiledTemplate` | — |
|
|
183
|
+
| `view` | `any` | — |
|
|
184
|
+
| `partials` | `{ [name: string]: CompiledTemplate }` | `{}` |
|
|
185
|
+
| `escape` | `(s: string) => string` | built-in HTML escape |
|
|
186
|
+
|
|
187
|
+
### `render(template, view, partials?, escape?): string`
|
|
188
|
+
|
|
189
|
+
Compiles and renders in one call. Accepts string partials (compiled lazily on
|
|
190
|
+
first use per `render` call).
|
|
191
|
+
|
|
192
|
+
| Parameter | Type | Default |
|
|
193
|
+
|-----------|------|---------|
|
|
194
|
+
| `template` | `string` | — |
|
|
195
|
+
| `view` | `any` | — |
|
|
196
|
+
| `partials` | `{ [name: string]: string }` | `{}` |
|
|
197
|
+
| `escape` | `(s: string) => string` | built-in HTML escape |
|
|
198
|
+
|
|
199
|
+
### `renderCompiledAsync(template, view, partials?, escape?): Promise<string>`
|
|
200
|
+
|
|
201
|
+
Renders a pre-compiled template; asynchronous view property functions are
|
|
202
|
+
resolved automatically (re-rendering until all promises settle). Partial
|
|
203
|
+
loaders are called at most once per `renderCompiledAsync` call.
|
|
204
|
+
|
|
205
|
+
| Parameter | Type | Default |
|
|
206
|
+
|-----------|------|---------|
|
|
207
|
+
| `template` | `CompiledTemplate` | — |
|
|
208
|
+
| `view` | `any` | — |
|
|
209
|
+
| `partials` | `{ [name: string]: () => Promise<CompiledTemplate> }` | `{}` |
|
|
210
|
+
| `escape` | `(s: string) => string` | built-in HTML escape |
|
|
211
|
+
|
|
212
|
+
### `renderAsync(template, view, partials?, escape?): Promise<string>`
|
|
213
|
+
|
|
214
|
+
Convenience wrapper around `renderCompiledAsync` that accepts a template
|
|
215
|
+
string and string / async-function partials.
|
|
216
|
+
|
|
217
|
+
| Parameter | Type | Default |
|
|
218
|
+
|-----------|------|---------|
|
|
219
|
+
| `template` | `string` | — |
|
|
220
|
+
| `view` | `any` | — |
|
|
221
|
+
| `partials` | `{ [name: string]: string \| (() => Promise<string>) }` | `{}` |
|
|
222
|
+
| `escape` | `(s: string) => string` | built-in HTML escape |
|
|
223
|
+
|
|
224
|
+
### `CompiledTemplate` (type)
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
type CompiledTemplate = (
|
|
228
|
+
view: any[],
|
|
229
|
+
partials: { [name: string]: CompiledTemplate },
|
|
230
|
+
blocks: { [name: string]: () => string },
|
|
231
|
+
escape: (string: string) => string
|
|
232
|
+
) => string;
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Mustache compatibility
|
|
238
|
+
|
|
239
|
+
| Feature | Supported |
|
|
240
|
+
|---------|-----------|
|
|
241
|
+
| Variables `{{name}}` | ✓ |
|
|
242
|
+
| Unescaped `{{{name}}}` / `{{& name}}` | ✓ |
|
|
243
|
+
| Sections `{{#name}}…{{/name}}` | ✓ |
|
|
244
|
+
| Inverted sections `{{^name}}…{{/name}}` | ✓ |
|
|
245
|
+
| Comments `{{! … }}` | ✓ |
|
|
246
|
+
| Partials `{{> name}}` | ✓ |
|
|
247
|
+
| Dynamic partials `{{> *name}}` | ✓ |
|
|
248
|
+
| Set delimiters `{{= <% %> =}}` | ✓ |
|
|
249
|
+
| Template inheritance `{{< parent}}` / `{{$ block}}` | ✓ (extension) |
|
|
250
|
+
| Lambda sections (raw text + render callback) | ✗ (optional spec feature) |
|
|
251
|
+
| Standalone tag whitespace stripping | ✗ |
|
|
252
|
+
| Partial indentation | ✗ |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
[MIT](LICENSE) © ABDK Consulting
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type CompiledTemplate = (view: any[], partials: {
|
|
2
|
+
[name: string]: CompiledTemplate;
|
|
3
|
+
}, blocks: {
|
|
4
|
+
[name: string]: () => string;
|
|
5
|
+
}, escape: (string: string) => string) => string;
|
|
6
|
+
export declare function compile(template: string): CompiledTemplate;
|
|
7
|
+
export declare function renderCompiled(template: CompiledTemplate, view: any, partials?: {
|
|
8
|
+
[name: string]: CompiledTemplate;
|
|
9
|
+
}, escape?: (string: string) => string): string;
|
|
10
|
+
export declare function render(template: string, view: any, partials?: {
|
|
11
|
+
[name: string]: string;
|
|
12
|
+
}, escape?: (string: string) => string): string;
|
|
13
|
+
export declare function renderCompiledAsync(template: CompiledTemplate, view: any, partials?: {
|
|
14
|
+
[name: string]: () => Promise<CompiledTemplate>;
|
|
15
|
+
}, escape?: (string: string) => string): Promise<string>;
|
|
16
|
+
export declare function renderAsync(template: string, view: any, partials?: {
|
|
17
|
+
[name: string]: string | (() => Promise<string>);
|
|
18
|
+
}, escape?: (string: string) => string): Promise<string>;
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAuBA,MAAM,MAAM,gBAAgB,GAAG,CAC7B,IAAI,EAAE,GAAG,EAAE,EACX,QAAQ,EAAE;IAAE,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAAA;CAAE,EAC9C,MAAM,EAAE;IAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,MAAM,CAAA;CAAE,EACxC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,KAAK,MAAM,CAAC;AAEhD,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CA0I1D;AAeD,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,GAAG,EACT,QAAQ,GAAE;IAAE,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAAA;CAAO,EACnD,MAAM,GAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAmB,GAC9C,MAAM,CAER;AAED,wBAAgB,MAAM,CACpB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,GAAG,EACT,QAAQ,GAAE;IAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAO,EACzC,MAAM,GAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAmB,GAC9C,MAAM,CAYR;AAED,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,GAAG,EACT,QAAQ,GAAE;IAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAA;CAAO,EAClE,MAAM,GAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAmB,GAC9C,OAAO,CAAC,MAAM,CAAC,CAiEjB;AAED,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,GAAG,EACT,QAAQ,GAAE;IAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;CAAO,EACnE,MAAM,GAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAmB,GAC9C,OAAO,CAAC,MAAM,CAAC,CAUjB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.compile = compile;
|
|
13
|
+
exports.renderCompiled = renderCompiled;
|
|
14
|
+
exports.render = render;
|
|
15
|
+
exports.renderCompiledAsync = renderCompiledAsync;
|
|
16
|
+
exports.renderAsync = renderAsync;
|
|
17
|
+
function throwError(error) {
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
function quoteString(string) {
|
|
21
|
+
return JSON.stringify(string);
|
|
22
|
+
}
|
|
23
|
+
function isIdentifier(string) {
|
|
24
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(string);
|
|
25
|
+
}
|
|
26
|
+
function escapeRegExp(string) {
|
|
27
|
+
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
28
|
+
}
|
|
29
|
+
function compileTagRegExp(leftDelimiter, rightDelimiter) {
|
|
30
|
+
const escapedLeftDelimiter = escapeRegExp(leftDelimiter);
|
|
31
|
+
const escapedRightDelimiter = escapeRegExp(rightDelimiter);
|
|
32
|
+
return new RegExp(`${escapedLeftDelimiter}\\s*(\\{[^}]*\\}\\s*|[^{\\s].*?)${escapedRightDelimiter}`, "g");
|
|
33
|
+
}
|
|
34
|
+
function compile(template) {
|
|
35
|
+
let code = ``;
|
|
36
|
+
const resolve = (path) => {
|
|
37
|
+
if (path === '.') {
|
|
38
|
+
code += `x=v[0];`;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const parts = path.split('.');
|
|
42
|
+
code += `x=o(${quoteString(parts[0])});`;
|
|
43
|
+
for (const part of parts) {
|
|
44
|
+
if (isIdentifier(part))
|
|
45
|
+
code += `x=x&&f(x.${part});`;
|
|
46
|
+
else
|
|
47
|
+
code += `x=x&&f(x[${quoteString(part)}]);`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
code += `const f=(x)=>typeof x==="function"?x():x;`;
|
|
52
|
+
code += `const a=(x)=>Array.isArray(x)?x:x?[x]:[];`;
|
|
53
|
+
code += `const s=(x)=>x==null?"":String(x);`;
|
|
54
|
+
code += `const S=(x)=>x==null?"":e(x);`;
|
|
55
|
+
code += `const o=(n)=>v.find(x=>x!=null&&typeof x==="object"&&n in x);`;
|
|
56
|
+
code += `let x;`;
|
|
57
|
+
code += `let r="";`;
|
|
58
|
+
let tagRegExp = compileTagRegExp('{{', '}}');
|
|
59
|
+
let index = 0;
|
|
60
|
+
let stack = [];
|
|
61
|
+
let inParent = false;
|
|
62
|
+
while (true) {
|
|
63
|
+
tagRegExp.lastIndex = index;
|
|
64
|
+
const match = tagRegExp.exec(template);
|
|
65
|
+
if (!match)
|
|
66
|
+
break;
|
|
67
|
+
if (!inParent && match.index > index)
|
|
68
|
+
code += `r+=${quoteString(template.slice(index, match.index))};`;
|
|
69
|
+
index = tagRegExp.lastIndex;
|
|
70
|
+
const tag = match[1].trim();
|
|
71
|
+
const tagMatch = tag.match(/^(?:=\s*([^=\s]+)\s+([^=\s]+)\s*=|#\s*(\S+)|\^\s*(\S+)|\/\s*(\S+)|\{\s*(\S+)\s*\}|&\s*(\S+)|!.*|>\s*\*\s*(\S+)|>\s*(\S+)|\$\s*(\S+)|<\s*\*\s*(\S+)|<\s*(\S+)|\s*(\S+))$/);
|
|
72
|
+
if (!tagMatch)
|
|
73
|
+
throw new Error(`Invalid tag: ${tag}`);
|
|
74
|
+
if (tagMatch[1] && tagMatch[2]) { // Set delimiters
|
|
75
|
+
tagRegExp = compileTagRegExp(tagMatch[1], tagMatch[2]);
|
|
76
|
+
}
|
|
77
|
+
else if (tagMatch[3]) { // Section
|
|
78
|
+
if (inParent)
|
|
79
|
+
throw new Error("Sections cannot be nested inside parents");
|
|
80
|
+
resolve(tagMatch[3]);
|
|
81
|
+
code += `x=a(x);`;
|
|
82
|
+
code += `for(const i of x){v.unshift(i);`;
|
|
83
|
+
stack.push([tagMatch[3], () => code += `v.shift();}`]);
|
|
84
|
+
}
|
|
85
|
+
else if (tagMatch[4]) { // Inverted section
|
|
86
|
+
if (inParent)
|
|
87
|
+
throw new Error("Sections cannot be nested inside parents");
|
|
88
|
+
resolve(tagMatch[4]);
|
|
89
|
+
code += `if(!a(x).length){`;
|
|
90
|
+
stack.push([tagMatch[4], () => code += `}`]);
|
|
91
|
+
}
|
|
92
|
+
else if (tagMatch[5]) { // End section
|
|
93
|
+
const [startTag, endCode] = stack.pop()
|
|
94
|
+
|| throwError(Error("Unmatched end tag: " + tagMatch[5]));
|
|
95
|
+
if (startTag !== tagMatch[5])
|
|
96
|
+
throw new Error(`Unmatched end tag: ${startTag} -> ${tagMatch[5]}`);
|
|
97
|
+
endCode();
|
|
98
|
+
}
|
|
99
|
+
else if (tagMatch[6]) { // Unescaped variable with {{{}}}
|
|
100
|
+
if (inParent)
|
|
101
|
+
throw new Error("Variables cannot be nested inside parents");
|
|
102
|
+
resolve(tagMatch[6]);
|
|
103
|
+
code += `r+=s(x);`;
|
|
104
|
+
}
|
|
105
|
+
else if (tagMatch[7]) { // Unescaped variable with {{& }}
|
|
106
|
+
if (inParent)
|
|
107
|
+
throw new Error("Variables cannot be nested inside parents");
|
|
108
|
+
resolve(tagMatch[7]);
|
|
109
|
+
code += `r+=s(x);`;
|
|
110
|
+
}
|
|
111
|
+
else if (tagMatch[8]) { // Dynamic partial
|
|
112
|
+
if (inParent)
|
|
113
|
+
throw new Error("Partials cannot be nested inside parents");
|
|
114
|
+
resolve(tagMatch[8]);
|
|
115
|
+
code += `x=p[x];`;
|
|
116
|
+
code += `r+=x?x(v,p,{},e):"";`;
|
|
117
|
+
}
|
|
118
|
+
else if (tagMatch[9]) { // Partial
|
|
119
|
+
if (inParent)
|
|
120
|
+
throw new Error("Partials cannot be nested inside parents");
|
|
121
|
+
if (isIdentifier(tagMatch[9]))
|
|
122
|
+
code += `x=p.${tagMatch[9]};`;
|
|
123
|
+
else
|
|
124
|
+
code += `x=p[${quoteString(tagMatch[9])}];`;
|
|
125
|
+
code += `r+=x?x(v,p,{},e):"";`;
|
|
126
|
+
}
|
|
127
|
+
else if (tagMatch[10]) { // Block
|
|
128
|
+
if (inParent) {
|
|
129
|
+
if (isIdentifier(tagMatch[10]))
|
|
130
|
+
code += `${tagMatch[10]}`;
|
|
131
|
+
else
|
|
132
|
+
code += `${quoteString(tagMatch[10])}`;
|
|
133
|
+
code += `:()=>{let r="";`;
|
|
134
|
+
inParent = false;
|
|
135
|
+
stack.push([tagMatch[10], () => {
|
|
136
|
+
code += `return r;},`;
|
|
137
|
+
inParent = true;
|
|
138
|
+
}]);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
if (isIdentifier(tagMatch[10]))
|
|
142
|
+
code += `x=b.${tagMatch[10]};`;
|
|
143
|
+
else
|
|
144
|
+
code += `x=b[${quoteString(tagMatch[10])}];`;
|
|
145
|
+
code += `if(x)r+=x();else{`;
|
|
146
|
+
stack.push([tagMatch[10], () => code += `}`]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (tagMatch[11]) { // Dynamic Parent
|
|
150
|
+
if (inParent)
|
|
151
|
+
throw new Error("Parents cannot be nested inside parents");
|
|
152
|
+
resolve(tagMatch[11]);
|
|
153
|
+
code += `x=p[x];`;
|
|
154
|
+
code += `if(x)r+=x(v,p,{`;
|
|
155
|
+
stack.push([tagMatch[11], () => code += `},e);`]);
|
|
156
|
+
}
|
|
157
|
+
else if (tagMatch[12]) { // Parent
|
|
158
|
+
if (inParent)
|
|
159
|
+
throw new Error("Parents cannot be nested inside parents");
|
|
160
|
+
if (isIdentifier(tagMatch[12]))
|
|
161
|
+
code += `x=p.${tagMatch[12]};`;
|
|
162
|
+
else
|
|
163
|
+
code += `x=p[${quoteString(tagMatch[12])}];`;
|
|
164
|
+
code += `if(x)r+=x(v,p,{`;
|
|
165
|
+
inParent = true;
|
|
166
|
+
stack.push([tagMatch[12], () => {
|
|
167
|
+
code += `},e);`;
|
|
168
|
+
inParent = false;
|
|
169
|
+
}]);
|
|
170
|
+
}
|
|
171
|
+
else if (tagMatch[13]) { // Variable
|
|
172
|
+
if (inParent)
|
|
173
|
+
throw new Error("Variables cannot be nested inside parents");
|
|
174
|
+
resolve(tagMatch[13]);
|
|
175
|
+
code += `r+=S(x);`;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// Comment, do nothing
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (stack.length > 0) {
|
|
182
|
+
const [startTag] = stack.pop();
|
|
183
|
+
throw new Error(`Unclosed tag: ${startTag}`);
|
|
184
|
+
}
|
|
185
|
+
code += `r+=${quoteString(template.slice(index))};`;
|
|
186
|
+
code += `return r;`;
|
|
187
|
+
try {
|
|
188
|
+
return new Function("v", "p", "b", "e", code);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (err instanceof SyntaxError) {
|
|
192
|
+
err.message += `\n${code}`;
|
|
193
|
+
}
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function escapeHTML(string) {
|
|
198
|
+
return String(string).replace(/[&<>"']/g, ch => {
|
|
199
|
+
switch (ch) {
|
|
200
|
+
case "&": return "&";
|
|
201
|
+
case "<": return "<";
|
|
202
|
+
case ">": return ">";
|
|
203
|
+
case '"': return """;
|
|
204
|
+
case "'": return "'";
|
|
205
|
+
default: throw Error("Unexpected character: " + ch);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function renderCompiled(template, view, partials = {}, escape = escapeHTML) {
|
|
210
|
+
return template([view], partials, {}, escape);
|
|
211
|
+
}
|
|
212
|
+
function render(template, view, partials = {}, escape = escapeHTML) {
|
|
213
|
+
const compiledPartials = {};
|
|
214
|
+
return renderCompiled(compile(template), view, Object.fromEntries(Object.entries(partials).map(([name, template]) => [name, (v, p, b, e) => {
|
|
215
|
+
if (!(name in compiledPartials)) {
|
|
216
|
+
compiledPartials[name] = compile(template);
|
|
217
|
+
}
|
|
218
|
+
return compiledPartials[name](v, p, b, e);
|
|
219
|
+
}])), escape);
|
|
220
|
+
}
|
|
221
|
+
function renderCompiledAsync(template_1, view_1) {
|
|
222
|
+
return __awaiter(this, arguments, void 0, function* (template, view, partials = {}, escape = escapeHTML) {
|
|
223
|
+
const values = new Map();
|
|
224
|
+
const pendingValues = new Map();
|
|
225
|
+
const wrapView = (view) => {
|
|
226
|
+
if (Array.isArray(view))
|
|
227
|
+
return view.map(wrapView);
|
|
228
|
+
else if (view && typeof view === "object")
|
|
229
|
+
return Object.fromEntries(Object.entries(view).map(([key, value]) => [key, wrapView(value)]));
|
|
230
|
+
else if (typeof view === "function") {
|
|
231
|
+
if (values.has(view))
|
|
232
|
+
return values.get(view);
|
|
233
|
+
else if (pendingValues.has(view))
|
|
234
|
+
return null;
|
|
235
|
+
else {
|
|
236
|
+
const promise = Promise.resolve(view());
|
|
237
|
+
pendingValues.set(view, promise);
|
|
238
|
+
promise.then(value => {
|
|
239
|
+
values.set(view, value);
|
|
240
|
+
pendingValues.delete(view);
|
|
241
|
+
}).catch(() => {
|
|
242
|
+
// Rejection is handled by Promise.all in the render loop;
|
|
243
|
+
// suppress the unhandled rejection on this detached chain.
|
|
244
|
+
});
|
|
245
|
+
return values.has(view) ? values.get(view) : null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else
|
|
249
|
+
return view;
|
|
250
|
+
};
|
|
251
|
+
const wrappedPartials = {};
|
|
252
|
+
const pendingPartials = {};
|
|
253
|
+
const wrapPartials = (partials) => {
|
|
254
|
+
return Object.fromEntries(Object.entries(partials)
|
|
255
|
+
.map(([name, partial]) => {
|
|
256
|
+
if (name in wrappedPartials)
|
|
257
|
+
return [name, wrappedPartials[name]];
|
|
258
|
+
else if (name in pendingPartials)
|
|
259
|
+
return null;
|
|
260
|
+
else {
|
|
261
|
+
const promise = Promise.resolve(partial());
|
|
262
|
+
pendingPartials[name] = promise;
|
|
263
|
+
promise.then(compiled => {
|
|
264
|
+
wrappedPartials[name] = compiled;
|
|
265
|
+
delete pendingPartials[name];
|
|
266
|
+
}).catch(() => {
|
|
267
|
+
// Rejection is handled by Promise.all in the render loop;
|
|
268
|
+
// suppress the unhandled rejection on this detached chain.
|
|
269
|
+
});
|
|
270
|
+
return name in wrappedPartials
|
|
271
|
+
? [name, wrappedPartials[name]]
|
|
272
|
+
: null;
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
.filter((entry) => entry !== null));
|
|
276
|
+
};
|
|
277
|
+
while (true) {
|
|
278
|
+
const result = template([wrapView(view)], wrapPartials(partials), {}, escape);
|
|
279
|
+
if (pendingValues.size === 0 && Object.keys(pendingPartials).length === 0)
|
|
280
|
+
return result;
|
|
281
|
+
yield Promise.all([
|
|
282
|
+
...pendingValues.values(),
|
|
283
|
+
...Object.values(pendingPartials)
|
|
284
|
+
]);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
function renderAsync(template_1, view_1) {
|
|
289
|
+
return __awaiter(this, arguments, void 0, function* (template, view, partials = {}, escape = escapeHTML) {
|
|
290
|
+
return renderCompiledAsync(compile(template), view, Object.fromEntries(Object.entries(partials).map(([name, template]) => [name, () => __awaiter(this, void 0, void 0, function* () {
|
|
291
|
+
return compile(typeof template === "function"
|
|
292
|
+
? yield template()
|
|
293
|
+
: template);
|
|
294
|
+
})
|
|
295
|
+
])), escape);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "abdk-mustache-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Mustache Template Engine for JavaScript by ABDK Consulting",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/index.js",
|
|
9
|
+
"dist/index.js.map",
|
|
10
|
+
"dist/index.d.ts",
|
|
11
|
+
"dist/index.d.ts.map",
|
|
12
|
+
"src/index.ts",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc --project tsconfig.json",
|
|
19
|
+
"build:watch": "tsc --project tsconfig.json --watch",
|
|
20
|
+
"test": "npm run build && node --test dist/test/index.test.js",
|
|
21
|
+
"lint": "tsc --project tsconfig.json --noEmit",
|
|
22
|
+
"prepublishOnly": "npm run build && npm test",
|
|
23
|
+
"clean": "rm -rf dist"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mustache",
|
|
27
|
+
"template"
|
|
28
|
+
],
|
|
29
|
+
"author": "ABDK Consulting",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/abdk-consulting/abdk-mustache-js.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/abdk-consulting/abdk-mustache-js/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/abdk-consulting/abdk-mustache-js#readme",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "20.0.0",
|
|
41
|
+
"typescript": "6.0.2"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
function throwError(error: Error): never {
|
|
2
|
+
throw error;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function quoteString(string: string): string {
|
|
6
|
+
return JSON.stringify(string);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isIdentifier(string: string): boolean {
|
|
10
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(string);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function escapeRegExp(string: string): string {
|
|
14
|
+
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function compileTagRegExp(leftDelimiter: string, rightDelimiter: string): RegExp {
|
|
18
|
+
const escapedLeftDelimiter = escapeRegExp(leftDelimiter);
|
|
19
|
+
const escapedRightDelimiter = escapeRegExp(rightDelimiter);
|
|
20
|
+
return new RegExp(`${escapedLeftDelimiter}\\s*(\\{[^}]*\\}\\s*|[^{\\s].*?)${escapedRightDelimiter}`,
|
|
21
|
+
"g");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type CompiledTemplate = (
|
|
25
|
+
view: any[],
|
|
26
|
+
partials: { [name: string]: CompiledTemplate },
|
|
27
|
+
blocks: { [name: string]: () => string },
|
|
28
|
+
escape: (string: string) => string) => string;
|
|
29
|
+
|
|
30
|
+
export function compile(template: string): CompiledTemplate {
|
|
31
|
+
let code = ``;
|
|
32
|
+
|
|
33
|
+
const resolve = (path: string) => {
|
|
34
|
+
if (path === '.') {
|
|
35
|
+
code += `x=v[0];`;
|
|
36
|
+
} else {
|
|
37
|
+
const parts = path.split('.');
|
|
38
|
+
code += `x=o(${quoteString(parts[0])});`;
|
|
39
|
+
for (const part of parts) {
|
|
40
|
+
if (isIdentifier(part))
|
|
41
|
+
code += `x=x&&f(x.${part});`;
|
|
42
|
+
else code += `x=x&&f(x[${quoteString(part)}]);`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
code += `const f=(x)=>typeof x==="function"?x():x;`;
|
|
48
|
+
code += `const a=(x)=>Array.isArray(x)?x:x?[x]:[];`;
|
|
49
|
+
code += `const s=(x)=>x==null?"":String(x);`;
|
|
50
|
+
code += `const S=(x)=>x==null?"":e(x);`;
|
|
51
|
+
code += `const o=(n)=>v.find(x=>x!=null&&typeof x==="object"&&n in x);`;
|
|
52
|
+
code += `let x;`;
|
|
53
|
+
code += `let r="";`;
|
|
54
|
+
|
|
55
|
+
let tagRegExp = compileTagRegExp('{{', '}}');
|
|
56
|
+
let index = 0;
|
|
57
|
+
let stack: [string, () => void][] = [];
|
|
58
|
+
let inParent = false;
|
|
59
|
+
while (true) {
|
|
60
|
+
tagRegExp.lastIndex = index;
|
|
61
|
+
const match = tagRegExp.exec(template);
|
|
62
|
+
if (!match) break;
|
|
63
|
+
if (!inParent && match.index > index)
|
|
64
|
+
code += `r+=${quoteString(template.slice(index, match.index))};`;
|
|
65
|
+
index = tagRegExp.lastIndex;
|
|
66
|
+
const tag = match[1].trim();
|
|
67
|
+
const tagMatch = tag.match(/^(?:=\s*([^=\s]+)\s+([^=\s]+)\s*=|#\s*(\S+)|\^\s*(\S+)|\/\s*(\S+)|\{\s*(\S+)\s*\}|&\s*(\S+)|!.*|>\s*\*\s*(\S+)|>\s*(\S+)|\$\s*(\S+)|<\s*\*\s*(\S+)|<\s*(\S+)|\s*(\S+))$/);
|
|
68
|
+
if (!tagMatch) throw new Error(`Invalid tag: ${tag}`);
|
|
69
|
+
if (tagMatch[1] && tagMatch[2]) { // Set delimiters
|
|
70
|
+
tagRegExp = compileTagRegExp(tagMatch[1], tagMatch[2]);
|
|
71
|
+
} else if (tagMatch[3]) { // Section
|
|
72
|
+
if (inParent) throw new Error("Sections cannot be nested inside parents");
|
|
73
|
+
resolve(tagMatch[3]);
|
|
74
|
+
code += `x=a(x);`;
|
|
75
|
+
code += `for(const i of x){v.unshift(i);`;
|
|
76
|
+
stack.push([tagMatch[3], () => code += `v.shift();}`]);
|
|
77
|
+
} else if (tagMatch[4]) { // Inverted section
|
|
78
|
+
if (inParent) throw new Error("Sections cannot be nested inside parents");
|
|
79
|
+
resolve(tagMatch[4]);
|
|
80
|
+
code += `if(!a(x).length){`;
|
|
81
|
+
stack.push([tagMatch[4], () => code += `}`]);
|
|
82
|
+
} else if (tagMatch[5]) { // End section
|
|
83
|
+
const [startTag, endCode] = stack.pop()
|
|
84
|
+
|| throwError(Error("Unmatched end tag: " + tagMatch[5]));
|
|
85
|
+
if (startTag !== tagMatch[5])
|
|
86
|
+
throw new Error(`Unmatched end tag: ${startTag} -> ${tagMatch[5]}`);
|
|
87
|
+
endCode();
|
|
88
|
+
} else if (tagMatch[6]) { // Unescaped variable with {{{}}}
|
|
89
|
+
if (inParent)
|
|
90
|
+
throw new Error("Variables cannot be nested inside parents");
|
|
91
|
+
resolve(tagMatch[6]);
|
|
92
|
+
code += `r+=s(x);`;
|
|
93
|
+
} else if (tagMatch[7]) { // Unescaped variable with {{& }}
|
|
94
|
+
if (inParent)
|
|
95
|
+
throw new Error("Variables cannot be nested inside parents");
|
|
96
|
+
resolve(tagMatch[7]);
|
|
97
|
+
code += `r+=s(x);`;
|
|
98
|
+
} else if (tagMatch[8]) { // Dynamic partial
|
|
99
|
+
if (inParent)
|
|
100
|
+
throw new Error("Partials cannot be nested inside parents");
|
|
101
|
+
resolve(tagMatch[8]);
|
|
102
|
+
code += `x=p[x];`;
|
|
103
|
+
code += `r+=x?x(v,p,{},e):"";`;
|
|
104
|
+
} else if (tagMatch[9]) { // Partial
|
|
105
|
+
if (inParent) throw new Error("Partials cannot be nested inside parents");
|
|
106
|
+
if (isIdentifier(tagMatch[9]))
|
|
107
|
+
code += `x=p.${tagMatch[9]};`;
|
|
108
|
+
else code += `x=p[${quoteString(tagMatch[9])}];`;
|
|
109
|
+
code += `r+=x?x(v,p,{},e):"";`;
|
|
110
|
+
} else if (tagMatch[10]) { // Block
|
|
111
|
+
if (inParent) {
|
|
112
|
+
if (isIdentifier(tagMatch[10]))
|
|
113
|
+
code += `${tagMatch[10]}`;
|
|
114
|
+
else code += `${quoteString(tagMatch[10])}`;
|
|
115
|
+
code += `:()=>{let r="";`;
|
|
116
|
+
inParent = false;
|
|
117
|
+
stack.push([tagMatch[10], () => {
|
|
118
|
+
code += `return r;},`;
|
|
119
|
+
inParent = true;
|
|
120
|
+
}]);
|
|
121
|
+
} else {
|
|
122
|
+
if (isIdentifier(tagMatch[10]))
|
|
123
|
+
code += `x=b.${tagMatch[10]};`;
|
|
124
|
+
else code += `x=b[${quoteString(tagMatch[10])}];`;
|
|
125
|
+
code += `if(x)r+=x();else{`;
|
|
126
|
+
stack.push([tagMatch[10], () => code += `}`]);
|
|
127
|
+
}
|
|
128
|
+
} else if (tagMatch[11]) { // Dynamic Parent
|
|
129
|
+
if (inParent) throw new Error("Parents cannot be nested inside parents");
|
|
130
|
+
resolve(tagMatch[11]);
|
|
131
|
+
code += `x=p[x];`;
|
|
132
|
+
code += `if(x)r+=x(v,p,{`;
|
|
133
|
+
stack.push([tagMatch[11], () => code += `},e);`]);
|
|
134
|
+
} else if (tagMatch[12]) { // Parent
|
|
135
|
+
if (inParent) throw new Error("Parents cannot be nested inside parents");
|
|
136
|
+
if (isIdentifier(tagMatch[12]))
|
|
137
|
+
code += `x=p.${tagMatch[12]};`;
|
|
138
|
+
else code += `x=p[${quoteString(tagMatch[12])}];`;
|
|
139
|
+
code += `if(x)r+=x(v,p,{`;
|
|
140
|
+
inParent = true;
|
|
141
|
+
stack.push([tagMatch[12], () => {
|
|
142
|
+
code += `},e);`;
|
|
143
|
+
inParent = false;
|
|
144
|
+
}]);
|
|
145
|
+
} else if (tagMatch[13]) { // Variable
|
|
146
|
+
if (inParent)
|
|
147
|
+
throw new Error("Variables cannot be nested inside parents");
|
|
148
|
+
resolve(tagMatch[13]);
|
|
149
|
+
code += `r+=S(x);`;
|
|
150
|
+
} else {
|
|
151
|
+
// Comment, do nothing
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (stack.length > 0) {
|
|
155
|
+
const [startTag] = stack.pop()!;
|
|
156
|
+
throw new Error(`Unclosed tag: ${startTag}`);
|
|
157
|
+
}
|
|
158
|
+
code += `r+=${quoteString(template.slice(index))};`;
|
|
159
|
+
code += `return r;`;
|
|
160
|
+
try {
|
|
161
|
+
return new Function("v", "p", "b", "e", code) as CompiledTemplate;
|
|
162
|
+
} catch (err: unknown) {
|
|
163
|
+
if (err instanceof SyntaxError) {
|
|
164
|
+
err.message += `\n${code}`;
|
|
165
|
+
}
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function escapeHTML(string: string): string {
|
|
171
|
+
return String(string).replace(/[&<>"']/g, ch => {
|
|
172
|
+
switch (ch) {
|
|
173
|
+
case "&": return "&";
|
|
174
|
+
case "<": return "<";
|
|
175
|
+
case ">": return ">";
|
|
176
|
+
case '"': return """;
|
|
177
|
+
case "'": return "'";
|
|
178
|
+
default: throw Error("Unexpected character: " + ch);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function renderCompiled(
|
|
184
|
+
template: CompiledTemplate,
|
|
185
|
+
view: any,
|
|
186
|
+
partials: { [name: string]: CompiledTemplate } = {},
|
|
187
|
+
escape: (string: string) => string = escapeHTML
|
|
188
|
+
): string {
|
|
189
|
+
return template([view], partials, {}, escape);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function render(
|
|
193
|
+
template: string,
|
|
194
|
+
view: any,
|
|
195
|
+
partials: { [name: string]: string } = {},
|
|
196
|
+
escape: (string: string) => string = escapeHTML
|
|
197
|
+
): string {
|
|
198
|
+
const compiledPartials: { [name: string]: CompiledTemplate } = {};
|
|
199
|
+
return renderCompiled(
|
|
200
|
+
compile(template),
|
|
201
|
+
view,
|
|
202
|
+
Object.fromEntries(Object.entries(partials).map(([name, template]) =>
|
|
203
|
+
[name, (v, p, b, e) => {
|
|
204
|
+
if (!(name in compiledPartials)) {
|
|
205
|
+
compiledPartials[name] = compile(template);
|
|
206
|
+
}
|
|
207
|
+
return compiledPartials[name](v, p, b, e);
|
|
208
|
+
}])), escape);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function renderCompiledAsync(
|
|
212
|
+
template: CompiledTemplate,
|
|
213
|
+
view: any,
|
|
214
|
+
partials: { [name: string]: () => Promise<CompiledTemplate> } = {},
|
|
215
|
+
escape: (string: string) => string = escapeHTML
|
|
216
|
+
): Promise<string> {
|
|
217
|
+
const values = new Map<Function, any>();
|
|
218
|
+
const pendingValues = new Map<Function, Promise<any>>();
|
|
219
|
+
|
|
220
|
+
const wrapView = (view: any): any => {
|
|
221
|
+
if (Array.isArray(view)) return view.map(wrapView);
|
|
222
|
+
else if (view && typeof view === "object")
|
|
223
|
+
return Object.fromEntries(Object.entries(view).map(([key, value]) =>
|
|
224
|
+
[key, wrapView(value)]));
|
|
225
|
+
else if (typeof view === "function") {
|
|
226
|
+
if (values.has(view)) return values.get(view);
|
|
227
|
+
else if (pendingValues.has(view)) return null;
|
|
228
|
+
else {
|
|
229
|
+
const promise = Promise.resolve(view());
|
|
230
|
+
pendingValues.set(view, promise);
|
|
231
|
+
promise.then(value => {
|
|
232
|
+
values.set(view, value);
|
|
233
|
+
pendingValues.delete(view);
|
|
234
|
+
}).catch(() => {
|
|
235
|
+
// Rejection is handled by Promise.all in the render loop;
|
|
236
|
+
// suppress the unhandled rejection on this detached chain.
|
|
237
|
+
});
|
|
238
|
+
return values.has(view) ? values.get(view) : null;
|
|
239
|
+
}
|
|
240
|
+
} else return view;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const wrappedPartials: { [name: string]: CompiledTemplate } = {};
|
|
244
|
+
const pendingPartials: { [name: string]: Promise<CompiledTemplate> } = {};
|
|
245
|
+
|
|
246
|
+
const wrapPartials = (partials: {
|
|
247
|
+
[name: string]: () => Promise<CompiledTemplate>
|
|
248
|
+
}) => {
|
|
249
|
+
return Object.fromEntries(
|
|
250
|
+
Object.entries(partials)
|
|
251
|
+
.map(([name, partial]) => {
|
|
252
|
+
if (name in wrappedPartials) return [name, wrappedPartials[name]];
|
|
253
|
+
else if (name in pendingPartials) return null;
|
|
254
|
+
else {
|
|
255
|
+
const promise = Promise.resolve(partial());
|
|
256
|
+
pendingPartials[name] = promise;
|
|
257
|
+
promise.then(compiled => {
|
|
258
|
+
wrappedPartials[name] = compiled;
|
|
259
|
+
delete pendingPartials[name];
|
|
260
|
+
}).catch(() => {
|
|
261
|
+
// Rejection is handled by Promise.all in the render loop;
|
|
262
|
+
// suppress the unhandled rejection on this detached chain.
|
|
263
|
+
});
|
|
264
|
+
return name in wrappedPartials
|
|
265
|
+
? [name, wrappedPartials[name]]
|
|
266
|
+
: null;
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
.filter((entry): entry is [string, CompiledTemplate] => entry !== null)
|
|
270
|
+
);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
while (true) {
|
|
274
|
+
const result = template([wrapView(view)], wrapPartials(partials), {}, escape);
|
|
275
|
+
if (pendingValues.size === 0 && Object.keys(pendingPartials).length === 0) return result;
|
|
276
|
+
await Promise.all([
|
|
277
|
+
...pendingValues.values(),
|
|
278
|
+
...Object.values(pendingPartials)
|
|
279
|
+
]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function renderAsync(
|
|
284
|
+
template: string,
|
|
285
|
+
view: any,
|
|
286
|
+
partials: { [name: string]: string | (() => Promise<string>) } = {},
|
|
287
|
+
escape: (string: string) => string = escapeHTML
|
|
288
|
+
): Promise<string> {
|
|
289
|
+
return renderCompiledAsync(
|
|
290
|
+
compile(template),
|
|
291
|
+
view,
|
|
292
|
+
Object.fromEntries(Object.entries(partials).map(([name, template]) =>
|
|
293
|
+
[name, async () =>
|
|
294
|
+
compile(typeof template === "function"
|
|
295
|
+
? await template()
|
|
296
|
+
: template)
|
|
297
|
+
])), escape);
|
|
298
|
+
}
|