@abide/abide 0.41.1 → 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 +19 -13
- package/CHANGELOG.md +48 -0
- package/README.md +13 -14
- 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/abideUiPlugin.ts +2 -1
- package/src/lib/ui/compile/compileShadow.ts +42 -10
- 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 +2 -2
- package/src/lib/ui/compile/generateSSR.ts +6 -6
- 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/structuralBlockTokens.ts +101 -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/dom/appendText.ts +1 -5
- package/src/lib/ui/dom/applyResolved.ts +1 -5
- package/src/lib/ui/dom/isComment.ts +6 -0
- package/src/lib/ui/runtime/createDoc.ts +3 -3
- package/src/lib/ui/runtime/pathSegments.ts +10 -0
- package/template/.zed/settings.json +4 -0
- package/template/src/ui/pages/page.abide +4 -4
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { SemanticToken } from './types/SemanticToken.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Lexical highlighting for `{#…}`/`{:…}`/`{/…}` control-flow framing — the part the
|
|
5
|
+
HTML grammar sees only as text and the shadow program lowers away. A pure scan of
|
|
6
|
+
raw source (independent of a successful parse, so it survives mid-edit), it emits
|
|
7
|
+
an `operator` token for the `{`+sigil opener and a `keyword` token for the block
|
|
8
|
+
word, plus the `of`/`by` connectors inside a `{#for …}` head. Expression interiors
|
|
9
|
+
(and the `{@const}`/`{@html}` tags and bare `{expr}` interpolations) are NOT touched
|
|
10
|
+
here — those are the shadow's job. A keyword allowlist after the sigil prevents
|
|
11
|
+
matching arbitrary `{:foo}` text. Longest phrases first so `for await` beats `for`
|
|
12
|
+
and `else if` beats `else`.
|
|
13
|
+
*/
|
|
14
|
+
const BLOCK_KEYWORDS = [
|
|
15
|
+
'for await',
|
|
16
|
+
'else if',
|
|
17
|
+
'if',
|
|
18
|
+
'for',
|
|
19
|
+
'await',
|
|
20
|
+
'switch',
|
|
21
|
+
'case',
|
|
22
|
+
'default',
|
|
23
|
+
'try',
|
|
24
|
+
'catch',
|
|
25
|
+
'finally',
|
|
26
|
+
'then',
|
|
27
|
+
'else',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const BLOCK_HEAD = new RegExp(`\\{([#:/])\\s*(${BLOCK_KEYWORDS.join('|')})\\b`, 'g')
|
|
31
|
+
|
|
32
|
+
export function structuralBlockTokens(source: string): SemanticToken[] {
|
|
33
|
+
const tokens: SemanticToken[] = []
|
|
34
|
+
for (const match of source.matchAll(BLOCK_HEAD)) {
|
|
35
|
+
const braceStart = match.index
|
|
36
|
+
const sigil = match[1]
|
|
37
|
+
const keyword = match[2]
|
|
38
|
+
if (keyword === undefined) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
const keywordStart = braceStart + match[0].length - keyword.length
|
|
42
|
+
tokens.push({ start: braceStart, length: 2, type: 'operator', modifiers: [] })
|
|
43
|
+
tokens.push({ start: keywordStart, length: keyword.length, type: 'keyword', modifiers: [] })
|
|
44
|
+
/* A `{#for …}` head carries abide-only connectors `of`/`by` that the shadow
|
|
45
|
+
lowers away (`of`) or never emits (`by`), so color them here. */
|
|
46
|
+
if (sigil === '#' && (keyword === 'for' || keyword === 'for await')) {
|
|
47
|
+
tokens.push(...forHeadConnectors(source, keywordStart + keyword.length))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return tokens
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
Scans a `{#for …}` head from `from` to its closing `}`, emitting `keyword` tokens
|
|
55
|
+
for the `of`/`by` connectors at brace depth 0 — skipping any `of`/`by` nested in a
|
|
56
|
+
destructure, call, or string so an identifier or object key is never miscolored.
|
|
57
|
+
*/
|
|
58
|
+
function forHeadConnectors(source: string, from: number): SemanticToken[] {
|
|
59
|
+
const tokens: SemanticToken[] = []
|
|
60
|
+
let depth = 0
|
|
61
|
+
let cursor = from
|
|
62
|
+
while (cursor < source.length) {
|
|
63
|
+
const char = source.charAt(cursor)
|
|
64
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
65
|
+
cursor += 1
|
|
66
|
+
while (cursor < source.length && source.charAt(cursor) !== char) {
|
|
67
|
+
if (source.charAt(cursor) === '\\') {
|
|
68
|
+
cursor += 1
|
|
69
|
+
}
|
|
70
|
+
cursor += 1
|
|
71
|
+
}
|
|
72
|
+
} else if (char === '{' || char === '(' || char === '[') {
|
|
73
|
+
depth += 1
|
|
74
|
+
} else if (char === ')' || char === ']') {
|
|
75
|
+
depth -= 1
|
|
76
|
+
} else if (char === '}') {
|
|
77
|
+
if (depth === 0) {
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
depth -= 1
|
|
81
|
+
} else if (depth === 0 && (char === 'o' || char === 'b')) {
|
|
82
|
+
const word = source.startsWith('of', cursor)
|
|
83
|
+
? 'of'
|
|
84
|
+
: source.startsWith('by', cursor)
|
|
85
|
+
? 'by'
|
|
86
|
+
: undefined
|
|
87
|
+
const isWordBoundary = (offset: number): boolean => /\s/.test(source.charAt(offset))
|
|
88
|
+
if (
|
|
89
|
+
word !== undefined &&
|
|
90
|
+
isWordBoundary(cursor - 1) &&
|
|
91
|
+
isWordBoundary(cursor + word.length)
|
|
92
|
+
) {
|
|
93
|
+
tokens.push({ start: cursor, length: word.length, type: 'keyword', modifiers: [] })
|
|
94
|
+
cursor += word.length
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
cursor += 1
|
|
99
|
+
}
|
|
100
|
+
return tokens
|
|
101
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A semantic-highlighting token in original `.abide` source coordinates. `type` and
|
|
3
|
+
`modifiers` are legend names (see ABIDE_SEMANTIC_TOKENS_LEGEND), resolved to wire
|
|
4
|
+
indices only at encode time.
|
|
5
|
+
*/
|
|
6
|
+
export type SemanticToken = {
|
|
7
|
+
start: number
|
|
8
|
+
length: number
|
|
9
|
+
type: string
|
|
10
|
+
modifiers: string[]
|
|
11
|
+
}
|
|
@@ -40,6 +40,11 @@ export type TemplateNode =
|
|
|
40
40
|
async: boolean
|
|
41
41
|
children: TemplateNode[]
|
|
42
42
|
loc?: number
|
|
43
|
+
/* Source offsets of the binding name, `by` key, and index — so the shadow
|
|
44
|
+
maps hover/highlighting onto them (absent for synthesised/missing parts). */
|
|
45
|
+
asLoc?: number
|
|
46
|
+
keyLoc?: number
|
|
47
|
+
indexLoc?: number
|
|
43
48
|
}
|
|
44
49
|
| { kind: 'if'; condition: string; children: TemplateNode[]; loc?: number }
|
|
45
50
|
| {
|
|
@@ -52,6 +57,8 @@ export type TemplateNode =
|
|
|
52
57
|
as: string | undefined
|
|
53
58
|
children: TemplateNode[]
|
|
54
59
|
loc?: number
|
|
60
|
+
/* Source offset of an inline blocking `then` binding (`{#await p then v}`). */
|
|
61
|
+
asLoc?: number
|
|
55
62
|
}
|
|
56
63
|
| { kind: 'try'; children: TemplateNode[] }
|
|
57
64
|
| {
|
|
@@ -59,6 +66,8 @@ export type TemplateNode =
|
|
|
59
66
|
branch: 'then' | 'catch' | 'finally'
|
|
60
67
|
as: string | undefined
|
|
61
68
|
children: TemplateNode[]
|
|
69
|
+
/* Source offset of the `then`/`catch` binding, so the shadow maps it. */
|
|
70
|
+
asLoc?: number
|
|
62
71
|
}
|
|
63
72
|
| {
|
|
64
73
|
kind: 'component'
|
|
@@ -4,6 +4,7 @@ import { effect } from '../effect.ts'
|
|
|
4
4
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
5
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
6
6
|
import { appendSnippet } from './appendSnippet.ts'
|
|
7
|
+
import { isComment } from './isComment.ts'
|
|
7
8
|
import { parseRawNodes } from './parseRawNodes.ts'
|
|
8
9
|
|
|
9
10
|
const CLOSE = '/abide:html'
|
|
@@ -106,8 +107,3 @@ function appendRawHtml(parent: Node, read: () => unknown): void {
|
|
|
106
107
|
set(markup())
|
|
107
108
|
})
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
-
/* A comment node carrying exactly `data`. */
|
|
111
|
-
function isComment(node: Node, data: string): boolean {
|
|
112
|
-
return (node as { data?: string }).data === data && node.childNodes.length === 0
|
|
113
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { seedResolved } from '../seedResolved.ts'
|
|
2
|
+
import { isComment } from './isComment.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
Bundle-side consumer of an SSR stream chunk, the counterpart of the doc stream's inline
|
|
@@ -71,11 +72,6 @@ export function applyResolved(root: Element, frame: string): void {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
/* A comment node carrying exactly `data`. */
|
|
75
|
-
function isComment(node: Node, data: string): boolean {
|
|
76
|
-
return (node as { data?: string }).data === data && node.childNodes.length === 0
|
|
77
|
-
}
|
|
78
|
-
|
|
79
75
|
/* Depth-first search for the parent + open-marker comment of a boundary. */
|
|
80
76
|
function findBoundary(node: Node, open: string): { parent: Node; start: Node } | undefined {
|
|
81
77
|
for (const child of [...node.childNodes]) {
|
|
@@ -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
|
+
}
|
|
@@ -3,13 +3,13 @@ import { batch } from './batch.ts'
|
|
|
3
3
|
import { createComputedNode } from './createComputedNode.ts'
|
|
4
4
|
import { createSignalNode } from './createSignalNode.ts'
|
|
5
5
|
import { PATCH_BUS } from './PATCH_BUS.ts'
|
|
6
|
+
import { pathSegments } from './pathSegments.ts'
|
|
6
7
|
import { readNode } from './readNode.ts'
|
|
7
8
|
import { trigger } from './trigger.ts'
|
|
8
9
|
import type { Cell } from './types/Cell.ts'
|
|
9
10
|
import type { Doc } from './types/Doc.ts'
|
|
10
11
|
import type { Patch } from './types/Patch.ts'
|
|
11
12
|
import type { ReactiveNode } from './types/ReactiveNode.ts'
|
|
12
|
-
import { unescapeKey } from './unescapeKey.ts'
|
|
13
13
|
import { walkPath } from './walkPath.ts'
|
|
14
14
|
import { writeNode } from './writeNode.ts'
|
|
15
15
|
|
|
@@ -132,7 +132,7 @@ export function createDoc(initial: unknown): Doc {
|
|
|
132
132
|
function apply(patch: Patch): void {
|
|
133
133
|
/* Segments index the tree, so they carry the REAL keys (unescaped); the path
|
|
134
134
|
strings (parentPath, node-map keys) stay escaped, re-walked through walkPath. */
|
|
135
|
-
const segments = patch.path === '' ? [] : patch.path
|
|
135
|
+
const segments = patch.path === '' ? [] : pathSegments(patch.path)
|
|
136
136
|
/* Capture the pre-image only when a consumer is listening (the inverse's only
|
|
137
137
|
cost): a replace/remove inverts to the value it overwrote, an add to a
|
|
138
138
|
remove (computed post-apply, below, to resolve an array append's index). */
|
|
@@ -220,7 +220,7 @@ export function createDoc(initial: unknown): Doc {
|
|
|
220
220
|
*/
|
|
221
221
|
function cell<T>(path: string): Cell<T> {
|
|
222
222
|
const node = nodeFor(path)
|
|
223
|
-
const segments = path
|
|
223
|
+
const segments = pathSegments(path)
|
|
224
224
|
const leafKey = segments[segments.length - 1] as string
|
|
225
225
|
/* Auto-vivify missing ancestor objects so binding a nested path on a doc
|
|
226
226
|
booted shallow (e.g. `state({})`) doesn't crash, and a later `set` writes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { unescapeKey } from './unescapeKey.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Splits an escaped JSON-Pointer-style doc path into its REAL key segments
|
|
5
|
+
(each segment unescaped). The path strings (parentPath, node-map keys) stay
|
|
6
|
+
escaped and re-walked through `walkPath`; segments index the live tree.
|
|
7
|
+
*/
|
|
8
|
+
export function pathSegments(path: string): string[] {
|
|
9
|
+
return path.split('/').map(unescapeKey)
|
|
10
|
+
}
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
Root page — served at GET /. Every folder under src/ui/pages/ that contains a
|
|
4
4
|
page.abide mounts at that folder's URL; the root layout.abide wraps it.
|
|
5
5
|
|
|
6
|
-
The blocking await-block below (the `then`
|
|
6
|
+
The blocking await-block below (the `then` clause sits in the `{#await}` head)
|
|
7
7
|
resolves on the server during SSR and renders inline — no pending placeholder. The
|
|
8
8
|
decoded body is captured into the per-request cache, serialized into the HTML, and
|
|
9
|
-
replayed on the client during hydration with no second fetch. A `then` *
|
|
9
|
+
replayed on the client during hydration with no second fetch. A `{:then}` *branch*
|
|
10
10
|
instead would stream the resolution in out of order.
|
|
11
11
|
*/
|
|
12
12
|
import { cache } from '@abide/abide/shared/cache'
|
|
13
13
|
import { getHello } from '$server/rpc/getHello.ts'
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
{#await cache(getHello)() then hello}
|
|
17
17
|
<h1>{hello.message}</h1>
|
|
18
|
-
|
|
18
|
+
{/await}
|
|
19
19
|
<p>Edit <code>src/ui/pages/page.abide</code> and the page hot-reloads.</p>
|