@transclude/core 0.1.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/LICENSE +21 -0
- package/README.md +121 -0
- package/bin/build.js +469 -0
- package/bin/check.js +78 -0
- package/bin/dev.js +348 -0
- package/bin/release.js +176 -0
- package/bin/serve.bun.js +15 -0
- package/bin/serve.deno.js +15 -0
- package/bin/serve.js +12 -0
- package/editor/server.js +172 -0
- package/editor/vscode/extension.js +49 -0
- package/editor/vscode/package.json +32 -0
- package/editor/vscode/syntaxes/transclude.injection.json +41 -0
- package/package.json +82 -0
- package/src/address.js +183 -0
- package/src/app.js +492 -0
- package/src/cache.js +137 -0
- package/src/compiler/bind.js +496 -0
- package/src/compiler/codegen.js +1061 -0
- package/src/compiler/expr.js +221 -0
- package/src/compiler/index.js +964 -0
- package/src/compiler/interp.js +82 -0
- package/src/compiler/script.js +620 -0
- package/src/compiler/shim.js +756 -0
- package/src/compiler/sourcemap.js +140 -0
- package/src/compiler/types.js +163 -0
- package/src/compress.js +104 -0
- package/src/cookies.js +157 -0
- package/src/csp.js +192 -0
- package/src/document.js +604 -0
- package/src/extract.js +339 -0
- package/src/feed.js +194 -0
- package/src/include.js +89 -0
- package/src/lookup.js +49 -0
- package/src/negotiate.js +95 -0
- package/src/plugin.js +423 -0
- package/src/pool.js +29 -0
- package/src/precache.js +68 -0
- package/src/production.js +159 -0
- package/src/project.js +110 -0
- package/src/proxy.js +319 -0
- package/src/public-files.js +77 -0
- package/src/rewrite.js +281 -0
- package/src/routes.js +199 -0
- package/src/runtime/index.js +1345 -0
- package/src/server.js +183 -0
- package/src/sitemap.js +124 -0
- package/src/static-cache.js +170 -0
- package/src/typecheck.js +492 -0
- package/src/worker.js +87 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
// A second pass over the same parse5 tree the renderer walks, emitting code
|
|
2
|
+
// that *updates* the rendered DOM instead of producing it.
|
|
3
|
+
//
|
|
4
|
+
// Nothing has to be worked out at runtime. The compiler already knows where every
|
|
5
|
+
// ${} lands, so a binding is a path. No diffing, no vdom, and the only thing in
|
|
6
|
+
// the served HTML that exists for the client is a pair of comment anchors around
|
|
7
|
+
// each `if` and `each`.
|
|
8
|
+
//
|
|
9
|
+
// Addressing works in two modes. Where the shape up to a node is known it is
|
|
10
|
+
// `parent.childNodes[i]`. Past a block, whose node count is not known at compile
|
|
11
|
+
// time, the walk switches to a cursor stepping sibling by sibling from the
|
|
12
|
+
// block's closing anchor. Both only ever run once: after bind, everything
|
|
13
|
+
// is held by reference.
|
|
14
|
+
//
|
|
15
|
+
// A block is not a wall. Each branch of an `if` and the item of an `each` get
|
|
16
|
+
// their own bind/update pair, so a block is re-rendered only when its
|
|
17
|
+
// structure changes, meaning a different branch or a new key. Its contents are
|
|
18
|
+
// written into like anything else. That is what keeps a form field inside an
|
|
19
|
+
// `if` from being destroyed because some text beside it changed.
|
|
20
|
+
//
|
|
21
|
+
// What it will not bind, it reports as `volatile`: prop names whose change
|
|
22
|
+
// means the caller repaints the whole shadow root. Being conservative is free.
|
|
23
|
+
|
|
24
|
+
import { Scope, collectRefs, emit, parseExpr } from './expr.js';
|
|
25
|
+
import { splitInterpolations } from './interp.js';
|
|
26
|
+
import { childrenOf, gatherChain } from './codegen.js';
|
|
27
|
+
|
|
28
|
+
const DIRECTIVES = new Set(['if', 'else-if', 'else', 'each', 'key']);
|
|
29
|
+
const VOID = new Set([
|
|
30
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
31
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
32
|
+
]);
|
|
33
|
+
const RAW_TEXT = new Set(['script', 'style']);
|
|
34
|
+
const EACH = /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+of\s+([\s\S]+?)\s*$/;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {object[]} nodes the same parse5 nodes the renderer walked
|
|
38
|
+
* @param {object} [opts]
|
|
39
|
+
* @returns {{ locate: string, writes: string, cursors: object, parts: string,
|
|
40
|
+
* volatile: string[] }} `volatile` is what it could not bind, which is what
|
|
41
|
+
* decides whether a light element may update in place
|
|
42
|
+
*/
|
|
43
|
+
export function compileBindings(nodes, opts = {}) {
|
|
44
|
+
const gen = new Bindgen(opts);
|
|
45
|
+
gen.walk(nodes, { parent: '__root', index: opts.rootOffset ?? 0 });
|
|
46
|
+
const root = gen.frame;
|
|
47
|
+
return {
|
|
48
|
+
locate: root.locate.join('\n'),
|
|
49
|
+
writes: root.writes.join('\n'),
|
|
50
|
+
cursors: root.cursors,
|
|
51
|
+
parts: gen.parts.join('\n'),
|
|
52
|
+
volatile: [...gen.volatile].sort(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One emission context: the body of bind/update for the template or a part. */
|
|
57
|
+
class Frame {
|
|
58
|
+
constructor(scope, loopArgs = []) {
|
|
59
|
+
this.scope = scope;
|
|
60
|
+
// The loop variables a block here would need, in the order codegen emits
|
|
61
|
+
// its parameters. Both passes count two per level, named or not.
|
|
62
|
+
this.loopArgs = loopArgs;
|
|
63
|
+
this.locate = [];
|
|
64
|
+
this.writes = [];
|
|
65
|
+
this.slot = 0;
|
|
66
|
+
this.cursors = 0;
|
|
67
|
+
this.gaveUp = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class Bindgen {
|
|
72
|
+
constructor({
|
|
73
|
+
components = new Map(),
|
|
74
|
+
shadowTags = new Set(),
|
|
75
|
+
blockOf = new Map(),
|
|
76
|
+
refs = new Map(),
|
|
77
|
+
} = {}) {
|
|
78
|
+
this.components = components;
|
|
79
|
+
this.shadowTags = shadowTags;
|
|
80
|
+
// Which tree node owns which compiled block. Sharing the map is what keeps
|
|
81
|
+
// this pass and the renderer from drifting apart over the same tree.
|
|
82
|
+
this.blockOf = blockOf;
|
|
83
|
+
// tag -> the local name the renderer imported that component under.
|
|
84
|
+
this.refs = refs;
|
|
85
|
+
this.frames = [new Frame(new Scope())];
|
|
86
|
+
this.parts = [];
|
|
87
|
+
this.volatile = new Set();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get frame() {
|
|
91
|
+
return this.frames[this.frames.length - 1];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get scope() {
|
|
95
|
+
return this.frame.scope;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
next() {
|
|
99
|
+
return this.frame.slot++;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
cursor() {
|
|
103
|
+
return `__c${this.frame.cursors++}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
locate(line) {
|
|
107
|
+
this.frame.locate.push(line);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
write(line) {
|
|
111
|
+
this.frame.writes.push(line);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- giving up ----------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
giveUp(source, scope = this.scope) {
|
|
117
|
+
let ast;
|
|
118
|
+
try {
|
|
119
|
+
ast = parseExpr(source);
|
|
120
|
+
} catch {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
for (const ref of collectRefs(ast, scope)) {
|
|
124
|
+
if (ref.base === 'data') this.volatile.add(ref.name);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
giveUpText(value, scope = this.scope) {
|
|
129
|
+
for (const part of splitInterpolations(value ?? '')) {
|
|
130
|
+
if (part.type === 'expr') this.giveUp(part.value, scope);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A directive's value is an expression, not an interpolation, so
|
|
136
|
+
* `if="tags.length"` has no ${} in it and reading it as text finds nothing.
|
|
137
|
+
*/
|
|
138
|
+
giveUpAll(node, scope = this.scope) {
|
|
139
|
+
const attrs = node.attrs ?? [];
|
|
140
|
+
let inner = scope;
|
|
141
|
+
|
|
142
|
+
const each = attrs.find((attr) => attr.name === 'each');
|
|
143
|
+
const spec = each && EACH.exec(each.value);
|
|
144
|
+
if (spec) {
|
|
145
|
+
this.giveUp(spec[3], scope);
|
|
146
|
+
inner = new Scope(scope);
|
|
147
|
+
// The name only has to exist for collectRefs to stop calling it data.
|
|
148
|
+
inner.declare(spec[1], spec[1]);
|
|
149
|
+
if (spec[2]) inner.declare(spec[2], spec[2]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const attr of attrs) {
|
|
153
|
+
if (attr.name === 'each') continue;
|
|
154
|
+
if (attr.name === 'if' || attr.name === 'else-if') {
|
|
155
|
+
this.giveUp(attr.value, scope);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (attr.name === 'key') {
|
|
159
|
+
this.giveUp(attr.value, inner);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (attr.name === 'else') continue;
|
|
163
|
+
this.giveUpText(attr.value, inner);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const child of childrenOf(node)) {
|
|
167
|
+
if (child.nodeName === '#text') this.giveUpText(child.value, inner);
|
|
168
|
+
else if (child.tagName) this.giveUpAll(child, inner);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
abandon(slot) {
|
|
173
|
+
if (slot.kind === 'text') {
|
|
174
|
+
for (const node of slot.nodes) this.giveUpText(node.value);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const node of slot.nodes) this.giveUpAll(node);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
js(source) {
|
|
181
|
+
return emit(parseExpr(source), this.scope);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- traversal ----------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* `at` is either `{ parent, index }`, when the shape up to here is known, or
|
|
188
|
+
* `{ parent, from }`, a node expression to start stepping from.
|
|
189
|
+
*/
|
|
190
|
+
walk(nodes, at) {
|
|
191
|
+
// `bare` means the directive on these nodes has already been consumed by
|
|
192
|
+
// the block that is asking for them. Without it a branch would find its own
|
|
193
|
+
// `if` again and recurse into itself forever.
|
|
194
|
+
const rendered = renderedChildren(nodes, at.bare);
|
|
195
|
+
const parentJs = at.parent;
|
|
196
|
+
let index = at.index ?? 0;
|
|
197
|
+
let cursor = null;
|
|
198
|
+
|
|
199
|
+
if (at.from) {
|
|
200
|
+
cursor = this.cursor();
|
|
201
|
+
this.locate(`${cursor} = ${at.from};`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
205
|
+
const slot = rendered[i];
|
|
206
|
+
const here = cursor ?? `${parentJs}.childNodes[${index}]`;
|
|
207
|
+
const advance = (from) => {
|
|
208
|
+
if (cursor) this.locate(`${cursor} = ${from}.nextSibling;`);
|
|
209
|
+
else index++;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
if (slot.kind === 'block') {
|
|
213
|
+
const ref = this.bindBlock(slot, here);
|
|
214
|
+
if (ref === null) {
|
|
215
|
+
for (const rest of rendered.slice(i)) this.abandon(rest);
|
|
216
|
+
this.frame.gaveUp = true;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Past a block the node count is not knowable, so addressing becomes
|
|
220
|
+
// relative from here on.
|
|
221
|
+
cursor = cursor ?? this.cursor();
|
|
222
|
+
this.locate(`${cursor} = __b[${ref}].end.nextSibling;`);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (slot.kind === 'text') {
|
|
227
|
+
const bound = this.bindText(slot, parentJs, here);
|
|
228
|
+
if (!bound) {
|
|
229
|
+
for (const rest of rendered.slice(i)) this.abandon(rest);
|
|
230
|
+
this.frame.gaveUp = true;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
advance(bound.from ?? here);
|
|
234
|
+
// A split leaves the static tail behind as its own node.
|
|
235
|
+
if (cursor && bound.suffix) this.locate(`${cursor} = ${cursor}.nextSibling;`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.bindElement(slot.nodes[0], here, cursor === null);
|
|
240
|
+
advance(here);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The block itself, plus a bind/update pair per branch (or per item) so its
|
|
246
|
+
* contents are written into rather than rebuilt.
|
|
247
|
+
*/
|
|
248
|
+
bindBlock(slot, here) {
|
|
249
|
+
const id = this.blockOf.get(slot.nodes[0]);
|
|
250
|
+
if (id === undefined) return null;
|
|
251
|
+
|
|
252
|
+
const ref = this.next();
|
|
253
|
+
const args = `[${this.frame.loopArgs.join(', ')}]`;
|
|
254
|
+
this.locate(`__b[${ref}] = __blockAt(${here}, __blk${id}, __d, ${args});`);
|
|
255
|
+
this.write(`__ok = __updateBlock(__b[${ref}], __blk${id}, __d, ${args}) && __ok;`);
|
|
256
|
+
|
|
257
|
+
if (slot.each) this.emitItemPart(id, slot.nodes[0]);
|
|
258
|
+
else this.emitBranchParts(id, slot.branches);
|
|
259
|
+
return ref;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Which branch of the chain is showing. The runtime compares it with what it
|
|
264
|
+
* last saw: the same branch means write into it, a different one means
|
|
265
|
+
* rebuild. -1 is "none of them", which an `if` with no `else` can be.
|
|
266
|
+
*/
|
|
267
|
+
emitBranchParts(id, branches) {
|
|
268
|
+
const pick = branches.reduceRight(
|
|
269
|
+
(rest, branch, at) =>
|
|
270
|
+
branch.kind === 'else' ? String(at) : `${this.js(branch.cond)} ? ${at} : ${rest}`,
|
|
271
|
+
'-1',
|
|
272
|
+
);
|
|
273
|
+
// The condition may read the loop variables, so pick takes them too. The
|
|
274
|
+
// runtime hands every piece of a block the same arguments.
|
|
275
|
+
const outer = this.frame.loopArgs;
|
|
276
|
+
this.parts.push(`__blk${id}.pick = (${['__d', ...outer].join(', ')}) => (${pick});`);
|
|
277
|
+
|
|
278
|
+
const parts = branches.map((branch) => {
|
|
279
|
+
const content = contentOf(branch.node);
|
|
280
|
+
return this.emitPart(content, outer, undefined, content[0] === branch.node, outer);
|
|
281
|
+
});
|
|
282
|
+
if (parts.every(Boolean)) this.parts.push(`__blk${id}.parts = [${parts.join(', ')}];`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** One part, reused for every item the loop produces. */
|
|
286
|
+
emitItemPart(id, element) {
|
|
287
|
+
const spec = EACH.exec(element.attrs.find((attr) => attr.name === 'each').value);
|
|
288
|
+
if (!spec) return;
|
|
289
|
+
|
|
290
|
+
const outer = this.frame.loopArgs;
|
|
291
|
+
const depth = outer.length / 2;
|
|
292
|
+
const item = `__it${depth}`;
|
|
293
|
+
const index = `__i${depth}`;
|
|
294
|
+
|
|
295
|
+
const scope = new Scope(this.scope);
|
|
296
|
+
scope.declare(spec[1], item);
|
|
297
|
+
if (spec[2]) scope.declare(spec[2], index);
|
|
298
|
+
|
|
299
|
+
const inner = [...outer, item, index];
|
|
300
|
+
// Same shape as a branch: the element itself, whose `each` is already
|
|
301
|
+
// consumed, or a template's content, which carries no directive at all.
|
|
302
|
+
const content = contentOf(element);
|
|
303
|
+
const part = this.emitPart(content, inner, scope, content[0] === element, inner);
|
|
304
|
+
if (part) this.parts.push(`__blk${id}.parts = [${part}];`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* A part is bind/update over a run of nodes starting at a node handed in
|
|
309
|
+
* rather than at a known index. Null where anything inside could not be
|
|
310
|
+
* bound, which leaves the block re-rendering as a whole. Still correct, just
|
|
311
|
+
* less precise.
|
|
312
|
+
*/
|
|
313
|
+
emitPart(nodes, extra, scope = new Scope(this.scope), bare = false, loopArgs = []) {
|
|
314
|
+
this.frames.push(new Frame(scope, loopArgs));
|
|
315
|
+
const frame = this.frame;
|
|
316
|
+
|
|
317
|
+
this.locate('const __p = __n.parentNode;');
|
|
318
|
+
this.walk(nodes, { parent: '__p', from: '__n', bare });
|
|
319
|
+
this.frames.pop();
|
|
320
|
+
|
|
321
|
+
if (frame.gaveUp) return null;
|
|
322
|
+
|
|
323
|
+
const args = ['__d', ...extra].join(', ');
|
|
324
|
+
const cursors = frame.cursors
|
|
325
|
+
? `let ${Array.from({ length: frame.cursors }, (_, i) => `__c${i}`).join(', ')};\n`
|
|
326
|
+
: '';
|
|
327
|
+
return (
|
|
328
|
+
`{ bind: (__n, ${args}) => { const __b = [];\n${cursors}${frame.locate.join('\n')}\n` +
|
|
329
|
+
`return __b; }, update: (__b, ${args}) => { let __ok = true;\n` +
|
|
330
|
+
`${frame.writes.join('\n')}\nreturn __ok; } }`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* A ${} in mixed content is not its own text node. `Hello ${name}!` is one node
|
|
336
|
+
* reading "Hello Ada!". Both static sides have lengths known at compile time,
|
|
337
|
+
* so the dynamic middle splits out of it exactly.
|
|
338
|
+
*
|
|
339
|
+
* Two or more of them have no findable boundary between them, so the whole
|
|
340
|
+
* node is rewritten instead. Same result, one node rather than five.
|
|
341
|
+
*/
|
|
342
|
+
bindText(slot, parentJs, here) {
|
|
343
|
+
const combined = slot.nodes.map((node) => node.value ?? '').join('');
|
|
344
|
+
const parts = splitInterpolations(combined);
|
|
345
|
+
const exprs = parts.filter((part) => part.type === 'expr');
|
|
346
|
+
|
|
347
|
+
if (!exprs.length) return { from: null, suffix: 0 };
|
|
348
|
+
|
|
349
|
+
let sources;
|
|
350
|
+
try {
|
|
351
|
+
sources = parts.map((part) =>
|
|
352
|
+
part.type === 'expr' ? this.js(part.value) : JSON.stringify(part.value),
|
|
353
|
+
);
|
|
354
|
+
} catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (exprs.length > 1) {
|
|
359
|
+
const ref = this.next();
|
|
360
|
+
this.locate(`__b[${ref}] = __textAt(${parentJs}, ${here}, 0, 0);`);
|
|
361
|
+
this.write(`__ok = __setParts(__b[${ref}], [${sources.join(', ')}]) && __ok;`);
|
|
362
|
+
return { from: `__b[${ref}]`, suffix: 0 };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const at = parts.indexOf(exprs[0]);
|
|
366
|
+
const prefix = parts.slice(0, at).reduce((n, part) => n + part.value.length, 0);
|
|
367
|
+
const suffix = parts.slice(at + 1).reduce((n, part) => n + part.value.length, 0);
|
|
368
|
+
|
|
369
|
+
const ref = this.next();
|
|
370
|
+
this.locate(`__b[${ref}] = __textAt(${parentJs}, ${here}, ${prefix}, ${suffix});`);
|
|
371
|
+
this.write(`__ok = __setText(__b[${ref}], ${sources[at]}) && __ok;`);
|
|
372
|
+
return { from: `__b[${ref}]`, suffix };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
bindElement(node, nodeExpr, stable) {
|
|
376
|
+
const tag = node.tagName;
|
|
377
|
+
const isComponent = this.components.has(tag);
|
|
378
|
+
// A light component's children in the DOM are its *own* rendered markup,
|
|
379
|
+
// not what we emitted here, and nothing repaints when its attributes
|
|
380
|
+
// change. A shadow one owns its shadow root and reacts to its attributes,
|
|
381
|
+
// so writing one is how an update reaches down into it.
|
|
382
|
+
const light = isComponent && !this.shadowTags.has(tag);
|
|
383
|
+
|
|
384
|
+
let ref = null;
|
|
385
|
+
// `update` runs long after `bind` returned, so anything it touches has to be
|
|
386
|
+
// held in the bindings array. A path like `__root.childNodes[0]` means nothing
|
|
387
|
+
// there.
|
|
388
|
+
const slotFor = () => {
|
|
389
|
+
if (ref === null) {
|
|
390
|
+
ref = this.next();
|
|
391
|
+
this.locate(`__b[${ref}] = ${nodeExpr};`);
|
|
392
|
+
}
|
|
393
|
+
return `__b[${ref}]`;
|
|
394
|
+
};
|
|
395
|
+
// Descending only ever happens inside `bind`, so a stable path is fine.
|
|
396
|
+
const parentExpr = () => (ref !== null ? `__b[${ref}]` : stable ? nodeExpr : slotFor());
|
|
397
|
+
|
|
398
|
+
for (const attr of node.attrs ?? []) {
|
|
399
|
+
if (DIRECTIVES.has(attr.name)) continue;
|
|
400
|
+
const parts = splitInterpolations(attr.value);
|
|
401
|
+
if (!parts.some((part) => part.type === 'expr')) continue;
|
|
402
|
+
|
|
403
|
+
if (light) {
|
|
404
|
+
this.giveUpText(attr.value);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let value;
|
|
409
|
+
try {
|
|
410
|
+
value =
|
|
411
|
+
parts.length === 1
|
|
412
|
+
? this.js(parts[0].value)
|
|
413
|
+
: parts
|
|
414
|
+
.map((part) =>
|
|
415
|
+
part.type === 'expr' ? `__str(${this.js(part.value)})` : JSON.stringify(part.value),
|
|
416
|
+
)
|
|
417
|
+
.join(' + ');
|
|
418
|
+
} catch {
|
|
419
|
+
this.giveUpText(attr.value);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
// A shadow child reads its attributes back through its own converters,
|
|
423
|
+
// so an update has to write them the way it will read them.
|
|
424
|
+
const childRef = this.refs.get(tag) ?? null;
|
|
425
|
+
this.write(
|
|
426
|
+
childRef
|
|
427
|
+
? `__setAttrProp(${childRef}, ${slotFor()}, ${JSON.stringify(attr.name)}, ${value});`
|
|
428
|
+
: `__setAttr(${slotFor()}, ${JSON.stringify(attr.name)}, ${value});`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (VOID.has(tag)) return;
|
|
433
|
+
|
|
434
|
+
// A component renders its own insides; a plain <template> keeps its content
|
|
435
|
+
// in a DocumentFragment nothing here can address; raw text is not markup.
|
|
436
|
+
if (isComponent || tag === 'template' || RAW_TEXT.has(tag)) {
|
|
437
|
+
for (const child of childrenOf(node)) {
|
|
438
|
+
if (child.nodeName === '#text') this.giveUpText(child.value);
|
|
439
|
+
else if (child.tagName) this.giveUpAll(child);
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const children = childrenOf(node);
|
|
445
|
+
if (children.length) this.walk(children, { parent: parentExpr(), index: 0 });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ---- helpers --------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
/** What a branch or an item renders: itself, or a template's content. */
|
|
452
|
+
function contentOf(node) {
|
|
453
|
+
return node.tagName === 'template' ? childrenOf(node) : [node];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* The child list as the browser will build it: comments are dropped by the
|
|
458
|
+
* renderer, adjacent text runs become one node, and an if/else chain is a
|
|
459
|
+
* single region, including the whitespace between its branches, which the renderer
|
|
460
|
+
* never emits.
|
|
461
|
+
*/
|
|
462
|
+
function renderedChildren(nodes, bare = false) {
|
|
463
|
+
const out = [];
|
|
464
|
+
let i = 0;
|
|
465
|
+
|
|
466
|
+
while (i < nodes.length) {
|
|
467
|
+
const node = nodes[i];
|
|
468
|
+
|
|
469
|
+
const chain = !bare && node.tagName ? gatherChain(nodes, i) : null;
|
|
470
|
+
if (chain) {
|
|
471
|
+
out.push({
|
|
472
|
+
kind: 'block',
|
|
473
|
+
nodes: chain.chain.map((branch) => branch.node),
|
|
474
|
+
branches: chain.chain,
|
|
475
|
+
});
|
|
476
|
+
i = chain.next;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
i++;
|
|
480
|
+
|
|
481
|
+
if (node.nodeName === '#comment') continue;
|
|
482
|
+
|
|
483
|
+
if (node.nodeName === '#text') {
|
|
484
|
+
const last = out[out.length - 1];
|
|
485
|
+
if (last?.kind === 'text') last.nodes.push(node);
|
|
486
|
+
else out.push({ kind: 'text', nodes: [node] });
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (!node.tagName) continue;
|
|
490
|
+
|
|
491
|
+
const each = !bare && (node.attrs ?? []).some((attr) => attr.name === 'each');
|
|
492
|
+
const structural = !bare && (node.attrs ?? []).some((attr) => DIRECTIVES.has(attr.name));
|
|
493
|
+
out.push({ kind: structural ? 'block' : 'element', nodes: [node], each });
|
|
494
|
+
}
|
|
495
|
+
return out;
|
|
496
|
+
}
|