sjabloon 0.10.0 → 0.12.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/EMBEDDING.md +209 -0
- package/README.md +152 -108
- package/lib/core.js +175 -209
- package/lib/html.d.ts +3 -12
- package/lib/html.js +8 -6
- package/lib/index.d.ts +13 -13
- package/lib/index.js +18 -24
- package/lib/text.d.ts +3 -12
- package/lib/text.js +6 -3
- package/lib/types.d.ts +51 -10
- package/package.json +11 -6
package/lib/core.js
CHANGED
|
@@ -8,14 +8,16 @@
|
|
|
8
8
|
* backs every entry, so the diagnostics store below authenticates diagnostics
|
|
9
9
|
* across all of them.
|
|
10
10
|
*/
|
|
11
|
+
import { adopt, mint, relocate as relocateFault, store } from "waarmerk";
|
|
11
12
|
import { compile, isDiagnostic as isXprsnDiagnostic, relocate as relocateXprsn } from "xprsn";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @import { SjabloonDiagnostic, SjabloonErrorCode, SjabloonFunctions, SjabloonRenderer, SjabloonValues } from './types.js'
|
|
16
|
+
* @import { Relocation, Store } from 'waarmerk'
|
|
17
|
+
* @import { XprsnErrorCode } from 'xprsn'
|
|
15
18
|
* @template A
|
|
16
|
-
* @typedef {(scope: any, acc: A
|
|
17
|
-
* `acc` and returns nothing.
|
|
18
|
-
* parameter to save a `let`; callers pass two arguments.
|
|
19
|
+
* @typedef {(scope: any, acc: A) => void} Node One compiled node: appends into
|
|
20
|
+
* `acc` and returns nothing.
|
|
19
21
|
*/
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -29,81 +31,54 @@ import { compile, isDiagnostic as isXprsnDiagnostic, relocate as relocateXprsn }
|
|
|
29
31
|
* @typedef {any[]} Tok
|
|
30
32
|
*/
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
/**
|
|
35
|
+
* This module's identity, carrying its code union. Naming the union here is
|
|
36
|
+
* what puts every code below under `tsc`: waarmerk checks a code against the
|
|
37
|
+
* store it is thrown into, so one this module does not declare fails at the
|
|
38
|
+
* line that throws it rather than shipping — which is how SJABLOON_TOO_DEEP
|
|
39
|
+
* got out for two releases.
|
|
40
|
+
*
|
|
41
|
+
* The union is spelled with xprsn's beside this module's own, because most
|
|
42
|
+
* diagnostics here are xprsn errors translated into template coordinates.
|
|
43
|
+
* Saying so here is what makes `SjabloonDiagnostic.code` true by construction,
|
|
44
|
+
* and it keeps `SjabloonErrorCode` meaning what its siblings' unions mean: the
|
|
45
|
+
* codes this package mints. The store's value is the per-template origin each
|
|
46
|
+
* renderer's own `isDiagnostic` compares against.
|
|
47
|
+
*
|
|
48
|
+
* @type {Store<SjabloonErrorCode | XprsnErrorCode>}
|
|
49
|
+
*/
|
|
50
|
+
const diags = store("sjabloon");
|
|
41
51
|
|
|
42
52
|
/**
|
|
43
53
|
* Check whether an error was produced or translated by sjabloon.
|
|
44
54
|
*
|
|
45
55
|
* Every entry shares one core, so a diagnostic thrown through any of them
|
|
46
56
|
* authenticates through all of them.
|
|
47
|
-
*
|
|
48
|
-
* @param {unknown} error Any thrown value.
|
|
49
|
-
* @returns {error is SjabloonDiagnostic} Whether `error` is an authentic sjabloon diagnostic.
|
|
50
57
|
*/
|
|
51
|
-
export const isDiagnostic =
|
|
58
|
+
export const isDiagnostic = diags.isDiagnostic;
|
|
52
59
|
|
|
53
|
-
/**
|
|
54
|
-
* Intrinsics captured at module load, exactly as `mark` is: a copy is built
|
|
55
|
-
* from a captured prototype table rather than through the original's
|
|
56
|
-
* `constructor`, so replacing a prototype's `constructor` cannot make
|
|
57
|
-
* `relocate` mint an authenticated value that is not an Error. Only
|
|
58
|
-
* `SyntaxError` is listed because `fault` mints nothing else; every other
|
|
59
|
-
* class a sjabloon diagnostic can carry arrives via xprsn and is relocated by
|
|
60
|
-
* xprsn above.
|
|
61
|
-
*/
|
|
62
|
-
const DESCS = Object.getOwnPropertyDescriptors,
|
|
63
|
-
DEFINE = Object.defineProperties,
|
|
64
|
-
PROTO = Object.getPrototypeOf,
|
|
65
|
-
SYNTAX = SyntaxError.prototype;
|
|
66
|
-
/** @type {(p: any) => (msg: string) => Error} */
|
|
67
|
-
const kindOf = (p) => (p === SYNTAX ? SyntaxError : Error);
|
|
68
60
|
/**
|
|
69
61
|
* Copy a diagnostic into an embedder's coordinates.
|
|
70
62
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
63
|
+
* Most diagnostics here are xprsn errors translated into template coordinates,
|
|
64
|
+
* and those are registered in both stores. Letting xprsn make that half of the
|
|
65
|
+
* copy is what keeps the copy registered in both — the same reason relocation
|
|
66
|
+
* lives with authentication in the first place. `adopt` with no fields then,
|
|
67
|
+
* not with `blocks`: the descriptors already carried that across, and defining
|
|
68
|
+
* it again would redefine a non-configurable property.
|
|
77
69
|
*
|
|
78
70
|
* @param {unknown} diag A diagnostic produced or translated by sjabloon.
|
|
79
|
-
* @param {
|
|
80
|
-
* the message verbatim; `offset` shifts `start` and `end`.
|
|
71
|
+
* @param {Relocation} [opts]
|
|
81
72
|
* @returns {SjabloonDiagnostic} The relocated copy.
|
|
82
73
|
* @throws {TypeError} When `diag` is not a sjabloon diagnostic.
|
|
83
74
|
*/
|
|
84
|
-
export const relocate = (diag,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// and attach would redefine a non-configurable property.
|
|
92
|
-
if (isXprsnDiagnostic(diag)) {
|
|
93
|
-
const moved = relocateXprsn(diag, { prefix, offset });
|
|
94
|
-
return (mark(moved, origin(diag)), /** @type {SjabloonDiagnostic} */ (moved));
|
|
95
|
-
}
|
|
96
|
-
let d = /** @type {any} */ (diag),
|
|
97
|
-
props = DESCS(d),
|
|
98
|
-
copy = kindOf(PROTO(d))(prefix + d.message);
|
|
99
|
-
delete props.message;
|
|
100
|
-
delete props.stack;
|
|
101
|
-
if (props.start) {
|
|
102
|
-
props.start.value += offset;
|
|
103
|
-
props.end.value += offset;
|
|
104
|
-
}
|
|
105
|
-
DEFINE(copy, props);
|
|
106
|
-
return (mark(copy, origin(d)), /** @type {SjabloonDiagnostic} */ (copy));
|
|
75
|
+
export const relocate = (diag, opts) => {
|
|
76
|
+
const d = /** @type {any} */ (diag);
|
|
77
|
+
return /** @type {any} */ (
|
|
78
|
+
isXprsnDiagnostic(d)
|
|
79
|
+
? adopt(diags, relocateXprsn(d, opts), undefined, diags.origin(d))
|
|
80
|
+
: relocateFault(diags, d, opts)
|
|
81
|
+
);
|
|
107
82
|
};
|
|
108
83
|
|
|
109
84
|
// Linear scan into text/tag/raw tokens. Dashes hug braces (`{{- x -}}` trims;
|
|
@@ -120,36 +95,19 @@ const EMPTY = Object.freeze({});
|
|
|
120
95
|
// Shared parser state; parsing is synchronous so this is safe.
|
|
121
96
|
// LIT/VAL/RAW are the compiling profile's node builders — read only while
|
|
122
97
|
// parsing, never at render time, so the hot path stays free of indirection.
|
|
123
|
-
|
|
124
|
-
let tokens;
|
|
125
|
-
/** @type {number} */
|
|
126
|
-
let
|
|
127
|
-
/** @type {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
let last;
|
|
131
|
-
/** @type {string[]} */
|
|
132
|
-
let bound;
|
|
133
|
-
/** @type {Set<string>} */
|
|
134
|
-
let names;
|
|
135
|
-
/** @type {Set<string>} */
|
|
136
|
-
let functions;
|
|
137
|
-
/** @type {string} */
|
|
138
|
-
let source;
|
|
98
|
+
// `nest` is the nesting budget shared by `#if`/`#each`/`#elif` (see opener).
|
|
99
|
+
let /** @type {Tok[]} */ tokens, /** @type {Tok} */ last, /** @type {string} */ source;
|
|
100
|
+
let /** @type {number} */ i, /** @type {number} */ nest;
|
|
101
|
+
let /** @type {SjabloonFunctions | undefined} */ fns, /** @type {string[]} */ bound;
|
|
102
|
+
let /** @type {Set<string>} */ names, /** @type {Set<string>} */ functions;
|
|
103
|
+
/** @type {{ name: string, start: number, end: number }[]} */
|
|
104
|
+
let reads;
|
|
139
105
|
/** @type {{ type: string, start: number, end: number }[]} */
|
|
140
106
|
let blocks;
|
|
141
|
-
/** Nesting budget shared by `#if`/`#each`/`#elif` (see DEPTH). */
|
|
142
|
-
/** @type {number} */
|
|
143
|
-
let nest;
|
|
144
107
|
// The profile's node builders. `any` rather than `Node<A>`: `make()` is generic
|
|
145
108
|
// per edition, but these are module-level and shared across all three, so no
|
|
146
109
|
// single A applies here.
|
|
147
|
-
/** @type {any} */
|
|
148
|
-
let LIT;
|
|
149
|
-
/** @type {any} */
|
|
150
|
-
let VAL;
|
|
151
|
-
/** @type {any} */
|
|
152
|
-
let RAW;
|
|
110
|
+
let /** @type {any} */ LIT, /** @type {any} */ VAL, /** @type {any} */ RAW;
|
|
153
111
|
let lexTriple = 1,
|
|
154
112
|
lxRaw = 0,
|
|
155
113
|
lxP = 0,
|
|
@@ -187,51 +145,38 @@ let takeScanned = (a) => {
|
|
|
187
145
|
|
|
188
146
|
/** @param {number} [a] */
|
|
189
147
|
let lexStep = (a = source.indexOf("{{", i)) => {
|
|
190
|
-
if (a
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
i
|
|
205
|
-
lexTriple = 1;
|
|
206
|
-
while (i < source.length && lexStep());
|
|
148
|
+
if (a >= 0) {
|
|
149
|
+
if (a > i) tokens.push([0, source.slice(i, a)]);
|
|
150
|
+
lxRaw = +(source[a + 2] === "{");
|
|
151
|
+
lxP = a + 2 + lxRaw;
|
|
152
|
+
lxL = +(source[lxP] === "-");
|
|
153
|
+
lxP += lxL;
|
|
154
|
+
findEnd(a);
|
|
155
|
+
if (lxB >= 0) return (takeScanned(a), 1);
|
|
156
|
+
// An opener with no closer is text from its own `{{` on: rewind to it and
|
|
157
|
+
// fall into the shared tail below.
|
|
158
|
+
i = a;
|
|
159
|
+
}
|
|
160
|
+
// Both dead ends end the scan the same way — the rest of the source is one
|
|
161
|
+
// final text token.
|
|
162
|
+
return (tokens.push([0, source.slice(i)]), 0);
|
|
207
163
|
};
|
|
208
164
|
|
|
209
165
|
let snap = () => Object.freeze(blocks.slice());
|
|
210
|
-
// Block nesting is capped so a pathological template fails as a deterministic
|
|
211
|
-
// SyntaxError at the offending opener, far below the native stack limit.
|
|
212
|
-
const DEPTH = 256;
|
|
213
166
|
/**
|
|
167
|
+
* Mint one block frame. Nesting is capped at 256 so a pathological template
|
|
168
|
+
* fails as a deterministic SyntaxError at the offending opener, far below the
|
|
169
|
+
* native stack limit.
|
|
170
|
+
*
|
|
214
171
|
* @param {string} type
|
|
215
172
|
* @param {Tok} t
|
|
216
173
|
*/
|
|
217
174
|
let opener = (type, t) => {
|
|
218
175
|
// oxlint-disable-next-line no-unused-expressions
|
|
219
|
-
nest <
|
|
176
|
+
nest < 256 || fault("Template too deeply nested", "SJABLOON_TOO_DEEP", t);
|
|
220
177
|
nest++;
|
|
221
178
|
return Object.freeze({ type, start: t[2], end: t[3] });
|
|
222
179
|
};
|
|
223
|
-
/**
|
|
224
|
-
* @template {object} E
|
|
225
|
-
* @param {E} e
|
|
226
|
-
* @param {any} context
|
|
227
|
-
* @param {any} own The owning compile's `names` set, this diagnostic's origin.
|
|
228
|
-
* @returns {E}
|
|
229
|
-
*/
|
|
230
|
-
let attach = (e, context, own) => {
|
|
231
|
-
Object.defineProperty(e, "blocks", { value: context, enumerable: true });
|
|
232
|
-
mark(e, own);
|
|
233
|
-
return e;
|
|
234
|
-
};
|
|
235
180
|
/**
|
|
236
181
|
* Throw a located compile-time diagnostic. `code` is typed to the published
|
|
237
182
|
* union, so a code that is not declared in `types.d.ts` fails to compile here
|
|
@@ -240,24 +185,11 @@ let attach = (e, context, own) => {
|
|
|
240
185
|
*
|
|
241
186
|
* @param {string} msg
|
|
242
187
|
* @param {SjabloonErrorCode} code
|
|
243
|
-
* @param {any[]} [t] The token to point at;
|
|
188
|
+
* @param {any[]} [t] The token to point at; defaults to an end-of-source point.
|
|
244
189
|
* @returns {never}
|
|
245
190
|
*/
|
|
246
|
-
const fault = (
|
|
247
|
-
msg,
|
|
248
|
-
code,
|
|
249
|
-
t,
|
|
250
|
-
start = t == null ? source.length : t[2],
|
|
251
|
-
end = t == null ? source.length : t[3],
|
|
252
|
-
) => {
|
|
253
|
-
const e = /** @type {SyntaxError & { code: SjabloonErrorCode, start: number, end: number }} */ (
|
|
254
|
-
SyntaxError(msg)
|
|
255
|
-
);
|
|
256
|
-
e.code = code;
|
|
257
|
-
e.start = start;
|
|
258
|
-
e.end = end;
|
|
259
|
-
throw attach(e, snap(), names);
|
|
260
|
-
};
|
|
191
|
+
const fault = (msg, code, t = [0, 0, source.length, source.length], start = t[2], end = t[3]) =>
|
|
192
|
+
mint(diags, SyntaxError, msg, { code, start, end, blocks: snap() }, names);
|
|
261
193
|
/**
|
|
262
194
|
* Re-locate a diagnostic thrown by a nested compile or render into this
|
|
263
195
|
* template's coordinates, then rethrow it as ours. Always throws.
|
|
@@ -271,7 +203,7 @@ const fault = (
|
|
|
271
203
|
*/
|
|
272
204
|
const translated = (e, start, context, own, guard = isXprsnDiagnostic) => {
|
|
273
205
|
if (!guard(e)) throw e;
|
|
274
|
-
throw
|
|
206
|
+
throw adopt(diags, relocateXprsn(e, { offset: start }), { blocks: context }, own);
|
|
275
207
|
};
|
|
276
208
|
/**
|
|
277
209
|
* @param {Tok} t
|
|
@@ -298,10 +230,11 @@ let run = (nodes, scope, acc) => {
|
|
|
298
230
|
/**
|
|
299
231
|
* @param {string} expr
|
|
300
232
|
* @param {number} start
|
|
301
|
-
* @param {any} context
|
|
233
|
+
* @param {any} [context] The block context this expression sits in; every
|
|
234
|
+
* caller wants the frames as they stand at the call, so it defaults to them.
|
|
302
235
|
* @returns {(v: any) => any}
|
|
303
236
|
*/
|
|
304
|
-
let compileExpr = (expr, start, context) => {
|
|
237
|
+
let compileExpr = (expr, start, context = snap()) => {
|
|
305
238
|
// The compiling template's origin, captured now: the render-time catch below
|
|
306
239
|
// runs long after the module-level `names` has moved on to other compiles.
|
|
307
240
|
const own = names;
|
|
@@ -321,6 +254,9 @@ let compileExpr = (expr, start, context) => {
|
|
|
321
254
|
// oxlint-disable-next-line no-unused-expressions
|
|
322
255
|
bound.includes(n) || names.add(n);
|
|
323
256
|
});
|
|
257
|
+
// Every read, shifted into template coordinates — bound names and loop
|
|
258
|
+
// variables included; `names` above stays the free, deduplicated view.
|
|
259
|
+
for (const r of e.reads) reads.push({ name: r.name, start: start + r.start, end: start + r.end });
|
|
324
260
|
for (const fn of e.functions) functions.add(fn);
|
|
325
261
|
return (v) => {
|
|
326
262
|
try {
|
|
@@ -335,61 +271,64 @@ let compileExpr = (expr, start, context) => {
|
|
|
335
271
|
};
|
|
336
272
|
|
|
337
273
|
/**
|
|
338
|
-
*
|
|
274
|
+
* The shared else tail of `#if` and `#each`. An `{{#else}}` parses one more
|
|
275
|
+
* branch; with or without one, the tag standing here has to be the closer, and
|
|
276
|
+
* anything else is an unexpected tag either way — so both paths leave through
|
|
277
|
+
* the one check below, and the else branch is whatever was parsed or nothing.
|
|
278
|
+
*
|
|
279
|
+
* @param {string} close
|
|
339
280
|
* @param {Node<any>[]} [nodes]
|
|
340
|
-
*/
|
|
341
|
-
let closeTail = (stop, nodes = parse([stop])) =>
|
|
342
|
-
// oxlint-disable-next-line no-unused-expressions
|
|
343
|
-
(last[1] === stop || unexpected(last), nodes);
|
|
344
|
-
|
|
345
|
-
// One `#if`/`#elif` link: parse its branch, then recurse on the chain tail.
|
|
346
|
-
/**
|
|
347
|
-
* @param {string} tag
|
|
348
|
-
* @param {Tok} [t]
|
|
349
281
|
* @returns {Node<any>[]}
|
|
350
282
|
*/
|
|
351
|
-
let
|
|
352
|
-
// Elif chains share the nest budget so they fail closed before the native
|
|
353
|
-
// stack, without appearing as extra `#if` frames in diagnostic context.
|
|
354
|
-
// oxlint-disable-next-line no-unused-expressions
|
|
355
|
-
nest < DEPTH || fault("Template too deeply nested", "SJABLOON_TOO_DEEP", t);
|
|
356
|
-
nest++;
|
|
357
|
-
const next = branch(compileExpr(tag.slice(6), t[4] + 6, snap()));
|
|
358
|
-
nest--;
|
|
359
|
-
return [next];
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
/** @param {string} tag @returns {Node<any>[]} */
|
|
363
|
-
let elseChain = (tag) => {
|
|
364
|
-
if (tag.startsWith("#elif ")) return nestElif(tag);
|
|
365
|
-
if (tag === "#else") return closeTail("/if");
|
|
283
|
+
let elseTail = (close, nodes = []) => {
|
|
366
284
|
// oxlint-disable-next-line no-unused-expressions
|
|
367
|
-
|
|
368
|
-
return [];
|
|
285
|
+
last[1] === "#else" && (nodes = parse([close]));
|
|
286
|
+
return last[1] === close ? nodes : unexpected(last);
|
|
369
287
|
};
|
|
370
288
|
|
|
371
289
|
/**
|
|
290
|
+
* One `#if` branch and whatever hangs off it. An `{{#elif}}` link is itself a
|
|
291
|
+
* branch, so the chain tail recurses straight back in here rather than through
|
|
292
|
+
* a helper. Elif links share the nest budget so they fail closed before the
|
|
293
|
+
* native stack, and the block `opener` mints for one is discarded, never
|
|
294
|
+
* pushed: an elif link must not appear as an extra `#if` frame in diagnostic
|
|
295
|
+
* context. `t` doubles as the scratch slot for the link it builds.
|
|
296
|
+
*
|
|
372
297
|
* @param {(v: any) => any} cond
|
|
298
|
+
* @param {Node<any>[]} [then]
|
|
299
|
+
* @param {any} [t]
|
|
300
|
+
* @param {any} [els]
|
|
373
301
|
* @returns {Node<any>}
|
|
374
302
|
*/
|
|
375
|
-
let branch =
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
303
|
+
let branch =
|
|
304
|
+
(
|
|
305
|
+
cond,
|
|
306
|
+
then = parse(["/if", "#elif", "#else"]),
|
|
307
|
+
t = last,
|
|
308
|
+
els = t[1].startsWith("#elif ")
|
|
309
|
+
? (opener("elif", t), (t = branch(compileExpr(t[1].slice(6), t[4] + 6))), nest--, [t])
|
|
310
|
+
: elseTail("/if"),
|
|
311
|
+
) =>
|
|
312
|
+
(scope, acc) =>
|
|
313
|
+
run(cond(scope) ? then : els, scope, acc);
|
|
380
314
|
|
|
381
315
|
/**
|
|
316
|
+
* What one `#each` walks: the values themselves for an array, the own keys for
|
|
317
|
+
* an object. The caller has already asked whether the collection is an array —
|
|
318
|
+
* that answer decides the key shape too, so it is passed in rather than asked
|
|
319
|
+
* twice, and it is what tells the loop whether an element is a value or a key
|
|
320
|
+
* to index back through. No pair array is built: the key list is the walk.
|
|
321
|
+
*
|
|
382
322
|
* @param {any} listValue
|
|
323
|
+
* @param {boolean} arr
|
|
324
|
+
* @returns {any[]}
|
|
383
325
|
*/
|
|
384
|
-
let eachPairs = (listValue) =>
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
let eachEmpty = () =>
|
|
392
|
-
last[1] === "#else" ? closeTail("/each") : last[1] === "/each" ? [] : unexpected(last);
|
|
326
|
+
let eachPairs = (listValue, arr) =>
|
|
327
|
+
arr
|
|
328
|
+
? listValue.slice()
|
|
329
|
+
: listValue && typeof listValue === "object"
|
|
330
|
+
? Object.keys(listValue)
|
|
331
|
+
: [];
|
|
393
332
|
|
|
394
333
|
/**
|
|
395
334
|
* @param {Tok} t
|
|
@@ -399,7 +338,7 @@ let eachEmpty = () =>
|
|
|
399
338
|
*/
|
|
400
339
|
let checkBinding = (t, tag, name, at) => {
|
|
401
340
|
// oxlint-disable-next-line no-unused-expressions
|
|
402
|
-
|
|
341
|
+
/^(?:__proto__|constructor|prototype)$/.test(name) &&
|
|
403
342
|
fault("Bad {{" + tag + "}}", "SJABLOON_BLOCKED_BINDING", t, at, at + name.length);
|
|
404
343
|
};
|
|
405
344
|
|
|
@@ -417,7 +356,7 @@ let parseEach = (t, tag, nodes) => {
|
|
|
417
356
|
idx = m[4],
|
|
418
357
|
at = t[4] + tag.length - m[2].length;
|
|
419
358
|
checkBinding(t, tag, name, at);
|
|
420
|
-
const list = compileExpr(m[1], t[4] + 6
|
|
359
|
+
const list = compileExpr(m[1], t[4] + 6);
|
|
421
360
|
const mark = bound.length;
|
|
422
361
|
bound.push(name);
|
|
423
362
|
if (idx) {
|
|
@@ -425,23 +364,24 @@ let parseEach = (t, tag, nodes) => {
|
|
|
425
364
|
bound.push(idx);
|
|
426
365
|
}
|
|
427
366
|
bound.push("loop");
|
|
428
|
-
const body = parse(["
|
|
367
|
+
const body = parse(["/each", "#else"]);
|
|
429
368
|
bound.length = mark;
|
|
430
|
-
const empty =
|
|
369
|
+
const empty = elseTail("/each");
|
|
431
370
|
blocks.pop();
|
|
432
371
|
nest--;
|
|
433
372
|
nodes.push((scope, acc) => {
|
|
434
373
|
const listValue = list(scope),
|
|
435
374
|
arr = Array.isArray(listValue);
|
|
436
|
-
const pairs = eachPairs(listValue);
|
|
375
|
+
const pairs = eachPairs(listValue, arr);
|
|
437
376
|
if (!pairs.length) return run(empty, scope, acc);
|
|
438
377
|
pairs.forEach((x, j) => {
|
|
439
|
-
const item = arr ? x : x[0],
|
|
440
|
-
key = arr ? j : x[1];
|
|
441
378
|
const child = Object.create(scope);
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
379
|
+
// The loop variable and the `@` anchor name the same element, so one
|
|
380
|
+
// write lands in both slots. `x` is the element itself for an array and
|
|
381
|
+
// the own key to index back through for an object; the index variable,
|
|
382
|
+
// wanted only when the template declared one, is the mirror of that.
|
|
383
|
+
child[name] = child["@"] = arr ? x : listValue[x];
|
|
384
|
+
if (idx) child[idx] = arr ? j : x;
|
|
445
385
|
child.loop = {
|
|
446
386
|
index: j + 1,
|
|
447
387
|
index0: j,
|
|
@@ -471,7 +411,7 @@ let emitLeaf = (t, nodes) => {
|
|
|
471
411
|
"SJABLOON_RAW_TAG",
|
|
472
412
|
t,
|
|
473
413
|
);
|
|
474
|
-
nodes.push(RAW(compileExpr(t[1], t[4]
|
|
414
|
+
nodes.push(RAW(compileExpr(t[1], t[4])));
|
|
475
415
|
};
|
|
476
416
|
|
|
477
417
|
/**
|
|
@@ -482,7 +422,7 @@ let emitLeaf = (t, nodes) => {
|
|
|
482
422
|
let emitBlock = (t, tag, nodes) => {
|
|
483
423
|
if (tag.startsWith("#if ")) {
|
|
484
424
|
blocks.push(opener("if", t));
|
|
485
|
-
nodes.push(branch(compileExpr(tag.slice(4), t[4] + 4
|
|
425
|
+
nodes.push(branch(compileExpr(tag.slice(4), t[4] + 4)));
|
|
486
426
|
blocks.pop();
|
|
487
427
|
nest--;
|
|
488
428
|
return;
|
|
@@ -501,7 +441,7 @@ let emitTag = (t, tag, nodes) => {
|
|
|
501
441
|
if (tag[0] === "!") return;
|
|
502
442
|
if (tag[0] === "#") return emitBlock(t, tag, nodes);
|
|
503
443
|
if (tag[0] === "/") unexpected(t);
|
|
504
|
-
nodes.push(VAL(compileExpr(t[1], t[4]
|
|
444
|
+
nodes.push(VAL(compileExpr(t[1], t[4])));
|
|
505
445
|
};
|
|
506
446
|
|
|
507
447
|
/**
|
|
@@ -517,6 +457,12 @@ let takeToken = (t, stops, nodes) => {
|
|
|
517
457
|
};
|
|
518
458
|
|
|
519
459
|
/**
|
|
460
|
+
* Parse until one of `stops` is reached, or to the end of the token stream.
|
|
461
|
+
*
|
|
462
|
+
* `stops[0]` is the block's closer and the rest are its interior tags, so the
|
|
463
|
+
* unclosed diagnostic below reads the head of the list rather than measuring
|
|
464
|
+
* its way to the tail. Membership is order-blind; keep the closer first.
|
|
465
|
+
*
|
|
520
466
|
* @param {string[]} stops
|
|
521
467
|
* @returns {Node<any>[]}
|
|
522
468
|
*/
|
|
@@ -526,19 +472,28 @@ let parse = (stops) => {
|
|
|
526
472
|
if (takeToken(t, stops, nodes)) return nodes;
|
|
527
473
|
}
|
|
528
474
|
// oxlint-disable-next-line no-unused-expressions
|
|
529
|
-
stops.length && fault("Missing {{" + stops[
|
|
475
|
+
stops.length && fault("Missing {{" + stops[0] + "}}", "SJABLOON_UNCLOSED_BLOCK");
|
|
530
476
|
return nodes;
|
|
531
477
|
};
|
|
532
478
|
|
|
533
479
|
/**
|
|
534
|
-
*
|
|
480
|
+
* Display text for one interpolated value — the scalar rule every edition and
|
|
481
|
+
* the root `text()` join share. A valid `Date` renders as ISO 8601 UTC
|
|
482
|
+
* (`toISOString()`), the same on every machine, where `String(date)` would
|
|
483
|
+
* bake in the host's timezone and locale; an invalid `Date` keeps its
|
|
484
|
+
* deterministic `"Invalid Date"` form. Nullish displays empty; everything
|
|
485
|
+
* else is `String(value)`.
|
|
535
486
|
*
|
|
536
|
-
* @param {
|
|
537
|
-
* @returns {
|
|
487
|
+
* @param {unknown} value One rendered value.
|
|
488
|
+
* @returns {string} The display text.
|
|
538
489
|
*/
|
|
539
|
-
export const
|
|
540
|
-
|
|
541
|
-
|
|
490
|
+
export const display = (value) =>
|
|
491
|
+
value instanceof Date && Number.isFinite(value.getTime())
|
|
492
|
+
? value.toISOString()
|
|
493
|
+
: // Stringifying an arbitrary value is this rule's documented contract, so
|
|
494
|
+
// `no-base-to-string` is describing the feature rather than a mistake.
|
|
495
|
+
// oxlint-disable-next-line typescript/no-base-to-string
|
|
496
|
+
String(value ?? "");
|
|
542
497
|
|
|
543
498
|
/**
|
|
544
499
|
* Bind the parser to an output profile. Each edition calls this once at module
|
|
@@ -552,8 +507,10 @@ export const litNode = (text) => (scope, acc) => {
|
|
|
552
507
|
* from your values, deduplicated. Loop variables the template introduces are
|
|
553
508
|
* not included, and neither is anything in `opts.bound` — names the embedder
|
|
554
509
|
* already has in scope (still resolved normally at render time, exactly like
|
|
555
|
-
* xprsn's own `bound`). It also exposes `
|
|
556
|
-
* the template
|
|
510
|
+
* xprsn's own `bound`). It also exposes `reads`: every root-name read with its
|
|
511
|
+
* span in the template source, in source order — duplicates, anchors, loop
|
|
512
|
+
* variables and bound names kept, so `names` is its free, deduplicated view.
|
|
513
|
+
* And `functions`: the registry functions the template calls, deduplicated. `isDiagnostic(error)` recognizes runtime
|
|
557
514
|
* diagnostics thrown through this renderer alone.
|
|
558
515
|
*
|
|
559
516
|
* Two anchors are always in scope: `$` is the root values, and `@` is the
|
|
@@ -615,19 +572,27 @@ export let make = ([lit, val, raw, seed, take]) => {
|
|
|
615
572
|
fns = funcs;
|
|
616
573
|
// `$` (root) and `@` (current item) are engine-bound anchors, always in
|
|
617
574
|
// scope, so they never count as caller-supplied `names` — and neither does
|
|
618
|
-
// anything the embedder declares bound.
|
|
575
|
+
// anything the embedder declares bound. `Array.from`, not a spread: the
|
|
619
576
|
// bundler's transpile turns an iterable spread into a concat that would
|
|
620
|
-
// wrap a Set instead of unpacking it.
|
|
621
|
-
|
|
622
|
-
|
|
577
|
+
// wrap a Set instead of unpacking it. `Object(opts)` stands in for the
|
|
578
|
+
// missing-opts check; nothing iterable comes out of an empty string.
|
|
579
|
+
bound = ["$", "@"].concat(
|
|
580
|
+
Array.from(/** @type {Iterable<string>} */ (Object(opts).bound || "")),
|
|
581
|
+
);
|
|
623
582
|
names = new Set();
|
|
583
|
+
reads = [];
|
|
624
584
|
functions = new Set();
|
|
625
585
|
source = String(str);
|
|
626
586
|
blocks = [];
|
|
627
587
|
nest = 0;
|
|
628
|
-
|
|
588
|
+
// The lexer's own state, then one linear pass: inline here because it runs
|
|
589
|
+
// exactly once per compile and the parser rewinds `i` straight after.
|
|
590
|
+
tokens = [];
|
|
591
|
+
i = 0;
|
|
592
|
+
lexTriple = 1;
|
|
593
|
+
while (i < source.length && lexStep());
|
|
629
594
|
i = 0;
|
|
630
|
-
// Deeply nested blocks fail as SJABLOON_TOO_DEEP at
|
|
595
|
+
// Deeply nested blocks fail as SJABLOON_TOO_DEEP at the cap in opener(),
|
|
631
596
|
// including elif chains — well below the native stack.
|
|
632
597
|
let nodes = parse([]);
|
|
633
598
|
// The trusted-scope render, and the one render body: the caller's chain
|
|
@@ -656,13 +621,14 @@ export let make = ([lit, val, raw, seed, take]) => {
|
|
|
656
621
|
// Array.from, not a spread: the bundler's transpile turns `[...set]` into
|
|
657
622
|
// `[].concat(set)`, which wraps the Set instead of unpacking it.
|
|
658
623
|
f.names = Array.from(names);
|
|
624
|
+
f.reads = reads;
|
|
659
625
|
f.functions = Array.from(functions);
|
|
660
626
|
// This compile's own `names` set doubles as its origin: every diagnostic
|
|
661
627
|
// thrown through this renderer was marked with it, at compile time by
|
|
662
628
|
// `fault` and at render time by the closures `compileExpr` built. Captured
|
|
663
629
|
// now — the module-level `names` moves on to the next compile.
|
|
664
630
|
const o = names;
|
|
665
|
-
f.isDiagnostic = (/** @type {unknown} */ x) => origin(x) === o;
|
|
631
|
+
f.isDiagnostic = (/** @type {unknown} */ x) => diags.origin(x) === o;
|
|
666
632
|
f.scoped = scoped;
|
|
667
633
|
return f;
|
|
668
634
|
}
|
package/lib/html.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SjabloonRender, SjabloonTemplate } from "./types.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Compile a template once, render it many times to an HTML string.
|
|
@@ -7,15 +7,6 @@ import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues } from "./type
|
|
|
7
7
|
* `{{ expr }}` HTML-escapes (`& < > " '`) and `{{{ expr }}}` interpolates raw.
|
|
8
8
|
* This is the only edition that knows what HTML is; the rest of sjabloon leaves
|
|
9
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
10
|
*/
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
funcs?: SjabloonFunctions,
|
|
17
|
-
opts?: { bound?: Iterable<string> },
|
|
18
|
-
): SjabloonRenderer<string>;
|
|
19
|
-
|
|
20
|
-
/** Compile and render in one go. Shorthand for `template(str, funcs)(values)`. */
|
|
21
|
-
export function render(str: string, values?: SjabloonValues, funcs?: SjabloonFunctions): string;
|
|
11
|
+
export const template: SjabloonTemplate<string>;
|
|
12
|
+
export const render: SjabloonRender<string>;
|
package/lib/html.js
CHANGED
|
@@ -3,19 +3,21 @@
|
|
|
3
3
|
* 0.6's behaviour, kept for templates that target HTML directly. Everything
|
|
4
4
|
* else in sjabloon is output-neutral; escaping lives here and nowhere else.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { display, make } from "./core.js";
|
|
7
7
|
|
|
8
8
|
/** @type {Record<string, string>} */
|
|
9
9
|
const ESC = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
10
|
-
/** @param {any} s */
|
|
11
|
-
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ESC[c]);
|
|
12
10
|
|
|
13
11
|
export { isDiagnostic, relocate } from "./core.js";
|
|
14
12
|
|
|
13
|
+
// The string accumulator is this edition's own shape, so its profile is stated
|
|
14
|
+
// here rather than named in the core: every node is shorter than an import of
|
|
15
|
+
// it, and the core stays the parser alone. `{{ }}` escapes at the markup edge;
|
|
16
|
+
// `{{{ }}}` is the same display text, verbatim.
|
|
15
17
|
export const { template, render } = make([
|
|
16
|
-
|
|
17
|
-
(expr) => (scope, acc
|
|
18
|
-
(expr) => (scope, acc
|
|
18
|
+
(text) => (scope, acc) => (acc.text += text),
|
|
19
|
+
(expr) => (scope, acc) => (acc.text += display(expr(scope)).replace(/[&<>"']/g, (c) => ESC[c])),
|
|
20
|
+
(expr) => (scope, acc) => (acc.text += display(expr(scope))),
|
|
19
21
|
() => ({ text: "" }),
|
|
20
22
|
(acc) => acc.text,
|
|
21
23
|
]);
|