@abide/abide 0.31.1 → 0.32.1
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/AGENTS.md +2 -1
- package/CHANGELOG.md +26 -0
- package/package.json +1 -2
- package/src/checkAbide.ts +11 -6
- package/src/lib/server/runtime/SSR_SWAP_SCRIPT.ts +4 -3
- package/src/lib/server/runtime/createServer.ts +28 -23
- package/src/lib/server/runtime/createUiPageRenderer.ts +1 -1
- package/src/lib/ui/compile/AbideCompileError.ts +16 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +0 -1
- package/src/lib/ui/compile/abideUiPlugin.ts +25 -2
- package/src/lib/ui/compile/bindListenEvent.ts +19 -0
- package/src/lib/ui/compile/compileShadow.ts +65 -52
- package/src/lib/ui/compile/componentWrapperTag.ts +20 -0
- package/src/lib/ui/compile/generateBuild.ts +114 -212
- package/src/lib/ui/compile/generateSSR.ts +54 -88
- package/src/lib/ui/compile/lowerContext.ts +64 -0
- package/src/lib/ui/compile/lowerDocAccess.ts +6 -1
- package/src/lib/ui/compile/offsetToLineColumn.ts +16 -0
- package/src/lib/ui/compile/scopeAttr.ts +9 -0
- package/src/lib/ui/compile/staticAttr.ts +11 -0
- package/src/lib/ui/compile/staticTextPart.ts +12 -0
- package/src/lib/ui/compile/unwrapParens.ts +10 -0
- package/src/lib/ui/dom/applyResolved.ts +9 -6
- package/src/lib/ui/dom/awaitBlock.ts +27 -21
- package/src/lib/ui/dom/clearBetween.ts +16 -0
- package/src/lib/ui/dom/each.ts +64 -38
- package/src/lib/ui/dom/eachAsync.ts +41 -54
- package/src/lib/ui/dom/fillBefore.ts +16 -0
- package/src/lib/ui/dom/moveRange.ts +19 -0
- package/src/lib/ui/dom/openMarker.ts +22 -0
- package/src/lib/ui/dom/removeRange.ts +18 -0
- package/src/lib/ui/dom/switchBlock.ts +32 -40
- package/src/lib/ui/dom/tryBlock.ts +31 -35
- package/src/lib/ui/dom/types/EachRow.ts +10 -3
- package/src/lib/ui/dom/types/SwitchCase.ts +3 -2
- package/src/lib/ui/dom/when.ts +34 -43
- package/src/lib/ui/installHotBridge.ts +0 -2
- package/src/lib/ui/renderToStream.ts +14 -11
- package/src/lib/ui/state.ts +14 -5
- package/src/lib/ui/compile/branchElements.ts +0 -50
- package/src/lib/ui/dom/openRoot.ts +0 -20
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { bindListenEvent } from './bindListenEvent.ts'
|
|
3
|
+
import { componentWrapperTag } from './componentWrapperTag.ts'
|
|
4
4
|
import { groupBindParts } from './groupBindParts.ts'
|
|
5
|
-
import {
|
|
5
|
+
import { lowerContext } from './lowerContext.ts'
|
|
6
6
|
import { partitionSlots } from './partitionSlots.ts'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { scopeAttr } from './scopeAttr.ts'
|
|
8
|
+
import { staticAttr } from './staticAttr.ts'
|
|
9
9
|
import { staticAttrValue } from './staticAttrValue.ts'
|
|
10
|
+
import { staticTextPart } from './staticTextPart.ts'
|
|
10
11
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
11
12
|
import { VOID_TAGS } from './VOID_TAGS.ts'
|
|
12
13
|
|
|
@@ -29,24 +30,12 @@ export function generateBuild(
|
|
|
29
30
|
let counter = 0
|
|
30
31
|
const nextVar = (prefix: string): string => `${prefix}${counter++}`
|
|
31
32
|
|
|
32
|
-
/*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
/* Rewrites signal refs, then lowers a single expression (no trailing `;`). */
|
|
40
|
-
function lowerExpression(code: string): string {
|
|
41
|
-
const renamed = renameSignalRefs(code, stateNames, derefScope())
|
|
42
|
-
return lowerDocAccess(renamed, 'model').trim().replace(/;$/, '')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/* As above but keeps the trailing `;` for a handler body. */
|
|
46
|
-
function lowerStatement(code: string): string {
|
|
47
|
-
const renamed = renameSignalRefs(code, stateNames, derefScope())
|
|
48
|
-
return lowerDocAccess(renamed, 'model').trim()
|
|
49
|
-
}
|
|
33
|
+
/* The shared signal→`model` lowering + branch-scoped nested-script deref scope. */
|
|
34
|
+
const {
|
|
35
|
+
expression: lowerExpression,
|
|
36
|
+
statement: lowerStatement,
|
|
37
|
+
withNestedScripts,
|
|
38
|
+
} = lowerContext(stateNames, derivedNames)
|
|
50
39
|
|
|
51
40
|
/* Builds an element and its children; returns the build code and its var.
|
|
52
41
|
`varExpr` is how the element is obtained — `openChild(parent, tag)` for a
|
|
@@ -90,39 +79,20 @@ export function generateBuild(
|
|
|
90
79
|
}
|
|
91
80
|
} else {
|
|
92
81
|
/* Two-way: drive the property from the path, and write the path
|
|
93
|
-
back on
|
|
94
|
-
|
|
82
|
+
back on the property's native event (`input` for most fields,
|
|
83
|
+
but `toggle` for `<details open>`, `change` for checked/select).
|
|
84
|
+
The path is an lvalue, so the write lowers to an assignment. */
|
|
85
|
+
const event = bindListenEvent(attr.property, node.tag)
|
|
95
86
|
code += `effect(() => { ${varName}.${attr.property} = ${lowerExpression(attr.code)}; });\n`
|
|
96
|
-
code += `on(${varName},
|
|
87
|
+
code += `on(${varName}, ${JSON.stringify(event)}, () => { ${lowerStatement(`${attr.code} = ${varName}.${attr.property}`)} });\n`
|
|
97
88
|
}
|
|
98
89
|
}
|
|
99
90
|
/* A `<script>` among the children scopes its bindings to this element's
|
|
100
91
|
subtree (its later siblings auto-deref them); pop after. */
|
|
101
|
-
|
|
102
|
-
code += generateChildren(node.children, varName)
|
|
103
|
-
for (const name of added) {
|
|
104
|
-
localDerived.delete(name)
|
|
105
|
-
}
|
|
92
|
+
code += withNestedScripts(node.children, () => generateChildren(node.children, varName))
|
|
106
93
|
return { code, varName }
|
|
107
94
|
}
|
|
108
95
|
|
|
109
|
-
/* Adds the binding names of any `<script>` children to the deref scope, returning
|
|
110
|
-
the names it added (for the caller to pop). */
|
|
111
|
-
function scopeNestedScripts(children: TemplateNode[]): string[] {
|
|
112
|
-
const added: string[] = []
|
|
113
|
-
for (const child of children) {
|
|
114
|
-
if (child.kind === 'script') {
|
|
115
|
-
for (const name of nestedBindingNames(child.code)) {
|
|
116
|
-
if (!localDerived.has(name)) {
|
|
117
|
-
localDerived.add(name)
|
|
118
|
-
added.push(name)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return added
|
|
124
|
-
}
|
|
125
|
-
|
|
126
96
|
/* Emits code appending `node` to `parentVar`. */
|
|
127
97
|
function generateChild(node: TemplateNode, parentVar: string): string {
|
|
128
98
|
if (node.kind === 'script') {
|
|
@@ -238,13 +208,11 @@ export function generateBuild(
|
|
|
238
208
|
(child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
|
|
239
209
|
)
|
|
240
210
|
.map((branch) => {
|
|
241
|
-
const param = nextVar('p')
|
|
242
|
-
const roots = elementRoots(branch.children, '<template case>', param)
|
|
243
211
|
const match =
|
|
244
212
|
branch.match === undefined
|
|
245
213
|
? 'undefined'
|
|
246
214
|
: `() => (${lowerExpression(branch.match)})`
|
|
247
|
-
return `{ match: ${match}, render:
|
|
215
|
+
return `{ match: ${match}, render: ${branchThunk(branch.children)} }`
|
|
248
216
|
})
|
|
249
217
|
.join(', ')
|
|
250
218
|
return `switchBlock(${parentVar}, () => (${lowerExpression(node.subject)}), [${cases}]);\n`
|
|
@@ -275,17 +243,12 @@ export function generateBuild(
|
|
|
275
243
|
|
|
276
244
|
/* Mounts a child component into a wrapper element, passing each prop as a
|
|
277
245
|
reactive thunk so the child re-reads when the parent expression changes. */
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
): string {
|
|
282
|
-
const wrapper = nextVar('cmp')
|
|
246
|
+
/* The prop + slot thunks a child mount receives — its props as value thunks and
|
|
247
|
+
its slot content as host-taking builders (`$children` / `$slots[name]`). */
|
|
248
|
+
function componentParts(node: Extract<TemplateNode, { kind: 'component' }>): string[] {
|
|
283
249
|
const parts = node.props.map(
|
|
284
250
|
(prop) => `${JSON.stringify(prop.name)}: () => (${lowerExpression(prop.code)})`,
|
|
285
251
|
)
|
|
286
|
-
/* Slot content compiles to builders the child mounts into the host it passes
|
|
287
|
-
from each <slot> position: the default markup as `$children`, and each
|
|
288
|
-
`slot="name"` group as `$slots[name]`. */
|
|
289
252
|
const groups = partitionSlots(node.children)
|
|
290
253
|
const slotCode = groups.default.map((child) => generateChild(child, '$slot')).join('')
|
|
291
254
|
if (slotCode.trim() !== '') {
|
|
@@ -300,13 +263,35 @@ export function generateBuild(
|
|
|
300
263
|
.join(', ')
|
|
301
264
|
parts.push(`"$slots": { ${entries} }`)
|
|
302
265
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
266
|
+
return parts
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* Mounts a child into a wrapper obtained via `varExpr` (openChild — appends on
|
|
270
|
+
create / claims on hydrate). Hydration stays active, so the child adopts its
|
|
271
|
+
server markup inside the wrapper. Returns the wrapper var. */
|
|
272
|
+
function mountComponent(
|
|
273
|
+
node: Extract<TemplateNode, { kind: 'component' }>,
|
|
274
|
+
varExpr: string,
|
|
275
|
+
): { code: string; varName: string } {
|
|
276
|
+
const wrapper = nextVar('cmp')
|
|
277
|
+
const code =
|
|
278
|
+
`const ${wrapper} = ${varExpr};\n` +
|
|
279
|
+
`mountChild(${wrapper}, ${node.name}, { ${componentParts(node).join(', ')} });\n`
|
|
280
|
+
return { code, varName: wrapper }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function generateComponent(
|
|
284
|
+
node: Extract<TemplateNode, { kind: 'component' }>,
|
|
285
|
+
parentVar: string,
|
|
286
|
+
): string {
|
|
287
|
+
const { tag, transparent } = componentWrapperTag(node.name)
|
|
288
|
+
const { code, varName } = mountComponent(
|
|
289
|
+
node,
|
|
290
|
+
`openChild(${parentVar}, ${JSON.stringify(tag)})`,
|
|
309
291
|
)
|
|
292
|
+
/* A void-name remap is layout-transparent so the child's root stays a direct
|
|
293
|
+
child of the parent (idempotent on a claimed SSR node that already has it). */
|
|
294
|
+
return transparent ? `${code}${varName}.setAttribute("style", "display:contents");\n` : code
|
|
310
295
|
}
|
|
311
296
|
|
|
312
297
|
/* An await block: pending → resolved(value) / error branches. Each branch is a
|
|
@@ -324,17 +309,16 @@ export function generateBuild(
|
|
|
324
309
|
const pending = node.blocking
|
|
325
310
|
? []
|
|
326
311
|
: node.children.filter((child) => child.kind !== 'branch')
|
|
312
|
+
const thenBranch = node.children.find(isBranch('then'))
|
|
327
313
|
const thenThunk = node.blocking
|
|
328
|
-
?
|
|
314
|
+
? branchThunk(
|
|
329
315
|
node.children.filter((child) => child.kind !== 'branch'),
|
|
330
316
|
node.as ?? '_value',
|
|
331
|
-
'<template await then>',
|
|
332
317
|
finallyChildren,
|
|
333
318
|
)
|
|
334
|
-
:
|
|
335
|
-
|
|
336
|
-
'_value',
|
|
337
|
-
'<template then>',
|
|
319
|
+
: branchThunk(
|
|
320
|
+
branchChildren(thenBranch),
|
|
321
|
+
branchVar(thenBranch) ?? '_value',
|
|
338
322
|
finallyChildren,
|
|
339
323
|
)
|
|
340
324
|
/* Neither catch nor finally → pass `undefined` so awaitBlock re-throws the
|
|
@@ -343,10 +327,15 @@ export function generateBuild(
|
|
|
343
327
|
const catchThunk =
|
|
344
328
|
catchBranch === undefined && finallyChildren.length === 0
|
|
345
329
|
? 'undefined'
|
|
346
|
-
:
|
|
330
|
+
: branchThunk(
|
|
331
|
+
branchChildren(catchBranch),
|
|
332
|
+
branchVar(catchBranch) ?? '_error',
|
|
333
|
+
finallyChildren,
|
|
334
|
+
)
|
|
335
|
+
const pendingThunk = hasRenderableContent(pending) ? branchThunk(pending) : 'undefined'
|
|
347
336
|
return (
|
|
348
337
|
`awaitBlock(${parentVar}, nextBlockId(), () => (${lowerExpression(node.promise)}), ` +
|
|
349
|
-
`${
|
|
338
|
+
`${pendingThunk}, ` +
|
|
350
339
|
`${thenThunk}, ` +
|
|
351
340
|
`${catchThunk});\n`
|
|
352
341
|
)
|
|
@@ -362,98 +351,45 @@ export function generateBuild(
|
|
|
362
351
|
return branch !== undefined && branch.kind === 'branch' ? branch.as : undefined
|
|
363
352
|
}
|
|
364
353
|
|
|
365
|
-
/*
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
354
|
+
/* A branch's content as a void render thunk `(parent[, value]) => void` that
|
|
355
|
+
builds its children — and an optional trailing `finally` branch — into
|
|
356
|
+
`parent`. The full-range model tracks the built content between markers, so a
|
|
357
|
+
branch holds ANY content (components, text, nested control-flow, snippets) and
|
|
358
|
+
is generated exactly like a normal child list. `valueParam` binds a resolved /
|
|
359
|
+
error / item value into scope. Nested `<script>`s are emitted in document order
|
|
360
|
+
by `generateChildren`; `withNestedScripts` puts their bindings in deref scope. */
|
|
361
|
+
function branchThunk(
|
|
369
362
|
children: TemplateNode[],
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
allowEmpty = false,
|
|
373
|
-
): { code: string; expr: string } {
|
|
374
|
-
/* Nested `<script>`s: add their bindings to the deref scope (so the script
|
|
375
|
-
body + this branch's markup auto-deref them), emit the lowered script
|
|
376
|
-
bodies first, then build the element roots — all within the scope, which
|
|
377
|
-
we pop afterward. The scripts run when the branch mounts, owned by its
|
|
378
|
-
scope. */
|
|
379
|
-
const added: string[] = []
|
|
380
|
-
for (const child of children) {
|
|
381
|
-
if (child.kind === 'script') {
|
|
382
|
-
for (const name of nestedBindingNames(child.code)) {
|
|
383
|
-
if (!localDerived.has(name)) {
|
|
384
|
-
localDerived.add(name)
|
|
385
|
-
added.push(name)
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
const scriptCode = children
|
|
391
|
-
.filter(
|
|
392
|
-
(child): child is Extract<TemplateNode, { kind: 'script' }> =>
|
|
393
|
-
child.kind === 'script',
|
|
394
|
-
)
|
|
395
|
-
.map((child) => `${lowerStatement(child.code)}\n`)
|
|
396
|
-
.join('')
|
|
397
|
-
const built = branchElements(children, context, allowEmpty).map((element) =>
|
|
398
|
-
generateElement(element, `openRoot(${parentVar}, ${JSON.stringify(element.tag)})`),
|
|
399
|
-
)
|
|
400
|
-
for (const name of added) {
|
|
401
|
-
localDerived.delete(name)
|
|
402
|
-
}
|
|
403
|
-
return {
|
|
404
|
-
code: scriptCode + built.map((part) => part.code).join(''),
|
|
405
|
-
expr: `[${built.map((part) => part.varName).join(', ')}]`,
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/* A `(parent[, value]) => Node[]` thunk over a branch's element roots, or
|
|
410
|
-
`undefined` when empty (a `<template await>` with no pending branch).
|
|
411
|
-
`paramName`/`fallback` name the resolved/error value the branch binds. */
|
|
412
|
-
function renderThunk(
|
|
413
|
-
children: TemplateNode[],
|
|
414
|
-
paramName: string | undefined,
|
|
415
|
-
context: string,
|
|
416
|
-
fallback?: string,
|
|
363
|
+
valueParam?: string,
|
|
364
|
+
finallyChildren: TemplateNode[] = [],
|
|
417
365
|
): string {
|
|
418
|
-
const hasElement = children.some((child) => child.kind === 'element')
|
|
419
366
|
const parentParam = nextVar('p')
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
roots, both possibly empty. `param` names a bound value (the resolved/error
|
|
431
|
-
value, or the caught error); undefined for a value-less branch (try/pending). */
|
|
432
|
-
function renderRangeThunk(
|
|
433
|
-
children: TemplateNode[],
|
|
434
|
-
param: string | undefined,
|
|
435
|
-
context: string,
|
|
436
|
-
finallyChildren: TemplateNode[],
|
|
437
|
-
): string {
|
|
438
|
-
const parentParam = nextVar('p')
|
|
439
|
-
const head = param === undefined ? `(${parentParam})` : `(${parentParam}, ${param})`
|
|
440
|
-
const roots = elementRoots(children, context, parentParam, true)
|
|
441
|
-
const finallyRoots = elementRoots(finallyChildren, '<template finally>', parentParam, true)
|
|
442
|
-
return `${head} => {\n${roots.code}${finallyRoots.code}return [...${roots.expr}, ...${finallyRoots.expr}];\n}`
|
|
367
|
+
const head =
|
|
368
|
+
valueParam === undefined ? `(${parentParam})` : `(${parentParam}, ${valueParam})`
|
|
369
|
+
const body = withNestedScripts(children, () => generateChildren(children, parentParam))
|
|
370
|
+
const finallyBody =
|
|
371
|
+
finallyChildren.length > 0
|
|
372
|
+
? withNestedScripts(finallyChildren, () =>
|
|
373
|
+
generateChildren(finallyChildren, parentParam),
|
|
374
|
+
)
|
|
375
|
+
: ''
|
|
376
|
+
return `${head} => {\n${body}${finallyBody}}`
|
|
443
377
|
}
|
|
444
378
|
|
|
445
|
-
/*
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
379
|
+
/* True when a branch has content worth a render thunk — vs an absent/empty branch
|
|
380
|
+
a block represents with `undefined` (an `await` with no pending markup). */
|
|
381
|
+
function hasRenderableContent(children: TemplateNode[]): boolean {
|
|
382
|
+
return children.some(
|
|
383
|
+
(child) =>
|
|
384
|
+
child.kind === 'element' ||
|
|
385
|
+
child.kind === 'component' ||
|
|
386
|
+
child.kind === 'if' ||
|
|
387
|
+
child.kind === 'each' ||
|
|
388
|
+
child.kind === 'await' ||
|
|
389
|
+
child.kind === 'try' ||
|
|
390
|
+
child.kind === 'switch' ||
|
|
391
|
+
child.kind === 'snippet' ||
|
|
392
|
+
(child.kind === 'text' && !isWhitespaceText(child)),
|
|
457
393
|
)
|
|
458
394
|
}
|
|
459
395
|
|
|
@@ -475,62 +411,46 @@ export function generateBuild(
|
|
|
475
411
|
const catchBranch = findBranch(node.children, 'catch')
|
|
476
412
|
const finallyChildren = branchChildren(findBranch(node.children, 'finally'))
|
|
477
413
|
const guarded = node.children.filter((child) => child.kind !== 'branch')
|
|
478
|
-
const tryThunk =
|
|
414
|
+
const tryThunk = branchThunk(guarded, undefined, finallyChildren)
|
|
479
415
|
const catchThunk =
|
|
480
416
|
catchBranch === undefined
|
|
481
417
|
? 'undefined'
|
|
482
|
-
:
|
|
418
|
+
: branchThunk(
|
|
483
419
|
branchChildren(catchBranch),
|
|
484
420
|
branchVar(catchBranch) ?? '_error',
|
|
485
|
-
'<template catch>',
|
|
486
421
|
finallyChildren,
|
|
487
422
|
)
|
|
488
423
|
return `tryBlock(${parentVar}, nextBlockId(), ${tryThunk}, ${catchThunk});\n`
|
|
489
424
|
}
|
|
490
425
|
|
|
491
|
-
/* A conditional with an optional nested `<template else>` (a `case` child).
|
|
492
|
-
|
|
426
|
+
/* A conditional with an optional nested `<template else>` (a `case` child). Each
|
|
427
|
+
branch is a content range the runtime tracks between markers. */
|
|
493
428
|
function generateIf(node: Extract<TemplateNode, { kind: 'if' }>, parentVar: string): string {
|
|
494
429
|
const elseBranch = node.children.find(
|
|
495
430
|
(child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
|
|
496
431
|
)
|
|
497
432
|
const thenChildren = node.children.filter((child) => child.kind !== 'case')
|
|
498
|
-
const
|
|
499
|
-
const thenRoots = elementRoots(thenChildren, '<template if>', thenParam)
|
|
500
|
-
const thenThunk = `(${thenParam}) => {\n${thenRoots.code}return ${thenRoots.expr};\n}`
|
|
433
|
+
const thenThunk = branchThunk(thenChildren)
|
|
501
434
|
if (elseBranch === undefined) {
|
|
502
435
|
return `when(${parentVar}, () => (${lowerExpression(node.condition)}), ${thenThunk});\n`
|
|
503
436
|
}
|
|
504
|
-
const
|
|
505
|
-
const elseRoots = elementRoots(elseBranch.children, '<template else>', elseParam)
|
|
506
|
-
const elseThunk = `(${elseParam}) => {\n${elseRoots.code}return ${elseRoots.expr};\n}`
|
|
437
|
+
const elseThunk = branchThunk(elseBranch.children)
|
|
507
438
|
return `when(${parentVar}, () => (${lowerExpression(node.condition)}), ${thenThunk}, ${elseThunk});\n`
|
|
508
439
|
}
|
|
509
440
|
|
|
510
|
-
/* A keyed each.
|
|
441
|
+
/* A keyed each. Each row is a content RANGE (any content, tracked between the
|
|
442
|
+
row's markers), built by a `(rowParent, item) => void` thunk. */
|
|
511
443
|
function generateEach(
|
|
512
444
|
node: Extract<TemplateNode, { kind: 'each' }>,
|
|
513
445
|
parentVar: string,
|
|
514
446
|
): string {
|
|
515
447
|
const rowParam = nextVar('p')
|
|
516
|
-
/*
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
.
|
|
521
|
-
(child): child is Extract<TemplateNode, { kind: 'script' }> =>
|
|
522
|
-
child.kind === 'script',
|
|
523
|
-
)
|
|
524
|
-
.map((child) => `${lowerStatement(child.code)}\n`)
|
|
525
|
-
.join('')
|
|
526
|
-
const row = singleElementRoot(
|
|
527
|
-
node.children,
|
|
528
|
-
'<template each> must contain a single element row',
|
|
529
|
-
rowParam,
|
|
448
|
+
/* The row body builds its children (a `<script>` declares per-row local signals,
|
|
449
|
+
emitted in document order) into the row parent. A `<template catch>` child is
|
|
450
|
+
consumed by the async-each, not the row — `generateChildren` skips it. */
|
|
451
|
+
const rowBody = withNestedScripts(node.children, () =>
|
|
452
|
+
generateChildren(node.children, rowParam),
|
|
530
453
|
)
|
|
531
|
-
for (const name of added) {
|
|
532
|
-
localDerived.delete(name)
|
|
533
|
-
}
|
|
534
454
|
const keyExpression = node.key === undefined ? node.as : lowerExpression(node.key)
|
|
535
455
|
/* `await` → the AsyncIterable runtime, drained row-by-row on the client, with an
|
|
536
456
|
optional `<template catch>` branch rendered (after the streamed rows) when the
|
|
@@ -540,30 +460,14 @@ export function generateBuild(
|
|
|
540
460
|
(child) => child.kind === 'branch' && child.branch === 'catch',
|
|
541
461
|
)
|
|
542
462
|
const catchArg = node.async
|
|
543
|
-
? `, ${catchBranch === undefined ? 'undefined' :
|
|
463
|
+
? `, ${catchBranch === undefined ? 'undefined' : branchThunk(branchChildren(catchBranch), branchVar(catchBranch) ?? '_error')}`
|
|
544
464
|
: ''
|
|
545
465
|
return (
|
|
546
466
|
`${fn}(${parentVar}, () => (${lowerExpression(node.items)}), ` +
|
|
547
|
-
`(${node.as}) => (${keyExpression}), (${rowParam}, ${node.as}) => {\n${
|
|
467
|
+
`(${node.as}) => (${keyExpression}), (${rowParam}, ${node.as}) => {\n${rowBody}}${catchArg});\n`
|
|
548
468
|
)
|
|
549
469
|
}
|
|
550
470
|
|
|
551
|
-
/* Builds the lone element child of a control-flow block (each/if return one
|
|
552
|
-
node), erroring if the block isn't a single element. The root is opened
|
|
553
|
-
with `openRoot(parentVar, tag)` so it's detached on create and claimed on
|
|
554
|
-
hydrate — `parentVar` is the render thunk's parent parameter. */
|
|
555
|
-
function singleElementRoot(
|
|
556
|
-
children: TemplateNode[],
|
|
557
|
-
message: string,
|
|
558
|
-
parentVar: string,
|
|
559
|
-
): { code: string; varName: string } {
|
|
560
|
-
const root = children.find((child) => child.kind === 'element')
|
|
561
|
-
if (root === undefined || root.kind !== 'element') {
|
|
562
|
-
throw new Error(`[abide] ${message}`)
|
|
563
|
-
}
|
|
564
|
-
return generateElement(root, `openRoot(${parentVar}, ${JSON.stringify(root.tag)})`)
|
|
565
|
-
}
|
|
566
|
-
|
|
567
471
|
return generateChildren(nodes, hostVar)
|
|
568
472
|
}
|
|
569
473
|
|
|
@@ -613,9 +517,7 @@ template and the server markup parse to the same DOM. Only handles the shapes
|
|
|
613
517
|
function staticHtml(node: TemplateNode): string {
|
|
614
518
|
if (node.kind === 'text') {
|
|
615
519
|
return node.parts
|
|
616
|
-
.map((part) =>
|
|
617
|
-
part.kind === 'static' && part.value.trim() !== '' ? escapeHtml(part.value) : '',
|
|
618
|
-
)
|
|
520
|
+
.map((part) => (part.kind === 'static' ? staticTextPart(part.value) : ''))
|
|
619
521
|
.join('')
|
|
620
522
|
}
|
|
621
523
|
if (node.kind !== 'element') {
|
|
@@ -623,11 +525,11 @@ function staticHtml(node: TemplateNode): string {
|
|
|
623
525
|
}
|
|
624
526
|
let html = `<${node.tag}`
|
|
625
527
|
for (const scope of node.scopes ?? []) {
|
|
626
|
-
html +=
|
|
528
|
+
html += scopeAttr(scope)
|
|
627
529
|
}
|
|
628
530
|
for (const attr of node.attrs) {
|
|
629
531
|
if (attr.kind === 'static') {
|
|
630
|
-
html +=
|
|
532
|
+
html += staticAttr(attr.name, attr.value)
|
|
631
533
|
}
|
|
632
534
|
}
|
|
633
535
|
html += '>'
|