@abide/abide 0.41.0 → 0.42.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/AGENTS.md +333 -326
- package/CHANGELOG.md +92 -0
- package/README.md +91 -83
- package/package.json +1 -1
- package/src/abideLsp.ts +46 -8
- package/src/abideResolverPlugin.ts +3 -5
- package/src/lib/server/runtime/devClientFingerprint.ts +2 -1
- package/src/lib/shared/escapeRegex.ts +9 -0
- package/src/lib/shared/fileName.ts +9 -0
- package/src/lib/shared/fileStem.ts +3 -1
- package/src/lib/shared/programNameForPackage.ts +3 -1
- package/src/lib/shared/stripImport.ts +3 -1
- package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +45 -0
- package/src/lib/ui/compile/SSR_ESCAPE.ts +3 -1
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
- package/src/lib/ui/compile/compileShadow.ts +46 -9
- package/src/lib/ui/compile/createShadowLanguageService.ts +48 -0
- package/src/lib/ui/compile/encodeSemanticTokens.ts +37 -0
- package/src/lib/ui/compile/generateBuild.ts +13 -6
- package/src/lib/ui/compile/generateSSR.ts +23 -14
- package/src/lib/ui/compile/makeVarNamer.ts +10 -0
- package/src/lib/ui/compile/offsetToPosition.ts +9 -0
- package/src/lib/ui/compile/parseTemplate.ts +524 -104
- package/src/lib/ui/compile/skeletonContext.ts +67 -79
- package/src/lib/ui/compile/structuralBlockTokens.ts +101 -0
- package/src/lib/ui/compile/templateAnchorAdapter.ts +61 -0
- package/src/lib/ui/compile/templateElementAdapter.ts +44 -0
- package/src/lib/ui/compile/types/SemanticToken.ts +11 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +9 -0
- package/src/lib/ui/compile/walkAnchorOrder.ts +57 -0
- package/src/lib/ui/compile/walkElementOrder.ts +60 -0
- package/src/lib/ui/dom/appendSnippet.ts +9 -8
- package/src/lib/ui/dom/appendText.ts +1 -5
- package/src/lib/ui/dom/applyResolved.ts +4 -9
- package/src/lib/ui/dom/awaitBlock.ts +37 -31
- package/src/lib/ui/dom/buildDetachedRange.ts +30 -0
- package/src/lib/ui/dom/depthZeroNodes.ts +34 -0
- package/src/lib/ui/dom/domAnchorAdapter.ts +29 -0
- package/src/lib/ui/dom/domElementAdapter.ts +20 -0
- package/src/lib/ui/dom/each.ts +7 -11
- package/src/lib/ui/dom/eachAsync.ts +12 -17
- package/src/lib/ui/dom/isComment.ts +6 -0
- package/src/lib/ui/dom/isElement.ts +6 -0
- package/src/lib/ui/dom/markerDepthDelta.ts +19 -0
- package/src/lib/ui/dom/mountRange.ts +4 -3
- package/src/lib/ui/dom/mountSlot.ts +4 -3
- package/src/lib/ui/dom/on.ts +6 -1
- package/src/lib/ui/dom/replaceRange.ts +24 -0
- package/src/lib/ui/dom/skeleton.ts +35 -92
- package/src/lib/ui/dom/switchBlock.ts +13 -10
- package/src/lib/ui/dom/when.ts +13 -10
- package/src/lib/ui/runtime/RANGE_MARKER.ts +16 -0
- package/src/lib/ui/runtime/batch.ts +22 -0
- package/src/lib/ui/runtime/clientPage.ts +3 -8
- package/src/lib/ui/runtime/createDoc.ts +12 -16
- package/src/lib/ui/runtime/pathSegments.ts +10 -0
- package/src/lib/ui/seedResolved.ts +28 -0
- package/src/lib/ui/startClient.ts +6 -4
- package/src/lib/ui/types/ResolvedFrame.ts +15 -0
- package/template/.zed/settings.json +4 -0
- package/template/src/ui/pages/page.abide +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { seedResolved } from '../seedResolved.ts'
|
|
2
|
+
import { isComment } from './isComment.ts'
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
Bundle-side consumer of an SSR stream chunk, the counterpart of the doc stream's inline
|
|
@@ -31,7 +31,7 @@ export function applyResolved(root: Element, frame: string): void {
|
|
|
31
31
|
bundle-consumed stream can't adopt a resolved branch while dropping its cache key. */
|
|
32
32
|
if (resolved.nodeName === 'ABIDE-CACHE') {
|
|
33
33
|
try {
|
|
34
|
-
|
|
34
|
+
seedResolved({ kind: 'cache', resolution: JSON.parse(resolved.textContent ?? 'null') })
|
|
35
35
|
} catch {
|
|
36
36
|
/* malformed payload — leave unseeded; the read falls back to a live fetch */
|
|
37
37
|
}
|
|
@@ -47,7 +47,7 @@ export function applyResolved(root: Element, frame: string): void {
|
|
|
47
47
|
recording it lets a later hydrate adopt this branch (no re-fetch). */
|
|
48
48
|
const payload = resolved.firstChild as Element | null
|
|
49
49
|
if (payload !== null && payload.nodeName === 'SCRIPT') {
|
|
50
|
-
|
|
50
|
+
seedResolved({ kind: 'resume', id: Number(id), resume: payload.textContent ?? '' })
|
|
51
51
|
payload.remove()
|
|
52
52
|
}
|
|
53
53
|
const open = `abide:await:${id}`
|
|
@@ -72,11 +72,6 @@ export function applyResolved(root: Element, frame: string): void {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/* A comment node carrying exactly `data`. */
|
|
76
|
-
function isComment(node: Node, data: string): boolean {
|
|
77
|
-
return (node as { data?: string }).data === data && node.childNodes.length === 0
|
|
78
|
-
}
|
|
79
|
-
|
|
80
75
|
/* Depth-first search for the parent + open-marker comment of a boundary. */
|
|
81
76
|
function findBoundary(node: Node, open: string): { parent: Node; start: Node } | undefined {
|
|
82
77
|
for (const child of [...node.childNodes]) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { decodeRefJson } from '../../shared/decodeRefJson.ts'
|
|
2
2
|
import { effect } from '../effect.ts'
|
|
3
3
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
4
|
+
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
4
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
5
6
|
import type { ResumeEntry } from '../runtime/RESUME.ts'
|
|
6
7
|
import { RESUME } from '../runtime/RESUME.ts'
|
|
@@ -8,14 +9,17 @@ import { scope } from '../runtime/scope.ts'
|
|
|
8
9
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
9
10
|
import type { State } from '../runtime/types/State.ts'
|
|
10
11
|
import { state } from '../state.ts'
|
|
12
|
+
import { buildDetachedRange } from './buildDetachedRange.ts'
|
|
11
13
|
import { discardBoundary } from './discardBoundary.ts'
|
|
12
|
-
import {
|
|
14
|
+
import { removeRange } from './removeRange.ts'
|
|
13
15
|
|
|
14
16
|
/*
|
|
15
17
|
Async binding — the runtime for `<template await>`. Renders the pending branch,
|
|
16
18
|
then swaps to the resolved branch (with the value) or the error branch on settle.
|
|
17
|
-
Each branch
|
|
18
|
-
branch inserts
|
|
19
|
+
Each branch's content lives in a RANGE bounded by two comment markers (`[`…`]`), the
|
|
20
|
+
same model `when`/`switch`/`each` use — so a multi-root branch inserts as a unit
|
|
21
|
+
(`buildDetachedRange`) and detaches as a unit (`removeRange`), rather than tracking and
|
|
22
|
+
removing a node array by hand.
|
|
19
23
|
|
|
20
24
|
The read runs inside a abide-ui `effect`, so it's reactive: `abide/shared/cache`'s
|
|
21
25
|
store subscribes the key it reads to this effect (createSubscriber is abide-ui-
|
|
@@ -49,7 +53,7 @@ export function awaitBlock(
|
|
|
49
53
|
/* The live branch's scope, registered with the owner so it disposes on owner
|
|
50
54
|
teardown — not only when a settle/re-run swaps branches via detach. */
|
|
51
55
|
const group = scopeGroup()
|
|
52
|
-
let active: {
|
|
56
|
+
let active: { start: Comment; end: Comment; dispose: () => void } | undefined
|
|
53
57
|
let anchor: Node | undefined
|
|
54
58
|
let first = true
|
|
55
59
|
/* Bumped each run so a prior run's in-flight promise can't clobber a newer one. */
|
|
@@ -67,29 +71,26 @@ export function awaitBlock(
|
|
|
67
71
|
const detach = (): void => {
|
|
68
72
|
if (active !== undefined) {
|
|
69
73
|
active.dispose()
|
|
70
|
-
/*
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
node.parentNode?.removeChild(node)
|
|
76
|
-
}
|
|
74
|
+
/* `removeRange` evicts the markers AND everything between them via the end
|
|
75
|
+
marker's LIVE parent — not the captured `parent`, which (when this await is a
|
|
76
|
+
bare child of a control-flow branch) is the branch's build fragment, emptied
|
|
77
|
+
into the document once the enclosing block placed it. */
|
|
78
|
+
removeRange(active.start, active.end)
|
|
77
79
|
active = undefined
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
/* Replace the current content with a freshly-built branch, before the anchor. The
|
|
82
|
-
|
|
83
|
-
— appends freely),
|
|
83
|
+
/* Replace the current content with a freshly-built branch, before the anchor. The branch
|
|
84
|
+
builds into a detached `[`…`]`-bracketed fragment (so any content — components, text,
|
|
85
|
+
nested blocks — appends freely), the same create primitive the keyed-list runtimes use,
|
|
86
|
+
which lands as a marker-bounded range the next swap detaches with `removeRange`. */
|
|
84
87
|
const place = (build: (parent: Node) => void): void => {
|
|
85
88
|
detach()
|
|
86
|
-
const
|
|
87
|
-
const dispose =
|
|
88
|
-
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
;(anchor?.parentNode ?? parent).insertBefore(fragment, anchor ?? null)
|
|
92
|
-
active = { nodes, dispose }
|
|
89
|
+
const namespaceParent = anchor?.parentNode ?? parent
|
|
90
|
+
const { start, end, fragment, dispose } = buildDetachedRange(namespaceParent, build)
|
|
91
|
+
const tracked = group.track(dispose)
|
|
92
|
+
namespaceParent.insertBefore(fragment, anchor ?? null)
|
|
93
|
+
active = { start, end, dispose: tracked }
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
/* Settle to a resolved value. then→then updates the cell in place — the branch and its
|
|
@@ -151,23 +152,28 @@ export function awaitBlock(
|
|
|
151
152
|
)
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
/* Adopt an SSR-resolved branch in place (its content claims the existing nodes),
|
|
155
|
-
|
|
156
|
-
content is everything the build claimed between
|
|
155
|
+
/* Adopt an SSR-resolved branch in place (its content claims the existing nodes), then
|
|
156
|
+
wrap the adopted region in `[`…`]` markers and park an anchor just before the close
|
|
157
|
+
marker for later swaps. The adopted content is everything the build claimed between
|
|
158
|
+
the open and close markers; bracketing it makes the adopted branch a marker-bounded
|
|
159
|
+
range identical to a freshly-`place`d one, so the FIRST swap detaches it with
|
|
160
|
+
`removeRange` like every later swap (no node-array special case). */
|
|
157
161
|
const adopt = (open: Node | null, build: (parent: Node) => void): void => {
|
|
158
162
|
const cursor = hydration as NonNullable<typeof hydration>
|
|
159
|
-
|
|
163
|
+
const firstAdopted = open?.nextSibling ?? null
|
|
164
|
+
cursor.next.set(parent, firstAdopted)
|
|
160
165
|
const dispose = group.track(scope(() => build(parent)))
|
|
161
166
|
const close = claimChild(cursor, parent)
|
|
162
167
|
cursor.next.set(parent, close?.nextSibling ?? null)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
/* Bracket the adopted nodes: `[` before the first claimed node (or before `close`
|
|
169
|
+
for an empty branch), `]` then the anchor just before `close`. */
|
|
170
|
+
const start = document.createComment(RANGE_OPEN)
|
|
171
|
+
parent.insertBefore(start, firstAdopted ?? close)
|
|
172
|
+
const end = document.createComment(RANGE_CLOSE)
|
|
173
|
+
parent.insertBefore(end, close)
|
|
168
174
|
anchor = document.createTextNode('')
|
|
169
175
|
parent.insertBefore(anchor, close)
|
|
170
|
-
active = {
|
|
176
|
+
active = { start, end, dispose }
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
/* Discard the SSR boundary and (re)build the block from the live promise, fresh
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
2
|
+
import { scope } from '../runtime/scope.ts'
|
|
3
|
+
import { enterNamespace } from './enterNamespace.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Builds a `[ … ]`-bounded content range into a DETACHED fragment under a fresh reactive
|
|
7
|
+
scope, then returns the markers, the fragment, and the scope disposer — leaving
|
|
8
|
+
INSERTION to the caller. The create-path range builder for the keyed list runtimes
|
|
9
|
+
(`each` / `eachAsync`): `each` holds the fragment in `pending` for deferred placement
|
|
10
|
+
(its reconcile reorders rows), `eachAsync` inserts it immediately at the stream anchor
|
|
11
|
+
(arrival order). Both need the same bracketed-fragment build, so it lives here once.
|
|
12
|
+
|
|
13
|
+
Unlike `openMarker`, this never touches a live parent or the hydrate claim cursor — the
|
|
14
|
+
markers are created fresh and parked in a fragment — so it is a pure CREATE primitive;
|
|
15
|
+
the hydrate paths claim their server markers directly. `namespaceParent` sets the
|
|
16
|
+
ambient foreign namespace for the build (a row's svg/math children read it off the
|
|
17
|
+
fragment's eventual parent), matching `fillBefore`/`enterNamespace`.
|
|
18
|
+
*/
|
|
19
|
+
export function buildDetachedRange(
|
|
20
|
+
namespaceParent: Node,
|
|
21
|
+
build: (into: Node) => void,
|
|
22
|
+
): { start: Comment; end: Comment; fragment: DocumentFragment; dispose: () => void } {
|
|
23
|
+
const start = document.createComment(RANGE_OPEN)
|
|
24
|
+
const end = document.createComment(RANGE_CLOSE)
|
|
25
|
+
const fragment = document.createDocumentFragment()
|
|
26
|
+
fragment.appendChild(start)
|
|
27
|
+
const dispose = enterNamespace(namespaceParent, () => scope(() => build(fragment)))
|
|
28
|
+
fragment.appendChild(end)
|
|
29
|
+
return { start, end, fragment, dispose }
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { commentData } from './commentData.ts'
|
|
2
|
+
import { markerDepthDelta } from './markerDepthDelta.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
A sibling list's DEPTH-0 nodes — those belonging to THIS skeleton's own structure, excluding
|
|
6
|
+
any node inside a block/component's `[`…`]` / `abide:` range (depth > 0), whose anchors belong
|
|
7
|
+
to that range's OWN skeleton. The realized-DOM analogue of the compiler's fresh-context
|
|
8
|
+
boundary: the shared anchor walk (`walkAnchorOrder`) classifies one node at a time and so can't
|
|
9
|
+
skip a sibling RUN, so the marker-depth skip lives here, feeding the walk only own-skeleton
|
|
10
|
+
nodes. In create mode the clone is shallow (no markers built yet), so depth stays 0 and every
|
|
11
|
+
node is returned — a plain document scan.
|
|
12
|
+
*/
|
|
13
|
+
export function depthZeroNodes(nodes: ArrayLike<Node>): Node[] {
|
|
14
|
+
const own: Node[] = []
|
|
15
|
+
let depth = 0
|
|
16
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
17
|
+
const node = nodes[index] as Node
|
|
18
|
+
const data = commentData(node)
|
|
19
|
+
if (data === undefined) {
|
|
20
|
+
if (depth === 0) {
|
|
21
|
+
own.push(node)
|
|
22
|
+
}
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
const delta = markerDepthDelta(data)
|
|
26
|
+
if (delta !== 0) {
|
|
27
|
+
depth += delta
|
|
28
|
+
} else if (depth === 0) {
|
|
29
|
+
/* A non-marker comment (this skeleton's own `a` anchor) at depth 0. */
|
|
30
|
+
own.push(node)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return own
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AnchorRole, AnchorWalkAdapter } from '../compile/walkAnchorOrder.ts'
|
|
2
|
+
import { ANCHOR } from '../runtime/RANGE_MARKER.ts'
|
|
3
|
+
import { commentData } from './commentData.ts'
|
|
4
|
+
import { depthZeroNodes } from './depthZeroNodes.ts'
|
|
5
|
+
import { isElement } from './isElement.ts'
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
The realized-DOM side of the shared anchor-ordering rule (`walkAnchorOrder`). Classifies each
|
|
9
|
+
recovered node by the SAME positions the template-AST side numbered (`templateAnchorAdapter`),
|
|
10
|
+
so `scanAnchors`'s collection lines up with the compiler's `anIndex`.
|
|
11
|
+
|
|
12
|
+
An `a` comment is this skeleton's own anchor (one position); an element is an own-skeleton
|
|
13
|
+
container to descend; anything else contributes nothing. `childrenOf` returns only depth-0
|
|
14
|
+
nodes — a nested block/component's marker-bracketed content is skipped exactly as the compiler
|
|
15
|
+
stops at a fresh-context boundary.
|
|
16
|
+
*/
|
|
17
|
+
export const domAnchorAdapter: AnchorWalkAdapter<Node> = {
|
|
18
|
+
classify: (node: Node): AnchorRole => {
|
|
19
|
+
const data = commentData(node)
|
|
20
|
+
if (data === ANCHOR) {
|
|
21
|
+
return { kind: 'anchor', positions: [node] }
|
|
22
|
+
}
|
|
23
|
+
if (data === undefined && isElement(node)) {
|
|
24
|
+
return { kind: 'recurse' }
|
|
25
|
+
}
|
|
26
|
+
return { kind: 'skip' }
|
|
27
|
+
},
|
|
28
|
+
childrenOf: (node: Node): readonly Node[] => depthZeroNodes(node.childNodes),
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ElementRole, ElementWalkAdapter } from '../compile/walkElementOrder.ts'
|
|
2
|
+
import { HOLE_ATTRIBUTE } from '../runtime/HOLE_ATTRIBUTE.ts'
|
|
3
|
+
import { isElement } from './isElement.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
The parsed-DOM side of the shared element-hole numbering rule (`walkElementOrder`). Classifies
|
|
7
|
+
each node of the parsed skeleton by the SAME holes the template-AST side numbered
|
|
8
|
+
(`templateElementAdapter`), so the runtime's collected element paths line up with the compiler's
|
|
9
|
+
`elIndex`. Runs over the SHALLOW parsed skeleton (blocks/components/slots are already `<!--a-->`
|
|
10
|
+
anchors, not elements), so a `HOLE_ATTRIBUTE` element is a hole and every element is descended;
|
|
11
|
+
everything else skips. The collected paths resolve against the expanded server DOM separately
|
|
12
|
+
(`resolveElementHole`, which skips nested ranges there).
|
|
13
|
+
*/
|
|
14
|
+
export const domElementAdapter: ElementWalkAdapter<Node> = {
|
|
15
|
+
classify: (node: Node): ElementRole =>
|
|
16
|
+
isElement(node)
|
|
17
|
+
? { kind: 'element', isHole: node.hasAttribute(HOLE_ATTRIBUTE) }
|
|
18
|
+
: { kind: 'skip' },
|
|
19
|
+
childrenOf: (node: Node): readonly Node[] => Array.from(node.childNodes),
|
|
20
|
+
}
|
package/src/lib/ui/dom/each.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { scope } from '../runtime/scope.ts'
|
|
|
6
6
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
7
7
|
import type { State } from '../runtime/types/State.ts'
|
|
8
8
|
import { state } from '../state.ts'
|
|
9
|
-
import {
|
|
9
|
+
import { buildDetachedRange } from './buildDetachedRange.ts'
|
|
10
10
|
import { moveRange } from './moveRange.ts'
|
|
11
11
|
import { removeRange } from './removeRange.ts'
|
|
12
12
|
import type { EachRow } from './types/EachRow.ts'
|
|
@@ -70,17 +70,13 @@ export function each<T>(
|
|
|
70
70
|
hydration.next.set(parent, end.nextSibling)
|
|
71
71
|
return { start, end, dispose, cell, indexCell }
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
built into the detached fragment are namespaced, not built as HTML. */
|
|
79
|
-
const dispose = group.track(
|
|
80
|
-
enterNamespace(parent, () => scope(() => render(pending, cell as State<T>, indexCell))),
|
|
73
|
+
/* Shared detached-range create primitive: a `[ … ]` fragment built under `parent`'s
|
|
74
|
+
foreign namespace (so svg/math row children are namespaced, not built as HTML),
|
|
75
|
+
held in `pending` until placement inserts it. */
|
|
76
|
+
const { start, end, fragment, dispose } = buildDetachedRange(parent, (host) =>
|
|
77
|
+
render(host, cell as State<T>, indexCell),
|
|
81
78
|
)
|
|
82
|
-
|
|
83
|
-
return { start, end, dispose, cell, indexCell, pending }
|
|
79
|
+
return { start, end, dispose: group.track(dispose), cell, indexCell, pending: fragment }
|
|
84
80
|
}
|
|
85
81
|
|
|
86
82
|
/* Place a row so its range ends just before `cursor`: insert a fresh row's
|
|
@@ -2,11 +2,10 @@ import { effect } from '../effect.ts'
|
|
|
2
2
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
3
3
|
import { OWNER } from '../runtime/OWNER.ts'
|
|
4
4
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
5
|
-
import { scope } from '../runtime/scope.ts'
|
|
6
5
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
7
6
|
import type { State } from '../runtime/types/State.ts'
|
|
8
7
|
import { state } from '../state.ts'
|
|
9
|
-
import {
|
|
8
|
+
import { buildDetachedRange } from './buildDetachedRange.ts'
|
|
10
9
|
import { removeRange } from './removeRange.ts'
|
|
11
10
|
import type { EachRow } from './types/EachRow.ts'
|
|
12
11
|
|
|
@@ -49,24 +48,20 @@ export function eachAsync<T>(
|
|
|
49
48
|
parent.insertBefore(anchor, before) // `before` places rows before a static suffix
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
/* Build a content range
|
|
53
|
-
range (no item cell) — the data-row
|
|
51
|
+
/* Build a content range (the shared detached-range create primitive) and insert it
|
|
52
|
+
just before the anchor (arrival order). A bare range (no item cell) — the data-row
|
|
53
|
+
site adds the cell, the error branch needs none. */
|
|
54
54
|
const insertRange = (
|
|
55
55
|
build: (into: Node) => void,
|
|
56
56
|
): { start: Node; end: Node; dispose: () => void } => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
/* Insert via the anchor's LIVE parent: when this `each` is a bare child of a
|
|
66
|
-
control-flow branch, the captured `parent` is the branch's build fragment,
|
|
67
|
-
emptied into the document once the enclosing block placed it. */
|
|
68
|
-
;(anchor.parentNode ?? parent).insertBefore(fragment, anchor)
|
|
69
|
-
return { start, end, dispose }
|
|
57
|
+
/* Namespace the build off the anchor's LIVE parent, and insert there too: when this
|
|
58
|
+
`each` is a bare child of a control-flow branch, the captured `parent` is the
|
|
59
|
+
branch's build fragment, emptied into the document once the enclosing block
|
|
60
|
+
placed it. */
|
|
61
|
+
const host = anchor.parentNode ?? parent
|
|
62
|
+
const { start, end, fragment, dispose } = buildDetachedRange(host, build)
|
|
63
|
+
host.insertBefore(fragment, anchor)
|
|
64
|
+
return { start, end, dispose: group.track(dispose) }
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
/* The mounted `<template catch>` range, disposed when a fresh run re-streams. */
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* A comment node carrying exactly `data`. Empty-child check distinguishes the
|
|
2
|
+
`<!--data-->` marker from an element that happens to expose a `data` property.
|
|
3
|
+
Shared by the marker-range scanners in appendText/applyResolved. */
|
|
4
|
+
export function isComment(node: Node, data: string): boolean {
|
|
5
|
+
return (node as { data?: string }).data === data && node.childNodes.length === 0
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* An element carries `hasAttribute`; comment/text nodes do not. Detected by method (not
|
|
2
|
+
`nodeType`) so every skeleton walk runs under the test mini-dom too. Shared by the
|
|
3
|
+
element-hole path resolver (`skeleton`) and both realized-DOM walk adapters. */
|
|
4
|
+
export function isElement(node: Node): node is Element {
|
|
5
|
+
return typeof (node as Element).hasAttribute === 'function'
|
|
6
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
2
|
+
|
|
3
|
+
/* The block-range nesting rule, the single source both marker-depth scans share
|
|
4
|
+
(`depthZeroNodes`'s anchor filter and `skeleton`'s `elementChildAt` element walk):
|
|
5
|
+
`+1` opening a range, `-1` closing one, `0` for any other comment. A control-flow
|
|
6
|
+
block's rendered content sits between an OPEN and CLOSE comment — `[`…`]` for each
|
|
7
|
+
rows / if / switch / slot ranges, named `abide:…`…`/abide:…` for await / try /
|
|
8
|
+
snippet / html; the skeleton's own anchor (`a`) sits OUTSIDE any range, so it scores
|
|
9
|
+
`0`. Keeping the rule here means a new marker family updates one place, so the two
|
|
10
|
+
scans can never disagree on what nests. */
|
|
11
|
+
export function markerDepthDelta(data: string): number {
|
|
12
|
+
if (data === RANGE_CLOSE || data.startsWith('/abide:')) {
|
|
13
|
+
return -1
|
|
14
|
+
}
|
|
15
|
+
if (data === RANGE_OPEN || data.startsWith('abide:')) {
|
|
16
|
+
return 1
|
|
17
|
+
}
|
|
18
|
+
return 0
|
|
19
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
1
2
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
2
3
|
import { scope } from '../runtime/scope.ts'
|
|
3
4
|
import type { UiProps } from '../runtime/types/UiProps.ts'
|
|
@@ -31,15 +32,15 @@ export function mountRange(
|
|
|
31
32
|
label: string | undefined = undefined,
|
|
32
33
|
): { start: Comment; end: Comment; dispose: () => void } {
|
|
33
34
|
const hydration = RENDER.hydration
|
|
34
|
-
const start = openMarker(parent,
|
|
35
|
+
const start = openMarker(parent, RANGE_OPEN, before)
|
|
35
36
|
if (hydration === undefined) {
|
|
36
|
-
const end = openMarker(parent,
|
|
37
|
+
const end = openMarker(parent, RANGE_CLOSE, before)
|
|
37
38
|
return fillRange(start, end, build, props, label)
|
|
38
39
|
}
|
|
39
40
|
/* Hydrate: adopt the server range in place. Establish the child's lexical scope
|
|
40
41
|
and render pass (same as `fillRange`), build claiming the existing nodes, then
|
|
41
42
|
claim the end marker the build's content stops before. */
|
|
42
43
|
const scoped = withScope(label, () => scope(() => build(parent, props)))
|
|
43
|
-
const end = openMarker(parent,
|
|
44
|
+
const end = openMarker(parent, RANGE_CLOSE)
|
|
44
45
|
return { start, end, dispose: disposeRange(scoped, start, end) }
|
|
45
46
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
1
2
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
2
3
|
import { scope } from '../runtime/scope.ts'
|
|
3
4
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
@@ -26,12 +27,12 @@ export function mountSlot(
|
|
|
26
27
|
dispose on owner teardown (a navigation) — the slot never toggles, so the
|
|
27
28
|
group only ever tracks this one child. */
|
|
28
29
|
const group = scopeGroup()
|
|
29
|
-
openMarker(parent,
|
|
30
|
+
openMarker(parent, RANGE_OPEN, before)
|
|
30
31
|
if (hydration !== undefined) {
|
|
31
32
|
group.track(scope(() => render(parent))) // content claims the SSR range in place
|
|
32
|
-
openMarker(parent,
|
|
33
|
+
openMarker(parent, RANGE_CLOSE)
|
|
33
34
|
} else {
|
|
34
|
-
const end = openMarker(parent,
|
|
35
|
+
const end = openMarker(parent, RANGE_CLOSE, before)
|
|
35
36
|
group.track(fillBefore(end, render))
|
|
36
37
|
}
|
|
37
38
|
}
|
package/src/lib/ui/dom/on.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { batch } from '../runtime/batch.ts'
|
|
1
2
|
import { CURRENT_SCOPE } from '../runtime/CURRENT_SCOPE.ts'
|
|
2
3
|
import { inScope } from '../runtime/inScope.ts'
|
|
3
4
|
import { OWNER } from '../runtime/OWNER.ts'
|
|
@@ -20,7 +21,11 @@ export function on(element: Element, type: string, handler: EventListener): void
|
|
|
20
21
|
return
|
|
21
22
|
}
|
|
22
23
|
const captured = CURRENT_SCOPE.current
|
|
23
|
-
|
|
24
|
+
/* Coalesce the handler's writes: a handler setting several signals re-runs each
|
|
25
|
+
dependent effect once on batch exit, not once per write (the unbatched default
|
|
26
|
+
flushed eagerly per `trigger`). Stays synchronous — flush runs at handler-end
|
|
27
|
+
before it returns, so the causal stack (dispatch → handler → effects) is intact. */
|
|
28
|
+
const wrapped: EventListener = (event) => inScope(captured, () => batch(() => handler(event)))
|
|
24
29
|
element.addEventListener(type, wrapped)
|
|
25
30
|
if (OWNER.current !== undefined) {
|
|
26
31
|
OWNER.current.push(() => element.removeEventListener(type, wrapped))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { clearBetween } from './clearBetween.ts'
|
|
2
|
+
import { fillBefore } from './fillBefore.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
The one "replace a marker-bounded region's contents" operation. A control-flow block
|
|
6
|
+
that swaps a branch as a unit (if/else, switch, snippet) clears the live range
|
|
7
|
+
(disposing its scope and removing its nodes via `clearBetween`), then — if the new
|
|
8
|
+
branch has content — builds it fresh into the range before the close marker
|
|
9
|
+
(`fillBefore`). Returns the new content's disposer, or `undefined` for an empty
|
|
10
|
+
branch.
|
|
11
|
+
|
|
12
|
+
This is the single seam those blocks shared by copy-paste: same `clearBetween` +
|
|
13
|
+
conditional `fillBefore` shape, hand-rolled three times. Routing them through one
|
|
14
|
+
helper makes the region-update strategy a named, testable primitive.
|
|
15
|
+
*/
|
|
16
|
+
export function replaceRange(
|
|
17
|
+
start: Node,
|
|
18
|
+
end: Node,
|
|
19
|
+
dispose: (() => void) | undefined,
|
|
20
|
+
content: ((into: Node) => void) | undefined,
|
|
21
|
+
): (() => void) | undefined {
|
|
22
|
+
clearBetween(start, end, dispose)
|
|
23
|
+
return content !== undefined ? fillBefore(end, content) : undefined
|
|
24
|
+
}
|