@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,6 @@
|
|
|
1
1
|
import { relative } from 'node:path'
|
|
2
2
|
import type { BunPlugin } from 'bun'
|
|
3
|
+
import { fileName } from '../../shared/fileName.ts'
|
|
3
4
|
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
4
5
|
import { AbideCompileError } from './AbideCompileError.ts'
|
|
5
6
|
import { analyzeComponent } from './analyzeComponent.ts'
|
|
@@ -42,7 +43,7 @@ export const abideUiPlugin: BunPlugin = {
|
|
|
42
43
|
build.onLoad({ filter: /\.abide$/ }, async (args) => {
|
|
43
44
|
const source = await Bun.file(args.path).text()
|
|
44
45
|
const moduleId = relative(nearestProjectRoot(args.path, process.cwd()), args.path)
|
|
45
|
-
const isLayout = (args.path
|
|
46
|
+
const isLayout = fileName(args.path) === 'layout.abide'
|
|
46
47
|
/* Bun frames a plugin throw at `<file>:0` regardless of the real spot, so
|
|
47
48
|
carry the component path + resolved line:col in the message — otherwise a
|
|
48
49
|
control-flow / compile error reads as `:0` and (in deep imports) can look
|
|
@@ -91,6 +91,10 @@ export function compileShadow(source: string): CompiledShadow {
|
|
|
91
91
|
pre-assembled scope line carrying its own embedded segments. */
|
|
92
92
|
type Builder = {
|
|
93
93
|
raw: (text: string) => void
|
|
94
|
+
/* Appends `text` verbatim and maps it back to source, WITHOUT the `expr`
|
|
95
|
+
parens — for binding names and other spans that must stay bare TS syntax
|
|
96
|
+
(`for (const NAME of …)`). `text` must equal the source at `sourceLoc`. */
|
|
97
|
+
mapped: (text: string, sourceLoc: number | undefined) => void
|
|
94
98
|
expr: (code: string, sourceLoc: number | undefined) => void
|
|
95
99
|
stmt: (code: string, sourceLoc: number | undefined) => void
|
|
96
100
|
flush: (line: ScopeLine) => void
|
|
@@ -115,6 +119,16 @@ function createBuilder(): Builder {
|
|
|
115
119
|
raw(text) {
|
|
116
120
|
code += text
|
|
117
121
|
},
|
|
122
|
+
mapped(text, sourceLoc) {
|
|
123
|
+
if (sourceLoc !== undefined) {
|
|
124
|
+
mappings.push({
|
|
125
|
+
shadowStart: code.length,
|
|
126
|
+
sourceStart: sourceLoc,
|
|
127
|
+
length: text.length,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
code += text
|
|
131
|
+
},
|
|
118
132
|
expr(exprCode, sourceLoc) {
|
|
119
133
|
code += '('
|
|
120
134
|
if (sourceLoc !== undefined) {
|
|
@@ -408,14 +422,24 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
408
422
|
}
|
|
409
423
|
case 'each':
|
|
410
424
|
/* `for await` over an async each's AsyncIterable, plain `for…of` otherwise —
|
|
411
|
-
so the item binds to the element type under either iteration protocol.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
)
|
|
425
|
+
so the item binds to the element type under either iteration protocol. The
|
|
426
|
+
binding name is `mapped` (not `raw`) so hover/highlighting land on it. */
|
|
427
|
+
builder.raw(node.async ? 'for await (const ' : 'for (const ')
|
|
428
|
+
builder.mapped(node.as, node.asLoc)
|
|
429
|
+
builder.raw(' of ')
|
|
415
430
|
builder.expr(node.items, node.loc)
|
|
416
431
|
builder.raw(') {\n')
|
|
417
432
|
if (node.key !== undefined) {
|
|
418
|
-
builder.raw(
|
|
433
|
+
builder.raw('void ')
|
|
434
|
+
builder.expr(node.key, node.keyLoc)
|
|
435
|
+
builder.raw(';\n')
|
|
436
|
+
}
|
|
437
|
+
/* `index="i"` binds the row's position — always a number (the row ordinal,
|
|
438
|
+
or an async stream's arrival count). Declare it so body references check. */
|
|
439
|
+
if (node.index !== undefined) {
|
|
440
|
+
builder.raw('const ')
|
|
441
|
+
builder.mapped(node.index, node.indexLoc)
|
|
442
|
+
builder.raw(': number = 0;\n')
|
|
419
443
|
}
|
|
420
444
|
emitNodes(node.children, builder)
|
|
421
445
|
builder.raw('}\n')
|
|
@@ -434,7 +458,9 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
434
458
|
const pending = node.children.filter((child) => child.kind !== 'branch')
|
|
435
459
|
const branches = node.children.filter((child) => child.kind === 'branch')
|
|
436
460
|
if (node.blocking && node.as !== undefined) {
|
|
437
|
-
builder.raw(
|
|
461
|
+
builder.raw('{\nconst ')
|
|
462
|
+
builder.mapped(node.as, node.asLoc)
|
|
463
|
+
builder.raw(` = ${resolved};\n`)
|
|
438
464
|
emitNodes(pending, builder)
|
|
439
465
|
builder.raw('}\n')
|
|
440
466
|
} else {
|
|
@@ -446,9 +472,13 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
446
472
|
}
|
|
447
473
|
builder.raw('{\n')
|
|
448
474
|
if (branch.branch === 'then' && branch.as !== undefined) {
|
|
449
|
-
builder.raw(
|
|
475
|
+
builder.raw('const ')
|
|
476
|
+
builder.mapped(branch.as, branch.asLoc)
|
|
477
|
+
builder.raw(` = ${resolved};\n`)
|
|
450
478
|
} else if (branch.branch === 'catch' && branch.as !== undefined) {
|
|
451
|
-
builder.raw(
|
|
479
|
+
builder.raw('const ')
|
|
480
|
+
builder.mapped(branch.as, branch.asLoc)
|
|
481
|
+
builder.raw(' = undefined as any;\n')
|
|
452
482
|
}
|
|
453
483
|
emitNodes(branch.children, builder)
|
|
454
484
|
builder.raw('}\n')
|
|
@@ -503,7 +533,14 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
503
533
|
builder.raw('}\n')
|
|
504
534
|
return
|
|
505
535
|
case 'snippet':
|
|
506
|
-
|
|
536
|
+
/* `args={…}` is the parameter list; `mapped` (not `raw`) so hover/highlighting
|
|
537
|
+
land on the binding. `loc` is the `args` expression's offset (see
|
|
538
|
+
toSnippetOrTemplate). The name is a static attribute with no tracked offset. */
|
|
539
|
+
builder.raw(`const ${node.name} = (`)
|
|
540
|
+
if (node.params !== undefined) {
|
|
541
|
+
builder.mapped(node.params, node.loc)
|
|
542
|
+
}
|
|
543
|
+
builder.raw(') => {\n')
|
|
507
544
|
emitNodes(node.children, builder)
|
|
508
545
|
builder.raw('};\n')
|
|
509
546
|
return
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import ts from 'typescript'
|
|
3
3
|
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
4
|
+
import { mapTsClassification } from './ABIDE_SEMANTIC_TOKENS_LEGEND.ts'
|
|
4
5
|
import { assetModulesFile } from './assetModulesFile.ts'
|
|
5
6
|
import { compileShadow } from './compileShadow.ts'
|
|
6
7
|
import { loadShadowTsConfig } from './loadShadowTsConfig.ts'
|
|
@@ -10,6 +11,7 @@ import { shadowNaming } from './shadowNaming.ts'
|
|
|
10
11
|
import { sourceToShadowOffset } from './sourceToShadowOffset.ts'
|
|
11
12
|
import type { AbideDiagnostic } from './types/AbideDiagnostic.ts'
|
|
12
13
|
import type { CompiledShadow } from './types/CompiledShadow.ts'
|
|
14
|
+
import type { SemanticToken } from './types/SemanticToken.ts'
|
|
13
15
|
|
|
14
16
|
const { suffixed, isShadow, sourceOf } = shadowNaming
|
|
15
17
|
|
|
@@ -27,6 +29,8 @@ export type ShadowLanguageService = {
|
|
|
27
29
|
/* Hover info at a source offset, or undefined if the offset isn't a checked
|
|
28
30
|
expression (markup, whitespace) or TypeScript has nothing to report. */
|
|
29
31
|
quickInfo: (abidePath: string, sourceOffset: number) => ShadowQuickInfo | undefined
|
|
32
|
+
/* Type-aware semantic tokens for every checked expression, in source coords. */
|
|
33
|
+
semanticClassifications: (abidePath: string) => SemanticToken[]
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/*
|
|
@@ -194,5 +198,49 @@ export function createShadowLanguageService(cwd: string): ShadowLanguageService
|
|
|
194
198
|
length: span?.length ?? 1,
|
|
195
199
|
}
|
|
196
200
|
},
|
|
201
|
+
semanticClassifications(abidePath) {
|
|
202
|
+
const fileName = suffixed(abidePath)
|
|
203
|
+
/* Compile first so the mappings cache is current. */
|
|
204
|
+
shadowText(abidePath)
|
|
205
|
+
const shadow = shadows.get(abidePath)
|
|
206
|
+
if (shadow === undefined) {
|
|
207
|
+
return []
|
|
208
|
+
}
|
|
209
|
+
const { spans } = service.getEncodedSemanticClassifications(
|
|
210
|
+
fileName,
|
|
211
|
+
{ start: 0, length: shadow.code.length },
|
|
212
|
+
ts.SemanticClassificationFormat.TwentyTwenty,
|
|
213
|
+
)
|
|
214
|
+
/* `spans` is flat triples [start, length, classification, …] in shadow
|
|
215
|
+
coords; keep only those overlapping a mapped expression segment. */
|
|
216
|
+
const tokens: SemanticToken[] = []
|
|
217
|
+
for (let index = 0; index + 2 < spans.length; index += 3) {
|
|
218
|
+
const spanStart = spans[index]
|
|
219
|
+
const spanLength = spans[index + 1]
|
|
220
|
+
const classification = spans[index + 2]
|
|
221
|
+
if (
|
|
222
|
+
spanStart === undefined ||
|
|
223
|
+
spanLength === undefined ||
|
|
224
|
+
classification === undefined
|
|
225
|
+
) {
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
const mapped = mapTsClassification(classification)
|
|
229
|
+
if (mapped === undefined) {
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
const located = remapShadowDiagnostic(shadow.mappings, spanStart, spanLength)
|
|
233
|
+
if (located === undefined) {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
tokens.push({
|
|
237
|
+
start: located.start,
|
|
238
|
+
length: located.length,
|
|
239
|
+
type: mapped.type,
|
|
240
|
+
modifiers: mapped.modifiers,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
return tokens
|
|
244
|
+
},
|
|
197
245
|
}
|
|
198
246
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ABIDE_SEMANTIC_TOKENS_LEGEND } from './ABIDE_SEMANTIC_TOKENS_LEGEND.ts'
|
|
2
|
+
import { offsetToPosition } from './offsetToPosition.ts'
|
|
3
|
+
import type { SemanticToken } from './types/SemanticToken.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Encodes source-coordinate tokens into the LSP semantic-tokens `data` array: five
|
|
7
|
+
integers per token (deltaLine, deltaStartChar, length, tokenTypeIndex,
|
|
8
|
+
modifierBitset), each position relative to the previous token. Tokens are sorted
|
|
9
|
+
by start; tokens with an unknown legend type, and any token overlapping the one
|
|
10
|
+
before it, are dropped — the protocol requires a strictly non-overlapping,
|
|
11
|
+
positionally-ordered stream.
|
|
12
|
+
*/
|
|
13
|
+
export function encodeSemanticTokens(text: string, tokens: SemanticToken[]): number[] {
|
|
14
|
+
const sorted = [...tokens].sort((a, b) => a.start - b.start || a.length - b.length)
|
|
15
|
+
const data: number[] = []
|
|
16
|
+
let previousLine = 0
|
|
17
|
+
let previousCharacter = 0
|
|
18
|
+
let previousEnd = -1
|
|
19
|
+
for (const token of sorted) {
|
|
20
|
+
const typeIndex = ABIDE_SEMANTIC_TOKENS_LEGEND.tokenTypes.indexOf(token.type)
|
|
21
|
+
if (typeIndex < 0 || token.start < previousEnd) {
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
const modifierBitset = token.modifiers.reduce((bits, name) => {
|
|
25
|
+
const bit = ABIDE_SEMANTIC_TOKENS_LEGEND.tokenModifiers.indexOf(name)
|
|
26
|
+
return bit < 0 ? bits : bits | (1 << bit)
|
|
27
|
+
}, 0)
|
|
28
|
+
const { line, character } = offsetToPosition(text, token.start)
|
|
29
|
+
const deltaLine = line - previousLine
|
|
30
|
+
const deltaCharacter = deltaLine === 0 ? character - previousCharacter : character
|
|
31
|
+
data.push(deltaLine, deltaCharacter, token.length, typeIndex, modifierBitset)
|
|
32
|
+
previousLine = line
|
|
33
|
+
previousCharacter = character
|
|
34
|
+
previousEnd = token.start + token.length
|
|
35
|
+
}
|
|
36
|
+
return data
|
|
37
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assertExhaustive } from '../../shared/assertExhaustive.ts'
|
|
2
2
|
import { HOLE_ATTRIBUTE } from '../runtime/HOLE_ATTRIBUTE.ts'
|
|
3
3
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
4
|
+
import { ANCHOR } from '../runtime/RANGE_MARKER.ts'
|
|
4
5
|
import { asOutlet } from './asOutlet.ts'
|
|
5
6
|
import { bindListenEvent } from './bindListenEvent.ts'
|
|
6
7
|
import { composeProps } from './composeProps.ts'
|
|
@@ -9,6 +10,7 @@ import { groupBindParts } from './groupBindParts.ts'
|
|
|
9
10
|
import { isControlFlow } from './isControlFlow.ts'
|
|
10
11
|
import { isWhitespaceText } from './isWhitespaceText.ts'
|
|
11
12
|
import { lowerContext } from './lowerContext.ts'
|
|
13
|
+
import { makeVarNamer } from './makeVarNamer.ts'
|
|
12
14
|
import { resolveBranches } from './resolveBranches.ts'
|
|
13
15
|
import { scopeAttr } from './scopeAttr.ts'
|
|
14
16
|
import { skeletonContext } from './skeletonContext.ts'
|
|
@@ -18,6 +20,12 @@ import { staticTextPart } from './staticTextPart.ts'
|
|
|
18
20
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
19
21
|
import { VOID_TAGS } from './VOID_TAGS.ts'
|
|
20
22
|
|
|
23
|
+
/* The skeleton positioning anchor a control-flow block / component / slot / outlet
|
|
24
|
+
stamps into its skeleton markup, sourced from the same `ANCHOR` wire-alphabet constant
|
|
25
|
+
the client's anchor scan (`skeleton`) matches — so the markup the build clones and the
|
|
26
|
+
markup the scan reads can never drift on a literal. */
|
|
27
|
+
const ANCHOR_COMMENT = `<!--${ANCHOR}-->`
|
|
28
|
+
|
|
21
29
|
/*
|
|
22
30
|
Generates the build statements for a parsed template: element creation, static
|
|
23
31
|
attributes, reactive `attr`/`text` bindings, `on` listeners, keyed `each`, and
|
|
@@ -54,8 +62,7 @@ export function generateBuild(
|
|
|
54
62
|
computedNames: ReadonlySet<string>,
|
|
55
63
|
isLayout = false,
|
|
56
64
|
): string {
|
|
57
|
-
|
|
58
|
-
const nextVar = (prefix: string): string => `${prefix}${counter++}`
|
|
65
|
+
const nextVar = makeVarNamer()
|
|
59
66
|
|
|
60
67
|
/* In a layout, `<slot/>` outlets are rewritten to `OUTLET_TAG` elements up front
|
|
61
68
|
(`asOutlet`) so the static-clone path carries them as ordinary structure. `asOutlet`
|
|
@@ -213,7 +220,7 @@ export function generateBuild(
|
|
|
213
220
|
binds.push(
|
|
214
221
|
`appendTextAt(${skVar}.an[${holeIndex(anIndex, part)}], ${namedThunk('text', `return (${lowerExpression(part.code)})`)});\n`,
|
|
215
222
|
)
|
|
216
|
-
return
|
|
223
|
+
return ANCHOR_COMMENT
|
|
217
224
|
})
|
|
218
225
|
.join('')
|
|
219
226
|
}
|
|
@@ -226,7 +233,7 @@ export function generateBuild(
|
|
|
226
233
|
element — so its root lays out as a true direct child of `anchor.parentNode`. */
|
|
227
234
|
const anchorVar = anchorVarAt(node, skVar, binds)
|
|
228
235
|
binds.push(generateChild(node, `${anchorVar}.parentNode`, `anchorCursor(${anchorVar})`))
|
|
229
|
-
return
|
|
236
|
+
return ANCHOR_COMMENT
|
|
230
237
|
}
|
|
231
238
|
if (node.kind === 'script') {
|
|
232
239
|
/* A nested `<script>` (scoped reactive block) emits no markup — its lowered body
|
|
@@ -251,7 +258,7 @@ export function generateBuild(
|
|
|
251
258
|
No wrapper element — the filled child lays out as a direct child of the parent. */
|
|
252
259
|
const anchorVar = anchorVarAt(node, skVar, binds)
|
|
253
260
|
binds.push(`outlet(${anchorVar}.parentNode, anchorCursor(${anchorVar}));\n`)
|
|
254
|
-
return
|
|
261
|
+
return ANCHOR_COMMENT
|
|
255
262
|
}
|
|
256
263
|
if (node.tag === 'slot') {
|
|
257
264
|
/* A `<slot>` outlet at its position: an `<!--a-->` anchor, the slot's content
|
|
@@ -261,7 +268,7 @@ export function generateBuild(
|
|
|
261
268
|
binds.push(
|
|
262
269
|
`mountSlot(${anchorVar}.parentNode, (${hostVar}) => {\n${generateSlot(node, hostVar)}}, anchorCursor(${anchorVar}));\n`,
|
|
263
270
|
)
|
|
264
|
-
return
|
|
271
|
+
return ANCHOR_COMMENT
|
|
265
272
|
}
|
|
266
273
|
const hasReactiveAttr = node.attrs.some((attr) => attr.kind !== 'static')
|
|
267
274
|
const reactiveTextChild = node.children.find(
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { assertExhaustive } from '../../shared/assertExhaustive.ts'
|
|
2
|
+
import { escapeRegex } from '../../shared/escapeRegex.ts'
|
|
2
3
|
import { OUTLET_CLOSE, OUTLET_OPEN } from '../runtime/OUTLET_MARKER.ts'
|
|
3
4
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
5
|
+
import {
|
|
6
|
+
ANCHOR,
|
|
7
|
+
RANGE_CLOSE as RANGE_CLOSE_DATA,
|
|
8
|
+
RANGE_OPEN as RANGE_OPEN_DATA,
|
|
9
|
+
} from '../runtime/RANGE_MARKER.ts'
|
|
4
10
|
import { asOutlet } from './asOutlet.ts'
|
|
5
11
|
import { composeProps } from './composeProps.ts'
|
|
6
12
|
import { groupBindParts } from './groupBindParts.ts'
|
|
7
13
|
import { isAnchorPositioned } from './isAnchorPositioned.ts'
|
|
8
14
|
import { lowerContext } from './lowerContext.ts'
|
|
15
|
+
import { makeVarNamer } from './makeVarNamer.ts'
|
|
9
16
|
import { resolveBranches } from './resolveBranches.ts'
|
|
10
17
|
import { scopeAttr } from './scopeAttr.ts'
|
|
11
18
|
import { skeletonContext } from './skeletonContext.ts'
|
|
@@ -16,12 +23,16 @@ import { stripEffects } from './stripEffects.ts'
|
|
|
16
23
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
17
24
|
import { VOID_TAGS } from './VOID_TAGS.ts'
|
|
18
25
|
|
|
19
|
-
/* The range boundary comments a control-flow block emits around its content.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const RANGE_OPEN =
|
|
24
|
-
const RANGE_CLOSE =
|
|
26
|
+
/* The range boundary comments a control-flow block emits around its content. Sourced
|
|
27
|
+
from the SAME wire-alphabet constants the client's `document.createComment` markers
|
|
28
|
+
use (`RANGE_MARKER`), wrapped in comment syntax — so a server-emitted boundary and the
|
|
29
|
+
client `[ … ]` boundary it claims can never drift on a literal. */
|
|
30
|
+
const RANGE_OPEN = `<!--${RANGE_OPEN_DATA}-->`
|
|
31
|
+
const RANGE_CLOSE = `<!--${RANGE_CLOSE_DATA}-->`
|
|
32
|
+
|
|
33
|
+
/* The skeleton positioning anchor a control-flow block / slot / outlet emits, sourced
|
|
34
|
+
from the same `ANCHOR` constant the client's anchor scan matches. */
|
|
35
|
+
const ANCHOR_COMMENT = `<!--${ANCHOR}-->`
|
|
25
36
|
|
|
26
37
|
/*
|
|
27
38
|
Server code generator: turns the parsed template into statements that push HTML
|
|
@@ -46,10 +57,9 @@ export function generateSSR(
|
|
|
46
57
|
computedNames: ReadonlySet<string>,
|
|
47
58
|
isLayout = false,
|
|
48
59
|
): string {
|
|
49
|
-
/*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const nextVar = (prefix: string): string => `${prefix}${varCounter++}`
|
|
60
|
+
/* Unique temp var names (child render results); runtime block ids are
|
|
61
|
+
allocated separately at runtime via `$ctx.next++`. */
|
|
62
|
+
const nextVar = makeVarNamer()
|
|
53
63
|
|
|
54
64
|
/* The shared signal→`model` lowering + branch-scoped nested-script deref scope. */
|
|
55
65
|
const {
|
|
@@ -81,13 +91,12 @@ export function generateSSR(
|
|
|
81
91
|
regex metacharacters escaped, or e.g. a trailing `$` would read as an end-anchor and the
|
|
82
92
|
call site would never match — leaving an un-awaited Promise stringified as `[object
|
|
83
93
|
Promise]`. */
|
|
84
|
-
const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
85
94
|
/* A leading boundary that, unlike `\b`, also fires before a `$`-leading name: `\b$row`
|
|
86
95
|
never matches (`$` is a non-word char, so there is no word boundary before it), which
|
|
87
96
|
would silently miss every `$row(...)` call. A negative lookbehind for word-or-`$`
|
|
88
97
|
matches the same call sites as `\b` for word-leading names while also catching them. */
|
|
89
98
|
const callPattern = (name: string): RegExp =>
|
|
90
|
-
new RegExp(`(?<![$\\w])${
|
|
99
|
+
new RegExp(`(?<![$\\w])${escapeRegex(name)}\\s*\\(`)
|
|
91
100
|
/* A subtree call to any of `names` from a TEXT interpolation (`name()` / `name(args)`). */
|
|
92
101
|
const subtreeCalls = (children: TemplateNode[], names: ReadonlySet<string>): boolean =>
|
|
93
102
|
children.some((child) => {
|
|
@@ -172,7 +181,7 @@ export function generateSSR(
|
|
|
172
181
|
Outside a skeleton (top-level / inside a branch) blocks mount on the host directly,
|
|
173
182
|
so no anchor. */
|
|
174
183
|
const anchorMark = (node: TemplateNode, target: string): string =>
|
|
175
|
-
inSkeleton.get(node) ? push(target,
|
|
184
|
+
inSkeleton.get(node) ? push(target, ANCHOR_COMMENT) : ''
|
|
176
185
|
|
|
177
186
|
function generate(node: TemplateNode, target: string): string {
|
|
178
187
|
/* Every kind that mounts as a marker range is positioned by an `<!--a-->` anchor when
|
|
@@ -197,7 +206,7 @@ export function generateSSR(
|
|
|
197
206
|
? `$text(await (${lowered}))`
|
|
198
207
|
: `$text(${lowered})`
|
|
199
208
|
return markText.get(node)
|
|
200
|
-
? `${target}.push('
|
|
209
|
+
? `${target}.push('${ANCHOR_COMMENT}' + ${value});\n`
|
|
201
210
|
: `${target}.push(${value});\n`
|
|
202
211
|
})
|
|
203
212
|
.join('')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Compile-time generator of unique temp var names: each call appends a fresh
|
|
3
|
+
incrementing suffix to `prefix` (`row` → `row0`, `row1`, …). Shared by the
|
|
4
|
+
generateSSR / generateBuild codegen passes so the two hand-mirrored traversals
|
|
5
|
+
allocate names one way. Runtime block ids stay separate (`$ctx.next++`).
|
|
6
|
+
*/
|
|
7
|
+
export function makeVarNamer(): (prefix: string) => string {
|
|
8
|
+
let counter = 0
|
|
9
|
+
return (prefix: string): string => `${prefix}${counter++}`
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* An absolute offset → LSP `{ line, character }` (0-based, UTF-16 code units). */
|
|
2
|
+
export function offsetToPosition(
|
|
3
|
+
text: string,
|
|
4
|
+
offset: number,
|
|
5
|
+
): { line: number; character: number } {
|
|
6
|
+
const before = text.slice(0, offset)
|
|
7
|
+
const line = before.split('\n').length - 1
|
|
8
|
+
return { line, character: offset - (before.lastIndexOf('\n') + 1) }
|
|
9
|
+
}
|