zzz-template 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +444 -0
- package/fs.d.ts +13 -0
- package/fs.js +15 -0
- package/index.d.ts +20 -0
- package/index.js +49 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Evgeny Sinitsyn
|
|
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,444 @@
|
|
|
1
|
+
# zzz-template
|
|
2
|
+
|
|
3
|
+
> The fastest, simplest JavaScript template engine with zero dependencies
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/zzz-template)
|
|
6
|
+
[](https://bundlephobia.com/package/zzz-template)
|
|
7
|
+
[](https://github.com/cuhuak/zzz-template/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
**zzz-template** is an ultra-lightweight JavaScript template engine that leverages native template literals for maximum performance. A fast, hackable alternative to EJS, Handlebars, and Mustache that works in both Node.js and browsers.
|
|
10
|
+
|
|
11
|
+
## Why zzz-template?
|
|
12
|
+
|
|
13
|
+
| Feature | zzz-template | EJS | Handlebars |
|
|
14
|
+
|---------|-------------|-----|------------|
|
|
15
|
+
| Size (min+gzip) | ~500 bytes | ~6KB | ~17KB |
|
|
16
|
+
| Dependencies | 0 | 1 | 0 |
|
|
17
|
+
| Performance | 24M ops/sec | 247K ops/sec | - |
|
|
18
|
+
| Browser + Node.js | Yes | Yes | Yes |
|
|
19
|
+
| Template Literals | Native | No | No |
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- **Echo variables**: `${data.user.name}`
|
|
24
|
+
- **Layouts**: Set layout in child template `${LAYOUT("layout.html")}`, echo content `${data.content}` in `layout.html`
|
|
25
|
+
- **Include (partial) templates**: `${INCLUDE('partial.html', data)}`
|
|
26
|
+
- **Local variables**: `${SET('title', 'Hello world')}`, then use it in template: `${local.title}`
|
|
27
|
+
- **Blazing fast**: Matches vanilla JavaScript performance (24M ops/sec)
|
|
28
|
+
- **Zero dependencies**: No bloat, no supply chain risk
|
|
29
|
+
- **Tiny footprint**: ~50 lines of code, ~500 bytes minified + gzipped
|
|
30
|
+
- **Hackable**: Easy to extend with plugins
|
|
31
|
+
- **Isomorphic**: Works on server (Node.js) and browser
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install zzz-template
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Compile example (see [examples/00-compile](examples/00-compile))
|
|
40
|
+
``` javascript
|
|
41
|
+
// file examples/00-compile/example.js
|
|
42
|
+
import {ZzzBrowser} from "zzz-template"
|
|
43
|
+
|
|
44
|
+
const zzz = new ZzzBrowser()
|
|
45
|
+
const fn = zzz.compile('Hello ${data.name}') // returns function that renders your template using data: `fn(data)`
|
|
46
|
+
console.log(fn({name: 'Jerry'})); // > "Hello Jerry"
|
|
47
|
+
console.log(fn({name: 'Tom'})); // > "Hello Tom"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Basic example (browser), render `<script>` template (see [examples/01-basic](examples/01-basic))
|
|
51
|
+
``` html
|
|
52
|
+
<!-- file examples/01-basic/page.html -->
|
|
53
|
+
<script id="template" type="plain/text">
|
|
54
|
+
<p>
|
|
55
|
+
Hello ${data.name}
|
|
56
|
+
</p>
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<script type="module">
|
|
60
|
+
import { ZzzBrowser } from '/zzz-template/index.js'
|
|
61
|
+
|
|
62
|
+
const zzz = new ZzzBrowser()
|
|
63
|
+
|
|
64
|
+
const result = zzz.render('template', { name: 'world' })
|
|
65
|
+
|
|
66
|
+
console.log(result)
|
|
67
|
+
|
|
68
|
+
document.body.innerHTML = result
|
|
69
|
+
</script>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Basic example (server), render `file` template (see [examples/02-basic](examples/02-basic))
|
|
73
|
+
``` html
|
|
74
|
+
<!-- file examples/02-basic/template.html -->
|
|
75
|
+
<p>
|
|
76
|
+
Hello ${data.name}
|
|
77
|
+
</p>
|
|
78
|
+
```
|
|
79
|
+
``` javascript
|
|
80
|
+
// file examples/02-basic/example.js
|
|
81
|
+
import { ZzzFs } from 'zzz-template/fs.js'
|
|
82
|
+
|
|
83
|
+
const zzz = new ZzzFs({ dir: import.meta.dirname })
|
|
84
|
+
const result = zzz.render('template.html', { name: 'Jerry' })
|
|
85
|
+
console.log(result)
|
|
86
|
+
// OUTPUT:
|
|
87
|
+
// <p>
|
|
88
|
+
// Hello Jerry
|
|
89
|
+
// </p>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Include (partial)
|
|
93
|
+
* `useInclude(zzz)` to enable include feature
|
|
94
|
+
* `${INCLUDE('partial', {name: 'universe'})}` to include `partial` template
|
|
95
|
+
|
|
96
|
+
### Example include (browser) (see [examples/03-include](examples/03-include))
|
|
97
|
+
``` html
|
|
98
|
+
<!-- file examples/03-include/page.html -->
|
|
99
|
+
<script type="module">
|
|
100
|
+
import { ZzzBrowser, useInclude } from '/zzz-template/index.js'
|
|
101
|
+
|
|
102
|
+
const zzz = new ZzzBrowser()
|
|
103
|
+
useInclude(zzz) // 👈 enables include feature
|
|
104
|
+
|
|
105
|
+
const result = zzz.render('template', { name: 'world' })
|
|
106
|
+
|
|
107
|
+
document.body.innerHTML = result
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<script id="template" type="plain/text">
|
|
111
|
+
<p>
|
|
112
|
+
Hello ${data.name}!
|
|
113
|
+
</p>
|
|
114
|
+
|
|
115
|
+
${INCLUDE('partial', {name: 'universe'})}
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<script id="partial" type="plain/text">
|
|
119
|
+
<p>
|
|
120
|
+
Hey ${data.name}!
|
|
121
|
+
</p>
|
|
122
|
+
</script>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Layouts
|
|
126
|
+
* `useLayout(zzz)` to enable layouts feature
|
|
127
|
+
* `${LAYOUT('layout', {name: 'universe'})}` to set layout in `template`
|
|
128
|
+
* `${data.content}` to echo content (result of `template`) in layout `layout`
|
|
129
|
+
|
|
130
|
+
### Example Layout (see [examples/06-layout](examples/06-layout))
|
|
131
|
+
``` html
|
|
132
|
+
<!-- file examples/06-layout/layouts.html -->
|
|
133
|
+
<script type="module">
|
|
134
|
+
import { ZzzBrowser, useLayout } from '/zzz-template/index.js'
|
|
135
|
+
|
|
136
|
+
const zzz = new ZzzBrowser()
|
|
137
|
+
useLayout(zzz) // 👈 enables layout feature
|
|
138
|
+
|
|
139
|
+
const result = zzz.render('template', { name: 'world' })
|
|
140
|
+
|
|
141
|
+
document.body.innerHTML = result
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<script id="template" type="plain/text">
|
|
145
|
+
${LAYOUT('layout', {name: 'universe'})}
|
|
146
|
+
<p>
|
|
147
|
+
Hello ${data.name}!
|
|
148
|
+
</p>
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<script id="layout" type="plain/text">
|
|
152
|
+
Hey ${data.name}!
|
|
153
|
+
<div>
|
|
154
|
+
${data.content}
|
|
155
|
+
</div>
|
|
156
|
+
</script>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Layout template itself can set global layout, and global layout can set more global layout, etc.
|
|
160
|
+
See example how layout can include each other: `examples/06-layout/layouts2.html`. Please note the example also uses local vars feature.
|
|
161
|
+
|
|
162
|
+
### Example Layout Advanced (see [examples/06-layout](examples/06-layout))
|
|
163
|
+
``` html
|
|
164
|
+
<!-- file examples/06-layout/layouts2.html -->
|
|
165
|
+
<script id="template" type="plain/text">
|
|
166
|
+
${LAYOUT('layout')}
|
|
167
|
+
${SET('title', 'My page')}
|
|
168
|
+
<p>
|
|
169
|
+
Hello ${data.name}
|
|
170
|
+
</p>
|
|
171
|
+
</script>
|
|
172
|
+
|
|
173
|
+
<script id="layout" type="plain/text">
|
|
174
|
+
${LAYOUT('global')}
|
|
175
|
+
<div style="background: #eee; padding: 1em; margin: 1em 0;">
|
|
176
|
+
<div>layout begin</div>
|
|
177
|
+
<section class="layout">
|
|
178
|
+
${data.content}
|
|
179
|
+
</section>
|
|
180
|
+
<div>layout end</div>
|
|
181
|
+
</div>
|
|
182
|
+
</script>
|
|
183
|
+
|
|
184
|
+
<script id="global" type="plain/text">
|
|
185
|
+
<div style="background: #ffb; padding: 1em;">
|
|
186
|
+
<nav>
|
|
187
|
+
<a href="#">link1</a>
|
|
188
|
+
<a href="#">link2</a>
|
|
189
|
+
<a href="#">link3</a>
|
|
190
|
+
</nav>
|
|
191
|
+
<h1>${local.title}</h1>
|
|
192
|
+
${data.content}
|
|
193
|
+
<footer>Footer</footer>
|
|
194
|
+
</div>
|
|
195
|
+
</script>
|
|
196
|
+
|
|
197
|
+
<script type="module">
|
|
198
|
+
import { ZzzBrowser, useLayout, useLocal } from '/zzz-template/index.js'
|
|
199
|
+
|
|
200
|
+
const zzz = new ZzzBrowser()
|
|
201
|
+
useLayout(zzz) // 👈 enables layout feature
|
|
202
|
+
useLocal(zzz) // 👈 enables local vars feature
|
|
203
|
+
|
|
204
|
+
const result = zzz.render('template', { name: 'world' })
|
|
205
|
+
|
|
206
|
+
console.log(result)
|
|
207
|
+
|
|
208
|
+
document.body.innerHTML = result
|
|
209
|
+
</script>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Conditions IF
|
|
213
|
+
* `useIfMap(zzz)` to enable "if, each" feature
|
|
214
|
+
* `${IF(data.n === 42, 'Hello ${data.name}!', {name: 'world'})}` to echo string on condition
|
|
215
|
+
* you may want to pass data into template
|
|
216
|
+
|
|
217
|
+
### Example Condition IF (see [examples/04-if](examples/04-if))
|
|
218
|
+
``` html
|
|
219
|
+
<!-- file examples/04-if/if.html -->
|
|
220
|
+
<script type="module">
|
|
221
|
+
import { ZzzBrowser, useIfMap } from '/zzz-template/index.js'
|
|
222
|
+
|
|
223
|
+
const zzz = new ZzzBrowser()
|
|
224
|
+
useIfMap(zzz) // 👈 enables "if/map" feature
|
|
225
|
+
|
|
226
|
+
const result = zzz.render('template', { name: 'world', n: 42 })
|
|
227
|
+
|
|
228
|
+
document.body.innerHTML = result
|
|
229
|
+
</script>
|
|
230
|
+
|
|
231
|
+
<script id="template" type="plain/text">
|
|
232
|
+
<p>
|
|
233
|
+
Hello ${data.name}!
|
|
234
|
+
</p>
|
|
235
|
+
|
|
236
|
+
${IF(data.n === 42, 'Hello ${data.n}!', {n: data.n})}
|
|
237
|
+
|
|
238
|
+
<hr>
|
|
239
|
+
|
|
240
|
+
${IF(data.n === 42, `
|
|
241
|
+
Hello ${data.n}! <br>
|
|
242
|
+
${IF(data.n % 2 === 0, `
|
|
243
|
+
doubled ${data.n} is \${data.number}.
|
|
244
|
+
`, {number: data.n * 2})}
|
|
245
|
+
`)}
|
|
246
|
+
</script>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Conditions IFI
|
|
250
|
+
* `useIfMap(zzz)` to enable "if, each" feature
|
|
251
|
+
* `${IFI(condition, 'template', data)}`: if `condition` then include `template` with `data`
|
|
252
|
+
* do not forget to pass data
|
|
253
|
+
|
|
254
|
+
### Example Condition IFI: `if (condition) include` (see [examples/04-if](examples/04-if))
|
|
255
|
+
``` html
|
|
256
|
+
<!-- file examples/04-if/ifi.html -->
|
|
257
|
+
<script type="module">
|
|
258
|
+
import { ZzzBrowser, useIfMap } from '/zzz-template/index.js'
|
|
259
|
+
|
|
260
|
+
const zzz = new ZzzBrowser()
|
|
261
|
+
useIfMap(zzz) // 👈 enables "if/map" feature
|
|
262
|
+
|
|
263
|
+
const result = zzz.render('template', { name: 'world', n: 42 })
|
|
264
|
+
|
|
265
|
+
document.body.innerHTML = result
|
|
266
|
+
</script>
|
|
267
|
+
|
|
268
|
+
<script id="template" type="plain/text">
|
|
269
|
+
<p>
|
|
270
|
+
Hello ${data.name}!
|
|
271
|
+
</p>
|
|
272
|
+
|
|
273
|
+
${IFI(data.n % 2 == 0, 'partial', {n: data.n})}
|
|
274
|
+
</script>
|
|
275
|
+
|
|
276
|
+
<script id="partial" type="plain/text">
|
|
277
|
+
<p>
|
|
278
|
+
${data.n} is even!
|
|
279
|
+
</p>
|
|
280
|
+
</script>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Iterate, loop, map, for (include template for elements)
|
|
284
|
+
* `useIfMap(zzz)` to enable "if, each" feature
|
|
285
|
+
* `${MAP('template string', elements)}`: for each el in elements render `template string` w/ el as data
|
|
286
|
+
|
|
287
|
+
### Example MAP (see [examples/05-map](examples/05-map))
|
|
288
|
+
``` html
|
|
289
|
+
<!-- file examples/05-map/map.html -->
|
|
290
|
+
<script type="module">
|
|
291
|
+
import { ZzzBrowser, useIfMap } from '/zzz-template/index.js'
|
|
292
|
+
|
|
293
|
+
const zzz = new ZzzBrowser()
|
|
294
|
+
useIfMap(zzz) // 👈 enables "if/map" feature
|
|
295
|
+
|
|
296
|
+
const pets = [{ name: 'cat', say: 'meow' }, { name: 'dog', say: 'woof' }]
|
|
297
|
+
const result = zzz.render('template', { name: 'John Doe', pets })
|
|
298
|
+
|
|
299
|
+
document.body.innerHTML = result
|
|
300
|
+
</script>
|
|
301
|
+
|
|
302
|
+
<script id="template" type="plain/text">
|
|
303
|
+
// [1] using javascript .map
|
|
304
|
+
<ul>
|
|
305
|
+
${data.pets.map(x => TEMPLATE('<li>${data.name}</li>', x)).join('')}
|
|
306
|
+
</ul>
|
|
307
|
+
// [2] using MAP and string
|
|
308
|
+
<ul>
|
|
309
|
+
${MAP(data.pets, '<li>${data.name}</li>')}
|
|
310
|
+
</ul>
|
|
311
|
+
// [2'] using MAP and string template (note that dollar-sign ($) is escaped)
|
|
312
|
+
<ul>
|
|
313
|
+
${MAP(data.pets, `<li>\${data.name}</li>`)}
|
|
314
|
+
</ul>
|
|
315
|
+
// [3] using MAPI (map include) to include template for each element
|
|
316
|
+
<ul>
|
|
317
|
+
${MAPI(data.pets, 'pet')}
|
|
318
|
+
</ul>
|
|
319
|
+
</script>
|
|
320
|
+
|
|
321
|
+
<script id="pet" type="plain/text">
|
|
322
|
+
<li>${data.name} (says: ${data.say})</li>
|
|
323
|
+
</script>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Extend and hack
|
|
327
|
+
ZzzTemplate already has a few built-in plugins. A plugin is just a function that monkey-patches the ZzzTemplate instance.
|
|
328
|
+
For instance, you can inject your code into the compile function. Here is a 'trim' example:
|
|
329
|
+
|
|
330
|
+
``` javascript
|
|
331
|
+
import {ZzzBrowser, useContentTrim} from "zzz-template"
|
|
332
|
+
|
|
333
|
+
const zzz = new ZzzBrowser()
|
|
334
|
+
useContentTrim(zzz)
|
|
335
|
+
const fn = zzz.compile(' Hello ${data.name} ')
|
|
336
|
+
// note that result is trimmed
|
|
337
|
+
const result = fn({name: 'Tom'})
|
|
338
|
+
console.log(result); // > "Hello Tom"
|
|
339
|
+
```
|
|
340
|
+
Here is the code of `useContentTrim` (built-in)
|
|
341
|
+
``` javascript
|
|
342
|
+
function useContentTrim(zzz) {
|
|
343
|
+
zzz.e.push('content = content.trim();')
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
This function pushes a code snippet to the end array that will be invoked after the template content is compiled.
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
Or you may want to introduce a new var in your templates.
|
|
350
|
+
|
|
351
|
+
``` javascript
|
|
352
|
+
import {ZzzBrowser, useContentTrim} from "zzz-template"
|
|
353
|
+
|
|
354
|
+
const zzz = new ZzzBrowser()
|
|
355
|
+
|
|
356
|
+
// zzz.s -- s means start (before template content compiled)
|
|
357
|
+
zzz.s.push('let $$ = data;') // introduce `$$` for `data`
|
|
358
|
+
// zzz.e -- e means end (after template content compiled)
|
|
359
|
+
zzz.e.push('content = content.trim();')
|
|
360
|
+
const fn = zzz.compile(' Hello ${$$.name} ') // we use new name `$$` for `data`
|
|
361
|
+
const result = fn({name: 'Tom'})
|
|
362
|
+
console.log(result); // > "Hello Tom"
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Example that introduces `ESCAPE` function that escapes string (see [examples/10-extend](examples/10-extend))
|
|
366
|
+
``` html
|
|
367
|
+
<!-- file examples/10-extend/escape.html -->
|
|
368
|
+
<script type="module">
|
|
369
|
+
import { ZzzBrowser, useFn } from '/zzz-template/index.js'
|
|
370
|
+
|
|
371
|
+
const zzz = new ZzzBrowser()
|
|
372
|
+
|
|
373
|
+
function escapeHtml (unsafe) {
|
|
374
|
+
return unsafe
|
|
375
|
+
.replaceAll("&", "&")
|
|
376
|
+
.replaceAll("<", "<")
|
|
377
|
+
.replaceAll(">", ">")
|
|
378
|
+
.replaceAll('"', """)
|
|
379
|
+
.replaceAll("'", "'")
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
useFn(zzz, escapeHtml, 'ESCAPE')
|
|
383
|
+
const evilString = 'John<img src="" onerror=alert("Boo!")>'
|
|
384
|
+
|
|
385
|
+
const result = zzz.render('template', { name: evilString })
|
|
386
|
+
|
|
387
|
+
console.log(result)
|
|
388
|
+
document.body.innerHTML = result
|
|
389
|
+
</script>
|
|
390
|
+
|
|
391
|
+
<script id="template" type="plain/text">
|
|
392
|
+
<p>
|
|
393
|
+
Hello ${ESCAPE(data.name)}!
|
|
394
|
+
</p>
|
|
395
|
+
</script>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Fast
|
|
399
|
+
|
|
400
|
+
Fastest JS engine ever :) That is true, see the benchmark results (ran on the author's old Intel i7):
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+
--------- Benchmark Render ---------
|
|
404
|
+
vanilla render x 24,478,910 ops/sec ±1.23% (91 runs sampled)
|
|
405
|
+
zzz render x 24,256,470 ops/sec ±1.25% (90 runs sampled)
|
|
406
|
+
literal render x 16,843,920 ops/sec ±1.63% (89 runs sampled)
|
|
407
|
+
zup render x 2,738,409 ops/sec ±1.43% (91 runs sampled)
|
|
408
|
+
ejs render x 247,632 ops/sec ±2.10% (91 runs sampled)
|
|
409
|
+
dot render x 1,096,741 ops/sec ±0.58% (93 runs sampled)
|
|
410
|
+
edge render x 8,037 ops/sec ±1.80% (90 runs sampled)
|
|
411
|
+
Fastest is vanilla render, zzz render
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Try to run benchmarks
|
|
415
|
+
```
|
|
416
|
+
# go to bench
|
|
417
|
+
cd bench
|
|
418
|
+
|
|
419
|
+
# install deps
|
|
420
|
+
npm i
|
|
421
|
+
|
|
422
|
+
# run
|
|
423
|
+
npm run bench
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Security
|
|
427
|
+
Do not render templates (by filename) that come from user input, or values in templates that come from user input—it is dangerous.
|
|
428
|
+
And if you do, please make sure you:
|
|
429
|
+
- provide a secure `zzz.read` function that reads files from a specified directory, not `../../../secret.passwords`
|
|
430
|
+
- escape all user input to prevent XSS attacks
|
|
431
|
+
|
|
432
|
+
## License
|
|
433
|
+
MIT
|
|
434
|
+
|
|
435
|
+
## Related
|
|
436
|
+
|
|
437
|
+
Looking for JavaScript template engines? Here are some alternatives:
|
|
438
|
+
- [EJS](https://www.npmjs.com/package/ejs) - Embedded JavaScript templates
|
|
439
|
+
- [Handlebars](https://www.npmjs.com/package/handlebars) - Semantic templates
|
|
440
|
+
- [Mustache](https://www.npmjs.com/package/mustache) - Logic-less templates
|
|
441
|
+
- [doT](https://www.npmjs.com/package/dot) - Fast template engine
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
Docs revision: 2025-11-24T08:05:23.013Z
|
package/fs.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ZzzBase } from "./index.js";
|
|
2
|
+
|
|
3
|
+
export * from './index.js';
|
|
4
|
+
|
|
5
|
+
export interface ZzzFsOptions extends Record<string, any> {
|
|
6
|
+
dir?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ZzzFs extends ZzzBase {
|
|
10
|
+
_dir: string;
|
|
11
|
+
constructor($this?: ZzzFsOptions);
|
|
12
|
+
read(f: string): string;
|
|
13
|
+
}
|
package/fs.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {readFileSync} from 'node:fs'
|
|
2
|
+
import {join} from "node:path"
|
|
3
|
+
import {ZzzBase} from "./index.js"
|
|
4
|
+
|
|
5
|
+
export * from './index.js'
|
|
6
|
+
|
|
7
|
+
export class ZzzFs extends ZzzBase {
|
|
8
|
+
constructor($this = {}) {
|
|
9
|
+
super($this)
|
|
10
|
+
this._dir = $this.dir || ''
|
|
11
|
+
}
|
|
12
|
+
read (f) {
|
|
13
|
+
return readFileSync(join(this._dir, f), "utf8")
|
|
14
|
+
}
|
|
15
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class ZzzBase {
|
|
2
|
+
s: string[];
|
|
3
|
+
e: string[];
|
|
4
|
+
$: Record<string, any>;
|
|
5
|
+
constructor($this?: Record<string, any>);
|
|
6
|
+
compile(str: string, local?: Record<string, any>, sign?: string): (data: any, parent?: any) => string;
|
|
7
|
+
render(template: string, data: any, local?: Record<string, any>): string;
|
|
8
|
+
read(f: string): string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ZzzBrowser extends ZzzBase {
|
|
12
|
+
read(f: string): string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useFn(zzz: ZzzBase, fn: Function, alias?: string | false): void;
|
|
16
|
+
export function useContentTrim(zzz: ZzzBase): void;
|
|
17
|
+
export function useInclude(zzz: ZzzBase, alias?: string): void;
|
|
18
|
+
export function useLayout(zzz: ZzzBase, alias?: string): void;
|
|
19
|
+
export function useLocal(zzz: ZzzBase, aliasSet?: string, aliasSeta?: string): void;
|
|
20
|
+
export function useIfMap(zzz: ZzzBase, aliases?: boolean): void;
|
package/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export class ZzzBase {
|
|
2
|
+
s = [] // s - start, before content
|
|
3
|
+
e = [] // e - end, after content
|
|
4
|
+
constructor($this = {}) {
|
|
5
|
+
this.$ = $this
|
|
6
|
+
}
|
|
7
|
+
compile(str, local, sign = 'data, parent') {
|
|
8
|
+
const fn = new Function(sign, `${this.s.join(';')}let content=\`${str}\`;${this.e.join(';')}return content;`)
|
|
9
|
+
return fn.bind({...this.$, local}) // shallow copy of $
|
|
10
|
+
}
|
|
11
|
+
render(template, data, local = {}) {
|
|
12
|
+
return this.compile(this.read(template), {...local})(data) // shallow copy of local
|
|
13
|
+
}
|
|
14
|
+
read(f) {} // must be implemented
|
|
15
|
+
}
|
|
16
|
+
export class ZzzBrowser extends ZzzBase {
|
|
17
|
+
read (f) {
|
|
18
|
+
return window.document.getElementById(f).innerText
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function useFn(zzz, fn, alias) {
|
|
22
|
+
zzz.$[fn.name] = fn
|
|
23
|
+
if (alias) zzz.s.push(`let ${alias} = this.${fn.name}.bind(this);`)
|
|
24
|
+
}
|
|
25
|
+
export function useContentTrim(zzz) {
|
|
26
|
+
zzz.e.push('content = content.trim();')
|
|
27
|
+
}
|
|
28
|
+
export function useInclude(zzz, alias = 'INCLUDE') {
|
|
29
|
+
useFn(zzz, function include(file, data) {return zzz.compile(zzz.read(file), this.local)(data)}, alias)
|
|
30
|
+
}
|
|
31
|
+
export function useLayout(zzz, alias = 'LAYOUT') {
|
|
32
|
+
zzz.$['include'] || useInclude(zzz)
|
|
33
|
+
zzz.e.push('if(this._layout)return this.include(this._layout.t,{...this._layout.d,content});')
|
|
34
|
+
useFn(zzz, function layout(t, d) {this._layout={t,d};return ''}, alias)
|
|
35
|
+
}
|
|
36
|
+
export function useLocal(zzz, aliasSet = 'SET', aliasSeta = 'SETA') {
|
|
37
|
+
zzz.s.push('let local = this.local;')
|
|
38
|
+
zzz.$.local = {}
|
|
39
|
+
useFn(zzz, function set(key, values) {this.local[key] = values;return ''}, aliasSet)
|
|
40
|
+
useFn(zzz, function seta(key, ...values) {this.local[key] = [(this.local[key] ?? []), ...values].flat();return ''}, aliasSeta)
|
|
41
|
+
}
|
|
42
|
+
export function useIfMap(zzz, aliases = true) {
|
|
43
|
+
zzz.$['include'] || useInclude(zzz)
|
|
44
|
+
useFn(zzz, function template(str, data) {return zzz.compile(str, this.local)(data)}, aliases && 'TEMPLATE')
|
|
45
|
+
useFn(zzz, function if_template(cond, str, data) {return cond ? zzz.compile(str, this.local)(data) : ''}, aliases && 'IF')
|
|
46
|
+
useFn(zzz, function if_include(cond, file, data) {return cond ? this.include(file, data) : ''}, aliases && 'IFI')
|
|
47
|
+
useFn(zzz, function map_template(arr, str) {return arr.map(x => {return zzz.compile(str, this.local)(x)}).join('')}, aliases && 'MAP')
|
|
48
|
+
useFn(zzz, function map_include(arr, file) {return arr.map(x => {return this.include(file, x)}).join('')}, aliases && 'MAPI')
|
|
49
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zzz-template",
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Fastest JavaScript template engine using native template literals. Zero dependencies, ~500 bytes, works in Node.js and browser. EJS/Handlebars alternative.",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Evgeny Sinitsyn",
|
|
8
|
+
"email": "cuhuak@gmail.com"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"template",
|
|
12
|
+
"template-engine",
|
|
13
|
+
"template-literal",
|
|
14
|
+
"template-literals",
|
|
15
|
+
"javascript-template",
|
|
16
|
+
"html-template",
|
|
17
|
+
"render",
|
|
18
|
+
"layout",
|
|
19
|
+
"partial",
|
|
20
|
+
"include",
|
|
21
|
+
"fast",
|
|
22
|
+
"lightweight",
|
|
23
|
+
"zero-dependency",
|
|
24
|
+
"browser",
|
|
25
|
+
"nodejs",
|
|
26
|
+
"isomorphic",
|
|
27
|
+
"ejs-alternative",
|
|
28
|
+
"handlebars-alternative",
|
|
29
|
+
"mustache-alternative",
|
|
30
|
+
"string-interpolation"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/cuhuak/zzz-template.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/cuhuak/zzz-template/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/cuhuak/zzz-template#readme",
|
|
41
|
+
"main": "index.js",
|
|
42
|
+
"types": "index.d.ts",
|
|
43
|
+
"files": [
|
|
44
|
+
"index.js",
|
|
45
|
+
"index.d.ts",
|
|
46
|
+
"fs.js",
|
|
47
|
+
"fs.d.ts",
|
|
48
|
+
"readme.md",
|
|
49
|
+
"LICENSE"
|
|
50
|
+
],
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"exports": {
|
|
55
|
+
".": {
|
|
56
|
+
"types": "./index.d.ts",
|
|
57
|
+
"import": "./index.js",
|
|
58
|
+
"require": "./index.js"
|
|
59
|
+
},
|
|
60
|
+
"./fs.js": {
|
|
61
|
+
"types": "./fs.d.ts",
|
|
62
|
+
"import": "./fs.js",
|
|
63
|
+
"require": "./fs.js"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|