sjabloon 0.8.0 → 0.9.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/README.md +83 -43
- package/lib/core.js +419 -233
- package/lib/html.d.ts +2 -2
- package/lib/html.js +9 -9
- package/lib/index.d.ts +2 -2
- package/lib/index.js +25 -16
- package/lib/text.d.ts +2 -2
- package/lib/text.js +7 -7
- package/lib/types.d.ts +36 -23
- package/package.json +108 -99
package/lib/core.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* backs every entry, so the WeakSet below authenticates diagnostics across all
|
|
9
9
|
* of them.
|
|
10
10
|
*/
|
|
11
|
-
import { compile, isDiagnostic as isXprsnDiagnostic } from
|
|
11
|
+
import { compile, isDiagnostic as isXprsnDiagnostic, relocate as relocateXprsn } from "xprsn";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* @import { SjabloonDiagnostic, SjabloonErrorCode, SjabloonFunctions, SjabloonRenderer, SjabloonValues } from './types.js'
|
|
@@ -44,42 +44,67 @@ const owns = DIAGNOSTICS.has.bind(DIAGNOSTICS);
|
|
|
44
44
|
* @param {unknown} error Any thrown value.
|
|
45
45
|
* @returns {error is SjabloonDiagnostic} Whether `error` is an authentic sjabloon diagnostic.
|
|
46
46
|
*/
|
|
47
|
-
export const isDiagnostic =
|
|
47
|
+
export const isDiagnostic = owns;
|
|
48
48
|
|
|
49
|
-
// Linear scan into text/tag/raw tokens. Dashes hug braces (`{{- x -}}` trims;
|
|
50
|
-
// `{{ -x }}` stays unary minus). Prefer {{{ }}} over {{ }}. `triple` latches
|
|
51
|
-
// off once }}} is gone so {{{...}}×N does not rescan to EOF (stays O(n)).
|
|
52
49
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
50
|
+
* Intrinsics captured at module load, exactly as `mark` is: a copy is built
|
|
51
|
+
* from a captured prototype table rather than through the original's
|
|
52
|
+
* `constructor`, so replacing a prototype's `constructor` cannot make
|
|
53
|
+
* `relocate` mint an authenticated value that is not an Error. Only
|
|
54
|
+
* `SyntaxError` is listed because `fault` mints nothing else; every other
|
|
55
|
+
* class a sjabloon diagnostic can carry arrives via xprsn and is relocated by
|
|
56
|
+
* xprsn above.
|
|
55
57
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
58
|
+
const DESCS = Object.getOwnPropertyDescriptors,
|
|
59
|
+
DEFINE = Object.defineProperties,
|
|
60
|
+
PROTO = Object.getPrototypeOf,
|
|
61
|
+
SYNTAX = SyntaxError.prototype;
|
|
62
|
+
/** @type {(p: any) => (msg: string) => Error} */
|
|
63
|
+
const kindOf = (p) => (p === SYNTAX ? SyntaxError : Error);
|
|
64
|
+
/**
|
|
65
|
+
* Copy a diagnostic into an embedder's coordinates.
|
|
66
|
+
*
|
|
67
|
+
* Relocation lives here, beside the authentication it has to satisfy: the copy
|
|
68
|
+
* is registered in the same WeakSet as the original, so it passes
|
|
69
|
+
* `isDiagnostic`. The original is never mutated, and every own field comes
|
|
70
|
+
* across by descriptor — the frozen `blocks` context stays frozen and
|
|
71
|
+
* non-writable — so a field added here is never a field an embedder forgets.
|
|
72
|
+
*
|
|
73
|
+
* @param {unknown} diag A diagnostic produced or translated by sjabloon.
|
|
74
|
+
* @param {{ prefix?: string, offset?: number }} [opts] `prefix` is prepended to
|
|
75
|
+
* the message verbatim; `offset` shifts `start` and `end`.
|
|
76
|
+
* @returns {SjabloonDiagnostic} The relocated copy.
|
|
77
|
+
* @throws {TypeError} When `diag` is not a sjabloon diagnostic.
|
|
78
|
+
*/
|
|
79
|
+
export const relocate = (diag, { prefix = "", offset = 0 } = {}) => {
|
|
80
|
+
if (!isDiagnostic(diag)) throw TypeError("Not a sjabloon diagnostic");
|
|
81
|
+
// Most diagnostics here are xprsn errors translated into template
|
|
82
|
+
// coordinates, and those are registered in both stores. Letting xprsn make
|
|
83
|
+
// that half of the copy is what keeps the copy registered in both — the same
|
|
84
|
+
// reason relocation lives with authentication in the first place. Bare
|
|
85
|
+
// `mark`, not `attach`: the descriptors already carried `blocks` across,
|
|
86
|
+
// and attach would redefine a non-configurable property.
|
|
87
|
+
if (isXprsnDiagnostic(diag)) {
|
|
88
|
+
const moved = relocateXprsn(diag, { prefix, offset });
|
|
89
|
+
return (mark(moved), /** @type {SjabloonDiagnostic} */ (moved));
|
|
90
|
+
}
|
|
91
|
+
let d = /** @type {any} */ (diag),
|
|
92
|
+
props = DESCS(d),
|
|
93
|
+
copy = kindOf(PROTO(d))(prefix + d.message);
|
|
94
|
+
delete props.message;
|
|
95
|
+
delete props.stack;
|
|
96
|
+
if (props.start) {
|
|
97
|
+
props.start.value += offset;
|
|
98
|
+
props.end.value += offset;
|
|
99
|
+
}
|
|
100
|
+
DEFINE(copy, props);
|
|
101
|
+
return (mark(copy), /** @type {SjabloonDiagnostic} */ (copy));
|
|
81
102
|
};
|
|
82
103
|
|
|
104
|
+
// Linear scan into text/tag/raw tokens. Dashes hug braces (`{{- x -}}` trims;
|
|
105
|
+
// `{{ -x }}` stays unary minus). Prefer {{{ }}} over {{ }}. `triple` latches
|
|
106
|
+
// off once }}} is gone so {{{...}}×N does not rescan to EOF (stays O(n)).
|
|
107
|
+
|
|
83
108
|
// One shared prototype for renders that omit `values` — the shape an embedder
|
|
84
109
|
// passing `{ root, item }` hits on every cell. A fresh `{}` here would give each
|
|
85
110
|
// wrapper its own hidden class, so lookups go megamorphic and such a render
|
|
@@ -88,11 +113,10 @@ let lex = s => {
|
|
|
88
113
|
const EMPTY = Object.freeze({});
|
|
89
114
|
|
|
90
115
|
// Shared parser state; parsing is synchronous so this is safe.
|
|
91
|
-
// `nms` collects free variables, `fnms` the registry functions called.
|
|
92
116
|
// LIT/VAL/RAW are the compiling profile's node builders — read only while
|
|
93
117
|
// parsing, never at render time, so the hot path stays free of indirection.
|
|
94
118
|
/** @type {Tok[]} */
|
|
95
|
-
let
|
|
119
|
+
let tokens;
|
|
96
120
|
/** @type {number} */
|
|
97
121
|
let i;
|
|
98
122
|
/** @type {SjabloonFunctions | undefined} */
|
|
@@ -102,13 +126,16 @@ let last;
|
|
|
102
126
|
/** @type {string[]} */
|
|
103
127
|
let bound;
|
|
104
128
|
/** @type {Set<string>} */
|
|
105
|
-
let
|
|
129
|
+
let names;
|
|
106
130
|
/** @type {Set<string>} */
|
|
107
|
-
let
|
|
131
|
+
let functions;
|
|
108
132
|
/** @type {string} */
|
|
109
|
-
let
|
|
110
|
-
/** @type {
|
|
133
|
+
let source;
|
|
134
|
+
/** @type {{ type: string, start: number, end: number }[]} */
|
|
111
135
|
let blocks;
|
|
136
|
+
/** Nesting budget shared by `#if`/`#each`/`#elif` (see DEPTH). */
|
|
137
|
+
/** @type {number} */
|
|
138
|
+
let nest;
|
|
112
139
|
// The profile's node builders. `any` rather than `Node<A>`: `make()` is generic
|
|
113
140
|
// per edition, but these are module-level and shared across all three, so no
|
|
114
141
|
// single A applies here.
|
|
@@ -118,6 +145,61 @@ let LIT;
|
|
|
118
145
|
let VAL;
|
|
119
146
|
/** @type {any} */
|
|
120
147
|
let RAW;
|
|
148
|
+
let lexTriple = 1,
|
|
149
|
+
lxRaw = 0,
|
|
150
|
+
lxP = 0,
|
|
151
|
+
lxL = 0,
|
|
152
|
+
lxB = 0;
|
|
153
|
+
|
|
154
|
+
/** @param {number} a */
|
|
155
|
+
let findEnd = (a) => {
|
|
156
|
+
lxB = lxRaw & lexTriple ? source.indexOf("}}}", lxP) : -1;
|
|
157
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
158
|
+
lxB < 0 &&
|
|
159
|
+
(lxRaw &&
|
|
160
|
+
((lexTriple = 0), (lxRaw = 0), (lxP = a + 2), (lxL = +(source[lxP] === "-")), (lxP += lxL)),
|
|
161
|
+
(lxB = source.indexOf("}}", lxP)));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
let trimPrev = (prev = tokens.at(-1)) => {
|
|
165
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
166
|
+
prev && prev[0] === 0 && prev[1] && (prev[1] = prev[1].trimEnd());
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** @param {number} a */
|
|
170
|
+
let takeScanned = (a) => {
|
|
171
|
+
const r = +(lxB > lxP) & +(source[lxB - 1] === "-"),
|
|
172
|
+
whole = source.slice(lxP, lxB - r),
|
|
173
|
+
body = whole.trim(),
|
|
174
|
+
start = lxP + whole.length - whole.trimStart().length,
|
|
175
|
+
end = lxB + 2 + lxRaw;
|
|
176
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
177
|
+
lxL && trimPrev();
|
|
178
|
+
tokens.push([2 - lxRaw, body, a, end, start]);
|
|
179
|
+
i = end;
|
|
180
|
+
if (r) while (/\s/.test(source[i])) i++;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** @param {number} [a] */
|
|
184
|
+
let lexStep = (a = source.indexOf("{{", i)) => {
|
|
185
|
+
if (a < 0) return (tokens.push([0, source.slice(i)]), 0);
|
|
186
|
+
if (a > i) tokens.push([0, source.slice(i, a)]);
|
|
187
|
+
lxRaw = +(source[a + 2] === "{");
|
|
188
|
+
lxP = a + 2 + lxRaw;
|
|
189
|
+
lxL = +(source[lxP] === "-");
|
|
190
|
+
lxP += lxL;
|
|
191
|
+
findEnd(a);
|
|
192
|
+
if (lxB < 0) return (tokens.push([0, source.slice(a)]), 0);
|
|
193
|
+
takeScanned(a);
|
|
194
|
+
return 1;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
let lex = () => {
|
|
198
|
+
tokens = [];
|
|
199
|
+
i = 0;
|
|
200
|
+
lexTriple = 1;
|
|
201
|
+
while (i < source.length && lexStep());
|
|
202
|
+
};
|
|
121
203
|
|
|
122
204
|
let snap = () => Object.freeze(blocks.slice());
|
|
123
205
|
// Block nesting is capped so a pathological template fails as a deterministic
|
|
@@ -128,8 +210,10 @@ const DEPTH = 256;
|
|
|
128
210
|
* @param {Tok} t
|
|
129
211
|
*/
|
|
130
212
|
let opener = (type, t) => {
|
|
131
|
-
|
|
132
|
-
|
|
213
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
214
|
+
nest < DEPTH || fault("Template too deeply nested", "SJABLOON_TOO_DEEP", t);
|
|
215
|
+
nest++;
|
|
216
|
+
return Object.freeze({ type, start: t[2], end: t[3] });
|
|
133
217
|
};
|
|
134
218
|
/**
|
|
135
219
|
* @template {object} E
|
|
@@ -138,9 +222,9 @@ let opener = (type, t) => {
|
|
|
138
222
|
* @returns {E}
|
|
139
223
|
*/
|
|
140
224
|
let attach = (e, context) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
225
|
+
Object.defineProperty(e, "blocks", { value: context, enumerable: true });
|
|
226
|
+
mark(e);
|
|
227
|
+
return e;
|
|
144
228
|
};
|
|
145
229
|
/**
|
|
146
230
|
* Throw a located compile-time diagnostic. `code` is typed to the published
|
|
@@ -153,12 +237,20 @@ let attach = (e, context) => {
|
|
|
153
237
|
* @param {any[]} [t] The token to point at; omitted for end-of-source faults.
|
|
154
238
|
* @returns {never}
|
|
155
239
|
*/
|
|
156
|
-
const fault = (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
240
|
+
const fault = (
|
|
241
|
+
msg,
|
|
242
|
+
code,
|
|
243
|
+
t,
|
|
244
|
+
start = t == null ? source.length : t[2],
|
|
245
|
+
end = t == null ? source.length : t[3],
|
|
246
|
+
) => {
|
|
247
|
+
const e = /** @type {SyntaxError & { code: SjabloonErrorCode, start: number, end: number }} */ (
|
|
248
|
+
SyntaxError(msg)
|
|
249
|
+
);
|
|
250
|
+
e.code = code;
|
|
251
|
+
e.start = start;
|
|
252
|
+
e.end = end;
|
|
253
|
+
throw attach(e, snap());
|
|
162
254
|
};
|
|
163
255
|
/**
|
|
164
256
|
* Re-locate a diagnostic thrown by a nested compile or render into this
|
|
@@ -171,170 +263,271 @@ const fault = (msg, code, t, start = t?.[2] ?? src.length, end = t?.[3] ?? src.l
|
|
|
171
263
|
* @type {(e: any, start: number, context: any, owns?: (e: unknown) => boolean) => never}
|
|
172
264
|
*/
|
|
173
265
|
const translated = (e, start, context, owns = isXprsnDiagnostic) => {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
e.end += start;
|
|
177
|
-
throw attach(e, context);
|
|
266
|
+
if (!owns(e)) throw e;
|
|
267
|
+
throw attach(relocateXprsn(e, { offset: start }), context);
|
|
178
268
|
};
|
|
179
269
|
/**
|
|
180
270
|
* @param {Tok} t
|
|
181
271
|
* @returns {never}
|
|
182
272
|
*/
|
|
183
|
-
let unexpected = t => fault(
|
|
273
|
+
let unexpected = (t) => fault("Unexpected {{" + t[1] + "}}", "SJABLOON_UNEXPECTED_TAG", t);
|
|
184
274
|
|
|
185
|
-
// Append every node's output into the accumulator `
|
|
275
|
+
// Append every node's output into the accumulator `acc`, which the root wrapper
|
|
186
276
|
// creates once per render and threads all the way down. Nodes return nothing:
|
|
187
277
|
// no intermediate array per node list, no join, and render order is just push
|
|
188
278
|
// order.
|
|
189
279
|
/**
|
|
190
280
|
* @param {Node<any>[]} nodes
|
|
191
|
-
* @param {any}
|
|
192
|
-
* @param {any}
|
|
193
|
-
*/
|
|
194
|
-
let run = (nodes, v, o) => { for (const n of nodes) n(v, o); };
|
|
195
|
-
|
|
196
|
-
// A leaf interpolation node: compile the expression, then let the profile turn
|
|
197
|
-
// the evaluated value into output. `k` is VAL for `{{ }}`, RAW for `{{{ }}}`.
|
|
198
|
-
/**
|
|
199
|
-
* @param {Tok} t
|
|
200
|
-
* @param {any} k
|
|
201
|
-
* @returns {Node<any>}
|
|
281
|
+
* @param {any} scope
|
|
282
|
+
* @param {any} acc
|
|
202
283
|
*/
|
|
203
|
-
let
|
|
284
|
+
let run = (nodes, scope, acc) => {
|
|
285
|
+
for (const n of nodes) n(scope, acc);
|
|
286
|
+
};
|
|
204
287
|
|
|
205
288
|
// Compile one expression and collect its free variables (minus the loop
|
|
206
289
|
// variables currently in scope, which belong to the template) and the registry
|
|
207
290
|
// functions it calls.
|
|
208
291
|
/**
|
|
209
|
-
* @param {string}
|
|
292
|
+
* @param {string} expr
|
|
210
293
|
* @param {number} start
|
|
211
294
|
* @param {any} context
|
|
212
295
|
* @returns {(v: any) => any}
|
|
213
296
|
*/
|
|
214
|
-
let
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
297
|
+
let compileExpr = (expr, start, context) => {
|
|
298
|
+
/** @type {ReturnType<typeof compile>} */
|
|
299
|
+
let e;
|
|
300
|
+
try {
|
|
301
|
+
// `SjabloonFunctions` is `Record<string, Function>`; xprsn's registry wants
|
|
302
|
+
// `Record<string, (...args: any[]) => any>`, and TypeScript deliberately
|
|
303
|
+
// refuses `Function` against a call signature. The registry is passed
|
|
304
|
+
// straight through untouched, so this is a published-type mismatch rather
|
|
305
|
+
// than a real one — narrowing `SjabloonFunctions` would change the API.
|
|
306
|
+
e = compile(expr, /** @type {any} */ (fns));
|
|
307
|
+
} catch (x) {
|
|
308
|
+
translated(x, start, context);
|
|
309
|
+
}
|
|
310
|
+
e.names.forEach((n) => {
|
|
311
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
312
|
+
bound.includes(n) || names.add(n);
|
|
313
|
+
});
|
|
314
|
+
for (const fn of e.functions) functions.add(fn);
|
|
315
|
+
return (v) => {
|
|
316
|
+
try {
|
|
317
|
+
return e(v);
|
|
318
|
+
} catch (x) {
|
|
319
|
+
// Read off the compiled expression to pass along, never called through
|
|
320
|
+
// `e` — xprsn's `isDiagnostic` is a closure over a WeakSet, not a method.
|
|
321
|
+
// oxlint-disable-next-line typescript/unbound-method
|
|
322
|
+
translated(x, start, context, e.isDiagnostic);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
236
325
|
};
|
|
237
326
|
|
|
327
|
+
/**
|
|
328
|
+
* @param {string} stop
|
|
329
|
+
* @param {Node<any>[]} [nodes]
|
|
330
|
+
*/
|
|
331
|
+
let closeTail = (stop, nodes = parse([stop])) =>
|
|
332
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
333
|
+
(last[1] === stop || unexpected(last), nodes);
|
|
334
|
+
|
|
238
335
|
// One `#if`/`#elif` link: parse its branch, then recurse on the chain tail.
|
|
336
|
+
/**
|
|
337
|
+
* @param {string} tag
|
|
338
|
+
* @param {Tok} [t]
|
|
339
|
+
* @returns {Node<any>[]}
|
|
340
|
+
*/
|
|
341
|
+
let nestElif = (tag, t = last) => {
|
|
342
|
+
// Elif chains share the nest budget so they fail closed before the native
|
|
343
|
+
// stack, without appearing as extra `#if` frames in diagnostic context.
|
|
344
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
345
|
+
nest < DEPTH || fault("Template too deeply nested", "SJABLOON_TOO_DEEP", t);
|
|
346
|
+
nest++;
|
|
347
|
+
const next = branch(compileExpr(tag.slice(6), t[4] + 6, snap()));
|
|
348
|
+
nest--;
|
|
349
|
+
return [next];
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/** @param {string} tag @returns {Node<any>[]} */
|
|
353
|
+
let elseChain = (tag) => {
|
|
354
|
+
if (tag.startsWith("#elif ")) return nestElif(tag);
|
|
355
|
+
if (tag === "#else") return closeTail("/if");
|
|
356
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
357
|
+
tag === "/if" || unexpected(last);
|
|
358
|
+
return [];
|
|
359
|
+
};
|
|
360
|
+
|
|
239
361
|
/**
|
|
240
362
|
* @param {(v: any) => any} cond
|
|
241
363
|
* @returns {Node<any>}
|
|
242
364
|
*/
|
|
243
|
-
let branch = cond => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
365
|
+
let branch = (cond) => {
|
|
366
|
+
const then = parse(["#elif", "#else", "/if"]);
|
|
367
|
+
const els = elseChain(last[1]);
|
|
368
|
+
return (scope, acc) => run(cond(scope) ? then : els, scope, acc);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* @param {any} listValue
|
|
373
|
+
*/
|
|
374
|
+
let eachPairs = (listValue) => {
|
|
375
|
+
if (Array.isArray(listValue)) return listValue.slice();
|
|
376
|
+
if (listValue && typeof listValue === "object")
|
|
377
|
+
return Object.keys(listValue).map((k) => [listValue[k], k]);
|
|
378
|
+
return [];
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
let eachEmpty = () =>
|
|
382
|
+
last[1] === "#else" ? closeTail("/each") : last[1] === "/each" ? [] : unexpected(last);
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* @param {Tok} t
|
|
386
|
+
* @param {string} tag
|
|
387
|
+
* @param {string} name
|
|
388
|
+
* @param {number} at
|
|
389
|
+
*/
|
|
390
|
+
let checkBinding = (t, tag, name, at) => {
|
|
391
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
392
|
+
BLOCKED.test(name) &&
|
|
393
|
+
fault("Bad {{" + tag + "}}", "SJABLOON_BLOCKED_BINDING", t, at, at + name.length);
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* @param {Tok} t
|
|
398
|
+
* @param {string} tag
|
|
399
|
+
* @param {Node<any>[]} nodes
|
|
400
|
+
*/
|
|
401
|
+
let parseEach = (t, tag, nodes) => {
|
|
402
|
+
blocks.push(opener("each", t));
|
|
403
|
+
const m =
|
|
404
|
+
/^#each ([\s\S]+) as ((\w+)(?:\s*,\s*(\w+))?)$/.exec(tag) ||
|
|
405
|
+
fault("Bad {{" + tag + "}}", "SJABLOON_EACH_SYNTAX", t);
|
|
406
|
+
const name = m[3],
|
|
407
|
+
idx = m[4],
|
|
408
|
+
at = t[4] + tag.length - m[2].length;
|
|
409
|
+
checkBinding(t, tag, name, at);
|
|
410
|
+
const list = compileExpr(m[1], t[4] + 6, snap());
|
|
411
|
+
const mark = bound.length;
|
|
412
|
+
bound.push(name);
|
|
413
|
+
if (idx) {
|
|
414
|
+
checkBinding(t, tag, idx, t[4] + tag.length - idx.length);
|
|
415
|
+
bound.push(idx);
|
|
416
|
+
}
|
|
417
|
+
bound.push("loop");
|
|
418
|
+
const body = parse(["#else", "/each"]);
|
|
419
|
+
bound.length = mark;
|
|
420
|
+
const empty = eachEmpty();
|
|
421
|
+
blocks.pop();
|
|
422
|
+
nest--;
|
|
423
|
+
nodes.push((scope, acc) => {
|
|
424
|
+
const listValue = list(scope),
|
|
425
|
+
arr = Array.isArray(listValue);
|
|
426
|
+
const pairs = eachPairs(listValue);
|
|
427
|
+
if (!pairs.length) return run(empty, scope, acc);
|
|
428
|
+
pairs.forEach((x, j) => {
|
|
429
|
+
const item = arr ? x : x[0],
|
|
430
|
+
key = arr ? j : x[1];
|
|
431
|
+
const child = Object.create(scope);
|
|
432
|
+
child[name] = item;
|
|
433
|
+
if (idx) child[idx] = key;
|
|
434
|
+
child["@"] = item;
|
|
435
|
+
child.loop = {
|
|
436
|
+
index: j + 1,
|
|
437
|
+
index0: j,
|
|
438
|
+
first: !j,
|
|
439
|
+
last: j === pairs.length - 1,
|
|
440
|
+
length: pairs.length,
|
|
441
|
+
};
|
|
442
|
+
run(body, child, acc);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* @param {Tok} t
|
|
449
|
+
* @param {Node<any>[]} nodes
|
|
450
|
+
*/
|
|
451
|
+
let emitLeaf = (t, nodes) => {
|
|
452
|
+
if (!t[0]) {
|
|
453
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
454
|
+
t[1] && nodes.push(LIT(t[1]));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
458
|
+
RAW ||
|
|
459
|
+
fault(
|
|
460
|
+
"Raw {{{" + t[1] + "}}} is not available here; {{ " + t[1] + " }} is already raw",
|
|
461
|
+
"SJABLOON_RAW_TAG",
|
|
462
|
+
t,
|
|
463
|
+
);
|
|
464
|
+
nodes.push(RAW(compileExpr(t[1], t[4], snap())));
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* @param {Tok} t
|
|
469
|
+
* @param {string} tag
|
|
470
|
+
* @param {Node<any>[]} nodes
|
|
471
|
+
*/
|
|
472
|
+
let emitBlock = (t, tag, nodes) => {
|
|
473
|
+
if (tag.startsWith("#if ")) {
|
|
474
|
+
blocks.push(opener("if", t));
|
|
475
|
+
nodes.push(branch(compileExpr(tag.slice(4), t[4] + 4, snap())));
|
|
476
|
+
blocks.pop();
|
|
477
|
+
nest--;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (/^#each(?:\s|$)/.test(tag)) return parseEach(t, tag, nodes);
|
|
481
|
+
if (/^#(?:if|elif|else)(?:\s|$)/.test(tag)) unexpected(t);
|
|
482
|
+
fault("Unknown {{" + tag + "}}", "SJABLOON_UNKNOWN_BLOCK", t);
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* @param {Tok} t
|
|
487
|
+
* @param {string} tag
|
|
488
|
+
* @param {Node<any>[]} nodes
|
|
489
|
+
*/
|
|
490
|
+
let emitTag = (t, tag, nodes) => {
|
|
491
|
+
if (tag[0] === "!") return;
|
|
492
|
+
if (tag[0] === "#") return emitBlock(t, tag, nodes);
|
|
493
|
+
if (tag[0] === "/") unexpected(t);
|
|
494
|
+
nodes.push(VAL(compileExpr(t[1], t[4], snap())));
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* @param {Tok} t
|
|
499
|
+
* @param {string[]} stops
|
|
500
|
+
* @param {Node<any>[]} nodes
|
|
501
|
+
*/
|
|
502
|
+
let takeToken = (t, stops, nodes) => {
|
|
503
|
+
if (t[0] < 2) return (emitLeaf(t, nodes), 0);
|
|
504
|
+
if (stops.includes(t[1].split(" ")[0])) return ((last = t), 1);
|
|
505
|
+
emitTag(t, t[1], nodes);
|
|
506
|
+
return 0;
|
|
253
507
|
};
|
|
254
508
|
|
|
255
509
|
/**
|
|
256
510
|
* @param {string[]} stops
|
|
257
511
|
* @returns {Node<any>[]}
|
|
258
512
|
*/
|
|
259
|
-
let parse = stops => {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
} else if (tag.startsWith('#if ')) {
|
|
278
|
-
blocks.push(opener('if', t));
|
|
279
|
-
nodes.push(branch(cp(tag.slice(4), t[4] + 4, snap())));
|
|
280
|
-
blocks.pop();
|
|
281
|
-
} else if (/^#each(?:\s|$)/.test(tag)) {
|
|
282
|
-
blocks.push(opener('each', t));
|
|
283
|
-
// `|| fault()` in the initializer, not as a follow-up statement: fault
|
|
284
|
-
// returns never, so `m` is non-null from here without a second check.
|
|
285
|
-
const m = /^#each ([\s\S]+) as ((\w+)(?:\s*,\s*(\w+))?)$/.exec(tag) || fault('Bad {{' + tag + '}}', 'SJABLOON_EACH_SYNTAX', t);
|
|
286
|
-
const name = m[3], idx = m[4], at = t[4] + tag.length - m[2].length;
|
|
287
|
-
if (BLOCKED.test(name)) fault('Bad {{' + tag + '}}', 'SJABLOON_BLOCKED_BINDING', t, at, at + name.length);
|
|
288
|
-
if (idx && BLOCKED.test(idx)) {
|
|
289
|
-
const p = t[4] + tag.length - idx.length;
|
|
290
|
-
fault('Bad {{' + tag + '}}', 'SJABLOON_BLOCKED_BINDING', t, p, p + idx.length);
|
|
291
|
-
}
|
|
292
|
-
const list = cp(m[1], t[4] + 6, snap());
|
|
293
|
-
// `name`, `idx`, and `loop` are engine-bound inside the body, so
|
|
294
|
-
// exclude them from names there and restore outer bindings after.
|
|
295
|
-
const mark = bound.length;
|
|
296
|
-
bound.push(name);
|
|
297
|
-
if (idx) bound.push(idx);
|
|
298
|
-
bound.push('loop');
|
|
299
|
-
const body = parse(['#else', '/each']);
|
|
300
|
-
bound.length = mark;
|
|
301
|
-
let empty = /** @type {Node<any>[]} */ ([]);
|
|
302
|
-
if (last[1] === '#else') {
|
|
303
|
-
empty = parse(['/each']);
|
|
304
|
-
last[1] === '/each' || unexpected(last);
|
|
305
|
-
} else if (last[1] !== '/each') unexpected(last);
|
|
306
|
-
blocks.pop();
|
|
307
|
-
// Child scopes inherit the parent via the prototype chain, so outer
|
|
308
|
-
// variables stay visible inside the loop body. `@` re-points to the
|
|
309
|
-
// current item at each level, `$` (root) rides the chain, and `loop`
|
|
310
|
-
// carries the iteration metadata (index/first/last/length).
|
|
311
|
-
nodes.push((v, o) => {
|
|
312
|
-
const lv = list(v), arr = Array.isArray(lv);
|
|
313
|
-
const ps = arr ? lv.slice() : lv && typeof lv === 'object' ? Object.keys(lv).map(k => [lv[k], k]) : [];
|
|
314
|
-
if (!ps.length) return run(empty, v, o);
|
|
315
|
-
// forEach, not a counted loop: `slice()` keeps holes and forEach
|
|
316
|
-
// skips them exactly as the `.map()` this replaced did, so sparse
|
|
317
|
-
// arrays iterate the same way with surrounding indexes unshifted.
|
|
318
|
-
ps.forEach((x, j) => {
|
|
319
|
-
const item = arr ? x : x[0], key = arr ? j : x[1];
|
|
320
|
-
const s = Object.create(v);
|
|
321
|
-
s[name] = item;
|
|
322
|
-
if (idx) s[idx] = key;
|
|
323
|
-
s['@'] = item;
|
|
324
|
-
s.loop = { index: j + 1, index0: j, first: !j, last: j === ps.length - 1, length: ps.length };
|
|
325
|
-
run(body, s, o);
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
} else if (/^#(?:if|elif|else)(?:\s|$)/.test(tag) || tag[0] === '/') {
|
|
329
|
-
unexpected(t);
|
|
330
|
-
} else if (tag[0] === '#') {
|
|
331
|
-
fault('Unknown {{' + tag + '}}', 'SJABLOON_UNKNOWN_BLOCK', t);
|
|
332
|
-
} else {
|
|
333
|
-
nodes.push(interp(t, VAL));
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
stops.length && fault('Missing {{' + stops[stops.length - 1] + '}}', 'SJABLOON_UNCLOSED_BLOCK');
|
|
337
|
-
return nodes;
|
|
513
|
+
let parse = (stops) => {
|
|
514
|
+
const nodes = /** @type {Node<any>[]} */ ([]);
|
|
515
|
+
for (let t; (t = tokens[i++]);) {
|
|
516
|
+
if (takeToken(t, stops, nodes)) return nodes;
|
|
517
|
+
}
|
|
518
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
519
|
+
stops.length && fault("Missing {{" + stops[stops.length - 1] + "}}", "SJABLOON_UNCLOSED_BLOCK");
|
|
520
|
+
return nodes;
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Literal text node for the string editions: append `text` onto `acc.text`.
|
|
525
|
+
*
|
|
526
|
+
* @param {string} text
|
|
527
|
+
* @returns {Node<{ text: string }>}
|
|
528
|
+
*/
|
|
529
|
+
export const litNode = (text) => (scope, acc) => {
|
|
530
|
+
acc.text += text;
|
|
338
531
|
};
|
|
339
532
|
|
|
340
533
|
/**
|
|
@@ -389,58 +582,51 @@ let parse = stops => {
|
|
|
389
582
|
* or bad expressions.
|
|
390
583
|
*/
|
|
391
584
|
export let make = ([lit, val, raw, seed, take]) => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
// Array.from, not a spread: the bundler's transpile turns `[...set]` into
|
|
440
|
-
// `[].concat(set)`, which wraps the Set instead of unpacking it.
|
|
441
|
-
f.names = Array.from(nms);
|
|
442
|
-
f.functions = Array.from(fnms);
|
|
443
|
-
return f;
|
|
444
|
-
}
|
|
445
|
-
return { template, render: (str, values, funcs) => template(str, funcs)(values) };
|
|
585
|
+
/**
|
|
586
|
+
* @param {string} str
|
|
587
|
+
* @param {SjabloonFunctions} [funcs]
|
|
588
|
+
* @returns {SjabloonRenderer<T>}
|
|
589
|
+
*/
|
|
590
|
+
function template(str, funcs) {
|
|
591
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
592
|
+
((LIT = lit), (VAL = val), (RAW = raw));
|
|
593
|
+
fns = funcs;
|
|
594
|
+
// `$` (root) and `@` (current item) are engine-bound anchors, always in
|
|
595
|
+
// scope, so they never count as caller-supplied `names`.
|
|
596
|
+
bound = ["$", "@"];
|
|
597
|
+
names = new Set();
|
|
598
|
+
functions = new Set();
|
|
599
|
+
source = String(str);
|
|
600
|
+
blocks = [];
|
|
601
|
+
nest = 0;
|
|
602
|
+
lex();
|
|
603
|
+
i = 0;
|
|
604
|
+
// Deeply nested blocks fail as SJABLOON_TOO_DEEP at DEPTH via opener(),
|
|
605
|
+
// including elif chains — well below the native stack.
|
|
606
|
+
let nodes = parse([]);
|
|
607
|
+
// Wrap the values in a root scope carrying the anchors, without mutating
|
|
608
|
+
// what the caller passed: by default `$` and `@` both point at the root.
|
|
609
|
+
// An embedder can override the anchors with a `{ root, item }` second arg:
|
|
610
|
+
// `$` = root, `@` = item (distinct objects). Omitting `item` leaves `@`
|
|
611
|
+
// unbound, so `@.x` throws through xprsn's guard — a group-header band that
|
|
612
|
+
// has no current row wants exactly that.
|
|
613
|
+
const f = (/** @type {any} */ values, /** @type {any} */ anchors) => {
|
|
614
|
+
values = values || EMPTY;
|
|
615
|
+
const r = Object.create(values);
|
|
616
|
+
r["$"] = anchors ? anchors.root : values;
|
|
617
|
+
r["@"] = anchors ? anchors.item : values;
|
|
618
|
+
// One accumulator per render, owned here and threaded down. A registry
|
|
619
|
+
// function that renders another template gets its own, so re-entrancy
|
|
620
|
+
// needs no bookkeeping.
|
|
621
|
+
const acc = seed();
|
|
622
|
+
run(nodes, r, acc);
|
|
623
|
+
return take(acc);
|
|
624
|
+
};
|
|
625
|
+
// Array.from, not a spread: the bundler's transpile turns `[...set]` into
|
|
626
|
+
// `[].concat(set)`, which wraps the Set instead of unpacking it.
|
|
627
|
+
f.names = Array.from(names);
|
|
628
|
+
f.functions = Array.from(functions);
|
|
629
|
+
return f;
|
|
630
|
+
}
|
|
631
|
+
return { template, render: (str, values, funcs) => template(str, funcs)(values) };
|
|
446
632
|
};
|