sjabloon 0.5.1 → 0.7.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/LICENSE +201 -21
- package/README.md +77 -20
- package/dist/core.js +1 -0
- package/dist/html.d.ts +17 -0
- package/dist/html.js +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +1 -1
- package/dist/text.d.ts +17 -0
- package/dist/text.js +1 -0
- package/dist/types.d.ts +83 -0
- package/package.json +42 -17
- package/dist/index.cjs +0 -1
- package/index.d.ts +0 -68
- package/src/index.js +0 -274
package/LICENSE
CHANGED
|
@@ -1,21 +1,201 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# sjabloon
|
|
2
2
|
|
|
3
|
-
A tiny, CSP-safe template engine for JavaScript. **~1.
|
|
3
|
+
A tiny, CSP-safe, target-neutral template engine for JavaScript. **~1.9KB min+brotli (~3.7KB with [xprsn](https://www.npmjs.com/package/xprsn)), one dependency.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/sjabloon)
|
|
6
|
-
[](https://github.com/getquario/sjabloon/actions/workflows/test.yml)
|
|
7
7
|
[](https://www.npmjs.com/package/sjabloon)
|
|
8
|
-
[](https://github.com/getquario/sjabloon/blob/main/LICENSE)
|
|
9
9
|
|
|
10
10
|
<a href="https://webstronauts.com?utm_source=github&utm_medium=readme&utm_campaign=sjabloon">
|
|
11
11
|
<picture>
|
|
@@ -13,16 +13,24 @@ A tiny, CSP-safe template engine for JavaScript. **~1.8KB min+gzip (~3.6KB with
|
|
|
13
13
|
</picture>
|
|
14
14
|
</a>
|
|
15
15
|
|
|
16
|
-
*Sjabloon* is Dutch for "template". It renders
|
|
16
|
+
*Sjabloon* is Dutch for "template". It renders templates with full [xprsn](https://github.com/getquario/xprsn) expressions inside every tag, without turning template text into JavaScript. There is no `eval` and no `new Function`, so it runs under a strict Content Security Policy where engines that compile templates to code cannot.
|
|
17
|
+
|
|
18
|
+
The engine emits **tokens**, not text. A render gives you the literal runs and the interpolated values, interleaved in render order — because different targets need different things from the same template. HTML wants escaped text; a spreadsheet wants the number `1000` and a cell format; a PDF wants styled runs. Escaping belongs at the output edge, not in the engine, so a string is just one way to consume the stream.
|
|
17
19
|
|
|
18
20
|
```js
|
|
19
|
-
import { template,
|
|
21
|
+
import { template, text } from 'sjabloon';
|
|
22
|
+
|
|
23
|
+
const cell = template('{{ total * 1.21 }}');
|
|
24
|
+
|
|
25
|
+
cell({ total: 1000 }); // => [{ value: 1210 }] — still a number
|
|
26
|
+
text(cell({ total: 1000 })); // => '1210' — when you want the string
|
|
27
|
+
```
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
If you only want a string, import the edition that produces one directly:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { render } from 'sjabloon/html'; // {{ }} HTML-escapes, {{{ }}} is raw
|
|
24
33
|
|
|
25
|
-
// Blocks, expressions, and custom functions:
|
|
26
34
|
render(
|
|
27
35
|
`<ul>{{#each items as it, i}}
|
|
28
36
|
<li>{{ i + 1 }}. {{ it.name }}: {{ fmt(it.price * it.qty) }}</li>
|
|
@@ -33,11 +41,25 @@ render(
|
|
|
33
41
|
);
|
|
34
42
|
```
|
|
35
43
|
|
|
44
|
+
## Editions
|
|
45
|
+
|
|
46
|
+
Three entry points, one engine. They share a parser, a syntax, and a diagnostics contract, and differ only in what a render produces.
|
|
47
|
+
|
|
48
|
+
| Import | `template(str, funcs?)` returns | `{{ expr }}` | `{{{ expr }}}` |
|
|
49
|
+
| --- | --- | --- | --- |
|
|
50
|
+
| `sjabloon` | `(values?, scope?) => Token[]` | value token | `SyntaxError` |
|
|
51
|
+
| `sjabloon/text` | `(values?, scope?) => string` | unescaped | `SyntaxError` |
|
|
52
|
+
| `sjabloon/html` | `(values?, scope?) => string` | HTML-escaped | raw |
|
|
53
|
+
|
|
54
|
+
`{{{ }}}` exists only in the HTML edition, where "raw" means something. Everywhere else `{{ }}` is already raw, so the triple form is a compile-time `SJABLOON_RAW_TAG` error rather than a silent synonym.
|
|
55
|
+
|
|
56
|
+
Every edition exports `template`, `render`, and `isDiagnostic`. They all resolve to one shared core, so a diagnostic thrown through any of them authenticates through all of them.
|
|
57
|
+
|
|
36
58
|
## API
|
|
37
59
|
|
|
38
60
|
### `template(str, functions?)`
|
|
39
61
|
|
|
40
|
-
Compiles the template and returns a renderer
|
|
62
|
+
Compiles the template and returns a renderer. What it renders to depends on the edition you imported from (see [Editions](#editions)); everything else on this page is identical across all three. Malformed tags, unclosed blocks, and invalid expressions throw a `SyntaxError` at compile time.
|
|
41
63
|
|
|
42
64
|
The anchors `$` (root) and `@` (current `{{#each}}` item) work as [described below](#syntax) with no extra arguments — at the root, before any loop, both point at `values`. If you're embedding sjabloon under an engine with its own scope model, pass `{ root, item }` as the second argument to seed the two root anchors from distinct objects: `$` becomes `root` and `@` becomes `item`. Omit `item` and `@` stays unbound at the root, so reading `@.x` throws where there is no current item. Either way, `{{#each}}` still re-points `@` to the current item inside its body.
|
|
43
65
|
|
|
@@ -47,7 +69,7 @@ tpl(base, { root: reportRoot, item: currentRow }); // $ = reportRoot, @ = curren
|
|
|
47
69
|
tpl(base, { root: reportRoot }); // no item → @.x throws
|
|
48
70
|
```
|
|
49
71
|
|
|
50
|
-
The renderer
|
|
72
|
+
The renderer carries `names` (every variable the template reads from your values, loop variables excluded) and `functions` (the registry functions it calls, methods excluded), both deduplicated. Check a stored template against your data model and its allowed functions before you render it, or fetch only the fields it needs.
|
|
51
73
|
|
|
52
74
|
```js
|
|
53
75
|
const tpl = template('{{ fmt(title) }}{{#each items as it}}{{ it.name }}{{/each}}', { fmt: s => s });
|
|
@@ -57,7 +79,28 @@ tpl.functions; // => ['fmt']
|
|
|
57
79
|
|
|
58
80
|
### `render(str, values?, functions?)`
|
|
59
81
|
|
|
60
|
-
Shorthand for `template(str, functions)(values)
|
|
82
|
+
Shorthand for `template(str, functions)(values)`, returning whatever its edition renders.
|
|
83
|
+
|
|
84
|
+
### `text(tokens)` — root entry only
|
|
85
|
+
|
|
86
|
+
Joins a token stream the way `sjabloon/text` would have rendered it: literals verbatim, values as `String(value ?? '')`. `text(template(str)(values))` and `sjabloon/text`'s `template(str)(values)` are equal for every template and every set of values — a property the test suite and the fuzzer both check.
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
import { template, text } from 'sjabloon';
|
|
90
|
+
|
|
91
|
+
const tokens = template('{{ qty }} × {{ name }}')({ qty: 2, name: 'Koffie' });
|
|
92
|
+
// => [{ value: 2 }, { literal: ' × ' }, { value: 'Koffie' }]
|
|
93
|
+
|
|
94
|
+
text(tokens); // => '2 × Koffie'
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A `Token` is either `{ literal: string }` or `{ value: unknown }`:
|
|
98
|
+
|
|
99
|
+
- **Values are pre-stringify.** `{{ total }}` holding `1000` yields the number `1000`, not `"1000"`, and nullish stays nullish. Stringification is deferred to `text()` — so a value with no primitive conversion reaches the stream intact and only fails when something asks for text.
|
|
100
|
+
- **Order is render order.** Loop bodies append once per iteration, untaken branches append nothing, and block expressions (`#if` conditions, `#each` collections) never appear — they steer the render rather than being part of it.
|
|
101
|
+
- **Literals are the template's static runs**, one token each, never merged and never empty. So the interleaving tells you the shape: a bare `{{ amount }}` is exactly one value token, while `Total: {{ amount }}` is a literal followed by a value. That distinction is why the engine emits tokens instead of a string plus a list of values — a spreadsheet cell that is *only* a number is a different thing from one that happens to contain one.
|
|
102
|
+
|
|
103
|
+
Literal tokens are frozen and shared across loop iterations; value tokens are fresh per emit.
|
|
61
104
|
|
|
62
105
|
### Error diagnostics
|
|
63
106
|
|
|
@@ -68,18 +111,18 @@ Sjabloon errors keep their native `SyntaxError` or `TypeError` class and expose:
|
|
|
68
111
|
- `end`: the exclusive template offset;
|
|
69
112
|
- `blocks`: a frozen, outermost-first array of `{ type, start, end }` opener spans.
|
|
70
113
|
|
|
71
|
-
Parser codes are `SJABLOON_EACH_SYNTAX`, `SJABLOON_BLOCKED_BINDING`, `SJABLOON_UNEXPECTED_TAG`, `SJABLOON_UNKNOWN_BLOCK`, `SJABLOON_UNCLOSED_BLOCK`, and `SJABLOON_TOO_DEEP` (block nesting past 256 levels, located at the opener that crossed the cap). A missing closer uses an empty span at the end of the template. Expression offsets refer to the original template, so surrounding braces, whitespace, and trim markers contribute to their absolute position.
|
|
114
|
+
Parser codes are `SJABLOON_EACH_SYNTAX`, `SJABLOON_BLOCKED_BINDING`, `SJABLOON_UNEXPECTED_TAG`, `SJABLOON_UNKNOWN_BLOCK`, `SJABLOON_UNCLOSED_BLOCK`, `SJABLOON_RAW_TAG` (a `{{{ }}}` tag outside the HTML edition, located at the whole tag), and `SJABLOON_TOO_DEEP` (block nesting past 256 levels, located at the opener that crossed the cap). A missing closer uses an empty span at the end of the template. Expression offsets refer to the original template, so surrounding braces, whitespace, and trim markers contribute to their absolute position.
|
|
72
115
|
|
|
73
116
|
Unauthenticated errors thrown by registered functions, getters, methods, or value coercion hooks are host errors. Sjabloon passes them through unchanged and does not attach template diagnostic fields.
|
|
74
117
|
|
|
75
|
-
Use `isDiagnostic(error)` when a host needs to distinguish those errors. It returns `true` only for errors produced or translated by the same sjabloon module instance. Copying a documented `code`, `start`, `end`, and `blocks` onto another error does not authenticate it. A diagnostic from another installed copy or module instance
|
|
118
|
+
Use `isDiagnostic(error)` when a host needs to distinguish those errors. It returns `true` only for errors produced or translated by the same sjabloon module instance. Copying a documented `code`, `start`, `end`, and `blocks` onto another error does not authenticate it. A diagnostic from another installed copy or module instance returns `false`. All three editions share one core, so mixing them in a single process is safe: an error thrown through `sjabloon/html` authenticates through `sjabloon`.
|
|
76
119
|
|
|
77
120
|
## Syntax
|
|
78
121
|
|
|
79
122
|
| Tag | Meaning |
|
|
80
123
|
| --- | --- |
|
|
81
|
-
| `{{ expr }}` | Interpolate an expression,
|
|
82
|
-
| `{{{ expr }}}` | Interpolate
|
|
124
|
+
| `{{ expr }}` | Interpolate an expression — a value token, or text escaped per edition |
|
|
125
|
+
| `{{{ expr }}}` | Interpolate raw. **`sjabloon/html` only**; a `SyntaxError` elsewhere |
|
|
83
126
|
| `{{#if expr}} … {{#elif expr}} … {{#else}} … {{/if}}` | Conditional block, with as many `{{#elif}}` links as you need |
|
|
84
127
|
| `{{#each expr as item}} … {{/each}}` | Loop over an array or an object's values |
|
|
85
128
|
| `{{#each expr as item, key}} … {{/each}}` | Second name binds the index (arrays) or the key (objects) |
|
|
@@ -88,7 +131,7 @@ Use `isDiagnostic(error)` when a host needs to distinguish those errors. It retu
|
|
|
88
131
|
| `{{! anything }}` | Comment, removed from output |
|
|
89
132
|
| `{{- expr -}}` | A dash hugging either brace trims the whitespace on that side, newlines included; works on every tag form |
|
|
90
133
|
|
|
91
|
-
Every `expr` is an [xprsn expression](https://github.com/
|
|
134
|
+
Every `expr` is an [xprsn expression](https://github.com/getquario/xprsn#syntax): literals, arithmetic, string concatenation with `~` (`{{ first ~ " " ~ last }}`), comparisons, `and`/`or`/`not`/`in`, ternaries, property and method access, and functions from the registry you pass in. `null` and `undefined` render as empty strings.
|
|
92
135
|
|
|
93
136
|
A loop body sees its loop variable plus the outer scope; reusing an outer name shadows it only inside that body. The engine keeps loop variables on a child scope, so the values you pass are never mutated.
|
|
94
137
|
|
|
@@ -119,15 +162,29 @@ That runtime CSP support costs some render speed. Handlebars and tempura generat
|
|
|
119
162
|
|
|
120
163
|
## Safety
|
|
121
164
|
|
|
122
|
-
- `
|
|
165
|
+
- `sjabloon/html` escapes `& < > " '` in `{{ }}`; unescaped output requires the explicit `{{{ }}}` form. **If you are rendering HTML, import that edition.** The other two are output-neutral by design and escape nothing, on the assumption that you escape at your own output edge.
|
|
123
166
|
- Expressions inherit all of xprsn's guards: no `__proto__`/`constructor`/`prototype` access, null-prototype hash literals, and functions resolved only from your registry.
|
|
124
167
|
- Templates read your values; they cannot assign to them.
|
|
125
168
|
- Registered functions are host-provided capabilities, not a sandbox boundary. Only register helpers that template authors are allowed to invoke; likewise, treat explicit raw output as trusted HTML.
|
|
126
169
|
|
|
127
170
|
## Environments
|
|
128
171
|
|
|
129
|
-
Node.js 22 and newer
|
|
172
|
+
Node.js 22.12 and newer, ESM only. Browser use is supported through a standards-based ESM bundler in environments supporting ES2024. Direct `<script>` globals, UMD, and CommonJS builds are not provided.
|
|
173
|
+
|
|
174
|
+
Shipping CommonJS alongside ESM would put two copies of the core in any process that mixed `require` and `import`, and therefore two diagnostic identities — `isDiagnostic` would silently return `false` across the seam. One format removes that failure mode instead of documenting it.
|
|
130
175
|
|
|
131
176
|
## License
|
|
132
177
|
|
|
133
|
-
|
|
178
|
+
Copyright 2026 Robin van der Vleuten
|
|
179
|
+
|
|
180
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
|
+
you may not use this file except in compliance with the License.
|
|
182
|
+
You may obtain a copy of the License at
|
|
183
|
+
|
|
184
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
185
|
+
|
|
186
|
+
Unless required by applicable law or agreed to in writing, software
|
|
187
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
188
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
189
|
+
See the License for the specific language governing permissions and
|
|
190
|
+
limitations under the License.
|
package/dist/core.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{compile as e,isDiagnostic as t}from"xprsn";const n=/^(?:__proto__|constructor|prototype)$/,r=new WeakSet,i=r.add.bind(r),a=r.has.bind(r),o=e=>a(e);let s=e=>{let t=[];for(let n=0,r=1;n<e.length;){let i=e.indexOf(`{{`,n);if(i<0){t.push([0,e.slice(n)]);break}i>n&&t.push([0,e.slice(n,i)]);let a=+(e[i+2]===`{`),o=i+2+a,s=e[o]===`-`,c=-1;if(s&&o++,a&&r&&(c=e.indexOf(`}}}`,o),c<0&&(r=0)),c<0&&(a&&(a=0,o=i+2,s=e[o]===`-`,s&&o++),c=e.indexOf(`}}`,o)),c<0){t.push([0,e.slice(i)]);break}let l=c>o&&e[c-1]===`-`,u=l?c-1:c,d=e.slice(o,u),f=d.trim(),p=o+d.length-d.trimStart().length,m=c+2+a,h=[a?1:2,f,i,m,p],g=t.at(-1);if(s&&g?.[0]===0&&g[1]&&(g[1]=g[1].trimEnd()),t.push(h),n=m,l)for(;/\s/.test(e[n]);)n++}return t},c,l,u,d,f,p,m,h,g,_,v,y,b=()=>Object.freeze(g.slice()),x=(e,t)=>(g.length<256||C(`Template too deeply nested`,`SJABLOON_TOO_DEEP`,t),Object.freeze({type:e,start:t[2],end:t[3]})),S=(e,t)=>(Object.defineProperty(e,"blocks",{value:t,enumerable:!0}),i(e),e);const C=(e,t,n,r=n?.[2]??h.length,i=n?.[3]??h.length)=>{let a=SyntaxError(e);throw a.code=t,a.start=r,a.end=i,S(a,b())};let w=(e,n,r,i=t)=>{throw i(e)?(e.start+=n,e.end+=n,S(e,r)):e},T=e=>C(`Unexpected {{`+e[1]+`}}`,`SJABLOON_UNEXPECTED_TAG`,e),E=(e,t,n)=>{for(let r of e)r(t,n)},D=(e,t)=>t(O(e[1],e[4],b())),O=(t,n,r)=>{let i;try{i=e(t,u)}catch(e){w(e,n,r)}for(let e of i.names)f.includes(e)||p.add(e);for(let e of i.functions)m.add(e);return e=>{try{return i(e)}catch(e){w(e,n,r,i.isDiagnostic)}}},k=e=>{let t=A([`#elif`,`#else`,`/if`]),n=d[1],r=[];return n.startsWith(`#elif `)?r=[k(O(n.slice(6),d[4]+6,b()))]:n===`#else`?(r=A([`/if`]),d[1]===`/if`||T(d)):n!==`/if`&&T(d),(n,i)=>E(e(n)?t:r,n,i)},A=e=>{let t=[];for(let r;r=c[l++];){let i=r[1];if(!r[0])i&&t.push(_(i));else if(r[0]===1)y||C(`Raw {{{`+i+`}}} is not available here; {{ `+i+` }} is already raw`,`SJABLOON_RAW_TAG`,r),t.push(D(r,y));else if(e.includes(i.split(` `)[0]))return d=r,t;else if(i[0]!==`!`)if(i.startsWith(`#if `))g.push(x(`if`,r)),t.push(k(O(i.slice(4),r[4]+4,b()))),g.pop();else if(/^#each(?:\s|$)/.test(i)){g.push(x(`each`,r));let e=/^#each ([\s\S]+) as ((\w+)(?:\s*,\s*(\w+))?)$/.exec(i)||C(`Bad {{`+i+`}}`,`SJABLOON_EACH_SYNTAX`,r),a=e[3],o=e[4],s=r[4]+i.length-e[2].length;if(n.test(a)&&C(`Bad {{`+i+`}}`,`SJABLOON_BLOCKED_BINDING`,r,s,s+a.length),o&&n.test(o)){let e=r[4]+i.length-o.length;C(`Bad {{`+i+`}}`,`SJABLOON_BLOCKED_BINDING`,r,e,e+o.length)}let c=O(e[1],r[4]+6,b()),l=f.length;f.push(a),o&&f.push(o),f.push(`loop`);let u=A([`#else`,`/each`]);f.length=l;let p=[];d[1]===`#else`?(p=A([`/each`]),d[1]===`/each`||T(d)):d[1]!==`/each`&&T(d),g.pop(),t.push((e,t)=>{let n=c(e),r=Array.isArray(n),i=r?n.slice():n&&typeof n==`object`?Object.keys(n).map(e=>[n[e],e]):[];if(!i.length)return E(p,e,t);i.forEach((n,s)=>{let c=r?n:n[0],l=r?s:n[1],d=Object.create(e);d[a]=c,o&&(d[o]=l),d[`@`]=c,d.loop={index:s+1,index0:s,first:!s,last:s===i.length-1,length:i.length},E(u,d,t)})})}else/^#(?:if|elif|else)(?:\s|$)/.test(i)||i[0]===`/`?T(r):i[0]===`#`?C(`Unknown {{`+i+`}}`,`SJABLOON_UNKNOWN_BLOCK`,r):t.push(D(r,v))}return e.length&&C(`Missing {{`+e[e.length-1]+`}}`,`SJABLOON_UNCLOSED_BLOCK`),t},j=([e,t,n,r,i])=>{function a(a,o){_=e,v=t,y=n,u=o,f=[`$`,`@`],p=new Set,m=new Set,h=String(a),g=[],c=s(h),l=0;let d;try{d=A([])}catch(e){throw e instanceof RangeError&&C(`Template too deeply nested`,`SJABLOON_TOO_DEEP`),e}let b=(e,t)=>{e||={};let n=Object.create(e);n.$=t?t.root:e,t?`item`in t&&(n[`@`]=t.item):n[`@`]=e;let a=r();return E(d,n,a),i(a)};return b.names=Array.from(p),b.functions=Array.from(m),b}return{template:a,render:(e,t,n)=>a(e,n)(t)}};export{j as n,o as t};
|
package/dist/html.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compile a template once, render it many times to an HTML string.
|
|
6
|
+
*
|
|
7
|
+
* `{{ expr }}` HTML-escapes (`& < > " '`) and `{{{ expr }}}` interpolates raw.
|
|
8
|
+
* This is the only edition that knows what HTML is; the rest of sjabloon leaves
|
|
9
|
+
* escaping to the output edge.
|
|
10
|
+
*
|
|
11
|
+
* @see SjabloonRenderer for `names`/`functions`, SjabloonScope for `$` and `@`.
|
|
12
|
+
* @throws {SyntaxError} On malformed tags, unclosed blocks, or bad expressions.
|
|
13
|
+
*/
|
|
14
|
+
export function template(str: string, funcs?: SjabloonFunctions): SjabloonRenderer<string>;
|
|
15
|
+
|
|
16
|
+
/** Compile and render in one go. Shorthand for `template(str, funcs)(values)`. */
|
|
17
|
+
export function render(str: string, values?: SjabloonValues, funcs?: SjabloonFunctions): string;
|
package/dist/html.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{n as e,t}from"./core.js";const n={"&":`&`,"<":`<`,">":`>`,'"':`"`,"'":`'`},r=e=>String(e).replace(/[&<>"']/g,e=>n[e]),{template:i,render:a}=e([e=>(t,n)=>{n.s+=e},e=>(t,n,i)=>(i=e(t),n.s+=r(i??``)),e=>(t,n,r)=>(r=e(t),n.s+=String(r??``)),()=>({s:``}),e=>e.s]);export{t as isDiagnostic,a as render,i as template};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues, Token } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compile a template once, render it many times to a token stream.
|
|
6
|
+
*
|
|
7
|
+
* `{{ expr }}` emits a value token; escaping belongs to whoever consumes the
|
|
8
|
+
* stream, so `{{{ expr }}}` has no meaning here and is a compile-time
|
|
9
|
+
* `SJABLOON_RAW_TAG` error.
|
|
10
|
+
*
|
|
11
|
+
* @see SjabloonRenderer for `names`/`functions`, SjabloonScope for `$` and `@`.
|
|
12
|
+
* @throws {SyntaxError} On malformed tags, unclosed blocks, or bad expressions.
|
|
13
|
+
*/
|
|
14
|
+
export function template(str: string, funcs?: SjabloonFunctions): SjabloonRenderer<Token[]>;
|
|
15
|
+
|
|
16
|
+
/** Compile and render in one go. Shorthand for `template(str, funcs)(values)`. */
|
|
17
|
+
export function render(str: string, values?: SjabloonValues, funcs?: SjabloonFunctions): Token[];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Join a token stream into the string `sjabloon/text` would have produced:
|
|
21
|
+
* literals verbatim, values as `String(value ?? '')`.
|
|
22
|
+
*/
|
|
23
|
+
export function text(tokens: readonly Token[]): string;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{n as e,t}from"./core.js";const{template:n,render:r}=e([e=>(e=>(t,n)=>{n.push(e)})(Object.freeze({literal:e})),e=>(t,n)=>{n.push({value:e(t)})},0,()=>[],e=>e]),i=e=>{let t=``;for(let n of e)t+=n.literal??String(n.value??``);return t};export{t as isDiagnostic,r as render,n as template,i as text};
|
package/dist/text.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compile a template once, render it many times to a plain string.
|
|
6
|
+
*
|
|
7
|
+
* `{{ expr }}` interpolates unescaped — escaping belongs at the output edge —
|
|
8
|
+
* so `{{{ expr }}}` has no meaning here and is a compile-time
|
|
9
|
+
* `SJABLOON_RAW_TAG` error.
|
|
10
|
+
*
|
|
11
|
+
* @see SjabloonRenderer for `names`/`functions`, SjabloonScope for `$` and `@`.
|
|
12
|
+
* @throws {SyntaxError} On malformed tags, unclosed blocks, or bad expressions.
|
|
13
|
+
*/
|
|
14
|
+
export function template(str: string, funcs?: SjabloonFunctions): SjabloonRenderer<string>;
|
|
15
|
+
|
|
16
|
+
/** Compile and render in one go. Shorthand for `template(str, funcs)(values)`. */
|
|
17
|
+
export function render(str: string, values?: SjabloonValues, funcs?: SjabloonFunctions): string;
|
package/dist/text.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{n as e,t}from"./core.js";const{template:n,render:r}=e([e=>(t,n)=>{n.s+=e},e=>(t,n,r)=>(r=e(t),n.s+=String(r??``)),0,()=>({s:``}),e=>e.s]);export{t as isDiagnostic,r as render,n as template};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { XprsnErrorCode } from 'xprsn';
|
|
2
|
+
|
|
3
|
+
export type SjabloonErrorCode =
|
|
4
|
+
| XprsnErrorCode
|
|
5
|
+
| 'SJABLOON_EACH_SYNTAX'
|
|
6
|
+
| 'SJABLOON_BLOCKED_BINDING'
|
|
7
|
+
| 'SJABLOON_UNEXPECTED_TAG'
|
|
8
|
+
| 'SJABLOON_UNKNOWN_BLOCK'
|
|
9
|
+
| 'SJABLOON_UNCLOSED_BLOCK'
|
|
10
|
+
| 'SJABLOON_TOO_DEEP'
|
|
11
|
+
| 'SJABLOON_RAW_TAG';
|
|
12
|
+
|
|
13
|
+
export interface SjabloonBlock {
|
|
14
|
+
readonly type: 'if' | 'each';
|
|
15
|
+
readonly start: number;
|
|
16
|
+
readonly end: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SjabloonDiagnostic extends Error {
|
|
20
|
+
readonly code: SjabloonErrorCode;
|
|
21
|
+
readonly start: number;
|
|
22
|
+
readonly end: number;
|
|
23
|
+
readonly blocks: readonly SjabloonBlock[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SjabloonValues = Record<string, any>;
|
|
27
|
+
export type SjabloonFunctions = Record<string, Function>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Per-render override of the scope anchors, for embedders with their own scope
|
|
31
|
+
* model.
|
|
32
|
+
*
|
|
33
|
+
* Two anchors are always in scope: `$` is the root values, and `@` is the
|
|
34
|
+
* current `#each` item (the root outside any loop). They let a nested loop
|
|
35
|
+
* reach the root (`$.company`) or the current item (`@.total`) explicitly,
|
|
36
|
+
* past any shadowing. Neither counts as a `name`.
|
|
37
|
+
*
|
|
38
|
+
* Passing this object as the renderer's second argument makes `$` become
|
|
39
|
+
* `root` and `@` become `item` (two distinct objects). Omit `item` to leave
|
|
40
|
+
* `@` unbound, so reading `@.x` throws through xprsn's guard — a group-header
|
|
41
|
+
* band that has no current row wants exactly that.
|
|
42
|
+
*/
|
|
43
|
+
export interface SjabloonScope {
|
|
44
|
+
root?: any;
|
|
45
|
+
item?: any;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A compiled template: render it many times.
|
|
50
|
+
*
|
|
51
|
+
* `names` are the variables the template reads from your values, deduplicated;
|
|
52
|
+
* loop variables the template introduces are not included. `functions` are the
|
|
53
|
+
* registry functions the template calls, deduplicated.
|
|
54
|
+
*/
|
|
55
|
+
export interface SjabloonRenderer<T> {
|
|
56
|
+
(values?: SjabloonValues, scope?: SjabloonScope): T;
|
|
57
|
+
names: string[];
|
|
58
|
+
functions: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One static text run of the template, verbatim. */
|
|
62
|
+
export interface LiteralToken {
|
|
63
|
+
literal: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One `{{ }}` interpolation, pre-stringify. Nullish values are preserved. */
|
|
67
|
+
export interface ValueToken {
|
|
68
|
+
value: unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A render's output in the token edition, in render order: loop bodies append
|
|
73
|
+
* once per iteration, untaken branches append nothing, and block expressions
|
|
74
|
+
* (`#if` conditions, `#each` collections) never appear.
|
|
75
|
+
*/
|
|
76
|
+
export type Token = LiteralToken | ValueToken;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check whether an error was produced or translated by sjabloon. Every entry
|
|
80
|
+
* shares one core, so a diagnostic thrown through any of them authenticates
|
|
81
|
+
* through all of them.
|
|
82
|
+
*/
|
|
83
|
+
export function isDiagnostic(error: unknown): error is SjabloonDiagnostic;
|
package/package.json
CHANGED
|
@@ -1,42 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sjabloon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Tiny, CSP-safe template engine for JavaScript, powered by xprsn expressions. No eval, no new Function.",
|
|
5
5
|
"repository": "getquario/sjabloon",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Robin van der Vleuten",
|
|
9
9
|
"email": "robin@webstronauts.com",
|
|
10
10
|
"url": "https://robinvdvleuten.nl"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
13
|
-
"source": "src/index.js",
|
|
14
|
-
"main": "dist/index.cjs",
|
|
15
13
|
"module": "dist/index.js",
|
|
16
|
-
"types": "index.d.ts",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
17
15
|
"exports": {
|
|
18
16
|
".": {
|
|
19
|
-
"types": "./index.d.ts",
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./text": {
|
|
21
|
+
"types": "./dist/text.d.ts",
|
|
22
|
+
"default": "./dist/text.js"
|
|
23
|
+
},
|
|
24
|
+
"./html": {
|
|
25
|
+
"types": "./dist/html.d.ts",
|
|
26
|
+
"default": "./dist/html.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
23
29
|
},
|
|
24
30
|
"engines": {
|
|
25
|
-
"node": ">=22.
|
|
31
|
+
"node": ">=22.12.0"
|
|
26
32
|
},
|
|
27
33
|
"files": [
|
|
28
|
-
"dist"
|
|
29
|
-
"src",
|
|
30
|
-
"index.d.ts"
|
|
34
|
+
"dist"
|
|
31
35
|
],
|
|
32
36
|
"size-limit": [
|
|
33
37
|
{
|
|
34
|
-
"
|
|
35
|
-
"
|
|
38
|
+
"name": "sjabloon",
|
|
39
|
+
"path": [
|
|
40
|
+
"dist/index.js",
|
|
41
|
+
"dist/core.js"
|
|
42
|
+
],
|
|
43
|
+
"limit": "1.95 kB"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "sjabloon/text",
|
|
47
|
+
"path": [
|
|
48
|
+
"dist/text.js",
|
|
49
|
+
"dist/core.js"
|
|
50
|
+
],
|
|
51
|
+
"limit": "1.9 kB"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "sjabloon/html",
|
|
55
|
+
"path": [
|
|
56
|
+
"dist/html.js",
|
|
57
|
+
"dist/core.js"
|
|
58
|
+
],
|
|
59
|
+
"limit": "1.97 kB"
|
|
36
60
|
},
|
|
37
61
|
{
|
|
38
|
-
"
|
|
39
|
-
"
|
|
62
|
+
"name": "core chunk (informational)",
|
|
63
|
+
"path": "dist/core.js",
|
|
64
|
+
"limit": "1.75 kB"
|
|
40
65
|
}
|
|
41
66
|
],
|
|
42
67
|
"scripts": {
|
package/dist/index.cjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("xprsn");const t={"&":`&`,"<":`<`,">":`>`,'"':`"`,"'":`'`},n=e=>String(e).replace(/[&<>"']/g,e=>t[e]),r=/^(?:__proto__|constructor|prototype)$/,i=new WeakSet,a=i.add.bind(i),o=i.has.bind(i),s=e=>o(e);let c=e=>{let t=[];for(let n=0,r=1;n<e.length;){let i=e.indexOf(`{{`,n);if(i<0){t.push([0,e.slice(n)]);break}i>n&&t.push([0,e.slice(n,i)]);let a=e[i+2]===`{`,o=i+2+a,s=e[o]===`-`,c=-1;if(s&&o++,a&&r&&(c=e.indexOf(`}}}`,o),c<0&&(r=0)),c<0&&(a&&(a=!1,o=i+2,s=e[o]===`-`,s&&o++),c=e.indexOf(`}}`,o)),c<0){t.push([0,e.slice(i)]);break}let l=c>o&&e[c-1]===`-`,u=l?c-1:c,d=e.slice(o,u),f=d.trim(),p=o+d.length-d.trimStart().length,m=c+2+a,h=[a?1:2,f,i,m,p],g=t.at(-1);if(s&&g?.[0]===0&&g[1]&&(g[1]=g[1].trimEnd()),t.push(h),n=m,l)for(;/\s/.test(e[n]);)n++}return t},l,u,d,f,p,m,h,g,_,v=()=>Object.freeze(_.slice()),y=(e,t)=>(_.length<256||x(`Template too deeply nested`,`SJABLOON_TOO_DEEP`,t),Object.freeze({type:e,start:t[2],end:t[3]})),b=(e,t)=>(Object.defineProperty(e,"blocks",{value:t,enumerable:!0}),a(e),e),x=(e,t,n,r=n?.[2]??g.length,i=n?.[3]??g.length)=>{let a=SyntaxError(e);throw a.code=t,a.start=r,a.end=i,b(a,v())},S=(t,n,r,i=e.isDiagnostic)=>{throw i(t)?(t.start+=n,t.end+=n,b(t,r)):t},C=e=>x(`Unexpected {{`+e[1]+`}}`,`SJABLOON_UNEXPECTED_TAG`,e),w=(e,t)=>e.map(e=>e(t)).join(``),T=(e,t)=>(e=>n=>t(e(n)??``))(E(e[1],e[4],v())),E=(t,n,r)=>{let i;try{i=(0,e.compile)(t,d)}catch(e){S(e,n,r)}for(let e of i.names)p.includes(e)||m.add(e);for(let e of i.functions)h.add(e);return e=>{try{return i(e)}catch(e){S(e,n,r,i.isDiagnostic)}}},D=e=>{let t=O([`#elif`,`#else`,`/if`]),n=f[1],r=[];return n.startsWith(`#elif `)?r=[D(E(n.slice(6),f[4]+6,v()))]:n===`#else`?(r=O([`/if`]),f[1]===`/if`||C(f)):n!==`/if`&&C(f),n=>w(e(n)?t:r,n)},O=e=>{let t=[];for(let i;i=l[u++];){let a=i[1];if(!i[0])t.push((e=>()=>e)(a));else if(i[0]===1)t.push(T(i,String));else if(e.includes(a.split(` `)[0]))return f=i,t;else if(a[0]!==`!`)if(a.startsWith(`#if `))_.push(y(`if`,i)),t.push(D(E(a.slice(4),i[4]+4,v()))),_.pop();else if(/^#each(?:\s|$)/.test(a)){_.push(y(`each`,i));let e=/^#each ([\s\S]+) as ((\w+)(?:\s*,\s*(\w+))?)$/.exec(a);e||x(`Bad {{`+a+`}}`,`SJABLOON_EACH_SYNTAX`,i);let n=e[3],o=e[4],s=i[4]+a.length-e[2].length;if(r.test(n)&&x(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,i,s,s+n.length),o&&r.test(o)){let e=i[4]+a.length-o.length;x(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,i,e,e+o.length)}let c=E(e[1],i[4]+6,v()),l=p.length;p.push(n),o&&p.push(o),p.push(`loop`);let u=O([`#else`,`/each`]);p.length=l;let d=[];f[1]===`#else`?(d=O([`/each`]),f[1]===`/each`||C(f)):f[1]!==`/each`&&C(f),_.pop(),t.push(e=>{let t=c(e),r=Array.isArray(t),i=r?t.slice():t&&typeof t==`object`?Object.keys(t).map(e=>[t[e],e]):[];return i.length?i.map((t,a)=>{let s=r?t:t[0],c=r?a:t[1],l=Object.create(e);return l[n]=s,o&&(l[o]=c),l[`@`]=s,l.loop={index:a+1,index0:a,first:!a,last:a===i.length-1,length:i.length},w(u,l)}).join(``):w(d,e)})}else/^#(?:if|elif|else)(?:\s|$)/.test(a)||a[0]===`/`?C(i):a[0]===`#`?x(`Unknown {{`+a+`}}`,`SJABLOON_UNKNOWN_BLOCK`,i):t.push(T(i,n))}return e.length&&x(`Missing {{`+e[e.length-1]+`}}`,`SJABLOON_UNCLOSED_BLOCK`),t};function k(e,t){d=t,p=[`$`,`@`],m=new Set,h=new Set,g=String(e),_=[],l=c(g),u=0;let n;try{n=O([])}catch(e){throw e instanceof RangeError&&x(`Template too deeply nested`,`SJABLOON_TOO_DEEP`),e}let r=(e,t)=>{e||={};let r=Object.create(e);return r.$=t?t.root:e,t?`item`in t&&(r[`@`]=t.item):r[`@`]=e,w(n,r)};return r.names=Array.from(m),r.functions=Array.from(h),r}function A(e,t,n){return k(e,n)(t)}exports.isDiagnostic=s,exports.render=A,exports.template=k;
|
package/index.d.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { XprsnErrorCode } from 'xprsn';
|
|
2
|
-
|
|
3
|
-
export type SjabloonErrorCode =
|
|
4
|
-
| XprsnErrorCode
|
|
5
|
-
| 'SJABLOON_EACH_SYNTAX'
|
|
6
|
-
| 'SJABLOON_BLOCKED_BINDING'
|
|
7
|
-
| 'SJABLOON_UNEXPECTED_TAG'
|
|
8
|
-
| 'SJABLOON_UNKNOWN_BLOCK'
|
|
9
|
-
| 'SJABLOON_UNCLOSED_BLOCK';
|
|
10
|
-
|
|
11
|
-
export interface SjabloonBlock {
|
|
12
|
-
readonly type: 'if' | 'each';
|
|
13
|
-
readonly start: number;
|
|
14
|
-
readonly end: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface SjabloonDiagnostic extends Error {
|
|
18
|
-
readonly code: SjabloonErrorCode;
|
|
19
|
-
readonly start: number;
|
|
20
|
-
readonly end: number;
|
|
21
|
-
readonly blocks: readonly SjabloonBlock[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Check whether an error was produced or translated by this sjabloon module instance.
|
|
26
|
-
*/
|
|
27
|
-
export function isDiagnostic(error: unknown): error is SjabloonDiagnostic;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Compile a template once, render it many times.
|
|
31
|
-
*
|
|
32
|
-
* The returned renderer exposes `names`: the variables the template reads
|
|
33
|
-
* from your values, deduplicated. Loop variables the template introduces are
|
|
34
|
-
* not included. It also exposes `functions`: the registry functions the
|
|
35
|
-
* template calls, deduplicated.
|
|
36
|
-
*
|
|
37
|
-
* Two anchors are always in scope: `$` is the root values, and `@` is the
|
|
38
|
-
* current `#each` item (the root outside any loop). They let a nested loop
|
|
39
|
-
* reach the root (`$.company`) or the current item (`@.total`) explicitly,
|
|
40
|
-
* past any shadowing. Neither counts as a `name`.
|
|
41
|
-
*
|
|
42
|
-
* An embedder with its own scope model can override the anchors per render by
|
|
43
|
-
* passing `{ root, item }` as the renderer's second argument: `$` becomes
|
|
44
|
-
* `root` and `@` becomes `item` (two distinct objects). Omit `item` to leave
|
|
45
|
-
* `@` unbound, so reading `@.x` throws through xprsn's guard.
|
|
46
|
-
*
|
|
47
|
-
* @param {string} str The template, e.g. `'Hello {{ user.name }}!'`.
|
|
48
|
-
* @param {Record<string, Function>} [funcs] Functions callable inside expressions.
|
|
49
|
-
* @returns {{(values?: Record<string, any>, scope?: { root?: any, item?: any }): string, names: string[], functions: string[]}} Renderer for the compiled template.
|
|
50
|
-
* @throws {SyntaxError} On malformed tags, unclosed blocks, or bad expressions.
|
|
51
|
-
*/
|
|
52
|
-
export function template(str: string, funcs?: Record<string, Function>): {
|
|
53
|
-
(values?: Record<string, any>, scope?: {
|
|
54
|
-
root?: any;
|
|
55
|
-
item?: any;
|
|
56
|
-
}): string;
|
|
57
|
-
names: string[];
|
|
58
|
-
functions: string[];
|
|
59
|
-
};
|
|
60
|
-
/**
|
|
61
|
-
* Compile and render a template in one go.
|
|
62
|
-
*
|
|
63
|
-
* @param {string} str The template to render.
|
|
64
|
-
* @param {Record<string, any>} [values] Variables available to the template.
|
|
65
|
-
* @param {Record<string, Function>} [funcs] Functions callable inside expressions.
|
|
66
|
-
* @returns {string} The rendered output.
|
|
67
|
-
*/
|
|
68
|
-
export function render(str: string, values?: Record<string, any>, funcs?: Record<string, Function>): string;
|
package/src/index.js
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tiny, CSP-safe template engine powered by xprsn expressions.
|
|
3
|
-
* Templates compile to a composition of closures; template text is never
|
|
4
|
-
* turned into JavaScript, so strict CSP is satisfied.
|
|
5
|
-
*/
|
|
6
|
-
import { compile, isDiagnostic as isXprsnDiagnostic } from 'xprsn';
|
|
7
|
-
|
|
8
|
-
const ESC = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
9
|
-
const esc = s => String(s).replace(/[&<>"']/g, c => ESC[c]);
|
|
10
|
-
const BLOCKED = /^(?:__proto__|constructor|prototype)$/;
|
|
11
|
-
const DIAGNOSTICS = new WeakSet();
|
|
12
|
-
const mark = DIAGNOSTICS.add.bind(DIAGNOSTICS);
|
|
13
|
-
const owns = DIAGNOSTICS.has.bind(DIAGNOSTICS);
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Check whether an error was produced or translated by sjabloon.
|
|
17
|
-
*
|
|
18
|
-
* @param {unknown} error Any thrown value.
|
|
19
|
-
* @returns {boolean} Whether `error` is an authentic sjabloon diagnostic.
|
|
20
|
-
*/
|
|
21
|
-
export const isDiagnostic = error => owns(error);
|
|
22
|
-
|
|
23
|
-
// Linear scan into text/tag/raw tokens. Dashes hug braces (`{{- x -}}` trims;
|
|
24
|
-
// `{{ -x }}` stays unary minus). Prefer {{{ }}} over {{ }}. `triple` latches
|
|
25
|
-
// off once }}} is gone so {{{...}}×N does not rescan to EOF (stays O(n)).
|
|
26
|
-
let lex = s => {
|
|
27
|
-
const out = [];
|
|
28
|
-
for (let i = 0, triple = 1; i < s.length; ) {
|
|
29
|
-
const a = s.indexOf('{{', i);
|
|
30
|
-
if (a < 0) { out.push([0, s.slice(i)]); break; }
|
|
31
|
-
if (a > i) out.push([0, s.slice(i, a)]);
|
|
32
|
-
let raw = s[a + 2] === '{', p = a + 2 + raw, l = s[p] === '-', b = -1;
|
|
33
|
-
if (l) p++;
|
|
34
|
-
if (raw && triple) { b = s.indexOf('}}}', p); if (b < 0) triple = 0; }
|
|
35
|
-
if (b < 0) {
|
|
36
|
-
if (raw) { raw = !1; p = a + 2; l = s[p] === '-'; if (l) p++; }
|
|
37
|
-
b = s.indexOf('}}', p);
|
|
38
|
-
}
|
|
39
|
-
if (b < 0) { out.push([0, s.slice(a)]); break; }
|
|
40
|
-
const r = b > p && s[b - 1] === '-';
|
|
41
|
-
const q = r ? b - 1 : b, whole = s.slice(p, q), body = whole.trim();
|
|
42
|
-
const start = p + whole.length - whole.trimStart().length, end = b + 2 + raw;
|
|
43
|
-
const t = [raw ? 1 : 2, body, a, end, start];
|
|
44
|
-
const prev = out.at(-1);
|
|
45
|
-
if (l && prev?.[0] === 0 && prev[1]) prev[1] = prev[1].trimEnd();
|
|
46
|
-
out.push(t);
|
|
47
|
-
i = end;
|
|
48
|
-
if (r) while (/\s/.test(s[i])) i++;
|
|
49
|
-
}
|
|
50
|
-
return out;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Shared parser state; parsing is synchronous so this is safe.
|
|
54
|
-
// `nms` collects free variables, `fnms` the registry functions called.
|
|
55
|
-
let toks, i, fns, last, bound, nms, fnms, src, blocks;
|
|
56
|
-
|
|
57
|
-
let snap = () => Object.freeze(blocks.slice());
|
|
58
|
-
// Block nesting is capped so a pathological template fails as a deterministic
|
|
59
|
-
// SyntaxError at the offending opener, far below the native stack limit.
|
|
60
|
-
const DEPTH = 256;
|
|
61
|
-
let opener = (type, t) => {
|
|
62
|
-
blocks.length < DEPTH || fault('Template too deeply nested', 'SJABLOON_TOO_DEEP', t);
|
|
63
|
-
return Object.freeze({ type, start: t[2], end: t[3] });
|
|
64
|
-
};
|
|
65
|
-
let attach = (e, context) => {
|
|
66
|
-
Object.defineProperty(e, 'blocks', { value: context, enumerable: true });
|
|
67
|
-
mark(e);
|
|
68
|
-
return e;
|
|
69
|
-
};
|
|
70
|
-
let fault = (msg, code, t, start = t?.[2] ?? src.length, end = t?.[3] ?? src.length) => {
|
|
71
|
-
const e = SyntaxError(msg);
|
|
72
|
-
e.code = code;
|
|
73
|
-
e.start = start;
|
|
74
|
-
e.end = end;
|
|
75
|
-
throw attach(e, snap());
|
|
76
|
-
};
|
|
77
|
-
let translated = (e, start, context, owns = isXprsnDiagnostic) => {
|
|
78
|
-
if (!owns(e)) throw e;
|
|
79
|
-
e.start += start;
|
|
80
|
-
e.end += start;
|
|
81
|
-
throw attach(e, context);
|
|
82
|
-
};
|
|
83
|
-
let unexpected = t => fault('Unexpected {{' + t[1] + '}}', 'SJABLOON_UNEXPECTED_TAG', t);
|
|
84
|
-
|
|
85
|
-
// Render a list of nodes against a scope.
|
|
86
|
-
let run = (nodes, v) => nodes.map(n => n(v)).join('');
|
|
87
|
-
|
|
88
|
-
// A leaf interpolation node: compile `src`, render nullish as '', apply `wrap`
|
|
89
|
-
// (`esc` for `{{ }}`, `String` for the raw `{{{ }}}` form).
|
|
90
|
-
let interp = (t, wrap) => (e => v => wrap(e(v) ?? ''))(cp(t[1], t[4], snap()));
|
|
91
|
-
|
|
92
|
-
// Compile one expression and collect its free variables (minus the loop
|
|
93
|
-
// variables currently in scope, which belong to the template) and the registry
|
|
94
|
-
// functions it calls.
|
|
95
|
-
let cp = (s, start, context) => {
|
|
96
|
-
let e;
|
|
97
|
-
try {
|
|
98
|
-
e = compile(s, fns);
|
|
99
|
-
} catch (x) {
|
|
100
|
-
translated(x, start, context);
|
|
101
|
-
}
|
|
102
|
-
for (const n of e.names) bound.includes(n) || nms.add(n);
|
|
103
|
-
for (const fn of e.functions) fnms.add(fn);
|
|
104
|
-
return v => {
|
|
105
|
-
try {
|
|
106
|
-
return e(v);
|
|
107
|
-
} catch (x) {
|
|
108
|
-
translated(x, start, context, e.isDiagnostic);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// One `#if`/`#elif` link: parse its branch, then recurse on the chain tail.
|
|
114
|
-
let branch = cond => {
|
|
115
|
-
const then = parse(['#elif', '#else', '/if']);
|
|
116
|
-
const tag = last[1];
|
|
117
|
-
let els = [];
|
|
118
|
-
if (tag.startsWith('#elif ')) els = [branch(cp(tag.slice(6), last[4] + 6, snap()))];
|
|
119
|
-
else if (tag === '#else') {
|
|
120
|
-
els = parse(['/if']);
|
|
121
|
-
last[1] === '/if' || unexpected(last);
|
|
122
|
-
} else if (tag !== '/if') unexpected(last);
|
|
123
|
-
return v => run(cond(v) ? then : els, v);
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
let parse = stops => {
|
|
127
|
-
const nodes = [];
|
|
128
|
-
for (let t; (t = toks[i++]); ) {
|
|
129
|
-
const tag = t[1];
|
|
130
|
-
if (!t[0]) {
|
|
131
|
-
nodes.push((s => () => s)(tag));
|
|
132
|
-
} else if (t[0] === 1) {
|
|
133
|
-
nodes.push(interp(t, String));
|
|
134
|
-
} else if (stops.includes(tag.split(' ')[0])) {
|
|
135
|
-
last = t;
|
|
136
|
-
return nodes;
|
|
137
|
-
} else if (tag[0] === '!') {
|
|
138
|
-
// comment
|
|
139
|
-
} else if (tag.startsWith('#if ')) {
|
|
140
|
-
blocks.push(opener('if', t));
|
|
141
|
-
nodes.push(branch(cp(tag.slice(4), t[4] + 4, snap())));
|
|
142
|
-
blocks.pop();
|
|
143
|
-
} else if (/^#each(?:\s|$)/.test(tag)) {
|
|
144
|
-
blocks.push(opener('each', t));
|
|
145
|
-
const m = /^#each ([\s\S]+) as ((\w+)(?:\s*,\s*(\w+))?)$/.exec(tag);
|
|
146
|
-
m || fault('Bad {{' + tag + '}}', 'SJABLOON_EACH_SYNTAX', t);
|
|
147
|
-
const name = m[3], idx = m[4], at = t[4] + tag.length - m[2].length;
|
|
148
|
-
if (BLOCKED.test(name)) fault('Bad {{' + tag + '}}', 'SJABLOON_BLOCKED_BINDING', t, at, at + name.length);
|
|
149
|
-
if (idx && BLOCKED.test(idx)) {
|
|
150
|
-
const p = t[4] + tag.length - idx.length;
|
|
151
|
-
fault('Bad {{' + tag + '}}', 'SJABLOON_BLOCKED_BINDING', t, p, p + idx.length);
|
|
152
|
-
}
|
|
153
|
-
const list = cp(m[1], t[4] + 6, snap());
|
|
154
|
-
// `name`, `idx`, and `loop` are engine-bound inside the body, so
|
|
155
|
-
// exclude them from names there and restore outer bindings after.
|
|
156
|
-
const mark = bound.length;
|
|
157
|
-
bound.push(name);
|
|
158
|
-
if (idx) bound.push(idx);
|
|
159
|
-
bound.push('loop');
|
|
160
|
-
const body = parse(['#else', '/each']);
|
|
161
|
-
bound.length = mark;
|
|
162
|
-
let empty = [];
|
|
163
|
-
if (last[1] === '#else') {
|
|
164
|
-
empty = parse(['/each']);
|
|
165
|
-
last[1] === '/each' || unexpected(last);
|
|
166
|
-
} else if (last[1] !== '/each') unexpected(last);
|
|
167
|
-
blocks.pop();
|
|
168
|
-
// Child scopes inherit the parent via the prototype chain, so outer
|
|
169
|
-
// variables stay visible inside the loop body. `@` re-points to the
|
|
170
|
-
// current item at each level, `$` (root) rides the chain, and `loop`
|
|
171
|
-
// carries the iteration metadata (index/first/last/length).
|
|
172
|
-
nodes.push(v => {
|
|
173
|
-
const lv = list(v), arr = Array.isArray(lv);
|
|
174
|
-
const ps = arr ? lv.slice() : lv && typeof lv === 'object' ? Object.keys(lv).map(k => [lv[k], k]) : [];
|
|
175
|
-
if (!ps.length) return run(empty, v);
|
|
176
|
-
return ps.map((x, j) => {
|
|
177
|
-
const item = arr ? x : x[0], key = arr ? j : x[1];
|
|
178
|
-
const s = Object.create(v);
|
|
179
|
-
s[name] = item;
|
|
180
|
-
if (idx) s[idx] = key;
|
|
181
|
-
s['@'] = item;
|
|
182
|
-
s.loop = { index: j + 1, index0: j, first: !j, last: j === ps.length - 1, length: ps.length };
|
|
183
|
-
return run(body, s);
|
|
184
|
-
}).join('');
|
|
185
|
-
});
|
|
186
|
-
} else if (/^#(?:if|elif|else)(?:\s|$)/.test(tag) || tag[0] === '/') {
|
|
187
|
-
unexpected(t);
|
|
188
|
-
} else if (tag[0] === '#') {
|
|
189
|
-
fault('Unknown {{' + tag + '}}', 'SJABLOON_UNKNOWN_BLOCK', t);
|
|
190
|
-
} else {
|
|
191
|
-
nodes.push(interp(t, esc));
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
stops.length && fault('Missing {{' + stops[stops.length - 1] + '}}', 'SJABLOON_UNCLOSED_BLOCK');
|
|
195
|
-
return nodes;
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Compile a template once, render it many times.
|
|
200
|
-
*
|
|
201
|
-
* The returned renderer exposes `names`: the variables the template reads
|
|
202
|
-
* from your values, deduplicated. Loop variables the template introduces are
|
|
203
|
-
* not included. It also exposes `functions`: the registry functions the
|
|
204
|
-
* template calls, deduplicated.
|
|
205
|
-
*
|
|
206
|
-
* Two anchors are always in scope: `$` is the root values, and `@` is the
|
|
207
|
-
* current `#each` item (the root outside any loop). They let a nested loop
|
|
208
|
-
* reach the root (`$.company`) or the current item (`@.total`) explicitly,
|
|
209
|
-
* past any shadowing. Neither counts as a `name`.
|
|
210
|
-
*
|
|
211
|
-
* An embedder with its own scope model can override the anchors per render by
|
|
212
|
-
* passing `{ root, item }` as the renderer's second argument: `$` becomes
|
|
213
|
-
* `root` and `@` becomes `item` (two distinct objects). Omit `item` to leave
|
|
214
|
-
* `@` unbound, so reading `@.x` throws through xprsn's guard.
|
|
215
|
-
*
|
|
216
|
-
* @param {string} str The template, e.g. `'Hello {{ user.name }}!'`.
|
|
217
|
-
* @param {Record<string, Function>} [funcs] Functions callable inside expressions.
|
|
218
|
-
* @returns {{(values?: Record<string, any>, scope?: { root?: any, item?: any }): string, names: string[], functions: string[]}} Renderer for the compiled template.
|
|
219
|
-
* @throws {SyntaxError} On malformed tags, unclosed blocks, or bad expressions.
|
|
220
|
-
*/
|
|
221
|
-
export function template(str, funcs) {
|
|
222
|
-
fns = funcs;
|
|
223
|
-
// `$` (root) and `@` (current item) are engine-bound anchors, always in
|
|
224
|
-
// scope, so they never count as caller-supplied `names`.
|
|
225
|
-
bound = ['$', '@'];
|
|
226
|
-
nms = new Set();
|
|
227
|
-
fnms = new Set();
|
|
228
|
-
src = String(str);
|
|
229
|
-
blocks = [];
|
|
230
|
-
toks = lex(src);
|
|
231
|
-
i = 0;
|
|
232
|
-
// Deeply nested blocks overflow the recursive-descent parser; surface that
|
|
233
|
-
// as a SyntaxError so malformed input keeps its documented compile-time
|
|
234
|
-
// contract (mirroring xprsn's XPRSN_TOO_DEEP for expressions).
|
|
235
|
-
let nodes;
|
|
236
|
-
try {
|
|
237
|
-
nodes = parse([]);
|
|
238
|
-
} catch (x) {
|
|
239
|
-
// An empty span at the end, like an unclosed block.
|
|
240
|
-
if (x instanceof RangeError) fault('Template too deeply nested', 'SJABLOON_TOO_DEEP');
|
|
241
|
-
throw x;
|
|
242
|
-
}
|
|
243
|
-
// Wrap the values in a root scope carrying the anchors, without mutating
|
|
244
|
-
// what the caller passed: by default `$` and `@` both point at the root.
|
|
245
|
-
// An embedder can override the anchors with a `{ root, item }` second arg:
|
|
246
|
-
// `$` = root, `@` = item (distinct objects). Omitting `item` leaves `@`
|
|
247
|
-
// unbound, so `@.x` throws through xprsn's guard — a group-header band that
|
|
248
|
-
// has no current row wants exactly that.
|
|
249
|
-
const f = (v, o) => {
|
|
250
|
-
v = v || {};
|
|
251
|
-
const r = Object.create(v);
|
|
252
|
-
r['$'] = o ? o.root : v;
|
|
253
|
-
if (!o) r['@'] = v;
|
|
254
|
-
else if ('item' in o) r['@'] = o.item;
|
|
255
|
-
return run(nodes, r);
|
|
256
|
-
};
|
|
257
|
-
// Array.from, not a spread: the bundler's transpile turns `[...set]` into
|
|
258
|
-
// `[].concat(set)`, which wraps the Set instead of unpacking it.
|
|
259
|
-
f.names = Array.from(nms);
|
|
260
|
-
f.functions = Array.from(fnms);
|
|
261
|
-
return f;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Compile and render a template in one go.
|
|
266
|
-
*
|
|
267
|
-
* @param {string} str The template to render.
|
|
268
|
-
* @param {Record<string, any>} [values] Variables available to the template.
|
|
269
|
-
* @param {Record<string, Function>} [funcs] Functions callable inside expressions.
|
|
270
|
-
* @returns {string} The rendered output.
|
|
271
|
-
*/
|
|
272
|
-
export function render(str, values, funcs) {
|
|
273
|
-
return template(str, funcs)(values);
|
|
274
|
-
}
|