sjabloon 0.10.0 → 0.11.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 +12 -1
- package/lib/core.js +30 -2
- package/lib/html.js +5 -5
- package/lib/index.d.ts +10 -1
- package/lib/index.js +8 -10
- package/lib/text.js +2 -2
- package/lib/types.d.ts +14 -3
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -95,6 +95,13 @@ tpl.functions; // => ['fmt']
|
|
|
95
95
|
template("{{ run.total }} of {{ count }}", undefined, { bound: ["run"] }).names; // => ['count']
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
The renderer also carries `reads`: every root-name read with its span in the template source, in source order. Duplicates, anchors, loop variables and bound names are all kept — `names` is the free, deduplicated view. This is what an editor squiggles, hovers, and jumps from:
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
template("{{ title }}: {{ total }}").reads;
|
|
102
|
+
// => [{ name: 'title', start: 3, end: 8 }, { name: 'total', start: 16, end: 21 }]
|
|
103
|
+
```
|
|
104
|
+
|
|
98
105
|
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
106
|
|
|
100
107
|
### `renderer.scoped(values)`
|
|
@@ -113,7 +120,11 @@ Shorthand for `template(str, functions)(values)`, returning whatever its edition
|
|
|
113
120
|
|
|
114
121
|
### `text(tokens)` (root entry only)
|
|
115
122
|
|
|
116
|
-
Joins a token stream the way `sjabloon/text` would have rendered it: literals verbatim, values
|
|
123
|
+
Joins a token stream the way `sjabloon/text` would have rendered it: literals verbatim, values through `display()`. `text(template(str)(values))` and `sjabloon/text`'s `template(str)(values)` are equal for every template and every set of values. The test suite and the fuzzer both check that.
|
|
124
|
+
|
|
125
|
+
### `display(value)` (root entry only)
|
|
126
|
+
|
|
127
|
+
The scalar display rule every edition and `text()` share, exported for embedders that stringify token values themselves. A valid `Date` renders as ISO 8601 UTC (`toISOString()`) — the same bytes on every machine, where `String(date)` would bake in the host's timezone and locale. An invalid `Date` keeps its deterministic `'Invalid Date'` form, nullish displays empty, and everything else is `String(value)`.
|
|
117
128
|
|
|
118
129
|
```js
|
|
119
130
|
import { template, text } from "sjabloon";
|
package/lib/core.js
CHANGED
|
@@ -132,6 +132,8 @@ let last;
|
|
|
132
132
|
let bound;
|
|
133
133
|
/** @type {Set<string>} */
|
|
134
134
|
let names;
|
|
135
|
+
/** @type {{ name: string, start: number, end: number }[]} */
|
|
136
|
+
let reads;
|
|
135
137
|
/** @type {Set<string>} */
|
|
136
138
|
let functions;
|
|
137
139
|
/** @type {string} */
|
|
@@ -321,6 +323,9 @@ let compileExpr = (expr, start, context) => {
|
|
|
321
323
|
// oxlint-disable-next-line no-unused-expressions
|
|
322
324
|
bound.includes(n) || names.add(n);
|
|
323
325
|
});
|
|
326
|
+
// Every read, shifted into template coordinates — bound names and loop
|
|
327
|
+
// variables included; `names` above stays the free, deduplicated view.
|
|
328
|
+
for (const r of e.reads) reads.push({ name: r.name, start: start + r.start, end: start + r.end });
|
|
324
329
|
for (const fn of e.functions) functions.add(fn);
|
|
325
330
|
return (v) => {
|
|
326
331
|
try {
|
|
@@ -540,6 +545,25 @@ export const litNode = (text) => (scope, acc) => {
|
|
|
540
545
|
acc.text += text;
|
|
541
546
|
};
|
|
542
547
|
|
|
548
|
+
/**
|
|
549
|
+
* Display text for one interpolated value — the scalar rule every edition and
|
|
550
|
+
* the root `text()` join share. A valid `Date` renders as ISO 8601 UTC
|
|
551
|
+
* (`toISOString()`), the same on every machine, where `String(date)` would
|
|
552
|
+
* bake in the host's timezone and locale; an invalid `Date` keeps its
|
|
553
|
+
* deterministic `"Invalid Date"` form. Nullish displays empty; everything
|
|
554
|
+
* else is `String(value)`.
|
|
555
|
+
*
|
|
556
|
+
* @param {unknown} value One rendered value.
|
|
557
|
+
* @returns {string} The display text.
|
|
558
|
+
*/
|
|
559
|
+
export const display = (value) =>
|
|
560
|
+
value instanceof Date && Number.isFinite(value.getTime())
|
|
561
|
+
? value.toISOString()
|
|
562
|
+
: // Stringifying an arbitrary value is this rule's documented contract, so
|
|
563
|
+
// `no-base-to-string` is describing the feature rather than a mistake.
|
|
564
|
+
// oxlint-disable-next-line typescript/no-base-to-string
|
|
565
|
+
String(value ?? "");
|
|
566
|
+
|
|
543
567
|
/**
|
|
544
568
|
* Bind the parser to an output profile. Each edition calls this once at module
|
|
545
569
|
* load and gets back its own `template` and `render`; the parser itself stays
|
|
@@ -552,8 +576,10 @@ export const litNode = (text) => (scope, acc) => {
|
|
|
552
576
|
* from your values, deduplicated. Loop variables the template introduces are
|
|
553
577
|
* not included, and neither is anything in `opts.bound` — names the embedder
|
|
554
578
|
* already has in scope (still resolved normally at render time, exactly like
|
|
555
|
-
* xprsn's own `bound`). It also exposes `
|
|
556
|
-
* the template
|
|
579
|
+
* xprsn's own `bound`). It also exposes `reads`: every root-name read with its
|
|
580
|
+
* span in the template source, in source order — duplicates, anchors, loop
|
|
581
|
+
* variables and bound names kept, so `names` is its free, deduplicated view.
|
|
582
|
+
* And `functions`: the registry functions the template calls, deduplicated. `isDiagnostic(error)` recognizes runtime
|
|
557
583
|
* diagnostics thrown through this renderer alone.
|
|
558
584
|
*
|
|
559
585
|
* Two anchors are always in scope: `$` is the root values, and `@` is the
|
|
@@ -621,6 +647,7 @@ export let make = ([lit, val, raw, seed, take]) => {
|
|
|
621
647
|
bound = ["$", "@"];
|
|
622
648
|
if (opts && opts.bound) for (const name of opts.bound) bound.push(name);
|
|
623
649
|
names = new Set();
|
|
650
|
+
reads = [];
|
|
624
651
|
functions = new Set();
|
|
625
652
|
source = String(str);
|
|
626
653
|
blocks = [];
|
|
@@ -656,6 +683,7 @@ export let make = ([lit, val, raw, seed, take]) => {
|
|
|
656
683
|
// Array.from, not a spread: the bundler's transpile turns `[...set]` into
|
|
657
684
|
// `[].concat(set)`, which wraps the Set instead of unpacking it.
|
|
658
685
|
f.names = Array.from(names);
|
|
686
|
+
f.reads = reads;
|
|
659
687
|
f.functions = Array.from(functions);
|
|
660
688
|
// This compile's own `names` set doubles as its origin: every diagnostic
|
|
661
689
|
// thrown through this renderer was marked with it, at compile time by
|
package/lib/html.js
CHANGED
|
@@ -3,19 +3,19 @@
|
|
|
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 { litNode, make } from "./core.js";
|
|
6
|
+
import { display, litNode, make } from "./core.js";
|
|
7
7
|
|
|
8
8
|
/** @type {Record<string, string>} */
|
|
9
9
|
const ESC = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
10
|
-
/** @param {
|
|
11
|
-
const esc = (s) =>
|
|
10
|
+
/** @param {string} s Already display text — both call sites pass `display()`. */
|
|
11
|
+
const esc = (s) => s.replace(/[&<>"']/g, (c) => ESC[c]);
|
|
12
12
|
|
|
13
13
|
export { isDiagnostic, relocate } from "./core.js";
|
|
14
14
|
|
|
15
15
|
export const { template, render } = make([
|
|
16
16
|
litNode,
|
|
17
|
-
(expr) => (scope, acc, value) => ((value = expr(scope)), (acc.text += esc(value
|
|
18
|
-
(expr) => (scope, acc, value) => ((value = expr(scope)), (acc.text +=
|
|
17
|
+
(expr) => (scope, acc, value) => ((value = expr(scope)), (acc.text += esc(display(value)))),
|
|
18
|
+
(expr) => (scope, acc, value) => ((value = expr(scope)), (acc.text += display(value))),
|
|
19
19
|
() => ({ text: "" }),
|
|
20
20
|
(acc) => acc.text,
|
|
21
21
|
]);
|
package/lib/index.d.ts
CHANGED
|
@@ -22,6 +22,15 @@ export function render(str: string, values?: SjabloonValues, funcs?: SjabloonFun
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Join a token stream into the string `sjabloon/text` would have produced:
|
|
25
|
-
* literals verbatim, values
|
|
25
|
+
* literals verbatim, values through `display()`.
|
|
26
26
|
*/
|
|
27
27
|
export function text(tokens: readonly Token[]): string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Display text for one interpolated value — the scalar rule `text()` and the
|
|
31
|
+
* string editions share. A valid `Date` renders as ISO 8601 UTC
|
|
32
|
+
* (`toISOString()`), deterministically across machines; an invalid `Date`
|
|
33
|
+
* stays `"Invalid Date"`; nullish displays empty; everything else is
|
|
34
|
+
* `String(value)`.
|
|
35
|
+
*/
|
|
36
|
+
export function display(value: unknown): string;
|
package/lib/index.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* so nothing here is HTML-aware and `{{{ }}}` has no meaning — `{{ }}` is
|
|
5
5
|
* already raw.
|
|
6
6
|
*/
|
|
7
|
-
import { make } from "./core.js";
|
|
7
|
+
import { display, make } from "./core.js";
|
|
8
8
|
|
|
9
|
-
export { isDiagnostic, relocate } from "./core.js";
|
|
9
|
+
export { display, isDiagnostic, relocate } from "./core.js";
|
|
10
10
|
|
|
11
11
|
export const { template, render } = make([
|
|
12
12
|
// Static text is a compile-time constant: hoist and freeze one token per
|
|
@@ -25,20 +25,18 @@ export const { template, render } = make([
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Join a token stream into the string `sjabloon/text` would have produced:
|
|
28
|
-
* literals verbatim, values
|
|
28
|
+
* literals verbatim, values through `display()` — the one scalar rule, so a
|
|
29
|
+
* `Date` joins as ISO 8601 UTC here exactly as the string editions render it.
|
|
29
30
|
*
|
|
30
31
|
* @param {readonly import('./types.js').Token[]} tokens A render's output.
|
|
31
32
|
* @returns {string} The joined text.
|
|
32
33
|
*/
|
|
33
34
|
export const text = (tokens) => {
|
|
34
35
|
let s = "";
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
36
|
+
// A literal never stringifies and a nullish value still renders empty.
|
|
37
|
+
// Widened here because each token carries one key or the other, which the
|
|
38
|
+
// public union deliberately does not model.
|
|
38
39
|
for (const t of /** @type {readonly { literal?: string, value?: unknown }[]} */ (tokens))
|
|
39
|
-
|
|
40
|
-
// `no-base-to-string` is describing the feature rather than a mistake.
|
|
41
|
-
// oxlint-disable-next-line typescript/no-base-to-string
|
|
42
|
-
s += t.literal ?? String(t.value ?? "");
|
|
40
|
+
s += t.literal ?? display(t.value);
|
|
43
41
|
return s;
|
|
44
42
|
};
|
package/lib/text.js
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* Definitionally `text(template(str)(values))` from the root entry, but built
|
|
7
7
|
* as a string accumulator so casual string users never allocate tokens.
|
|
8
8
|
*/
|
|
9
|
-
import { litNode, make } from "./core.js";
|
|
9
|
+
import { display, litNode, make } from "./core.js";
|
|
10
10
|
|
|
11
11
|
export { isDiagnostic, relocate } from "./core.js";
|
|
12
12
|
|
|
13
13
|
export const { template, render } = make([
|
|
14
14
|
litNode,
|
|
15
|
-
(expr) => (scope, acc, value) => ((value = expr(scope)), (acc.text +=
|
|
15
|
+
(expr) => (scope, acc, value) => ((value = expr(scope)), (acc.text += display(value))),
|
|
16
16
|
0,
|
|
17
17
|
() => ({ text: "" }),
|
|
18
18
|
(acc) => acc.text,
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { XprsnErrorCode } from "xprsn";
|
|
1
|
+
import type { XprsnErrorCode, XprsnRead } from "xprsn";
|
|
2
2
|
|
|
3
3
|
export type SjabloonErrorCode =
|
|
4
4
|
| XprsnErrorCode
|
|
@@ -24,6 +24,13 @@ export interface SjabloonDiagnostic extends Error {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export type SjabloonValues = Record<string, any>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One root-name read, with its span in the template source — xprsn's read
|
|
30
|
+
* record, forwarded with only its coordinates shifted.
|
|
31
|
+
*/
|
|
32
|
+
export type SjabloonRead = XprsnRead;
|
|
33
|
+
|
|
27
34
|
export type SjabloonFunctions = Record<string, Function>;
|
|
28
35
|
|
|
29
36
|
/**
|
|
@@ -50,8 +57,11 @@ export interface SjabloonScope {
|
|
|
50
57
|
*
|
|
51
58
|
* `names` are the variables the template reads from your values, deduplicated;
|
|
52
59
|
* loop variables the template introduces are not included, and neither is
|
|
53
|
-
* anything the compile's `bound` option declared. `
|
|
54
|
-
*
|
|
60
|
+
* anything the compile's `bound` option declared. `reads` are every root-name
|
|
61
|
+
* read with its span in the template source, in source order — duplicates,
|
|
62
|
+
* anchors, loop variables and bound names kept, so `names` is its free,
|
|
63
|
+
* deduplicated view. `functions` are the registry functions the template
|
|
64
|
+
* calls, deduplicated.
|
|
55
65
|
*
|
|
56
66
|
* `isDiagnostic(error)` recognizes runtime diagnostics thrown through this
|
|
57
67
|
* renderer alone — the per-renderer twin of the module-wide `isDiagnostic`.
|
|
@@ -65,6 +75,7 @@ export interface SjabloonScope {
|
|
|
65
75
|
export interface SjabloonRenderer<T> {
|
|
66
76
|
(values?: SjabloonValues, scope?: SjabloonScope): T;
|
|
67
77
|
names: string[];
|
|
78
|
+
reads: SjabloonRead[];
|
|
68
79
|
functions: string[];
|
|
69
80
|
isDiagnostic(error: unknown): boolean;
|
|
70
81
|
scoped(values: SjabloonValues): T;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sjabloon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Tiny, CSP-safe template engine for JavaScript, powered by xprsn expressions. No eval, no new Function.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"csp",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"test:unit": "c8 --100 --src lib/ node --disallow-code-generation-from-strings --test --test-concurrency=1 test/*.test.js"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"xprsn": "^0.
|
|
64
|
+
"xprsn": "^0.11.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@arethetypeswrong/cli": "^0.18.3",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"ignore": [
|
|
86
86
|
"xprsn"
|
|
87
87
|
],
|
|
88
|
-
"limit": "2.
|
|
88
|
+
"limit": "2.35 kB"
|
|
89
89
|
},
|
|
90
90
|
{
|
|
91
91
|
"name": "sjabloon/text",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"ignore": [
|
|
94
94
|
"xprsn"
|
|
95
95
|
],
|
|
96
|
-
"limit": "2.
|
|
96
|
+
"limit": "2.35 kB"
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
"name": "sjabloon/html",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"ignore": [
|
|
102
102
|
"xprsn"
|
|
103
103
|
],
|
|
104
|
-
"limit": "2.
|
|
104
|
+
"limit": "2.4 kB"
|
|
105
105
|
}
|
|
106
106
|
],
|
|
107
107
|
"engines": {
|