@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.
- package/CHANGELOG.md +24 -0
- package/bin/abide.ts +4 -1
- package/docs/REGISTRY.md +156 -44
- package/docs/RULEBOOK.md +375 -87
- package/docs/abide.md +7243 -10256
- package/package.json +4 -1
- package/scaffold/AGENTS.md +2 -0
- package/scaffold/src/getGreeting.ts +6 -0
- package/scaffold/src/index.abide +3 -1
- package/scaffold/src/tests/getGreeting.test.ts +6 -0
- package/scaffold/src/tests/index.test.ts +18 -0
- package/src/compiler/DOCS_SITE.ts +2 -0
- package/src/compiler/Lowering.ts +234 -14
- package/src/compiler/PositionTable.ts +14 -7
- package/src/compiler/PositionedRefusal.ts +13 -0
- package/src/compiler/Refusals.ts +18 -11
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowMap.ts +37 -0
- package/src/compiler/TEMPLATE_BLOCKS.ts +12 -0
- package/src/compiler/TypeSession.ts +410 -31
- package/src/compiler/appLayout.ts +18 -2
- package/src/compiler/argsSchemaOf.ts +55 -9
- package/src/compiler/checkApp.ts +33 -0
- package/src/compiler/checkedShadow.ts +36 -22
- package/src/compiler/citedClauses.ts +14 -0
- package/src/compiler/classifyRead.ts +6 -0
- package/src/compiler/classifyTemplate.ts +2 -1
- package/src/compiler/compileApp.ts +2 -2
- package/src/compiler/declaredExports.ts +27 -6
- package/src/compiler/emitClient.ts +304 -103
- package/src/compiler/handlersUnder.ts +4 -0
- package/src/compiler/invocationProps.ts +1 -1
- package/src/compiler/isIgnored.ts +27 -0
- package/src/compiler/parseAbide.ts +176 -76
- package/src/compiler/projectShadow.ts +30 -6
- package/src/compiler/readSources.ts +1 -1
- package/src/compiler/readsIn.ts +100 -1
- package/src/compiler/renderMarkup.ts +36 -5
- package/src/compiler/scaffold.ts +19 -6
- package/src/compiler/serveLanguage.ts +156 -21
- package/src/compiler/templateContext.ts +150 -0
- package/src/compiler/tokenize.ts +6 -0
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/Inbox.ts +8 -0
- package/src/server/MCP_PROTOCOL_VERSION.ts +1 -1
- package/src/server/MOUNTABLE.ts +6 -0
- package/src/server/abideModules.ts +8 -1
- package/src/server/answerRequest.ts +3 -1
- package/src/server/answerRpc.ts +26 -12
- package/src/server/answerSocket.ts +26 -1
- package/src/server/argsFor.ts +4 -3
- package/src/server/assistant.ts +19 -13
- package/src/server/bridgeRelay.ts +108 -20
- package/src/server/claudeApi.ts +7 -7
- package/src/server/claudeCode.ts +11 -11
- package/src/server/commands.ts +73 -9
- package/src/server/createApp.ts +41 -7
- package/src/server/devWorker.ts +3 -3
- package/src/server/generatedSurfaces.ts +37 -4
- package/src/server/muxUpgrade.ts +47 -0
- package/src/server/preload.ts +13 -2
- package/src/server/render.ts +4 -1
- package/src/server/runCompiled.ts +1 -0
- package/src/server/seedBuffer.ts +31 -1
- package/src/server/serveDev.ts +83 -72
- package/src/server/surfaceOf.ts +3 -0
- package/src/server/testMount.ts +53 -0
- package/src/shared/ADDRESSES.ts +1 -1
- package/src/shared/ERROR_META.ts +3 -0
- package/src/shared/Schema.ts +1 -1
- package/src/shared/WORK.ts +14 -12
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/boundValue.ts +81 -0
- package/src/shared/index.ts +7 -2
- package/src/shared/jsonSchema.ts +25 -2
- package/src/shared/log.ts +11 -1
- package/src/shared/reactive/Keyed.ts +30 -6
- package/src/shared/reactive/PATCHES.ts +1 -1
- package/src/shared/reactive/Reactive.ts +118 -60
- package/src/shared/reactive/canonicalKey.ts +1 -1
- package/src/shared/reactive/index.ts +13 -5
- package/src/shared/reactive/recordWrites.ts +1 -1
- package/src/shared/reactive/watch.ts +26 -4
- package/src/shared/rpc.ts +6 -1
- package/src/shared/url.ts +7 -3
- package/src/shared/validateJson.ts +1 -1
- package/src/ui/ArmRegion.ts +2 -2
- package/src/ui/List.ts +9 -9
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +194 -36
- package/src/ui/Scope.ts +29 -0
- package/src/ui/adoptWalk.ts +3 -3
- package/src/ui/applyHead.ts +25 -3
- package/src/ui/bindProperty.ts +70 -22
- package/src/ui/index.ts +1 -1
- package/src/ui/leave.ts +5 -0
- package/src/ui/listenForRoutes.ts +14 -3
- package/src/ui/reconcile.ts +48 -28
- package/src/ui/regions.ts +36 -4
- 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
|
|
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 (
|
|
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 (
|
|
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
|
|
411
|
-
block.kind
|
|
412
|
-
block.block
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
515
|
-
//
|
|
516
|
-
|
|
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
|
-
|
|
524
|
-
if (
|
|
525
|
-
|
|
526
|
-
|
|
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:
|
|
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
|
-
//
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
package/src/compiler/readsIn.ts
CHANGED
|
@@ -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.
|