@abide/abide 0.105.0 → 0.106.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.
Files changed (100) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/bin/abide.ts +4 -1
  3. package/docs/REGISTRY.md +156 -44
  4. package/docs/RULEBOOK.md +375 -87
  5. package/docs/abide.md +7243 -10256
  6. package/package.json +4 -1
  7. package/scaffold/AGENTS.md +2 -0
  8. package/scaffold/src/getGreeting.ts +6 -0
  9. package/scaffold/src/index.abide +3 -1
  10. package/scaffold/src/tests/getGreeting.test.ts +6 -0
  11. package/scaffold/src/tests/index.test.ts +18 -0
  12. package/src/compiler/DOCS_SITE.ts +2 -0
  13. package/src/compiler/Lowering.ts +234 -14
  14. package/src/compiler/PositionTable.ts +14 -7
  15. package/src/compiler/PositionedRefusal.ts +13 -0
  16. package/src/compiler/Refusals.ts +18 -11
  17. package/src/compiler/SHARED_NAMES.ts +2 -0
  18. package/src/compiler/ShadowMap.ts +37 -0
  19. package/src/compiler/TEMPLATE_BLOCKS.ts +12 -0
  20. package/src/compiler/TypeSession.ts +410 -31
  21. package/src/compiler/appLayout.ts +18 -2
  22. package/src/compiler/argsSchemaOf.ts +55 -9
  23. package/src/compiler/checkApp.ts +33 -0
  24. package/src/compiler/checkedShadow.ts +36 -22
  25. package/src/compiler/citedClauses.ts +14 -0
  26. package/src/compiler/classifyRead.ts +6 -0
  27. package/src/compiler/classifyTemplate.ts +2 -1
  28. package/src/compiler/compileApp.ts +2 -2
  29. package/src/compiler/declaredExports.ts +27 -6
  30. package/src/compiler/emitClient.ts +304 -103
  31. package/src/compiler/handlersUnder.ts +4 -0
  32. package/src/compiler/invocationProps.ts +1 -1
  33. package/src/compiler/isIgnored.ts +27 -0
  34. package/src/compiler/parseAbide.ts +176 -76
  35. package/src/compiler/projectShadow.ts +30 -6
  36. package/src/compiler/readSources.ts +1 -1
  37. package/src/compiler/readsIn.ts +100 -1
  38. package/src/compiler/renderMarkup.ts +36 -5
  39. package/src/compiler/scaffold.ts +19 -6
  40. package/src/compiler/serveLanguage.ts +156 -21
  41. package/src/compiler/templateContext.ts +150 -0
  42. package/src/compiler/tokenize.ts +6 -0
  43. package/src/server/BRIDGE_VERSION.ts +1 -1
  44. package/src/server/Inbox.ts +8 -0
  45. package/src/server/MCP_PROTOCOL_VERSION.ts +1 -1
  46. package/src/server/MOUNTABLE.ts +6 -0
  47. package/src/server/abideModules.ts +8 -1
  48. package/src/server/answerRequest.ts +3 -1
  49. package/src/server/answerRpc.ts +26 -12
  50. package/src/server/answerSocket.ts +26 -1
  51. package/src/server/argsFor.ts +4 -3
  52. package/src/server/assistant.ts +19 -13
  53. package/src/server/bridgeRelay.ts +108 -20
  54. package/src/server/claudeApi.ts +7 -7
  55. package/src/server/claudeCode.ts +11 -11
  56. package/src/server/commands.ts +73 -9
  57. package/src/server/createApp.ts +41 -7
  58. package/src/server/devWorker.ts +3 -3
  59. package/src/server/generatedSurfaces.ts +37 -4
  60. package/src/server/muxUpgrade.ts +47 -0
  61. package/src/server/preload.ts +13 -2
  62. package/src/server/render.ts +4 -1
  63. package/src/server/runCompiled.ts +1 -0
  64. package/src/server/seedBuffer.ts +31 -1
  65. package/src/server/serveDev.ts +83 -72
  66. package/src/server/surfaceOf.ts +3 -0
  67. package/src/server/testMount.ts +53 -0
  68. package/src/shared/ADDRESSES.ts +1 -1
  69. package/src/shared/ERROR_META.ts +3 -0
  70. package/src/shared/Schema.ts +1 -1
  71. package/src/shared/WORK.ts +14 -12
  72. package/src/shared/applyPatch.ts +1 -1
  73. package/src/shared/boundValue.ts +81 -0
  74. package/src/shared/index.ts +7 -2
  75. package/src/shared/jsonSchema.ts +25 -2
  76. package/src/shared/log.ts +11 -1
  77. package/src/shared/reactive/Keyed.ts +30 -6
  78. package/src/shared/reactive/PATCHES.ts +1 -1
  79. package/src/shared/reactive/Reactive.ts +118 -60
  80. package/src/shared/reactive/canonicalKey.ts +1 -1
  81. package/src/shared/reactive/index.ts +13 -5
  82. package/src/shared/reactive/recordWrites.ts +1 -1
  83. package/src/shared/reactive/watch.ts +26 -4
  84. package/src/shared/rpc.ts +6 -1
  85. package/src/shared/url.ts +7 -3
  86. package/src/shared/validateJson.ts +1 -1
  87. package/src/ui/ArmRegion.ts +2 -2
  88. package/src/ui/List.ts +9 -9
  89. package/src/ui/RawRun.ts +1 -1
  90. package/src/ui/Router.ts +194 -36
  91. package/src/ui/Scope.ts +29 -0
  92. package/src/ui/adoptWalk.ts +3 -3
  93. package/src/ui/applyHead.ts +25 -3
  94. package/src/ui/bindProperty.ts +70 -22
  95. package/src/ui/index.ts +1 -1
  96. package/src/ui/leave.ts +5 -0
  97. package/src/ui/listenForRoutes.ts +14 -3
  98. package/src/ui/reconcile.ts +48 -28
  99. package/src/ui/regions.ts +36 -4
  100. package/src/ui/rows.ts +108 -28
@@ -3,6 +3,9 @@ import { URL_ATTRIBUTES } from '#shared/URL_ATTRIBUTES.ts'
3
3
  import { isComponentTag, isSlot, propKeyOf } from './classifyTemplate.ts'
4
4
  import { decodeCharacterReferences } from './decodeCharacterReferences.ts'
5
5
  import { isNamedAlone } from './isNamedAlone.ts'
6
+ import { lineOf } from './lineOf.ts'
7
+ import { PositionedRefusal } from './PositionedRefusal.ts'
8
+ import { TEMPLATE_BLOCKS } from './TEMPLATE_BLOCKS.ts'
6
9
  import { importedNames } from './splitImports.ts'
7
10
  import {
8
11
  type Attribute,
@@ -30,6 +33,11 @@ import { VOID_ELEMENTS } from './VOID_ELEMENTS.ts'
30
33
  // source text and handed to the emitter verbatim, which is what keeps 19.11 a property of
31
34
  // the file rather than a promise this parser has to keep.
32
35
 
36
+ /** 32.64 — a close tag, naming the block it closes. */
37
+ const CLOSER = new RegExp(
38
+ `^\\/\\s*(${Object.keys(TEMPLATE_BLOCKS).join('|')})$`,
39
+ )
40
+
33
41
  /** A `.abide` file split into its script, module, style and head blocks and its parsed template. */
34
42
  export type AbideFile = {
35
43
  /** `<script>` bodies in source order — per-instance setup, 34.1. */
@@ -94,6 +102,10 @@ type Origins = {
94
102
  keys: Map<object, number>
95
103
  /** 38.39 — per `{#for}` block, where its item and its index start (`-1` where it has none). */
96
104
  bindings: Map<object, { item: number; index: number }>
105
+ /** 38.116 — per attribute but a spread, where its name starts. */
106
+ names: Map<object, number>
107
+ /** 38.116 — per element closed by a tag of its own, where that close tag's name starts. */
108
+ closes: Map<object, number>
97
109
  }
98
110
 
99
111
  /**
@@ -109,6 +121,66 @@ function armLevel(open: (string | null)[]): number {
109
121
  return level
110
122
  }
111
123
 
124
+ /** The name a block's own tags spell — `if` for `{#if}` and `{/if}`. */
125
+ function blockName(block: TemplateNode | InlineComponent): string {
126
+ return block.kind === 'block' ? block.block : 'component'
127
+ }
128
+
129
+ /** Which block IS open where a tag closes or continues another, and where it opened — or none. */
130
+ function innermostOpen(
131
+ source: string,
132
+ blocks: (TemplateNode | InlineComponent)[],
133
+ openedAt: number[],
134
+ ): string {
135
+ const block = blocks[blocks.length - 1]
136
+ if (block === undefined) return ' No block is open here.'
137
+ const { line, column } = lineOf(
138
+ source,
139
+ openedAt[openedAt.length - 1] as number,
140
+ )
141
+ return ` The block open here is the {#${blockName(block)}} at ${line}:${column}.`
142
+ }
143
+
144
+ /** What to write instead of a block abide has none of, where there is one thing to write. */
145
+ function unknownHint(inner: string): string {
146
+ if (/^#each(?![\w$])/.test(inner)) return ' — a list is {#for item of list}'
147
+ if (/^#await(?![\w$])/.test(inner))
148
+ return ' — there is no {#await}, and a loading arm is {#if value.ready()}'
149
+ if (/^@html(?![\w$])/.test(inner)) return ' — markup is {raw(value)}'
150
+ if (/^:else/.test(inner))
151
+ return ' — an else arm is {:else} or {:else if condition}'
152
+ return ''
153
+ }
154
+
155
+ /**
156
+ * 32.63 — a close tag that names an element opened OUTSIDE a block still open: the block was never
157
+ * closed inside it. Ignored, the close tag was dropped and the block took the rest of the element
158
+ * and everything after it as its body — `<p>{#component Card()}x</p>` lost the `<p>` entirely. A
159
+ * close tag that names no open element at all is still dropped (`parseAbide`).
160
+ */
161
+ function refuseCrossing(
162
+ closing: string,
163
+ open: (string | null)[],
164
+ blocks: (TemplateNode | InlineComponent)[],
165
+ openedAt: number[],
166
+ ): void {
167
+ let arms = 0
168
+ for (let level = open.length - 1; level > 0; level -= 1) {
169
+ const name = open[level]
170
+ if (name === ARM) arms += 1
171
+ else if (typeof name === 'string' && sameTag(name, closing)) {
172
+ if (arms === 0) return
173
+ // The first block opened inside the element, which every block above it sits in.
174
+ const index = blocks.length - arms
175
+ const block = blocks[index] as TemplateNode | InlineComponent
176
+ throw new PositionedRefusal(
177
+ `abide: {#${blockName(block)}} is never closed before </${closing}>, and the element it was opened in ends there. Write {/${blockName(block)}} before </${closing}>. RULEBOOK 32.63.`,
178
+ openedAt[index] as number,
179
+ )
180
+ }
181
+ }
182
+ }
183
+
112
184
  /** Pops `open` and `stack` together down to `level`, closing whatever the arm still holds open. */
113
185
  function unwindTo(
114
186
  level: number,
@@ -142,6 +214,8 @@ export function parseAbide(source: string): AbideFile {
142
214
  holders: new Map(),
143
215
  keys: new Map(),
144
216
  bindings: new Map(),
217
+ names: new Map(),
218
+ closes: new Map(),
145
219
  }
146
220
  const root: TemplateNode[] = []
147
221
  // The open elements, innermost last. `root` is the floor and is never popped.
@@ -155,6 +229,9 @@ export function parseAbide(source: string): AbideFile {
155
229
  // The open control blocks, so a `{:else}` knows which block it belongs to, and the open
156
230
  // `{#component}` blocks, whose body a `<script>` or a `<style>` written in it belongs to.
157
231
  const blocks: (TemplateNode | InlineComponent)[] = []
232
+ // Where each open block's own tag starts, beside `blocks`, so a block left open is refused at
233
+ // the tag that opened it.
234
+ const openedAt: number[] = []
158
235
  // 32.43 — each `{#component}` under the list it was written in, which says whose scope it is in
159
236
  // once the tree is whole (`fileComponents`).
160
237
  const declared = new Map<TemplateNode[], InlineComponent[]>()
@@ -186,9 +263,20 @@ export function parseAbide(source: string): AbideFile {
186
263
  )
187
264
  const innermost = open[open.length - 1]
188
265
  if (typeof innermost === 'string' && sameTag(innermost, closing)) {
266
+ // The element closed is the last node of the level around it, written before
267
+ // every node now inside it — and placed only where the close spells its name.
268
+ const around = stack[stack.length - 2] as TemplateNode[]
269
+ if (closing === innermost) {
270
+ let named = at + 2
271
+ while (isSpace(source[named] as string)) named += 1
272
+ origins.closes.set(
273
+ around[around.length - 1] as TemplateNode,
274
+ named,
275
+ )
276
+ }
189
277
  stack.pop()
190
278
  open.pop()
191
- }
279
+ } else refuseCrossing(closing, open, blocks, openedAt)
192
280
  at = close + 1
193
281
  continue
194
282
  }
@@ -213,7 +301,7 @@ export function parseAbide(source: string): AbideFile {
213
301
  continue
214
302
  }
215
303
 
216
- const tag = readTag(source, at, origins.holders)
304
+ const tag = readTag(source, at, origins)
217
305
  if (tag === undefined) {
218
306
  // A `<` THAT OPENS NO TAG IS TEXT. `<p>a < b</p>` lost its `<` to a parser that
219
307
  // stepped over what it could not read.
@@ -369,26 +457,15 @@ export function parseAbide(source: string): AbideFile {
369
457
  if (here === undefined) declared.set(children, [component])
370
458
  else here.push(component)
371
459
  blocks.push(component)
460
+ openedAt.push(at)
372
461
  stack.push(component.children)
373
462
  open.push(ARM)
374
463
  at = end + 1
375
464
  continue
376
465
  }
377
- if (inner === '/component') {
378
- const level = armLevel(open)
379
- if (
380
- blocks[blocks.length - 1]?.kind === 'component' &&
381
- level > 0
382
- ) {
383
- blocks.pop()
384
- unwindTo(level, open, stack)
385
- }
386
- at = end + 1
387
- continue
388
- }
389
466
  // `{#if}`, `{:else}`, `{:else if}`, `{/if}` — a control block opens a new ARM rather
390
467
  // than a new element, so it keeps its own stack beside the element one.
391
- if (inner.startsWith('#if')) {
468
+ if (/^#if(?![\w$])/.test(inner)) {
392
469
  const subject = trimmed(source, slot.at + 3, end)
393
470
  const branch = newBranch(subject.text)
394
471
  origins.holders.set(branch, subject.at)
@@ -399,37 +476,43 @@ export function parseAbide(source: string): AbideFile {
399
476
  }
400
477
  children.push(block)
401
478
  blocks.push(block)
479
+ openedAt.push(at)
402
480
  stack.push(branch.children)
403
481
  open.push(ARM)
404
482
  at = end + 1
405
483
  continue
406
484
  }
407
- if (inner.startsWith(':else')) {
485
+ if (/^:else(?![\w$])/.test(inner)) {
408
486
  const block = blocks[blocks.length - 1]
487
+ // 32.64 — AN ELSE ARM OUTSIDE A `{#if}` IS REFUSED, for the reason 32.56 refuses a
488
+ // stray catch: dropped, the text after it read as the body it stood in.
409
489
  if (
410
- block !== undefined &&
411
- block.kind === 'block' &&
412
- block.block === 'if'
413
- ) {
414
- // `{:else}` carries no condition, which is what makes it always take.
415
- const subject = trimmed(
416
- source,
417
- slot.at + elseSubjectAt(inner),
418
- end,
490
+ block === undefined ||
491
+ block.kind !== 'block' ||
492
+ block.block !== 'if'
493
+ )
494
+ throw new PositionedRefusal(
495
+ `abide: {${inner}} stands outside a {#if}, and only a {#if} has an else arm.${innermostOpen(source, blocks, openedAt)} RULEBOOK 32.64.`,
496
+ at,
419
497
  )
420
- const branch = newBranch(subject.text)
421
- if (branch.subject !== '')
422
- origins.holders.set(branch, subject.at)
423
- // UNWOUND TO THE ARM'S OWN LEVEL, for the reason `{/if}` below is: an
424
- // element the author left open in the PREVIOUS arm sits above it, and
425
- // popping one level left `open` naming that element while `stack` held the
426
- // new arm — so its close tag popped the arm and every node after it landed
427
- // in the first arm, which renders on the opposite condition.
428
- unwindTo(armLevel(open), open, stack)
429
- block.branches.push(branch)
430
- stack.push(branch.children)
431
- open.push(ARM)
432
- }
498
+ // `{:else}` carries no condition, which is what makes it always take.
499
+ const subject = trimmed(
500
+ source,
501
+ slot.at + elseSubjectAt(inner),
502
+ end,
503
+ )
504
+ const branch = newBranch(subject.text)
505
+ if (branch.subject !== '')
506
+ origins.holders.set(branch, subject.at)
507
+ // UNWOUND TO THE ARM'S OWN LEVEL, for the reason `{/if}` below is: an
508
+ // element the author left open in the PREVIOUS arm sits above it, and
509
+ // popping one level left `open` naming that element while `stack` held the
510
+ // new arm — so its close tag popped the arm and every node after it landed
511
+ // in the first arm, which renders on the opposite condition.
512
+ unwindTo(armLevel(open), open, stack)
513
+ block.branches.push(branch)
514
+ stack.push(branch.children)
515
+ open.push(ARM)
433
516
  at = end + 1
434
517
  continue
435
518
  }
@@ -444,12 +527,13 @@ export function parseAbide(source: string): AbideFile {
444
527
  }
445
528
  children.push(block)
446
529
  blocks.push(block)
530
+ openedAt.push(at)
447
531
  stack.push(body.children)
448
532
  open.push(ARM)
449
533
  at = end + 1
450
534
  continue
451
535
  }
452
- if (inner.startsWith(':catch') || inner === ':finally') {
536
+ if (/^:catch(?![\w$])/.test(inner) || inner === ':finally') {
453
537
  const block = blocks[blocks.length - 1]
454
538
  // 32.56 — ONLY A `{#try}` HAS THESE ARMS. Anywhere else the tag was dropped and what
455
539
  // followed it read as the body it stood in: a `{#for}`'s catch rendered in every row.
@@ -458,8 +542,9 @@ export function parseAbide(source: string): AbideFile {
458
542
  block.kind !== 'block' ||
459
543
  block.block !== 'try'
460
544
  )
461
- throw new Error(
545
+ throw new PositionedRefusal(
462
546
  `abide: {${inner}} stands outside a {#try}, and only a {#try} has a catch or a finally arm. RULEBOOK 32.56.`,
547
+ at,
463
548
  )
464
549
  unwindTo(armLevel(open), open, stack)
465
550
  // The finally arm is a third branch until `{/try}` folds it into the other two.
@@ -472,33 +557,7 @@ export function parseAbide(source: string): AbideFile {
472
557
  at = end + 1
473
558
  continue
474
559
  }
475
- if (inner === '/try') {
476
- const level = armLevel(open)
477
- const block = blocks[blocks.length - 1]
478
- if (
479
- block !== undefined &&
480
- block.kind === 'block' &&
481
- block.block === 'try' &&
482
- level > 0
483
- ) {
484
- blocks.pop()
485
- unwindTo(level, open, stack)
486
- const after = closeTry(block)
487
- const around = stack[stack.length - 1] as TemplateNode[]
488
- around.push(...after)
489
- // The `{:finally}` markup stands in the list around the block now, and so
490
- // does what it declares.
491
- const moved = declared.get(after)
492
- if (moved !== undefined) {
493
- const here = declared.get(around)
494
- if (here === undefined) declared.set(around, moved)
495
- else here.push(...moved)
496
- }
497
- }
498
- at = end + 1
499
- continue
500
- }
501
- if (inner.startsWith('#for')) {
560
+ if (/^#for(?![\w$])/.test(inner)) {
502
561
  const block = forBlock(
503
562
  source,
504
563
  trimmed(source, slot.at + 4, end),
@@ -506,28 +565,60 @@ export function parseAbide(source: string): AbideFile {
506
565
  )
507
566
  children.push(block)
508
567
  blocks.push(block)
568
+ openedAt.push(at)
509
569
  stack.push(block.children)
510
570
  open.push(ARM)
511
571
  at = end + 1
512
572
  continue
513
573
  }
514
- // A `{/if}` with no block open closes NOTHING — left to pop, it ends the element
515
- // around it and every node after it hangs outside.
516
- if (inner === '/for' || inner === '/if') {
574
+ // 32.64 — A CLOSE TAG CLOSES THE BLOCK IT NAMES, AND ONLY THAT ONE. Ignored, a stray
575
+ // `{/if}` let the text after it stand where the author never put it; and closing
576
+ // whatever was innermost let `{/if}` end a `{#try}` without folding its arms.
577
+ const closer = CLOSER.exec(inner)
578
+ if (closer !== null) {
579
+ const name = closer[1] as string
580
+ const block = blocks[blocks.length - 1]
581
+ if (block === undefined || blockName(block) !== name)
582
+ throw new PositionedRefusal(
583
+ `abide: {/${name}} closes no {#${name}}.${innermostOpen(source, blocks, openedAt)} RULEBOOK 32.64.`,
584
+ at,
585
+ )
586
+ blocks.pop()
587
+ openedAt.pop()
517
588
  // UNWOUND TO THE ARM'S OWN LEVEL, because an element the author left open inside
518
589
  // the block sits above it. Requiring the arm to be innermost, `<div>{#if a}<b>x
519
590
  // {/if}</div><p>after</p>` closed NOTHING — the `</div>` then matched nothing
520
591
  // either, and the `<p>` landed inside the if-arm and vanished whenever the
521
592
  // condition was false. Closing whatever the block still holds open is what the
522
593
  // close tag of the element AROUND it would have to do anyway.
523
- const level = armLevel(open)
524
- if (blocks[blocks.length - 1]?.kind === 'block' && level > 0) {
525
- blocks.pop()
526
- unwindTo(level, open, stack)
594
+ unwindTo(armLevel(open), open, stack)
595
+ if (block.kind === 'block' && block.block === 'try') {
596
+ const after = closeTry(block)
597
+ const around = stack[stack.length - 1] as TemplateNode[]
598
+ around.push(...after)
599
+ // The `{:finally}` markup stands in the list around the block now, and so
600
+ // does what it declares.
601
+ const moved = declared.get(after)
602
+ if (moved !== undefined) {
603
+ const here = declared.get(around)
604
+ if (here === undefined) declared.set(around, moved)
605
+ else here.push(...moved)
606
+ }
527
607
  }
528
608
  at = end + 1
529
609
  continue
530
610
  }
611
+ // 32.65 — `{#each}`, `{#await}`, `{@html}` and every other tag abide has no block for.
612
+ // Read as an expression, each surfaced as a TypeScript syntax error about a `#` or an
613
+ // `@`, which names nothing the author wrote.
614
+ const unknown = /^(?:[#:@][A-Za-z]|\/\s*[A-Za-z][\w$]*$)/.test(
615
+ inner,
616
+ )
617
+ if (unknown)
618
+ throw new PositionedRefusal(
619
+ `abide: {${inner}} is not a block abide has. A template has {#if}, {#for}, {#try} and {#component Name(pattern)}, each closed by its own {/if}, {/for}, {/try} or {/component}${unknownHint(inner)}. RULEBOOK 32.65.`,
620
+ at,
621
+ )
531
622
 
532
623
  const expression: TemplateNode = {
533
624
  kind: 'expression',
@@ -541,6 +632,14 @@ export function parseAbide(source: string): AbideFile {
541
632
 
542
633
  at = pushText(children, source, at, at)
543
634
  }
635
+ // 32.63 — a block still open at the end of the file closes nowhere, and what it holds would
636
+ // render only where its arm takes.
637
+ const unclosed = blocks[blocks.length - 1]
638
+ if (unclosed !== undefined)
639
+ throw new PositionedRefusal(
640
+ `abide: {#${blockName(unclosed)}} is never closed. Write {/${blockName(unclosed)}} where its body ends. RULEBOOK 32.63.`,
641
+ openedAt[openedAt.length - 1] as number,
642
+ )
544
643
 
545
644
  const components = fileComponents(root, declared)
546
645
  refuseImported(components, [...modules, ...servers, ...scripts])
@@ -1148,7 +1247,7 @@ function leadingSpace(text: string): number {
1148
1247
  function readTag(
1149
1248
  source: string,
1150
1249
  open: number,
1151
- origins: Map<object, number>,
1250
+ origins: Origins,
1152
1251
  ):
1153
1252
  | {
1154
1253
  name: string
@@ -1177,7 +1276,8 @@ function readTag(
1177
1276
  const place = (from: number, position: number, named: number): void => {
1178
1277
  for (let index = from; index < attributes.length; index += 1) {
1179
1278
  const placed = attributes[index] as Attribute
1180
- origins.set(placed, placed.expression ? position : named)
1279
+ origins.holders.set(placed, placed.expression ? position : named)
1280
+ if (placed.name !== '...') origins.names.set(placed, named)
1181
1281
  }
1182
1282
  }
1183
1283
  while (at < source.length) {
@@ -60,6 +60,8 @@ export type Projection = {
60
60
  * Per `{#for}` block, the position of its `by` key, which stands inside the loop and sees the row.
61
61
  */
62
62
  keys: Map<object, Position>
63
+ /** 32.67 — per `{#for}` block, where its row's item binding starts. */
64
+ items: Map<object, number>
63
65
  /**
64
66
  * 32.34 — per invocation, the position of its tag: a read there is a component chosen by a value.
65
67
  */
@@ -265,6 +267,7 @@ function project(file: AbideFile): Projection {
265
267
  const positions: Position[] = []
266
268
  const slots = new Map<object, Position>()
267
269
  const keys = new Map<object, Position>()
270
+ const items = new Map<object, number>()
268
271
  const tags = new Map<object, Position>()
269
272
  let inlineProps = false
270
273
  // Spans are written in ascending order, so the table is sorted as it is pushed.
@@ -355,10 +358,23 @@ function project(file: AbideFile): Projection {
355
358
  if (attribute.expression) refused.push(attribute)
356
359
  continue
357
360
  }
358
- // The key is scaffolding, and a finding on it — a value of the wrong type, a prop the
359
- // child does not declare — is placed at the value it keys, or a literal's name.
360
- place(file.origins.holders.get(attribute), 0)
361
- text += `${JSON.stringify(key)}: `
361
+ // 38.116 — A KEY A NAME CAN SPELL IS COPIED FROM THE ATTRIBUTE'S NAME, so the prop's
362
+ // references reach the tag that passes it. A finding on the key — a value of the wrong
363
+ // type, a prop the child does not declare — is still placed at the value it keys, or a
364
+ // literal's name: the empty run at the key's start, after the name's own, is the one a
365
+ // finding there maps back by.
366
+ const named = file.origins.names.get(attribute)
367
+ const holder = file.origins.holders.get(attribute)
368
+ if (named !== undefined && isNamedAlone(key)) {
369
+ const keyAt = text.length
370
+ copied(key, named + attribute.name.length - key.length)
371
+ if (holder !== undefined)
372
+ origins.push({ at: keyAt, source: holder, length: 0 })
373
+ } else {
374
+ place(holder, 0)
375
+ text += JSON.stringify(key)
376
+ }
377
+ text += ': '
362
378
  if (attribute.expression) prop(attribute)
363
379
  else text += `${JSON.stringify(attribute.value)},\n`
364
380
  }
@@ -371,6 +387,13 @@ function project(file: AbideFile): Projection {
371
387
  text += ',\n'
372
388
  }
373
389
  text += '}))\n'
390
+ // 38.116 — a close tag of its own is a second read of the name, so it is renamed with it.
391
+ const closed = file.origins.closes.get(node)
392
+ if (closed !== undefined) {
393
+ text += ';('
394
+ copied(node.tag, closed)
395
+ text += ')\n'
396
+ }
374
397
  for (const attribute of refused)
375
398
  slots.set(
376
399
  attribute,
@@ -537,7 +560,7 @@ function project(file: AbideFile): Projection {
537
560
  // the binding's. The index is a `number`: `= 0` alone types it the literal `0`.
538
561
  const bound = file.origins.bindings.get(node)
539
562
  text += 'for (const '
540
- copied(node.item, bound?.item)
563
+ items.set(node, copied(node.item, bound?.item).start)
541
564
  text += ` of ${ROWS_OF}(${subject})) {\n`
542
565
  if (node.index !== '') {
543
566
  text += 'const '
@@ -589,6 +612,7 @@ function project(file: AbideFile): Projection {
589
612
  positions: new PositionTable(positions),
590
613
  slots,
591
614
  keys,
615
+ items,
592
616
  tags,
593
617
  origins,
594
618
  }
@@ -800,7 +824,7 @@ function refuseTwoFaces(
800
824
  (binding.imported === 'default' || binding.imported === '*')
801
825
  )
802
826
  throw new Error(
803
- `abide: <script server> imports ${binding.local} from '${statement.specifier}' as another script block of its file does — a server block's .abide is the function binding its props (34.16), the other block's is its module (32.17), and one name cannot be both. Import it under another name in the <script server>. RULEBOOK 34.22.`,
827
+ `abide: <script server> imports ${binding.local} from '${statement.specifier}' as another script block of its file does — a server block's .abide is the function binding its props (RULEBOOK 34.16), the other block's is its module (RULEBOOK 32.17), and one name cannot be both. Import it under another name in the <script server>. RULEBOOK 34.22.`,
804
828
  )
805
829
  }
806
830
 
@@ -37,7 +37,7 @@ export function parseSources(
37
37
  try {
38
38
  file = parseAbide(source.text)
39
39
  } catch (thrown) {
40
- if (!quiet) refusals.at(source.path, thrown)
40
+ if (!quiet) refusals.at(source.path, thrown, source.text)
41
41
  continue
42
42
  }
43
43
  parsed.push({
@@ -9,6 +9,7 @@ import {
9
9
  isClassDeclaration,
10
10
  isEnumDeclaration,
11
11
  isExpressionStatement,
12
+ isForOfStatement,
12
13
  isFunctionDeclaration,
13
14
  isFunctionExpression,
14
15
  isIdentifier,
@@ -21,6 +22,7 @@ import {
21
22
  isPropertyAccessExpression,
22
23
  isReturnStatement,
23
24
  isVariableDeclaration,
25
+ isVariableDeclarationList,
24
26
  isVariableStatement,
25
27
  } from 'typescript/unstable/ast/is'
26
28
  import {
@@ -71,6 +73,8 @@ export type ShadowSpans = {
71
73
  servers: Span
72
74
  scripts: Span
73
75
  positions: PositionTable
76
+ /** 32.67 — where each `{#for}` row's item binding starts. */
77
+ items: Set<number>
74
78
  }
75
79
 
76
80
  const SETUP: ReadonlySet<PositionKind> = new Set(['setup'])
@@ -186,6 +190,7 @@ export async function readsIn(
186
190
  // listening and be reported unhandled; `await props` still throws it.
187
191
  props.catch(() => {})
188
192
  const params = routeReads(checker, abide, sourceFile, reads)
193
+ const rows = rowReads(checker, sourceFile, shadowPath, spans, reads)
189
194
  const configs = configReads(
190
195
  checker,
191
196
  abide,
@@ -256,6 +261,7 @@ export async function readsIn(
256
261
  await Promise.all(holding)
257
262
  await props
258
263
  await params
264
+ await rows
259
265
  await configs
260
266
  const statics = new Set<number>()
261
267
  const expressions = spans.positions.nodesIn(
@@ -493,11 +499,13 @@ async function proveSlots(
493
499
  statics: Set<number>,
494
500
  ): Promise<void> {
495
501
  // Reads are unsorted here; a position holding one is not asked about. A `raw` callee is the
496
- // framework's name rather than a read of the page's, and `provePosition` exempts its call.
502
+ // framework's name rather than a read of the page's, and `provePosition` exempts its call. A
503
+ // row's item (32.67) is the value on the server, and the client's `Lowering.isLive` binds it.
497
504
  const read = (span: Span): boolean => {
498
505
  for (const one of reads)
499
506
  if (
500
507
  one.shape !== 'raw' &&
508
+ one.item === -1 &&
501
509
  one.start >= span.start &&
502
510
  one.start < span.end
503
511
  )
@@ -733,6 +741,97 @@ async function routeReads(
733
741
  }
734
742
  }
735
743
 
744
+ /**
745
+ * 32.67 — every read of a `{#for}` row's item, which a client lowering reads off the row's record or
746
+ * its cell inside the row's mount (`Lowering.readItemsAs`), so the value a kept key is handed is
747
+ * written into the row rather than the row remade.
748
+ *
749
+ * EVERY OCCURRENCE IS A READ OF THE ITEM, as a prop's is (`classifyPropRead`): a member written,
750
+ * `row.done = true`, is the value's member, and a call is the value called. A pattern's names are
751
+ * each one, read through the item. The ROW'S OWN KEY is not: the key is asked of the value before
752
+ * there is a row (`List.key`), so it reads the value itself.
753
+ */
754
+ async function rowReads(
755
+ checker: Checker,
756
+ sourceFile: Node,
757
+ shadowPath: string,
758
+ spans: ShadowSpans,
759
+ reads: Read[],
760
+ ): Promise<void> {
761
+ if (spans.items.size === 0) return
762
+ const bound: Node[] = []
763
+ const loops: Node[] = []
764
+ const bind = (name: Node | undefined, loop: Node): void => {
765
+ if (name === undefined) return
766
+ if (isIdentifier(name)) {
767
+ bound.push(name)
768
+ loops.push(loop)
769
+ return
770
+ }
771
+ if (isObjectBindingPattern(name) || isArrayBindingPattern(name))
772
+ for (const element of name.elements)
773
+ bind((element as { name?: Node }).name, loop)
774
+ }
775
+ const visit = (node: Node): void => {
776
+ const declared = rowItemOf(node, spans.items)
777
+ if (declared !== undefined) bind(declared, node)
778
+ node.forEachChild(visit)
779
+ }
780
+ sourceFile.forEachChild(visit)
781
+ if (bound.length === 0) return
782
+ const symbols = await checker.getSymbolAtLocation(bound)
783
+ const found = await Promise.all(
784
+ symbols.map(async (symbol) =>
785
+ symbol === undefined
786
+ ? []
787
+ : await Promise.all(
788
+ (
789
+ await checker.getReferencesToSymbolInFile(
790
+ shadowPath,
791
+ symbol,
792
+ )
793
+ ).map((reference) => reference.resolve()),
794
+ ),
795
+ ),
796
+ )
797
+ for (let at = 0; at < found.length; at += 1) {
798
+ const name = bound[at] as Node
799
+ const loop = loops[at] as Node
800
+ const item = (rowItemOf(loop, spans.items) as Node).getStart()
801
+ for (const node of found[at] as (Node | undefined)[]) {
802
+ if (node === undefined || node === name) continue
803
+ if (
804
+ spans.positions.holding(node.getStart())?.kind === 'key' &&
805
+ rowLoopOf(node, spans.items) === loop
806
+ )
807
+ continue
808
+ const read = classifyPropRead(node)
809
+ if (read === undefined) continue
810
+ read.item = item
811
+ reads.push(read)
812
+ }
813
+ }
814
+ }
815
+
816
+ /** The item binding `node` declares, where it is a `{#for}` row loop. */
817
+ function rowItemOf(node: Node, items: Set<number>): Node | undefined {
818
+ if (!isForOfStatement(node)) return undefined
819
+ const list = node.initializer
820
+ const declared = isVariableDeclarationList(list)
821
+ ? list.declarations[0]?.name
822
+ : undefined
823
+ return declared !== undefined && items.has(declared.getStart())
824
+ ? declared
825
+ : undefined
826
+ }
827
+
828
+ /** The nearest `{#for}` row loop around `node`. */
829
+ function rowLoopOf(node: Node, items: Set<number>): Node | undefined {
830
+ for (let at = node.parent; at !== undefined; at = at.parent)
831
+ if (rowItemOf(at, items) !== undefined) return at
832
+ return undefined
833
+ }
834
+
736
835
  /**
737
836
  * Throws where an inline component the template's own body declares shares a name with one the
738
837
  * file's `<script>` or `<script module>` declares.