sjabloon 0.11.0 → 0.13.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 +245 -0
- package/README.md +151 -117
- package/lib/core.js +160 -220
- package/lib/html.d.ts +3 -12
- package/lib/html.js +8 -6
- package/lib/index.d.ts +3 -12
- package/lib/index.js +18 -15
- package/lib/text.d.ts +3 -12
- package/lib/text.js +6 -3
- package/lib/types.d.ts +60 -8
- package/package.json +12 -4
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,38 +95,23 @@ 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
|
-
/** @type {Tok[]} */
|
|
124
|
-
let
|
|
125
|
-
/** @type {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
let
|
|
131
|
-
/** @type {string
|
|
132
|
-
let bound;
|
|
133
|
-
/** @type {Set<string>} */
|
|
134
|
-
let names;
|
|
98
|
+
let /** @type {Tok[]} */ tokens, /** @type {Tok} */ last, /** @type {string} */ source;
|
|
99
|
+
let /** @type {number} */ i;
|
|
100
|
+
let /** @type {SjabloonFunctions | undefined} */ fns, /** @type {string[]} */ bound;
|
|
101
|
+
// The compile's `tag`, read once per interpolation while parsing and never at
|
|
102
|
+
// render time. Always callable, so the parse has no branch to take for the
|
|
103
|
+
// embedders that do not ask: this one names nothing.
|
|
104
|
+
let NO_TAG = () => undefined;
|
|
105
|
+
let /** @type {(expr: string) => object | undefined} */ tagOf = NO_TAG;
|
|
106
|
+
let /** @type {Set<string>} */ names, /** @type {Set<string>} */ functions;
|
|
135
107
|
/** @type {{ name: string, start: number, end: number }[]} */
|
|
136
108
|
let reads;
|
|
137
|
-
/** @type {Set<string>} */
|
|
138
|
-
let functions;
|
|
139
|
-
/** @type {string} */
|
|
140
|
-
let source;
|
|
141
109
|
/** @type {{ type: string, start: number, end: number }[]} */
|
|
142
110
|
let blocks;
|
|
143
|
-
/** Nesting budget shared by `#if`/`#each`/`#elif` (see DEPTH). */
|
|
144
|
-
/** @type {number} */
|
|
145
|
-
let nest;
|
|
146
111
|
// The profile's node builders. `any` rather than `Node<A>`: `make()` is generic
|
|
147
112
|
// per edition, but these are module-level and shared across all three, so no
|
|
148
113
|
// single A applies here.
|
|
149
|
-
/** @type {any} */
|
|
150
|
-
let LIT;
|
|
151
|
-
/** @type {any} */
|
|
152
|
-
let VAL;
|
|
153
|
-
/** @type {any} */
|
|
154
|
-
let RAW;
|
|
114
|
+
let /** @type {any} */ LIT, /** @type {any} */ VAL, /** @type {any} */ RAW;
|
|
155
115
|
let lexTriple = 1,
|
|
156
116
|
lxRaw = 0,
|
|
157
117
|
lxP = 0,
|
|
@@ -178,7 +138,8 @@ let takeScanned = (a) => {
|
|
|
178
138
|
const r = +(lxB > lxP) & +(source[lxB - 1] === "-"),
|
|
179
139
|
whole = source.slice(lxP, lxB - r),
|
|
180
140
|
body = whole.trim(),
|
|
181
|
-
|
|
141
|
+
// search returns -1 on all-whitespace, so start sits on the last `{`.
|
|
142
|
+
start = lxP + whole.search(/\S/),
|
|
182
143
|
end = lxB + 2 + lxRaw;
|
|
183
144
|
// oxlint-disable-next-line no-unused-expressions
|
|
184
145
|
lxL && trimPrev();
|
|
@@ -189,51 +150,37 @@ let takeScanned = (a) => {
|
|
|
189
150
|
|
|
190
151
|
/** @param {number} [a] */
|
|
191
152
|
let lexStep = (a = source.indexOf("{{", i)) => {
|
|
192
|
-
if (a
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
i
|
|
207
|
-
lexTriple = 1;
|
|
208
|
-
while (i < source.length && lexStep());
|
|
153
|
+
if (a >= 0) {
|
|
154
|
+
if (a > i) tokens.push([0, source.slice(i, a)]);
|
|
155
|
+
lxRaw = +(source[a + 2] === "{");
|
|
156
|
+
lxP = a + 2 + lxRaw;
|
|
157
|
+
lxL = +(source[lxP] === "-");
|
|
158
|
+
lxP += lxL;
|
|
159
|
+
findEnd(a);
|
|
160
|
+
if (lxB >= 0) return (takeScanned(a), 1);
|
|
161
|
+
// An opener with no closer is text from its own `{{` on: rewind to it and
|
|
162
|
+
// fall into the shared tail below.
|
|
163
|
+
i = a;
|
|
164
|
+
}
|
|
165
|
+
// Both dead ends end the scan the same way — the rest of the source is one
|
|
166
|
+
// final text token.
|
|
167
|
+
return (tokens.push([0, source.slice(i)]), 0);
|
|
209
168
|
};
|
|
210
169
|
|
|
211
170
|
let snap = () => Object.freeze(blocks.slice());
|
|
212
|
-
// Block nesting is capped so a pathological template fails as a deterministic
|
|
213
|
-
// SyntaxError at the offending opener, far below the native stack limit.
|
|
214
|
-
const DEPTH = 256;
|
|
215
171
|
/**
|
|
172
|
+
* Mint one block frame. Nesting is capped at 256 so a pathological template
|
|
173
|
+
* fails as a deterministic SyntaxError at the offending opener, far below the
|
|
174
|
+
* native stack limit.
|
|
175
|
+
*
|
|
216
176
|
* @param {string} type
|
|
217
177
|
* @param {Tok} t
|
|
218
178
|
*/
|
|
219
179
|
let opener = (type, t) => {
|
|
220
180
|
// oxlint-disable-next-line no-unused-expressions
|
|
221
|
-
|
|
222
|
-
nest++;
|
|
181
|
+
blocks.length < 256 || fault("Template too deeply nested", "SJABLOON_TOO_DEEP", t);
|
|
223
182
|
return Object.freeze({ type, start: t[2], end: t[3] });
|
|
224
183
|
};
|
|
225
|
-
/**
|
|
226
|
-
* @template {object} E
|
|
227
|
-
* @param {E} e
|
|
228
|
-
* @param {any} context
|
|
229
|
-
* @param {any} own The owning compile's `names` set, this diagnostic's origin.
|
|
230
|
-
* @returns {E}
|
|
231
|
-
*/
|
|
232
|
-
let attach = (e, context, own) => {
|
|
233
|
-
Object.defineProperty(e, "blocks", { value: context, enumerable: true });
|
|
234
|
-
mark(e, own);
|
|
235
|
-
return e;
|
|
236
|
-
};
|
|
237
184
|
/**
|
|
238
185
|
* Throw a located compile-time diagnostic. `code` is typed to the published
|
|
239
186
|
* union, so a code that is not declared in `types.d.ts` fails to compile here
|
|
@@ -242,24 +189,11 @@ let attach = (e, context, own) => {
|
|
|
242
189
|
*
|
|
243
190
|
* @param {string} msg
|
|
244
191
|
* @param {SjabloonErrorCode} code
|
|
245
|
-
* @param {any[]} [t] The token to point at;
|
|
192
|
+
* @param {any[]} [t] The token to point at; defaults to an end-of-source point.
|
|
246
193
|
* @returns {never}
|
|
247
194
|
*/
|
|
248
|
-
const fault = (
|
|
249
|
-
msg,
|
|
250
|
-
code,
|
|
251
|
-
t,
|
|
252
|
-
start = t == null ? source.length : t[2],
|
|
253
|
-
end = t == null ? source.length : t[3],
|
|
254
|
-
) => {
|
|
255
|
-
const e = /** @type {SyntaxError & { code: SjabloonErrorCode, start: number, end: number }} */ (
|
|
256
|
-
SyntaxError(msg)
|
|
257
|
-
);
|
|
258
|
-
e.code = code;
|
|
259
|
-
e.start = start;
|
|
260
|
-
e.end = end;
|
|
261
|
-
throw attach(e, snap(), names);
|
|
262
|
-
};
|
|
195
|
+
const fault = (msg, code, t = [0, 0, source.length, source.length], start = t[2], end = t[3]) =>
|
|
196
|
+
mint(diags, SyntaxError, msg, { code, start, end, blocks: snap() }, names);
|
|
263
197
|
/**
|
|
264
198
|
* Re-locate a diagnostic thrown by a nested compile or render into this
|
|
265
199
|
* template's coordinates, then rethrow it as ours. Always throws.
|
|
@@ -273,7 +207,7 @@ const fault = (
|
|
|
273
207
|
*/
|
|
274
208
|
const translated = (e, start, context, own, guard = isXprsnDiagnostic) => {
|
|
275
209
|
if (!guard(e)) throw e;
|
|
276
|
-
throw
|
|
210
|
+
throw adopt(diags, relocateXprsn(e, { offset: start }), { blocks: context }, own);
|
|
277
211
|
};
|
|
278
212
|
/**
|
|
279
213
|
* @param {Tok} t
|
|
@@ -300,10 +234,11 @@ let run = (nodes, scope, acc) => {
|
|
|
300
234
|
/**
|
|
301
235
|
* @param {string} expr
|
|
302
236
|
* @param {number} start
|
|
303
|
-
* @param {any} context
|
|
237
|
+
* @param {any} [context] The block context this expression sits in; every
|
|
238
|
+
* caller wants the frames as they stand at the call, so it defaults to them.
|
|
304
239
|
* @returns {(v: any) => any}
|
|
305
240
|
*/
|
|
306
|
-
let compileExpr = (expr, start, context) => {
|
|
241
|
+
let compileExpr = (expr, start, context = snap()) => {
|
|
307
242
|
// The compiling template's origin, captured now: the render-time catch below
|
|
308
243
|
// runs long after the module-level `names` has moved on to other compiles.
|
|
309
244
|
const own = names;
|
|
@@ -340,61 +275,64 @@ let compileExpr = (expr, start, context) => {
|
|
|
340
275
|
};
|
|
341
276
|
|
|
342
277
|
/**
|
|
343
|
-
*
|
|
278
|
+
* The shared else tail of `#if` and `#each`. An `{{#else}}` parses one more
|
|
279
|
+
* branch; with or without one, the tag standing here has to be the closer, and
|
|
280
|
+
* anything else is an unexpected tag either way — so both paths leave through
|
|
281
|
+
* the one check below, and the else branch is whatever was parsed or nothing.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} close
|
|
344
284
|
* @param {Node<any>[]} [nodes]
|
|
345
|
-
*/
|
|
346
|
-
let closeTail = (stop, nodes = parse([stop])) =>
|
|
347
|
-
// oxlint-disable-next-line no-unused-expressions
|
|
348
|
-
(last[1] === stop || unexpected(last), nodes);
|
|
349
|
-
|
|
350
|
-
// One `#if`/`#elif` link: parse its branch, then recurse on the chain tail.
|
|
351
|
-
/**
|
|
352
|
-
* @param {string} tag
|
|
353
|
-
* @param {Tok} [t]
|
|
354
285
|
* @returns {Node<any>[]}
|
|
355
286
|
*/
|
|
356
|
-
let
|
|
357
|
-
// Elif chains share the nest budget so they fail closed before the native
|
|
358
|
-
// stack, without appearing as extra `#if` frames in diagnostic context.
|
|
359
|
-
// oxlint-disable-next-line no-unused-expressions
|
|
360
|
-
nest < DEPTH || fault("Template too deeply nested", "SJABLOON_TOO_DEEP", t);
|
|
361
|
-
nest++;
|
|
362
|
-
const next = branch(compileExpr(tag.slice(6), t[4] + 6, snap()));
|
|
363
|
-
nest--;
|
|
364
|
-
return [next];
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
/** @param {string} tag @returns {Node<any>[]} */
|
|
368
|
-
let elseChain = (tag) => {
|
|
369
|
-
if (tag.startsWith("#elif ")) return nestElif(tag);
|
|
370
|
-
if (tag === "#else") return closeTail("/if");
|
|
287
|
+
let elseTail = (close, nodes = []) => {
|
|
371
288
|
// oxlint-disable-next-line no-unused-expressions
|
|
372
|
-
|
|
373
|
-
return [];
|
|
289
|
+
last[1] === "#else" && (nodes = parse([close]));
|
|
290
|
+
return last[1] === close ? nodes : unexpected(last);
|
|
374
291
|
};
|
|
375
292
|
|
|
376
293
|
/**
|
|
294
|
+
* One `#if` branch and whatever hangs off it. An `{{#elif}}` link is itself a
|
|
295
|
+
* branch, so the chain tail recurses straight back in here rather than through
|
|
296
|
+
* a helper. The block `opener` mints for one is discarded, never pushed: an
|
|
297
|
+
* elif link is not an extra `#if` frame in diagnostic context and does not
|
|
298
|
+
* count toward the nesting cap. `t` doubles as the scratch slot for the link
|
|
299
|
+
* it builds.
|
|
300
|
+
*
|
|
377
301
|
* @param {(v: any) => any} cond
|
|
302
|
+
* @param {Node<any>[]} [then]
|
|
303
|
+
* @param {any} [t]
|
|
304
|
+
* @param {any} [els]
|
|
378
305
|
* @returns {Node<any>}
|
|
379
306
|
*/
|
|
380
|
-
let branch =
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
307
|
+
let branch =
|
|
308
|
+
(
|
|
309
|
+
cond,
|
|
310
|
+
then = parse(["/if", "#elif", "#else"]),
|
|
311
|
+
t = last,
|
|
312
|
+
els = t[1].startsWith("#elif ")
|
|
313
|
+
? (opener("elif", t), (t = branch(compileExpr(t[1].slice(6), t[4] + 6))), [t])
|
|
314
|
+
: elseTail("/if"),
|
|
315
|
+
) =>
|
|
316
|
+
(scope, acc) =>
|
|
317
|
+
run(cond(scope) ? then : els, scope, acc);
|
|
385
318
|
|
|
386
319
|
/**
|
|
320
|
+
* What one `#each` walks: the values themselves for an array, the own keys for
|
|
321
|
+
* an object. The caller has already asked whether the collection is an array —
|
|
322
|
+
* that answer decides the key shape too, so it is passed in rather than asked
|
|
323
|
+
* twice, and it is what tells the loop whether an element is a value or a key
|
|
324
|
+
* to index back through. No pair array is built: the key list is the walk.
|
|
325
|
+
*
|
|
387
326
|
* @param {any} listValue
|
|
327
|
+
* @param {boolean} arr
|
|
328
|
+
* @returns {any[]}
|
|
388
329
|
*/
|
|
389
|
-
let eachPairs = (listValue) =>
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
let eachEmpty = () =>
|
|
397
|
-
last[1] === "#else" ? closeTail("/each") : last[1] === "/each" ? [] : unexpected(last);
|
|
330
|
+
let eachPairs = (listValue, arr) =>
|
|
331
|
+
arr
|
|
332
|
+
? listValue.slice()
|
|
333
|
+
: listValue && typeof listValue === "object"
|
|
334
|
+
? Object.keys(listValue)
|
|
335
|
+
: [];
|
|
398
336
|
|
|
399
337
|
/**
|
|
400
338
|
* @param {Tok} t
|
|
@@ -404,7 +342,7 @@ let eachEmpty = () =>
|
|
|
404
342
|
*/
|
|
405
343
|
let checkBinding = (t, tag, name, at) => {
|
|
406
344
|
// oxlint-disable-next-line no-unused-expressions
|
|
407
|
-
|
|
345
|
+
/^(?:__proto__|constructor|prototype)$/.test(name) &&
|
|
408
346
|
fault("Bad {{" + tag + "}}", "SJABLOON_BLOCKED_BINDING", t, at, at + name.length);
|
|
409
347
|
};
|
|
410
348
|
|
|
@@ -422,7 +360,7 @@ let parseEach = (t, tag, nodes) => {
|
|
|
422
360
|
idx = m[4],
|
|
423
361
|
at = t[4] + tag.length - m[2].length;
|
|
424
362
|
checkBinding(t, tag, name, at);
|
|
425
|
-
const list = compileExpr(m[1], t[4] + 6
|
|
363
|
+
const list = compileExpr(m[1], t[4] + 6);
|
|
426
364
|
const mark = bound.length;
|
|
427
365
|
bound.push(name);
|
|
428
366
|
if (idx) {
|
|
@@ -430,23 +368,23 @@ let parseEach = (t, tag, nodes) => {
|
|
|
430
368
|
bound.push(idx);
|
|
431
369
|
}
|
|
432
370
|
bound.push("loop");
|
|
433
|
-
const body = parse(["
|
|
371
|
+
const body = parse(["/each", "#else"]);
|
|
434
372
|
bound.length = mark;
|
|
435
|
-
const empty =
|
|
373
|
+
const empty = elseTail("/each");
|
|
436
374
|
blocks.pop();
|
|
437
|
-
nest--;
|
|
438
375
|
nodes.push((scope, acc) => {
|
|
439
376
|
const listValue = list(scope),
|
|
440
377
|
arr = Array.isArray(listValue);
|
|
441
|
-
const pairs = eachPairs(listValue);
|
|
378
|
+
const pairs = eachPairs(listValue, arr);
|
|
442
379
|
if (!pairs.length) return run(empty, scope, acc);
|
|
443
380
|
pairs.forEach((x, j) => {
|
|
444
|
-
const item = arr ? x : x[0],
|
|
445
|
-
key = arr ? j : x[1];
|
|
446
381
|
const child = Object.create(scope);
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
382
|
+
// The loop variable and the `@` anchor name the same element, so one
|
|
383
|
+
// write lands in both slots. `x` is the element itself for an array and
|
|
384
|
+
// the own key to index back through for an object; the index variable,
|
|
385
|
+
// wanted only when the template declared one, is the mirror of that.
|
|
386
|
+
child[name] = child["@"] = arr ? x : listValue[x];
|
|
387
|
+
if (idx) child[idx] = arr ? j : x;
|
|
450
388
|
child.loop = {
|
|
451
389
|
index: j + 1,
|
|
452
390
|
index0: j,
|
|
@@ -476,7 +414,7 @@ let emitLeaf = (t, nodes) => {
|
|
|
476
414
|
"SJABLOON_RAW_TAG",
|
|
477
415
|
t,
|
|
478
416
|
);
|
|
479
|
-
nodes.push(RAW(compileExpr(t[1], t[4]
|
|
417
|
+
nodes.push(RAW(compileExpr(t[1], t[4])));
|
|
480
418
|
};
|
|
481
419
|
|
|
482
420
|
/**
|
|
@@ -487,9 +425,8 @@ let emitLeaf = (t, nodes) => {
|
|
|
487
425
|
let emitBlock = (t, tag, nodes) => {
|
|
488
426
|
if (tag.startsWith("#if ")) {
|
|
489
427
|
blocks.push(opener("if", t));
|
|
490
|
-
nodes.push(branch(compileExpr(tag.slice(4), t[4] + 4
|
|
428
|
+
nodes.push(branch(compileExpr(tag.slice(4), t[4] + 4)));
|
|
491
429
|
blocks.pop();
|
|
492
|
-
nest--;
|
|
493
430
|
return;
|
|
494
431
|
}
|
|
495
432
|
if (/^#each(?:\s|$)/.test(tag)) return parseEach(t, tag, nodes);
|
|
@@ -506,7 +443,7 @@ let emitTag = (t, tag, nodes) => {
|
|
|
506
443
|
if (tag[0] === "!") return;
|
|
507
444
|
if (tag[0] === "#") return emitBlock(t, tag, nodes);
|
|
508
445
|
if (tag[0] === "/") unexpected(t);
|
|
509
|
-
nodes.push(VAL(compileExpr(t[1], t[4],
|
|
446
|
+
nodes.push(VAL(compileExpr(t[1], t[4]), tagOf(t[1])));
|
|
510
447
|
};
|
|
511
448
|
|
|
512
449
|
/**
|
|
@@ -522,6 +459,12 @@ let takeToken = (t, stops, nodes) => {
|
|
|
522
459
|
};
|
|
523
460
|
|
|
524
461
|
/**
|
|
462
|
+
* Parse until one of `stops` is reached, or to the end of the token stream.
|
|
463
|
+
*
|
|
464
|
+
* `stops[0]` is the block's closer and the rest are its interior tags, so the
|
|
465
|
+
* unclosed diagnostic below reads the head of the list rather than measuring
|
|
466
|
+
* its way to the tail. Membership is order-blind; keep the closer first.
|
|
467
|
+
*
|
|
525
468
|
* @param {string[]} stops
|
|
526
469
|
* @returns {Node<any>[]}
|
|
527
470
|
*/
|
|
@@ -531,20 +474,10 @@ let parse = (stops) => {
|
|
|
531
474
|
if (takeToken(t, stops, nodes)) return nodes;
|
|
532
475
|
}
|
|
533
476
|
// oxlint-disable-next-line no-unused-expressions
|
|
534
|
-
stops.length && fault("Missing {{" + stops[
|
|
477
|
+
stops.length && fault("Missing {{" + stops[0] + "}}", "SJABLOON_UNCLOSED_BLOCK");
|
|
535
478
|
return nodes;
|
|
536
479
|
};
|
|
537
480
|
|
|
538
|
-
/**
|
|
539
|
-
* Literal text node for the string editions: append `text` onto `acc.text`.
|
|
540
|
-
*
|
|
541
|
-
* @param {string} text
|
|
542
|
-
* @returns {Node<{ text: string }>}
|
|
543
|
-
*/
|
|
544
|
-
export const litNode = (text) => (scope, acc) => {
|
|
545
|
-
acc.text += text;
|
|
546
|
-
};
|
|
547
|
-
|
|
548
481
|
/**
|
|
549
482
|
* Display text for one interpolated value — the scalar rule every edition and
|
|
550
483
|
* the root `text()` join share. A valid `Date` renders as ISO 8601 UTC
|
|
@@ -605,7 +538,8 @@ export const display = (value) =>
|
|
|
605
538
|
*
|
|
606
539
|
* A profile is `[lit, val, raw, seed, take]`:
|
|
607
540
|
* lit(text) node emitting one static text run
|
|
608
|
-
* val(expr) node emitting a `{{ }}` interpolation
|
|
541
|
+
* val(expr, tagged) node emitting a `{{ }}` interpolation, carrying what
|
|
542
|
+
* the compile's `tag` returned for it (undefined when untagged)
|
|
609
543
|
* raw(expr) node emitting a `{{{ }}}` interpolation
|
|
610
544
|
* seed() a fresh output accumulator, one per render
|
|
611
545
|
* take(acc) the render's return value
|
|
@@ -615,7 +549,7 @@ export const display = (value) =>
|
|
|
615
549
|
* @template T What one render returns.
|
|
616
550
|
* @param {[
|
|
617
551
|
* lit: (text: string) => Node<A>,
|
|
618
|
-
* val: (expr: (scope: any) => any) => Node<A>,
|
|
552
|
+
* val: (expr: (scope: any) => any, tagged: object | undefined) => Node<A>,
|
|
619
553
|
* raw: ((expr: (scope: any) => any) => Node<A>) | 0,
|
|
620
554
|
* seed: () => A,
|
|
621
555
|
* take: (acc: A) => T,
|
|
@@ -632,7 +566,7 @@ export let make = ([lit, val, raw, seed, take]) => {
|
|
|
632
566
|
/**
|
|
633
567
|
* @param {string} str
|
|
634
568
|
* @param {SjabloonFunctions} [funcs]
|
|
635
|
-
* @param {{ bound?: Iterable<string
|
|
569
|
+
* @param {{ bound?: Iterable<string>, tag?: (expr: string) => object | undefined }} [opts]
|
|
636
570
|
* @returns {SjabloonRenderer<T>}
|
|
637
571
|
*/
|
|
638
572
|
function template(str, funcs, opts) {
|
|
@@ -641,21 +575,27 @@ export let make = ([lit, val, raw, seed, take]) => {
|
|
|
641
575
|
fns = funcs;
|
|
642
576
|
// `$` (root) and `@` (current item) are engine-bound anchors, always in
|
|
643
577
|
// scope, so they never count as caller-supplied `names` — and neither does
|
|
644
|
-
// anything the embedder declares bound.
|
|
578
|
+
// anything the embedder declares bound. `Array.from`, not a spread: the
|
|
645
579
|
// bundler's transpile turns an iterable spread into a concat that would
|
|
646
|
-
// wrap a Set instead of unpacking it.
|
|
647
|
-
|
|
648
|
-
|
|
580
|
+
// wrap a Set instead of unpacking it. `Object(opts)` stands in for the
|
|
581
|
+
// missing-opts check; nothing iterable comes out of an empty string.
|
|
582
|
+
let { bound: declared = "", tag = NO_TAG } = Object(opts);
|
|
583
|
+
bound = ["$", "@"].concat(Array.from(/** @type {Iterable<string>} */ (declared)));
|
|
584
|
+
tagOf = tag;
|
|
649
585
|
names = new Set();
|
|
650
586
|
reads = [];
|
|
651
587
|
functions = new Set();
|
|
652
588
|
source = String(str);
|
|
653
589
|
blocks = [];
|
|
654
|
-
|
|
655
|
-
|
|
590
|
+
// The lexer's own state, then one linear pass: inline here because it runs
|
|
591
|
+
// exactly once per compile and the parser rewinds `i` straight after.
|
|
592
|
+
tokens = [];
|
|
593
|
+
i = 0;
|
|
594
|
+
lexTriple = 1;
|
|
595
|
+
while (i < source.length && lexStep());
|
|
656
596
|
i = 0;
|
|
657
|
-
// Deeply nested blocks fail as SJABLOON_TOO_DEEP at
|
|
658
|
-
//
|
|
597
|
+
// Deeply nested blocks fail as SJABLOON_TOO_DEEP at the cap in opener(),
|
|
598
|
+
// well below the native stack.
|
|
659
599
|
let nodes = parse([]);
|
|
660
600
|
// The trusted-scope render, and the one render body: the caller's chain
|
|
661
601
|
// already carries the anchors, so no wrapper is created and nothing is
|
|
@@ -690,7 +630,7 @@ export let make = ([lit, val, raw, seed, take]) => {
|
|
|
690
630
|
// `fault` and at render time by the closures `compileExpr` built. Captured
|
|
691
631
|
// now — the module-level `names` moves on to the next compile.
|
|
692
632
|
const o = names;
|
|
693
|
-
f.isDiagnostic = (/** @type {unknown} */ x) => origin(x) === o;
|
|
633
|
+
f.isDiagnostic = (/** @type {unknown} */ x) => diags.origin(x) === o;
|
|
694
634
|
f.scoped = scoped;
|
|
695
635
|
return f;
|
|
696
636
|
}
|
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 { display,
|
|
6
|
+
import { display, make } from "./core.js";
|
|
7
7
|
|
|
8
8
|
/** @type {Record<string, string>} */
|
|
9
9
|
const ESC = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
10
|
-
/** @param {string} s Already display text — both call sites pass `display()`. */
|
|
11
|
-
const esc = (s) => 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
|
]);
|