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/EMBEDDING.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Embedding sjabloon
|
|
2
|
+
|
|
3
|
+
For hosts that compile templates out of a larger document — a cell in a report, a
|
|
4
|
+
field in a form, a block in a page builder — and that need to seed their own
|
|
5
|
+
scope chain, introspect templates, drive an editor, or report faults in their own
|
|
6
|
+
coordinates.
|
|
7
|
+
|
|
8
|
+
None of this is needed to render templates. [README.md](README.md) covers the
|
|
9
|
+
ordinary surface: the editions, the syntax, `template`, `render`, and the error
|
|
10
|
+
codes.
|
|
11
|
+
|
|
12
|
+
- [Seeding the anchors](#seeding-the-anchors)
|
|
13
|
+
- [`renderer.scoped(values)`](#rendererscopedvalues)
|
|
14
|
+
- [Introspection](#introspection)
|
|
15
|
+
- [`options.bound`](#optionsbound)
|
|
16
|
+
- [`reads`](#reads)
|
|
17
|
+
- [Diagnostic identity](#diagnostic-identity)
|
|
18
|
+
- [`relocate(diagnostic, options)`](#relocatediagnostic-options)
|
|
19
|
+
- [The token contract](#the-token-contract)
|
|
20
|
+
- [`display(value)`](#displayvalue)
|
|
21
|
+
|
|
22
|
+
## Seeding the anchors
|
|
23
|
+
|
|
24
|
+
By default the two anchors both point at the values you pass: `$` is the root and
|
|
25
|
+
`@` is the current `{{#each}}` item, which outside a loop is the root as well.
|
|
26
|
+
That is what an ordinary caller wants, and it needs no extra argument.
|
|
27
|
+
|
|
28
|
+
An engine with its own scope model usually wants them seeded from distinct
|
|
29
|
+
objects. Pass `{ root, item }` as the renderer's second argument:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
const tpl = template("{{ $.report }} / {{ @.row }}");
|
|
33
|
+
|
|
34
|
+
tpl(base, { root: reportRoot, item: currentRow }); // $ = reportRoot, @ = currentRow
|
|
35
|
+
tpl(base, { root: reportRoot }); // no item → @.x throws
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Omitting `item` leaves `@` unbound, so `@.x` throws where there is no current
|
|
39
|
+
item. That is the useful behaviour for a banded report: a group header has no
|
|
40
|
+
representative row, and an unbound `@` turns a mistake into an error instead of a
|
|
41
|
+
silently plausible number. `{{#each}}` still re-points `@` to the current item
|
|
42
|
+
inside its body either way.
|
|
43
|
+
|
|
44
|
+
## `renderer.scoped(values)`
|
|
45
|
+
|
|
46
|
+
The trusted-scope render, for an embedder whose scope chain already binds the
|
|
47
|
+
anchors. The default call wraps `values` in a fresh scope and seeds `$` and `@`
|
|
48
|
+
into it; `scoped` skips the wrapper. `$` and `@` resolve from `values` itself,
|
|
49
|
+
and a chain that omits `@` leaves it unbound. `{{#each}}` still re-points `@`
|
|
50
|
+
inside its body, and nothing is ever written to your objects.
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
const row = Object.create(base); // base binds $ once per render
|
|
54
|
+
row["@"] = item;
|
|
55
|
+
tpl.scoped(row);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Rendering one template per cell per row over scopes you already build, this is
|
|
59
|
+
the path with zero per-call allocations beyond the output.
|
|
60
|
+
|
|
61
|
+
## Introspection
|
|
62
|
+
|
|
63
|
+
Every renderer carries `names` and `functions`, documented in the
|
|
64
|
+
[README](README.md#templatestr-functions-options). The two below are for
|
|
65
|
+
validators and editors.
|
|
66
|
+
|
|
67
|
+
### `options.bound`
|
|
68
|
+
|
|
69
|
+
`options.bound` lists names your engine already has in scope — a loop variable, a
|
|
70
|
+
handle, a `page` anchor. They are excluded from `names` and still resolve
|
|
71
|
+
normally at render time, the same contract as
|
|
72
|
+
[xprsn's own `bound`](https://github.com/getquario/xprsn/blob/main/EMBEDDING.md#optionsbound):
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
template("{{ run.total }} of {{ count }}", undefined, { bound: ["run"] }).names;
|
|
76
|
+
// => ['count']
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Without it, every stored template would appear to depend on variables its author
|
|
80
|
+
never supplied, and a schema check like `tpl.names.every(n => n in model)` would
|
|
81
|
+
reject valid templates.
|
|
82
|
+
|
|
83
|
+
### `reads`
|
|
84
|
+
|
|
85
|
+
`reads` is every root-name read with its span in the template source, in source
|
|
86
|
+
order. Duplicates, anchors, loop variables and bound names are all kept — `names`
|
|
87
|
+
is the free, deduplicated view.
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
template("{{ title }}: {{ total }}").reads;
|
|
91
|
+
// => [{ name: 'title', start: 3, end: 8 }, { name: 'total', start: 16, end: 21 }]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Spans are offsets into the original template, not into the expression inside the
|
|
95
|
+
tag, so an editor can squiggle, hover, and jump straight from them. An unknown
|
|
96
|
+
variable is not an error — it renders empty — so an editor that wants to warn
|
|
97
|
+
about typos has to do it from `reads` against a known data model.
|
|
98
|
+
|
|
99
|
+
## Diagnostic identity
|
|
100
|
+
|
|
101
|
+
`isDiagnostic(error)` returns `true` only for errors produced or translated by
|
|
102
|
+
the same sjabloon module instance. It is an identity check, not a shape check:
|
|
103
|
+
copying a documented `code`, `start`, `end`, and `blocks` onto another error does
|
|
104
|
+
not authenticate it, and a diagnostic from another installed copy returns
|
|
105
|
+
`false`. All three editions share one core, so mixing them in a single process is
|
|
106
|
+
safe — an error thrown through `sjabloon/html` authenticates through `sjabloon`.
|
|
107
|
+
|
|
108
|
+
That matters because a host has to tell three kinds of failure apart:
|
|
109
|
+
|
|
110
|
+
1. sjabloon's own faults, which have a span in the template;
|
|
111
|
+
2. errors thrown by _your_ registered functions, getters, methods, or coercion
|
|
112
|
+
hooks, which sjabloon passes through unchanged and does not annotate;
|
|
113
|
+
3. everything else.
|
|
114
|
+
|
|
115
|
+
Only the first can be pointed at a source location.
|
|
116
|
+
|
|
117
|
+
Every renderer also carries its own `isDiagnostic(error)`, `true` only for
|
|
118
|
+
runtime diagnostics thrown through _that_ renderer. An embedder holding many
|
|
119
|
+
compiled templates asks the one that just rendered, so a diagnostic that leaked
|
|
120
|
+
from an unrelated template is not mistaken for this cell's. Compile-time
|
|
121
|
+
diagnostics happen before a renderer exists, so they authenticate only through
|
|
122
|
+
the module-wide predicate.
|
|
123
|
+
|
|
124
|
+
## `relocate(diagnostic, options)`
|
|
125
|
+
|
|
126
|
+
An embedder that compiles templates out of a larger document reports the fault in
|
|
127
|
+
its own coordinates, not the template's.
|
|
128
|
+
`relocate(diagnostic, { prefix, offset })` returns the copy to re-throw:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { isDiagnostic, relocate, template } from "sjabloon";
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
template(cell.value);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (!isDiagnostic(error)) throw error;
|
|
137
|
+
throw relocate(error, { prefix: "detail.cells[0].value: " });
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The copy keeps the original's class, prepends `prefix` to the message verbatim,
|
|
142
|
+
moves the span, and carries every other field across by
|
|
143
|
+
descriptor — including the frozen `blocks` context, which stays frozen and
|
|
144
|
+
non-writable on the copy. `blocks` is the same array, so its openers' own
|
|
145
|
+
`start`/`end` stay in template coordinates while the error's span moves.
|
|
146
|
+
|
|
147
|
+
The copy is registered exactly as the original was, so it passes `isDiagnostic`.
|
|
148
|
+
An expression fault is an xprsn diagnostic that sjabloon translated into template
|
|
149
|
+
coordinates; relocating it goes through xprsn, so the copy stays authentic to
|
|
150
|
+
both packages just as the original is. The original is left untouched. Passing
|
|
151
|
+
anything but a sjabloon diagnostic throws a `TypeError`.
|
|
152
|
+
|
|
153
|
+
`offset` shifts the span, and it is right whenever the template was a verbatim
|
|
154
|
+
slice of your text. It is wrong when your text was **decoded** first — a template
|
|
155
|
+
read out of a JSON string literal, where an escape makes every later offset
|
|
156
|
+
slide. There is no offset that fixes that, so name the region the template came
|
|
157
|
+
from instead:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
throw relocate(error, { prefix: "cells[3].template: ", span: [16, 41] });
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`span` replaces the span outright and wins if you pass both. Neither option adds
|
|
164
|
+
a span to a diagnostic that had none.
|
|
165
|
+
|
|
166
|
+
Relocation lives here rather than in the embedder because authentication is by
|
|
167
|
+
identity: a copy an embedder builds itself cannot be authenticated, and a field
|
|
168
|
+
added to a diagnostic here would be a field the embedder's copy silently drops.
|
|
169
|
+
|
|
170
|
+
## The token contract
|
|
171
|
+
|
|
172
|
+
The root entry renders to `Token[]`, where a `Token` is either
|
|
173
|
+
`{ literal: string }` or `{ value: unknown }`. The guarantees a consumer can rely
|
|
174
|
+
on:
|
|
175
|
+
|
|
176
|
+
- **Values are pre-stringify.** `{{ total }}` holding `1000` yields the number
|
|
177
|
+
`1000`, not `"1000"`, and nullish stays nullish. Stringification is deferred to
|
|
178
|
+
`text()`, so a value with no primitive conversion reaches the stream intact and
|
|
179
|
+
only fails when something asks for text.
|
|
180
|
+
- **Order is render order.** Loop bodies append once per iteration, untaken
|
|
181
|
+
branches append nothing, and block expressions (`#if` conditions, `#each`
|
|
182
|
+
collections) never appear. They steer the render; they are not part of the
|
|
183
|
+
stream.
|
|
184
|
+
- **Literals are the template's static runs**, one token each, never merged and
|
|
185
|
+
never empty. The interleaving tells you the shape: a bare `{{ amount }}` is
|
|
186
|
+
exactly one value token, while `Total: {{ amount }}` is a literal followed by a
|
|
187
|
+
value. A spreadsheet cell that is _only_ a number is a different thing from one
|
|
188
|
+
that happens to contain one — which is why the engine emits tokens rather than
|
|
189
|
+
a string plus a list of values.
|
|
190
|
+
- Literal tokens are frozen and shared across loop iterations; value tokens are
|
|
191
|
+
fresh per emit. Do not mutate or key a cache on a literal token's identity
|
|
192
|
+
across iterations.
|
|
193
|
+
|
|
194
|
+
### Naming an interpolation with `tag`
|
|
195
|
+
|
|
196
|
+
A value token carries its value and nothing about where it came from, so an
|
|
197
|
+
embedder that has to treat one interpolation differently from the rest — a page
|
|
198
|
+
number a word processor writes as a live field, rather than the number the
|
|
199
|
+
render happened to see — cannot tell them apart from the stream. `tag` is that
|
|
200
|
+
seam:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
const FIELD = { "page.number": { field: "page.number" } };
|
|
204
|
+
const tpl = template("Page {{ page.number }} of {{ page.total }}", fns, {
|
|
205
|
+
tag: (expr) => FIELD[expr],
|
|
206
|
+
});
|
|
207
|
+
tpl({ page: { number: 1, total: 2 } });
|
|
208
|
+
// [{ literal: 'Page ' }, { value: 1, field: 'page.number' }, { literal: ' of ' }, { value: 2 }]
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- **Called once per interpolation, while compiling.** Never at render time, so
|
|
212
|
+
what a token carries is a compile-time constant and a hot render pays nothing
|
|
213
|
+
for the keys it does not have. A template compiled without `tag` emits the
|
|
214
|
+
exact closure and the exact stream it always did.
|
|
215
|
+
- **The argument is the expression source as written and trimmed.** `{{ x }}`,
|
|
216
|
+
`{{x}}` and `{{- x -}}` all arrive as `'x'`, so an equality test against the
|
|
217
|
+
spelling you are looking for is exact. Match it, or return `undefined` and the
|
|
218
|
+
token is untouched.
|
|
219
|
+
- **The returned keys join every value token that interpolation emits** — once
|
|
220
|
+
per loop iteration, under the names you chose. `value` and `literal` are the
|
|
221
|
+
stream's own: `value` is written last so a tag cannot take it over, and
|
|
222
|
+
returning `literal` would make a token answer to both kinds, so do not.
|
|
223
|
+
- **It runs inside the parse**, which is shared, synchronous state. Read the
|
|
224
|
+
expression and return; do not compile another template from within it.
|
|
225
|
+
- **Block expressions are not offered.** `#if` conditions and `#each`
|
|
226
|
+
collections steer the render and emit no token, so there is nothing to name.
|
|
227
|
+
- The token edition alone has tokens to carry the keys; `sjabloon/text` and
|
|
228
|
+
`sjabloon/html` ignore the option.
|
|
229
|
+
|
|
230
|
+
`text(tokens)` joins a stream the way `sjabloon/text` would have rendered it, and
|
|
231
|
+
the two are equal for every template and every set of values. The test suite and
|
|
232
|
+
the fuzzer both check that.
|
|
233
|
+
|
|
234
|
+
### `display(value)`
|
|
235
|
+
|
|
236
|
+
The scalar display rule every edition and `text()` share, exported from the root
|
|
237
|
+
entry for embedders that stringify token values themselves.
|
|
238
|
+
|
|
239
|
+
A valid `Date` renders as ISO 8601 UTC (`toISOString()`) — the same bytes on
|
|
240
|
+
every machine, where `String(date)` would bake in the host's timezone and locale.
|
|
241
|
+
An invalid `Date` keeps its deterministic `'Invalid Date'` form, nullish displays
|
|
242
|
+
empty, and everything else is `String(value)`.
|
|
243
|
+
|
|
244
|
+
Use it rather than your own `String(...)` when you consume tokens and want a
|
|
245
|
+
target's output to agree with `sjabloon/text` on the same template.
|