sjabloon 0.9.0 → 0.10.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 +19 -1
- package/lib/core.js +77 -39
- package/lib/html.d.ts +5 -1
- package/lib/index.d.ts +5 -1
- package/lib/text.d.ts +5 -1
- package/lib/types.d.ts +14 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ Every edition exports `template`, `render`, `isDiagnostic`, and `relocate`. They
|
|
|
67
67
|
|
|
68
68
|
## API
|
|
69
69
|
|
|
70
|
-
### `template(str, functions?)`
|
|
70
|
+
### `template(str, functions?, options?)`
|
|
71
71
|
|
|
72
72
|
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.
|
|
73
73
|
|
|
@@ -89,6 +89,24 @@ tpl.names; // => ['title', 'items']
|
|
|
89
89
|
tpl.functions; // => ['fmt']
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
`options.bound` lists names your engine already has in scope — a loop variable, a handle, a `page` anchor. They are excluded from `names` and still resolve normally at render time, the same contract as xprsn's own `bound`:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
template("{{ run.total }} of {{ count }}", undefined, { bound: ["run"] }).names; // => ['count']
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The renderer also carries its own `isDiagnostic(error)`: `true` only for runtime diagnostics thrown through this renderer, where the module-wide [`isDiagnostic`](#diagnostics) answers for every template. An embedder holding many compiled templates asks the one that just rendered, so a diagnostic that leaked from an unrelated template is not mistaken for this cell's.
|
|
99
|
+
|
|
100
|
+
### `renderer.scoped(values)`
|
|
101
|
+
|
|
102
|
+
The trusted-scope render, for an embedder whose scope chain already binds the anchors. The default call wraps `values` in a fresh scope and seeds `$` and `@` into it; `scoped` skips the wrapper: `$` and `@` resolve from `values` itself, and a chain that omits `@` leaves it unbound so `@.x` throws where there is no current item. `{{#each}}` still re-points `@` inside its body, and nothing is ever written to your objects. Rendering one template per cell per row over scopes you already build, this is the path with zero per-call allocations beyond the output.
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
const row = Object.create(base); // base binds $ once per render
|
|
106
|
+
row["@"] = item;
|
|
107
|
+
tpl.scoped(row);
|
|
108
|
+
```
|
|
109
|
+
|
|
92
110
|
### `render(str, values?, functions?)`
|
|
93
111
|
|
|
94
112
|
Shorthand for `template(str, functions)(values)`, returning whatever its edition renders.
|
package/lib/core.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* This is the shared core: the lexer, parser and diagnostics, with output left
|
|
7
7
|
* to the profile each entry passes to `make()`. Exactly one copy of this module
|
|
8
|
-
* backs every entry, so the
|
|
9
|
-
* of them.
|
|
8
|
+
* backs every entry, so the diagnostics store below authenticates diagnostics
|
|
9
|
+
* across all of them.
|
|
10
10
|
*/
|
|
11
11
|
import { compile, isDiagnostic as isXprsnDiagnostic, relocate as relocateXprsn } from "xprsn";
|
|
12
12
|
|
|
@@ -30,10 +30,14 @@ import { compile, isDiagnostic as isXprsnDiagnostic, relocate as relocateXprsn }
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
const BLOCKED = /^(?:__proto__|constructor|prototype)$/;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// Error -> the `names` set of the compile that threw it. The map's keys are
|
|
34
|
+
// what authenticates a diagnostic module-wide; the value is the per-template
|
|
35
|
+
// origin each renderer's own `isDiagnostic` compares against.
|
|
36
|
+
/** @type {WeakMap<any, any>} */
|
|
37
|
+
const DIAGNOSTICS = new WeakMap();
|
|
38
|
+
const mark = DIAGNOSTICS.set.bind(DIAGNOSTICS);
|
|
36
39
|
const owns = DIAGNOSTICS.has.bind(DIAGNOSTICS);
|
|
40
|
+
const origin = DIAGNOSTICS.get.bind(DIAGNOSTICS);
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
43
|
* Check whether an error was produced or translated by sjabloon.
|
|
@@ -65,8 +69,9 @@ const kindOf = (p) => (p === SYNTAX ? SyntaxError : Error);
|
|
|
65
69
|
* Copy a diagnostic into an embedder's coordinates.
|
|
66
70
|
*
|
|
67
71
|
* Relocation lives here, beside the authentication it has to satisfy: the copy
|
|
68
|
-
* is registered in the same
|
|
69
|
-
*
|
|
72
|
+
* is registered in the same store as the original, under the original's origin,
|
|
73
|
+
* so it passes `isDiagnostic` and the owning renderer's own `isDiagnostic`
|
|
74
|
+
* alike. The original is never mutated, and every own field comes
|
|
70
75
|
* across by descriptor — the frozen `blocks` context stays frozen and
|
|
71
76
|
* non-writable — so a field added here is never a field an embedder forgets.
|
|
72
77
|
*
|
|
@@ -86,7 +91,7 @@ export const relocate = (diag, { prefix = "", offset = 0 } = {}) => {
|
|
|
86
91
|
// and attach would redefine a non-configurable property.
|
|
87
92
|
if (isXprsnDiagnostic(diag)) {
|
|
88
93
|
const moved = relocateXprsn(diag, { prefix, offset });
|
|
89
|
-
return (mark(moved), /** @type {SjabloonDiagnostic} */ (moved));
|
|
94
|
+
return (mark(moved, origin(diag)), /** @type {SjabloonDiagnostic} */ (moved));
|
|
90
95
|
}
|
|
91
96
|
let d = /** @type {any} */ (diag),
|
|
92
97
|
props = DESCS(d),
|
|
@@ -98,7 +103,7 @@ export const relocate = (diag, { prefix = "", offset = 0 } = {}) => {
|
|
|
98
103
|
props.end.value += offset;
|
|
99
104
|
}
|
|
100
105
|
DEFINE(copy, props);
|
|
101
|
-
return (mark(copy), /** @type {SjabloonDiagnostic} */ (copy));
|
|
106
|
+
return (mark(copy, origin(d)), /** @type {SjabloonDiagnostic} */ (copy));
|
|
102
107
|
};
|
|
103
108
|
|
|
104
109
|
// Linear scan into text/tag/raw tokens. Dashes hug braces (`{{- x -}}` trims;
|
|
@@ -219,11 +224,12 @@ let opener = (type, t) => {
|
|
|
219
224
|
* @template {object} E
|
|
220
225
|
* @param {E} e
|
|
221
226
|
* @param {any} context
|
|
227
|
+
* @param {any} own The owning compile's `names` set, this diagnostic's origin.
|
|
222
228
|
* @returns {E}
|
|
223
229
|
*/
|
|
224
|
-
let attach = (e, context) => {
|
|
230
|
+
let attach = (e, context, own) => {
|
|
225
231
|
Object.defineProperty(e, "blocks", { value: context, enumerable: true });
|
|
226
|
-
mark(e);
|
|
232
|
+
mark(e, own);
|
|
227
233
|
return e;
|
|
228
234
|
};
|
|
229
235
|
/**
|
|
@@ -250,21 +256,22 @@ const fault = (
|
|
|
250
256
|
e.code = code;
|
|
251
257
|
e.start = start;
|
|
252
258
|
e.end = end;
|
|
253
|
-
throw attach(e, snap());
|
|
259
|
+
throw attach(e, snap(), names);
|
|
254
260
|
};
|
|
255
261
|
/**
|
|
256
262
|
* Re-locate a diagnostic thrown by a nested compile or render into this
|
|
257
263
|
* template's coordinates, then rethrow it as ours. Always throws.
|
|
258
264
|
*
|
|
259
|
-
* `
|
|
265
|
+
* `guard` is a plain predicate rather than a type guard: `e` is retyped here,
|
|
260
266
|
* not narrowed. `const` with an explicit `never` type is what lets callers
|
|
261
267
|
* treat the catch block as terminal.
|
|
262
268
|
*
|
|
263
|
-
* @type {(e: any, start: number, context: any,
|
|
269
|
+
* @type {(e: any, start: number, context: any, own: any,
|
|
270
|
+
* guard?: (e: unknown) => boolean) => never}
|
|
264
271
|
*/
|
|
265
|
-
const translated = (e, start, context,
|
|
266
|
-
if (!
|
|
267
|
-
throw attach(relocateXprsn(e, { offset: start }), context);
|
|
272
|
+
const translated = (e, start, context, own, guard = isXprsnDiagnostic) => {
|
|
273
|
+
if (!guard(e)) throw e;
|
|
274
|
+
throw attach(relocateXprsn(e, { offset: start }), context, own);
|
|
268
275
|
};
|
|
269
276
|
/**
|
|
270
277
|
* @param {Tok} t
|
|
@@ -295,6 +302,9 @@ let run = (nodes, scope, acc) => {
|
|
|
295
302
|
* @returns {(v: any) => any}
|
|
296
303
|
*/
|
|
297
304
|
let compileExpr = (expr, start, context) => {
|
|
305
|
+
// The compiling template's origin, captured now: the render-time catch below
|
|
306
|
+
// runs long after the module-level `names` has moved on to other compiles.
|
|
307
|
+
const own = names;
|
|
298
308
|
/** @type {ReturnType<typeof compile>} */
|
|
299
309
|
let e;
|
|
300
310
|
try {
|
|
@@ -305,7 +315,7 @@ let compileExpr = (expr, start, context) => {
|
|
|
305
315
|
// than a real one — narrowing `SjabloonFunctions` would change the API.
|
|
306
316
|
e = compile(expr, /** @type {any} */ (fns));
|
|
307
317
|
} catch (x) {
|
|
308
|
-
translated(x, start, context);
|
|
318
|
+
translated(x, start, context, own);
|
|
309
319
|
}
|
|
310
320
|
e.names.forEach((n) => {
|
|
311
321
|
// oxlint-disable-next-line no-unused-expressions
|
|
@@ -317,9 +327,9 @@ let compileExpr = (expr, start, context) => {
|
|
|
317
327
|
return e(v);
|
|
318
328
|
} catch (x) {
|
|
319
329
|
// Read off the compiled expression to pass along, never called through
|
|
320
|
-
// `e` — xprsn's `isDiagnostic` is a closure over
|
|
330
|
+
// `e` — xprsn's `isDiagnostic` is a closure over its store, not a method.
|
|
321
331
|
// oxlint-disable-next-line typescript/unbound-method
|
|
322
|
-
translated(x, start, context, e.isDiagnostic);
|
|
332
|
+
translated(x, start, context, own, e.isDiagnostic);
|
|
323
333
|
}
|
|
324
334
|
};
|
|
325
335
|
};
|
|
@@ -533,14 +543,18 @@ export const litNode = (text) => (scope, acc) => {
|
|
|
533
543
|
/**
|
|
534
544
|
* Bind the parser to an output profile. Each edition calls this once at module
|
|
535
545
|
* load and gets back its own `template` and `render`; the parser itself stays
|
|
536
|
-
* module-level and shared, so there is exactly one diagnostics
|
|
546
|
+
* module-level and shared, so there is exactly one diagnostics store.
|
|
537
547
|
*
|
|
538
|
-
* `template(str, funcs?)` compiles a template once, to render it many
|
|
548
|
+
* `template(str, funcs?, opts?)` compiles a template once, to render it many
|
|
549
|
+
* times.
|
|
539
550
|
*
|
|
540
551
|
* The returned renderer exposes `names`: the variables the template reads
|
|
541
552
|
* from your values, deduplicated. Loop variables the template introduces are
|
|
542
|
-
* not included
|
|
543
|
-
*
|
|
553
|
+
* not included, and neither is anything in `opts.bound` — names the embedder
|
|
554
|
+
* already has in scope (still resolved normally at render time, exactly like
|
|
555
|
+
* xprsn's own `bound`). It also exposes `functions`: the registry functions
|
|
556
|
+
* the template calls, deduplicated. `isDiagnostic(error)` recognizes runtime
|
|
557
|
+
* diagnostics thrown through this renderer alone.
|
|
544
558
|
*
|
|
545
559
|
* Two anchors are always in scope: `$` is the root values, and `@` is the
|
|
546
560
|
* current `#each` item (the root outside any loop). They let a nested loop
|
|
@@ -552,6 +566,12 @@ export const litNode = (text) => (scope, acc) => {
|
|
|
552
566
|
* `root` and `@` becomes `item` (two distinct objects). Omit `item` to leave
|
|
553
567
|
* `@` unbound, so reading `@.x` throws through xprsn's guard.
|
|
554
568
|
*
|
|
569
|
+
* An embedder whose scope chain already binds the anchors renders through
|
|
570
|
+
* `scoped(values)` instead: no wrapper scope is created, `$` and `@` resolve
|
|
571
|
+
* from `values` itself, and a chain that omits `@` leaves it unbound the same
|
|
572
|
+
* way. That is the zero-allocation seam for a host rendering one template per
|
|
573
|
+
* cell per row over scopes it already builds.
|
|
574
|
+
*
|
|
555
575
|
* Render order is push order into a single accumulator: loop bodies append once
|
|
556
576
|
* per iteration, untaken branches append nothing, and block expressions (`#if`
|
|
557
577
|
* conditions, `#each` collections) never append at all. The token edition
|
|
@@ -575,7 +595,8 @@ export const litNode = (text) => (scope, acc) => {
|
|
|
575
595
|
* take: (acc: A) => T,
|
|
576
596
|
* ]} profile The output profile, as above.
|
|
577
597
|
* @returns {{
|
|
578
|
-
* template: (str: string, funcs?: SjabloonFunctions
|
|
598
|
+
* template: (str: string, funcs?: SjabloonFunctions,
|
|
599
|
+
* opts?: { bound?: Iterable<string> }) => SjabloonRenderer<T>,
|
|
579
600
|
* render: (str: string, values?: SjabloonValues, funcs?: SjabloonFunctions) => T,
|
|
580
601
|
* }} That edition's API.
|
|
581
602
|
* @throws {SyntaxError} `template` throws on malformed tags, unclosed blocks,
|
|
@@ -585,15 +606,20 @@ export let make = ([lit, val, raw, seed, take]) => {
|
|
|
585
606
|
/**
|
|
586
607
|
* @param {string} str
|
|
587
608
|
* @param {SjabloonFunctions} [funcs]
|
|
609
|
+
* @param {{ bound?: Iterable<string> }} [opts]
|
|
588
610
|
* @returns {SjabloonRenderer<T>}
|
|
589
611
|
*/
|
|
590
|
-
function template(str, funcs) {
|
|
612
|
+
function template(str, funcs, opts) {
|
|
591
613
|
// oxlint-disable-next-line no-unused-expressions
|
|
592
614
|
((LIT = lit), (VAL = val), (RAW = raw));
|
|
593
615
|
fns = funcs;
|
|
594
616
|
// `$` (root) and `@` (current item) are engine-bound anchors, always in
|
|
595
|
-
// scope, so they never count as caller-supplied `names
|
|
617
|
+
// scope, so they never count as caller-supplied `names` — and neither does
|
|
618
|
+
// anything the embedder declares bound. A loop, not a spread: the
|
|
619
|
+
// bundler's transpile turns an iterable spread into a concat that would
|
|
620
|
+
// wrap a Set instead of unpacking it.
|
|
596
621
|
bound = ["$", "@"];
|
|
622
|
+
if (opts && opts.bound) for (const name of opts.bound) bound.push(name);
|
|
597
623
|
names = new Set();
|
|
598
624
|
functions = new Set();
|
|
599
625
|
source = String(str);
|
|
@@ -604,28 +630,40 @@ export let make = ([lit, val, raw, seed, take]) => {
|
|
|
604
630
|
// Deeply nested blocks fail as SJABLOON_TOO_DEEP at DEPTH via opener(),
|
|
605
631
|
// including elif chains — well below the native stack.
|
|
606
632
|
let nodes = parse([]);
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
|
|
633
|
+
// The trusted-scope render, and the one render body: the caller's chain
|
|
634
|
+
// already carries the anchors, so no wrapper is created and nothing is
|
|
635
|
+
// written anywhere. One accumulator per render, owned here and threaded
|
|
636
|
+
// down. A registry function that renders another template gets its own,
|
|
637
|
+
// so re-entrancy needs no bookkeeping.
|
|
638
|
+
const scoped = (/** @type {any} */ values) => {
|
|
639
|
+
const acc = seed();
|
|
640
|
+
run(nodes, values, acc);
|
|
641
|
+
return take(acc);
|
|
642
|
+
};
|
|
643
|
+
// The default render wraps the values in a root scope carrying the
|
|
644
|
+
// anchors, without mutating what the caller passed: by default `$` and `@`
|
|
645
|
+
// both point at the root. An embedder can override the anchors with a
|
|
646
|
+
// `{ root, item }` second arg: `$` = root, `@` = item (distinct objects).
|
|
647
|
+
// Omitting `item` leaves `@` unbound, so `@.x` throws through xprsn's
|
|
648
|
+
// guard — a group-header band that has no current row wants exactly that.
|
|
613
649
|
const f = (/** @type {any} */ values, /** @type {any} */ anchors) => {
|
|
614
650
|
values = values || EMPTY;
|
|
615
651
|
const r = Object.create(values);
|
|
616
652
|
r["$"] = anchors ? anchors.root : values;
|
|
617
653
|
r["@"] = anchors ? anchors.item : values;
|
|
618
|
-
|
|
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);
|
|
654
|
+
return scoped(r);
|
|
624
655
|
};
|
|
625
656
|
// Array.from, not a spread: the bundler's transpile turns `[...set]` into
|
|
626
657
|
// `[].concat(set)`, which wraps the Set instead of unpacking it.
|
|
627
658
|
f.names = Array.from(names);
|
|
628
659
|
f.functions = Array.from(functions);
|
|
660
|
+
// This compile's own `names` set doubles as its origin: every diagnostic
|
|
661
|
+
// thrown through this renderer was marked with it, at compile time by
|
|
662
|
+
// `fault` and at render time by the closures `compileExpr` built. Captured
|
|
663
|
+
// now — the module-level `names` moves on to the next compile.
|
|
664
|
+
const o = names;
|
|
665
|
+
f.isDiagnostic = (/** @type {unknown} */ x) => origin(x) === o;
|
|
666
|
+
f.scoped = scoped;
|
|
629
667
|
return f;
|
|
630
668
|
}
|
|
631
669
|
return { template, render: (str, values, funcs) => template(str, funcs)(values) };
|
package/lib/html.d.ts
CHANGED
|
@@ -11,7 +11,11 @@ import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues } from "./type
|
|
|
11
11
|
* @see SjabloonRenderer for `names`/`functions`, SjabloonScope for `$` and `@`.
|
|
12
12
|
* @throws {SyntaxError} On malformed tags, unclosed blocks, or bad expressions.
|
|
13
13
|
*/
|
|
14
|
-
export function template(
|
|
14
|
+
export function template(
|
|
15
|
+
str: string,
|
|
16
|
+
funcs?: SjabloonFunctions,
|
|
17
|
+
opts?: { bound?: Iterable<string> },
|
|
18
|
+
): SjabloonRenderer<string>;
|
|
15
19
|
|
|
16
20
|
/** Compile and render in one go. Shorthand for `template(str, funcs)(values)`. */
|
|
17
21
|
export function render(str: string, values?: SjabloonValues, funcs?: SjabloonFunctions): string;
|
package/lib/index.d.ts
CHANGED
|
@@ -11,7 +11,11 @@ import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues, Token } from
|
|
|
11
11
|
* @see SjabloonRenderer for `names`/`functions`, SjabloonScope for `$` and `@`.
|
|
12
12
|
* @throws {SyntaxError} On malformed tags, unclosed blocks, or bad expressions.
|
|
13
13
|
*/
|
|
14
|
-
export function template(
|
|
14
|
+
export function template(
|
|
15
|
+
str: string,
|
|
16
|
+
funcs?: SjabloonFunctions,
|
|
17
|
+
opts?: { bound?: Iterable<string> },
|
|
18
|
+
): SjabloonRenderer<Token[]>;
|
|
15
19
|
|
|
16
20
|
/** Compile and render in one go. Shorthand for `template(str, funcs)(values)`. */
|
|
17
21
|
export function render(str: string, values?: SjabloonValues, funcs?: SjabloonFunctions): Token[];
|
package/lib/text.d.ts
CHANGED
|
@@ -11,7 +11,11 @@ import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues } from "./type
|
|
|
11
11
|
* @see SjabloonRenderer for `names`/`functions`, SjabloonScope for `$` and `@`.
|
|
12
12
|
* @throws {SyntaxError} On malformed tags, unclosed blocks, or bad expressions.
|
|
13
13
|
*/
|
|
14
|
-
export function template(
|
|
14
|
+
export function template(
|
|
15
|
+
str: string,
|
|
16
|
+
funcs?: SjabloonFunctions,
|
|
17
|
+
opts?: { bound?: Iterable<string> },
|
|
18
|
+
): SjabloonRenderer<string>;
|
|
15
19
|
|
|
16
20
|
/** Compile and render in one go. Shorthand for `template(str, funcs)(values)`. */
|
|
17
21
|
export function render(str: string, values?: SjabloonValues, funcs?: SjabloonFunctions): string;
|
package/lib/types.d.ts
CHANGED
|
@@ -49,13 +49,25 @@ export interface SjabloonScope {
|
|
|
49
49
|
* A compiled template: render it many times.
|
|
50
50
|
*
|
|
51
51
|
* `names` are the variables the template reads from your values, deduplicated;
|
|
52
|
-
* loop variables the template introduces are not included
|
|
53
|
-
*
|
|
52
|
+
* loop variables the template introduces are not included, and neither is
|
|
53
|
+
* anything the compile's `bound` option declared. `functions` are the registry
|
|
54
|
+
* functions the template calls, deduplicated.
|
|
55
|
+
*
|
|
56
|
+
* `isDiagnostic(error)` recognizes runtime diagnostics thrown through this
|
|
57
|
+
* renderer alone — the per-renderer twin of the module-wide `isDiagnostic`.
|
|
58
|
+
*
|
|
59
|
+
* `scoped(values)` renders over a scope chain that already binds the anchors:
|
|
60
|
+
* no wrapper scope is created, `$` and `@` resolve from `values` itself, and a
|
|
61
|
+
* chain that omits `@` leaves it unbound so `@.x` throws through xprsn's
|
|
62
|
+
* guard. The zero-allocation seam for an embedder rendering over scopes it
|
|
63
|
+
* already builds; `{{#each}}` still re-points `@` inside its body.
|
|
54
64
|
*/
|
|
55
65
|
export interface SjabloonRenderer<T> {
|
|
56
66
|
(values?: SjabloonValues, scope?: SjabloonScope): T;
|
|
57
67
|
names: string[];
|
|
58
68
|
functions: string[];
|
|
69
|
+
isDiagnostic(error: unknown): boolean;
|
|
70
|
+
scoped(values: SjabloonValues): T;
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
/** One static text run of the template, verbatim. */
|