@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.
Files changed (41) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +26 -0
  3. package/package.json +1 -2
  4. package/src/checkAbide.ts +11 -6
  5. package/src/lib/server/runtime/SSR_SWAP_SCRIPT.ts +4 -3
  6. package/src/lib/server/runtime/createServer.ts +28 -23
  7. package/src/lib/server/runtime/createUiPageRenderer.ts +1 -1
  8. package/src/lib/ui/compile/AbideCompileError.ts +16 -0
  9. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +0 -1
  10. package/src/lib/ui/compile/abideUiPlugin.ts +25 -2
  11. package/src/lib/ui/compile/bindListenEvent.ts +19 -0
  12. package/src/lib/ui/compile/compileShadow.ts +65 -52
  13. package/src/lib/ui/compile/componentWrapperTag.ts +20 -0
  14. package/src/lib/ui/compile/generateBuild.ts +114 -212
  15. package/src/lib/ui/compile/generateSSR.ts +54 -88
  16. package/src/lib/ui/compile/lowerContext.ts +64 -0
  17. package/src/lib/ui/compile/lowerDocAccess.ts +6 -1
  18. package/src/lib/ui/compile/offsetToLineColumn.ts +16 -0
  19. package/src/lib/ui/compile/scopeAttr.ts +9 -0
  20. package/src/lib/ui/compile/staticAttr.ts +11 -0
  21. package/src/lib/ui/compile/staticTextPart.ts +12 -0
  22. package/src/lib/ui/compile/unwrapParens.ts +10 -0
  23. package/src/lib/ui/dom/applyResolved.ts +9 -6
  24. package/src/lib/ui/dom/awaitBlock.ts +27 -21
  25. package/src/lib/ui/dom/clearBetween.ts +16 -0
  26. package/src/lib/ui/dom/each.ts +64 -38
  27. package/src/lib/ui/dom/eachAsync.ts +41 -54
  28. package/src/lib/ui/dom/fillBefore.ts +16 -0
  29. package/src/lib/ui/dom/moveRange.ts +19 -0
  30. package/src/lib/ui/dom/openMarker.ts +22 -0
  31. package/src/lib/ui/dom/removeRange.ts +18 -0
  32. package/src/lib/ui/dom/switchBlock.ts +32 -40
  33. package/src/lib/ui/dom/tryBlock.ts +31 -35
  34. package/src/lib/ui/dom/types/EachRow.ts +10 -3
  35. package/src/lib/ui/dom/types/SwitchCase.ts +3 -2
  36. package/src/lib/ui/dom/when.ts +34 -43
  37. package/src/lib/ui/installHotBridge.ts +0 -2
  38. package/src/lib/ui/renderToStream.ts +14 -11
  39. package/src/lib/ui/state.ts +14 -5
  40. package/src/lib/ui/compile/branchElements.ts +0 -50
  41. 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 { branchElements } from './branchElements.ts'
3
- import { escapeHtml } from './escapeHtml.ts'
2
+ import { bindListenEvent } from './bindListenEvent.ts'
3
+ import { componentWrapperTag } from './componentWrapperTag.ts'
4
4
  import { groupBindParts } from './groupBindParts.ts'
5
- import { lowerDocAccess } from './lowerDocAccess.ts'
5
+ import { lowerContext } from './lowerContext.ts'
6
6
  import { partitionSlots } from './partitionSlots.ts'
7
- import { nestedBindingNames } from './prepareNestedScript.ts'
8
- import { renameSignalRefs } from './renameSignalRefs.ts'
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
- /* Branch-scoped signal bindings (from nested `<script>`s) they deref to
33
- `.value` like a `derived`. Pushed while a branch's script + markup compile,
34
- popped after, so they shadow only within that subtree. */
35
- const localDerived = new Set<string>()
36
- const derefScope = (): ReadonlySet<string> =>
37
- localDerived.size === 0 ? derivedNames : new Set([...derivedNames, ...localDerived])
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 input. The path is an lvalue, so the write is lowered
94
- as an assignment statement. */
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}, "input", () => { ${lowerStatement(`${attr.code} = ${varName}.${attr.property}`)} });\n`
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
- const added = scopeNestedScripts(node.children)
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: (${param}) => {\n${roots.code}return ${roots.expr};\n} }`
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
- function generateComponent(
279
- node: Extract<TemplateNode, { kind: 'component' }>,
280
- parentVar: string,
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
- /* openChild appends (create) or claims the SSR wrapper (hydrate); since
304
- hydration is still active, the child's own build then adopts its server
305
- markup inside the wrapper. */
306
- return (
307
- `const ${wrapper} = openChild(${parentVar}, ${JSON.stringify(node.name.toLowerCase())});\n` +
308
- `mountChild(${wrapper}, ${node.name}, { ${parts.join(', ')} });\n`
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
- ? renderRangeThunk(
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
- : renderSettledThunk(
335
- node.children.find(isBranch('then')),
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
- : renderSettledThunk(catchBranch, '_error', '<template catch>', finallyChildren)
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
- `${renderThunk(pending, undefined, '<template await> pending')}, ` +
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
- /* Builds the element roots of a branch into `parentVar` (each via openRoot, so
366
- detached on create / claimed on hydrate), returning the code plus an array
367
- expression of the root nodes the block tracks as a range. */
368
- function elementRoots(
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
- context: string,
371
- parentVar: string,
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
- if (!hasElement) {
421
- const value = fallback === undefined ? '' : `, ${paramName ?? fallback}`
422
- return fallback === undefined ? 'undefined' : `(${parentParam}${value}) => []`
423
- }
424
- const roots = elementRoots(children, context, parentParam)
425
- const value = fallback === undefined ? '' : `, ${paramName ?? fallback}`
426
- return `(${parentParam}${value}) => {\n${roots.code}return ${roots.expr};\n}`
427
- }
428
-
429
- /* A thunk over a node range: `children`'s roots concatenated with the `finally`
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
- /* A settled (then/catch) thunk: the outcome branch's roots ++ `finally`. */
446
- function renderSettledThunk(
447
- branch: TemplateNode | undefined,
448
- fallback: string,
449
- context: string,
450
- finallyChildren: TemplateNode[],
451
- ): string {
452
- return renderRangeThunk(
453
- branchChildren(branch),
454
- branchVar(branch) ?? fallback,
455
- context,
456
- finallyChildren,
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 = renderRangeThunk(guarded, undefined, '<template try>', finallyChildren)
414
+ const tryThunk = branchThunk(guarded, undefined, finallyChildren)
479
415
  const catchThunk =
480
416
  catchBranch === undefined
481
417
  ? 'undefined'
482
- : renderRangeThunk(
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
- Both branches are single-element roots. */
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 thenParam = nextVar('p')
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 elseParam = nextVar('p')
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. The row must have a single element root (it returns one node). */
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
- /* A `<script>` in the row body declares per-row local signals (seeded from
517
- the row item), scoped to this row's render thunk. */
518
- const added = scopeNestedScripts(node.children)
519
- const scriptCode = node.children
520
- .filter(
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' : renderSettledThunk(catchBranch, '_error', '<template catch>', [])}`
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${scriptCode}${row.code}return ${row.varName};\n}${catchArg});\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 += ` ${scope}=""`
528
+ html += scopeAttr(scope)
627
529
  }
628
530
  for (const attr of node.attrs) {
629
531
  if (attr.kind === 'static') {
630
- html += ` ${attr.name}="${escapeHtml(attr.value)}"`
532
+ html += staticAttr(attr.name, attr.value)
631
533
  }
632
534
  }
633
535
  html += '>'