@abide/abide 0.41.1 → 0.43.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 +234 -294
- package/CHANGELOG.md +82 -0
- package/README.md +51 -95
- package/package.json +3 -2
- package/src/abideLsp.ts +46 -8
- package/src/abideResolverPlugin.ts +107 -19
- package/src/buildCli.ts +1 -1
- package/src/discoveryEntry.ts +3 -3
- package/src/lib/bundle/BundleMenuItem.ts +1 -1
- package/src/lib/cli/createClient.ts +12 -12
- package/src/lib/cli/dispatchCommand.ts +1 -1
- package/src/lib/cli/printSessionStatus.ts +1 -1
- package/src/lib/cli/resolveCliTarget.ts +1 -1
- package/src/lib/cli/runCli.ts +1 -1
- package/src/lib/cli/types/CliManifest.ts +1 -1
- package/src/lib/cli/types/CliManifestEntry.ts +2 -2
- package/src/lib/mcp/annotationsForMethod.ts +5 -5
- package/src/lib/mcp/createMcpServer.ts +3 -3
- package/src/lib/mcp/mcpSurface.ts +14 -14
- package/src/lib/mcp/types/McpServerOptions.ts +1 -1
- package/src/lib/server/DELETE.ts +4 -4
- package/src/lib/server/GET.ts +4 -4
- package/src/lib/server/HEAD.ts +4 -4
- package/src/lib/server/PATCH.ts +4 -4
- package/src/lib/server/POST.ts +4 -4
- package/src/lib/server/PUT.ts +4 -4
- package/src/lib/server/agent.ts +4 -4
- package/src/lib/server/env.ts +1 -1
- package/src/lib/server/error.ts +49 -7
- package/src/lib/server/json.ts +1 -1
- package/src/lib/server/rpc/buildErrorConstructors.ts +19 -0
- package/src/lib/server/rpc/{defineVerb.ts → defineRpc.ts} +52 -34
- package/src/lib/server/rpc/{dispatchVerbInProcess.ts → dispatchRpcInProcess.ts} +3 -3
- package/src/lib/server/rpc/fieldErrorsFromIssues.ts +23 -0
- package/src/lib/server/rpc/{findVerbByCommandName.ts → findRpcByCommandName.ts} +5 -5
- package/src/lib/server/rpc/parseArgs.ts +6 -6
- package/src/lib/server/rpc/readBodyWithinLimit.ts +2 -2
- package/src/lib/server/rpc/registerRpc.ts +6 -0
- package/src/lib/server/rpc/{verbRegistry.ts → rpcRegistry.ts} +4 -4
- package/src/lib/server/rpc/{runWithVerbTimeout.ts → runWithRpcTimeout.ts} +4 -4
- package/src/lib/server/rpc/types/RemoteHandler.ts +9 -3
- package/src/lib/server/rpc/types/{VerbHelper.ts → RpcHelper.ts} +57 -23
- package/src/lib/server/rpc/types/{VerbRegistryEntry.ts → RpcRegistryEntry.ts} +3 -3
- package/src/lib/server/rpc/types/TypedResponse.ts +2 -2
- package/src/lib/server/rpc/unprocessed.ts +6 -7
- package/src/lib/server/rpc/validationError.ts +17 -0
- package/src/lib/server/runtime/buildInspectorSurface.ts +7 -7
- package/src/lib/server/runtime/buildOpenApiSpec.ts +6 -6
- package/src/lib/server/runtime/createRouteDispatcher.ts +7 -7
- package/src/lib/server/runtime/createServer.ts +4 -4
- package/src/lib/server/runtime/crossOriginForbidden.ts +1 -1
- package/src/lib/server/runtime/crossOriginGate.ts +4 -4
- package/src/lib/server/runtime/devClientFingerprint.ts +2 -1
- package/src/lib/server/runtime/logExposedSurfaces.ts +4 -4
- package/src/lib/server/runtime/registryManifests.ts +2 -2
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/InspectorRpc.ts +27 -0
- package/src/lib/server/runtime/types/InspectorSurface.ts +4 -4
- package/src/lib/server/runtime/types/RequestStore.ts +1 -1
- package/src/lib/server/runtime/warnUnguardedMcp.ts +2 -2
- package/src/lib/server/sockets/createSocketDispatcher.ts +15 -14
- package/src/lib/server/sockets/types/SocketOperation.ts +2 -2
- package/src/lib/shared/HttpError.ts +15 -1
- package/src/lib/shared/UNREACHABLE_STATUSES.ts +13 -0
- package/src/lib/shared/buildRpcRequest.ts +5 -5
- package/src/lib/shared/cache.ts +50 -168
- package/src/lib/shared/carriesBodyArgs.ts +4 -4
- package/src/lib/shared/createCacheStore.ts +12 -0
- package/src/lib/shared/createRemoteFunction.ts +11 -6
- package/src/lib/shared/decodeResponse.ts +2 -2
- package/src/lib/shared/detectRpcMethod.ts +17 -0
- package/src/lib/shared/emitLogRecord.ts +3 -3
- package/src/lib/shared/escapeRegex.ts +9 -0
- package/src/lib/shared/extraForwardHeaders.ts +1 -1
- package/src/lib/shared/fileName.ts +9 -0
- package/src/lib/shared/fileStem.ts +3 -1
- package/src/lib/shared/findExportCallSite.ts +30 -273
- package/src/lib/shared/forwardHeaders.ts +2 -2
- package/src/lib/shared/httpErrorFor.ts +26 -0
- package/src/lib/shared/isAsciiWhitespace.ts +5 -0
- package/src/lib/shared/isIdentPart.ts +9 -0
- package/src/lib/shared/isIdentStart.ts +8 -0
- package/src/lib/shared/isReadOnlyMethod.ts +4 -4
- package/src/lib/shared/keyForRemoteCall.ts +4 -4
- package/src/lib/shared/log.ts +1 -1
- package/src/lib/shared/outboxProbeSlot.ts +20 -0
- package/src/lib/shared/pending.ts +4 -1
- package/src/lib/shared/prepareRpcModule.ts +96 -14
- package/src/lib/shared/probeRegistries.ts +17 -4
- package/src/lib/shared/programNameForPackage.ts +3 -1
- package/src/lib/shared/queryStringFromArgs.ts +1 -1
- package/src/lib/shared/remoteMetaStore.ts +2 -2
- package/src/lib/shared/resolveClientFlags.ts +1 -1
- package/src/lib/shared/skipNonCode.ts +230 -0
- package/src/lib/shared/streamResponse.ts +9 -6
- package/src/lib/shared/stripImport.ts +6 -4
- package/src/lib/shared/types/CacheEntry.ts +2 -4
- package/src/lib/shared/types/CacheOnContext.ts +1 -11
- package/src/lib/shared/types/CacheStore.ts +13 -7
- package/src/lib/shared/types/ClientFlags.ts +1 -1
- package/src/lib/shared/types/ErrorConstructors.ts +17 -0
- package/src/lib/shared/types/ErrorDescriptor.ts +10 -0
- package/src/lib/shared/types/ErrorSpec.ts +11 -0
- package/src/lib/shared/types/HttpMethod.ts +1 -0
- package/src/lib/shared/types/Outbox.ts +9 -0
- package/src/lib/shared/types/OutboxEntry.ts +27 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +2 -2
- package/src/lib/shared/types/RemoteCallable.ts +2 -2
- package/src/lib/shared/types/RemoteFunction.ts +21 -6
- package/src/lib/shared/types/RequestScopeInfo.ts +1 -1
- package/src/lib/shared/types/RpcErrorGuard.ts +40 -0
- package/src/lib/shared/types/RpcInvoker.ts +1 -1
- package/src/lib/shared/types/StandardSchemaV1.ts +1 -1
- package/src/lib/shared/types/ValidationErrorData.ts +20 -0
- package/src/lib/shared/url.ts +3 -3
- package/src/lib/shared/writeRpcDts.ts +7 -7
- package/src/lib/shared/writeTestRpcDts.ts +3 -3
- package/src/lib/test/createTestApp.ts +10 -10
- 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/awaitPlan.ts +48 -0
- package/src/lib/ui/compile/compileShadow.ts +117 -31
- package/src/lib/ui/compile/createShadowLanguageService.ts +48 -0
- package/src/lib/ui/compile/desugarSignals.ts +42 -6
- package/src/lib/ui/compile/encodeSemanticTokens.ts +37 -0
- package/src/lib/ui/compile/generateBuild.ts +45 -62
- package/src/lib/ui/compile/generateSSR.ts +90 -60
- package/src/lib/ui/compile/ifPlan.ts +30 -0
- 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 +565 -115
- package/src/lib/ui/compile/stripEffects.ts +17 -9
- package/src/lib/ui/compile/structuralBlockTokens.ts +101 -0
- package/src/lib/ui/compile/switchPlan.ts +22 -0
- package/src/lib/ui/compile/tryPlan.ts +29 -0
- package/src/lib/ui/compile/types/SemanticToken.ts +11 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +8 -2
- package/src/lib/ui/compile/types/TemplateNode.ts +15 -2
- package/src/lib/ui/createScope.ts +20 -2
- package/src/lib/ui/dom/appendText.ts +1 -5
- package/src/lib/ui/dom/applyResolved.ts +1 -5
- package/src/lib/ui/dom/disposeRange.ts +6 -10
- package/src/lib/ui/dom/hydrate.ts +2 -5
- package/src/lib/ui/dom/isComment.ts +6 -0
- package/src/lib/ui/dom/mount.ts +1 -2
- package/src/lib/ui/dom/withScope.ts +6 -9
- package/src/lib/ui/effect.ts +4 -2
- package/src/lib/ui/navigate.ts +40 -13
- package/src/lib/ui/outbox.ts +30 -110
- package/src/lib/ui/remoteProxy.ts +160 -10
- package/src/lib/ui/router.ts +3 -3
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +281 -0
- package/src/lib/ui/rpcOutbox/outboxRegistry.ts +69 -0
- package/src/lib/ui/runtime/createDoc.ts +3 -3
- package/src/lib/ui/runtime/pathSegments.ts +10 -0
- package/src/lib/ui/socketChannel.ts +7 -1
- package/src/lib/ui/types/Scope.ts +20 -5
- package/template/.zed/settings.json +4 -0
- package/template/CLAUDE.md +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- package/template/src/ui/pages/page.abide +4 -4
- package/template/test/app.test.ts +2 -2
- package/src/lib/server/rpc/registerVerb.ts +0 -6
- package/src/lib/server/runtime/types/InspectorVerb.ts +0 -27
- package/src/lib/shared/detectVerbMethod.ts +0 -17
- package/src/lib/shared/types/HttpVerb.ts +0 -1
- package/src/lib/ui/types/Outbox.ts +0 -14
|
@@ -90,11 +90,323 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
90
90
|
return { kind: 'style', css }
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/* True when the cursor sits on a block-directive open `{#`. A `{:`/`{/` is only
|
|
94
|
+
valid INSIDE a block (consumed by readBlockChildren), so at top level it would be
|
|
95
|
+
a stray-branch error — handled where blocks are read, not here. */
|
|
96
|
+
function atBlock(): boolean {
|
|
97
|
+
return source.charAt(cursor) === '{' && source.charAt(cursor + 1) === '#'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Reads a `{#…}` / `{:…}` / `{/…}` token starting on `{`. Tracks string literals
|
|
101
|
+
and nested braces (same scan as readBracedExpression) so a brace/quote inside an
|
|
102
|
+
expression (`{#if obj.has('}')}`) doesn't end the token early. Returns the sigil,
|
|
103
|
+
the trimmed directive body WITHOUT the sigil, and the absolute loc of the body's
|
|
104
|
+
first post-trim char. */
|
|
105
|
+
function readBlockToken(): { sigil: '#' | ':' | '/'; body: string; loc: number } {
|
|
106
|
+
const sigil = source.charAt(cursor + 1) as '#' | ':' | '/'
|
|
107
|
+
cursor += 2 // past `{` and the sigil
|
|
108
|
+
const start = cursor
|
|
109
|
+
let depth = 1
|
|
110
|
+
while (cursor < source.length && depth > 0) {
|
|
111
|
+
const char = source.charAt(cursor)
|
|
112
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
113
|
+
cursor += 1
|
|
114
|
+
while (cursor < source.length && source.charAt(cursor) !== char) {
|
|
115
|
+
if (source.charAt(cursor) === '\\') {
|
|
116
|
+
cursor += 1
|
|
117
|
+
}
|
|
118
|
+
cursor += 1
|
|
119
|
+
}
|
|
120
|
+
} else if (char === '{') {
|
|
121
|
+
depth += 1
|
|
122
|
+
} else if (char === '}') {
|
|
123
|
+
depth -= 1
|
|
124
|
+
}
|
|
125
|
+
cursor += 1
|
|
126
|
+
}
|
|
127
|
+
const raw = source.slice(start, cursor - 1)
|
|
128
|
+
const leading = raw.length - raw.trimStart().length
|
|
129
|
+
return { sigil, body: raw.trim(), loc: baseOffset + start + leading }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* The leading keyword of a directive body (`if`, `for`, `await`, `switch`, `try`,
|
|
133
|
+
`else`, `then`, `catch`, `finally`, `case`, `default`). */
|
|
134
|
+
function headKeyword(body: string): string {
|
|
135
|
+
const match = body.match(/^\s*(\S+)/)
|
|
136
|
+
return match?.[1] ?? ''
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Reads a `{#…}` control block: the open token, its children up to a continuation
|
|
140
|
+
`{:…}` (a branch) or close `{/…}`, recursing. Emits the same nodes toControlFlow
|
|
141
|
+
does today (if/each/await/switch/try + case/branch children). */
|
|
142
|
+
function readBlock(): TemplateNode {
|
|
143
|
+
const open = readBlockToken() // sigil is '#'
|
|
144
|
+
const keyword = headKeyword(open.body)
|
|
145
|
+
if (keyword === 'if') {
|
|
146
|
+
const start = open.body.indexOf('if') + 2
|
|
147
|
+
const condition = open.body.slice(start).trim()
|
|
148
|
+
if (condition === '') {
|
|
149
|
+
throw new Error('[abide] {#if} requires a condition expression')
|
|
150
|
+
}
|
|
151
|
+
const children = readBlockChildren('if')
|
|
152
|
+
return { kind: 'if', condition, children, loc: exprLoc(open.loc, open.body, start) }
|
|
153
|
+
}
|
|
154
|
+
if (keyword === 'for') {
|
|
155
|
+
const head = parseForHead(open.body, open.loc)
|
|
156
|
+
const children = readBlockChildren('for')
|
|
157
|
+
return {
|
|
158
|
+
kind: 'each',
|
|
159
|
+
items: head.items,
|
|
160
|
+
as: head.as,
|
|
161
|
+
index: head.index,
|
|
162
|
+
key: head.key,
|
|
163
|
+
async: head.async,
|
|
164
|
+
children,
|
|
165
|
+
loc: head.loc,
|
|
166
|
+
asLoc: head.asLoc,
|
|
167
|
+
keyLoc: head.keyLoc,
|
|
168
|
+
indexLoc: head.indexLoc,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (keyword === 'await') {
|
|
172
|
+
const start = open.body.indexOf('await') + 5
|
|
173
|
+
const afterAwait = open.body.slice(start)
|
|
174
|
+
const thenAt = keywordAtDepthZero(afterAwait, 'then')
|
|
175
|
+
const promise = (thenAt === -1 ? afterAwait : afterAwait.slice(0, thenAt)).trim()
|
|
176
|
+
if (promise === '') {
|
|
177
|
+
throw new Error('[abide] {#await} requires a promise expression')
|
|
178
|
+
}
|
|
179
|
+
const as =
|
|
180
|
+
thenAt === -1
|
|
181
|
+
? undefined
|
|
182
|
+
: afterAwait.slice(thenAt + 'then'.length).trim() || undefined
|
|
183
|
+
const children = readBlockChildren('await')
|
|
184
|
+
return {
|
|
185
|
+
kind: 'await',
|
|
186
|
+
promise,
|
|
187
|
+
blocking: thenAt !== -1,
|
|
188
|
+
as,
|
|
189
|
+
children,
|
|
190
|
+
loc: exprLoc(open.loc, open.body, start),
|
|
191
|
+
asLoc:
|
|
192
|
+
as === undefined
|
|
193
|
+
? undefined
|
|
194
|
+
: exprLoc(open.loc, open.body, start + thenAt + 'then'.length),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (keyword === 'switch') {
|
|
198
|
+
const start = open.body.indexOf('switch') + 6
|
|
199
|
+
const subject = open.body.slice(start).trim()
|
|
200
|
+
if (subject === '') {
|
|
201
|
+
throw new Error('[abide] {#switch} requires a subject expression')
|
|
202
|
+
}
|
|
203
|
+
const children = readBlockChildren('switch')
|
|
204
|
+
return { kind: 'switch', subject, children, loc: exprLoc(open.loc, open.body, start) }
|
|
205
|
+
}
|
|
206
|
+
if (keyword === 'try') {
|
|
207
|
+
const children = readBlockChildren('try')
|
|
208
|
+
return { kind: 'try', children }
|
|
209
|
+
}
|
|
210
|
+
throw new Error(
|
|
211
|
+
`[abide] unknown control block {#${keyword}} — expected if/for/await/switch/try`,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Shared scan body for block/branch child loops. Reads one node at the current
|
|
216
|
+
cursor position and pushes it onto `nodes`. Returns false when the caller's
|
|
217
|
+
terminator fires (caller decides what to do next); returns true while scanning
|
|
218
|
+
continues. `onBranch` fires when `{:` is seen — block-children consume it as a
|
|
219
|
+
new branch node; branch-children let the caller exit instead. */
|
|
220
|
+
function scanNode(nodes: TemplateNode[], onBranch: (() => boolean) | undefined): boolean {
|
|
221
|
+
/* Branch/close tokens — delegate to the caller's terminator logic. */
|
|
222
|
+
if (source.startsWith('{/', cursor)) {
|
|
223
|
+
return false
|
|
224
|
+
}
|
|
225
|
+
if (source.startsWith('{:', cursor)) {
|
|
226
|
+
return onBranch === undefined ? false : onBranch()
|
|
227
|
+
}
|
|
228
|
+
if (atBlock()) {
|
|
229
|
+
nodes.push(readBlock())
|
|
230
|
+
} else if (source.startsWith('<!--', cursor)) {
|
|
231
|
+
skipComment()
|
|
232
|
+
} else if (atStyleTag()) {
|
|
233
|
+
nodes.push(readStyle())
|
|
234
|
+
} else if (source.charAt(cursor) === '<') {
|
|
235
|
+
nodes.push(readElement())
|
|
236
|
+
} else {
|
|
237
|
+
nodes.push(readText())
|
|
238
|
+
}
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* Reads children of a block until its close `{/<keyword>}`. A continuation token
|
|
243
|
+
`{:…}` ends the current branch's children and starts a new `case`/`branch` node
|
|
244
|
+
(per construct). The leading children (before the first `{:…}`) are the block's
|
|
245
|
+
own children (the `if`/`await`/`try` then-content). Returns the full children list
|
|
246
|
+
INCLUDING the case/branch nodes, matching toControlFlow's output. */
|
|
247
|
+
function readBlockChildren(keyword: string): TemplateNode[] {
|
|
248
|
+
const nodes: TemplateNode[] = []
|
|
249
|
+
while (cursor < source.length) {
|
|
250
|
+
const keepGoing = scanNode(nodes, () => {
|
|
251
|
+
nodes.push(readBranch(keyword))
|
|
252
|
+
return true
|
|
253
|
+
})
|
|
254
|
+
if (!keepGoing) {
|
|
255
|
+
const close = readBlockToken() // consume the close `{/keyword}`
|
|
256
|
+
const closeKeyword = headKeyword(close.body)
|
|
257
|
+
/* The close must name its open block — a mismatch (`{#if}…{/for}`) or
|
|
258
|
+
crossed nesting (`{#if}{#for}…{/if}{/for}`) would otherwise silently
|
|
259
|
+
mis-parse into a structurally wrong tree. */
|
|
260
|
+
if (closeKeyword !== keyword) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`[abide] {/${closeKeyword}} does not close the open {#${keyword}} — expected {/${keyword}}`,
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
return nodes
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
throw new Error(`[abide] unterminated {#${keyword}} block — missing {/${keyword}}`)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* Reads a continuation token `{:…}` and the children up to the NEXT continuation or
|
|
272
|
+
close, returning the branch node for the parent construct. Handles `if`-chain
|
|
273
|
+
branches (else / else if) and `for await` catch branches. */
|
|
274
|
+
function readBranch(parentKeyword: string): TemplateNode {
|
|
275
|
+
const token = readBlockToken() // sigil ':'
|
|
276
|
+
const keyword = headKeyword(token.body)
|
|
277
|
+
const branchChildren = readBranchChildren()
|
|
278
|
+
if (parentKeyword === 'if') {
|
|
279
|
+
if (keyword === 'else' && headKeyword(token.body.slice(4).trim()) === 'if') {
|
|
280
|
+
const start = token.body.indexOf('if') + 2
|
|
281
|
+
const condition = token.body.slice(start).trim()
|
|
282
|
+
if (condition === '') {
|
|
283
|
+
throw new Error('[abide] {:else if} requires a condition expression')
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
kind: 'case',
|
|
287
|
+
match: undefined,
|
|
288
|
+
condition,
|
|
289
|
+
children: branchChildren,
|
|
290
|
+
loc: exprLoc(token.loc, token.body, start),
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (keyword === 'else') {
|
|
294
|
+
return { kind: 'case', match: undefined, children: branchChildren }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (parentKeyword === 'for' && keyword === 'catch') {
|
|
298
|
+
const as = token.body.slice(token.body.indexOf('catch') + 5).trim() || undefined
|
|
299
|
+
return {
|
|
300
|
+
kind: 'branch',
|
|
301
|
+
branch: 'catch',
|
|
302
|
+
as,
|
|
303
|
+
children: branchChildren,
|
|
304
|
+
asLoc:
|
|
305
|
+
as === undefined
|
|
306
|
+
? undefined
|
|
307
|
+
: exprLoc(token.loc, token.body, token.body.indexOf('catch') + 5),
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (parentKeyword === 'switch') {
|
|
311
|
+
if (keyword === 'case') {
|
|
312
|
+
const start = token.body.indexOf('case') + 4
|
|
313
|
+
const match = token.body.slice(start).trim()
|
|
314
|
+
if (match === '') {
|
|
315
|
+
throw new Error('[abide] {:case} requires a value expression')
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
kind: 'case',
|
|
319
|
+
match,
|
|
320
|
+
children: branchChildren,
|
|
321
|
+
loc: exprLoc(token.loc, token.body, start),
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (keyword === 'default') {
|
|
325
|
+
return { kind: 'case', match: undefined, children: branchChildren }
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (parentKeyword === 'await') {
|
|
329
|
+
if (keyword === 'then') {
|
|
330
|
+
const as = token.body.slice(token.body.indexOf('then') + 4).trim() || undefined
|
|
331
|
+
return {
|
|
332
|
+
kind: 'branch',
|
|
333
|
+
branch: 'then',
|
|
334
|
+
as,
|
|
335
|
+
children: branchChildren,
|
|
336
|
+
asLoc:
|
|
337
|
+
as === undefined
|
|
338
|
+
? undefined
|
|
339
|
+
: exprLoc(token.loc, token.body, token.body.indexOf('then') + 4),
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (keyword === 'catch') {
|
|
343
|
+
const as = token.body.slice(token.body.indexOf('catch') + 5).trim() || undefined
|
|
344
|
+
return {
|
|
345
|
+
kind: 'branch',
|
|
346
|
+
branch: 'catch',
|
|
347
|
+
as,
|
|
348
|
+
children: branchChildren,
|
|
349
|
+
asLoc:
|
|
350
|
+
as === undefined
|
|
351
|
+
? undefined
|
|
352
|
+
: exprLoc(token.loc, token.body, token.body.indexOf('catch') + 5),
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (keyword === 'finally') {
|
|
356
|
+
return {
|
|
357
|
+
kind: 'branch',
|
|
358
|
+
branch: 'finally',
|
|
359
|
+
as: undefined,
|
|
360
|
+
children: branchChildren,
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (parentKeyword === 'try') {
|
|
365
|
+
if (keyword === 'catch') {
|
|
366
|
+
const as = token.body.slice(token.body.indexOf('catch') + 5).trim() || undefined
|
|
367
|
+
return {
|
|
368
|
+
kind: 'branch',
|
|
369
|
+
branch: 'catch',
|
|
370
|
+
as,
|
|
371
|
+
children: branchChildren,
|
|
372
|
+
asLoc:
|
|
373
|
+
as === undefined
|
|
374
|
+
? undefined
|
|
375
|
+
: exprLoc(token.loc, token.body, token.body.indexOf('catch') + 5),
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (keyword === 'finally') {
|
|
379
|
+
return {
|
|
380
|
+
kind: 'branch',
|
|
381
|
+
branch: 'finally',
|
|
382
|
+
as: undefined,
|
|
383
|
+
children: branchChildren,
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
throw new Error(`[abide] {:${keyword}} is not valid inside {#${parentKeyword}}`)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* Children of a branch: read until the next `{:…}` or `{/…}` WITHOUT consuming it
|
|
391
|
+
(the caller's readBlockChildren loop handles those). */
|
|
392
|
+
function readBranchChildren(): TemplateNode[] {
|
|
393
|
+
const nodes: TemplateNode[] = []
|
|
394
|
+
/* Pass undefined for onBranch so scanNode returns false on `{:`, leaving it unconsumed. */
|
|
395
|
+
while (cursor < source.length && scanNode(nodes, undefined)) {
|
|
396
|
+
// continue
|
|
397
|
+
}
|
|
398
|
+
return nodes
|
|
399
|
+
}
|
|
400
|
+
|
|
93
401
|
function readText(): TemplateNode {
|
|
94
402
|
const parts: TextPart[] = []
|
|
95
403
|
let literal = ''
|
|
96
404
|
while (cursor < source.length && source.charAt(cursor) !== '<') {
|
|
97
405
|
if (source.charAt(cursor) === '{') {
|
|
406
|
+
const next = source.charAt(cursor + 1)
|
|
407
|
+
if (next === '#' || next === ':' || next === '/') {
|
|
408
|
+
break // a block/continuation/close token — not interpolation
|
|
409
|
+
}
|
|
98
410
|
if (literal !== '') {
|
|
99
411
|
parts.push({ kind: 'static', value: decodeHtmlEntities(literal) })
|
|
100
412
|
literal = ''
|
|
@@ -141,6 +453,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
141
453
|
continue
|
|
142
454
|
}
|
|
143
455
|
let name = ''
|
|
456
|
+
const nameLoc = baseOffset + cursor // absolute offset of the attribute name
|
|
144
457
|
while (cursor < source.length && !/[\s=>/]/.test(source.charAt(cursor))) {
|
|
145
458
|
name += source.charAt(cursor)
|
|
146
459
|
cursor += 1
|
|
@@ -149,7 +462,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
149
462
|
cursor += 1
|
|
150
463
|
}
|
|
151
464
|
if (source.charAt(cursor) !== '=') {
|
|
152
|
-
attrs.push({ kind: 'static', name, value: '', bare: true }) // boolean attribute
|
|
465
|
+
attrs.push({ kind: 'static', name, value: '', bare: true, nameLoc }) // boolean attribute
|
|
153
466
|
continue
|
|
154
467
|
}
|
|
155
468
|
cursor += 1 // past '='
|
|
@@ -162,10 +475,14 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
162
475
|
attrs.push({ kind: 'event', event: name.slice(2), code, loc })
|
|
163
476
|
} else if (name.startsWith('bind:')) {
|
|
164
477
|
attrs.push({ kind: 'bind', property: name.slice(5), code, loc })
|
|
478
|
+
} else if (name.startsWith('class:')) {
|
|
479
|
+
attrs.push({ kind: 'class', name: name.slice(6), code, loc })
|
|
480
|
+
} else if (name.startsWith('style:')) {
|
|
481
|
+
attrs.push({ kind: 'style', property: name.slice(6), code, loc })
|
|
165
482
|
} else if (name === 'attach') {
|
|
166
483
|
attrs.push({ kind: 'attach', code, loc })
|
|
167
484
|
} else {
|
|
168
|
-
attrs.push({ kind: 'expression', name, code, loc })
|
|
485
|
+
attrs.push({ kind: 'expression', name, code, loc, nameLoc })
|
|
169
486
|
}
|
|
170
487
|
} else if (source.charAt(cursor) === '"' || source.charAt(cursor) === "'") {
|
|
171
488
|
const quote = source.charAt(cursor)
|
|
@@ -176,7 +493,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
176
493
|
cursor += 1
|
|
177
494
|
}
|
|
178
495
|
cursor += 1 // past closing quote
|
|
179
|
-
attrs.push({ kind: 'static', name, value })
|
|
496
|
+
attrs.push({ kind: 'static', name, value, nameLoc })
|
|
180
497
|
} else {
|
|
181
498
|
/* Unquoted value (`<input type=text>`): runs to the next whitespace or
|
|
182
499
|
`>`, per the HTML unquoted-attribute rule. No delimiter to consume. */
|
|
@@ -200,6 +517,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
200
517
|
function readElement(): TemplateNode {
|
|
201
518
|
cursor += 1 // past '<'
|
|
202
519
|
let tag = ''
|
|
520
|
+
const tagStart = cursor // absolute (baseOffset-relative) offset of the tag name
|
|
203
521
|
while (cursor < source.length && !/[\s>/]/.test(source.charAt(cursor))) {
|
|
204
522
|
tag += source.charAt(cursor)
|
|
205
523
|
cursor += 1
|
|
@@ -237,7 +555,13 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
237
555
|
its children become slot content (rendered where the child puts <slot>). */
|
|
238
556
|
if (/^[A-Z]/.test(tag)) {
|
|
239
557
|
const slotted = selfClosing ? [] : readChildren(tag)
|
|
240
|
-
return {
|
|
558
|
+
return {
|
|
559
|
+
kind: 'component',
|
|
560
|
+
name: tag,
|
|
561
|
+
loc: baseOffset + tagStart,
|
|
562
|
+
props: toProps(attrs),
|
|
563
|
+
children: slotted,
|
|
564
|
+
}
|
|
241
565
|
}
|
|
242
566
|
/* `{...expr}` spreads onto a component (its props) or a native element (its
|
|
243
567
|
attributes), but a `<template>` directive has no such bag — reject it there. */
|
|
@@ -246,11 +570,23 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
246
570
|
}
|
|
247
571
|
const children = selfClosing || VOID_TAGS.has(tag) ? [] : readChildren(tag)
|
|
248
572
|
if (tag === 'template') {
|
|
249
|
-
return
|
|
573
|
+
return toSnippetOrTemplate(attrs, children)
|
|
250
574
|
}
|
|
251
575
|
return { kind: 'element', tag, attrs, children }
|
|
252
576
|
}
|
|
253
577
|
|
|
578
|
+
/* A `{:…}`/`{/…}` reached OUTSIDE a block (the root scan or an element's children —
|
|
579
|
+
not readBlockChildren/readBranchChildren, which consume their own) is a continuation
|
|
580
|
+
or close with no open `{#…}`. Surface it as an error: readText breaks on `{:`/`{/`
|
|
581
|
+
without advancing, so falling through would loop forever. */
|
|
582
|
+
function throwIfStrayBranch(): void {
|
|
583
|
+
if (source.startsWith('{:', cursor) || source.startsWith('{/', cursor)) {
|
|
584
|
+
const end = source.indexOf('}', cursor)
|
|
585
|
+
const token = source.slice(cursor, end === -1 ? source.length : end + 1)
|
|
586
|
+
throw new Error(`[abide] ${token} has no open {#…} block`)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
254
590
|
function readChildren(closeTag: string): TemplateNode[] {
|
|
255
591
|
const nodes: TemplateNode[] = []
|
|
256
592
|
while (cursor < source.length) {
|
|
@@ -258,10 +594,13 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
258
594
|
cursor = source.indexOf('>', cursor) + 1
|
|
259
595
|
break
|
|
260
596
|
}
|
|
597
|
+
throwIfStrayBranch()
|
|
261
598
|
if (source.startsWith('<!--', cursor)) {
|
|
262
599
|
skipComment()
|
|
263
600
|
} else if (atStyleTag()) {
|
|
264
601
|
nodes.push(readStyle())
|
|
602
|
+
} else if (atBlock()) {
|
|
603
|
+
nodes.push(readBlock())
|
|
265
604
|
} else if (source.charAt(cursor) === '<') {
|
|
266
605
|
nodes.push(readElement())
|
|
267
606
|
} else {
|
|
@@ -273,10 +612,13 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
273
612
|
|
|
274
613
|
const roots: TemplateNode[] = []
|
|
275
614
|
while (cursor < source.length) {
|
|
615
|
+
throwIfStrayBranch()
|
|
276
616
|
if (source.startsWith('<!--', cursor)) {
|
|
277
617
|
skipComment()
|
|
278
618
|
} else if (atStyleTag()) {
|
|
279
619
|
roots.push(readStyle())
|
|
620
|
+
} else if (atBlock()) {
|
|
621
|
+
roots.push(readBlock())
|
|
280
622
|
} else if (source.charAt(cursor) === '<') {
|
|
281
623
|
roots.push(readElement())
|
|
282
624
|
} else {
|
|
@@ -287,6 +629,165 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
287
629
|
return { nodes: roots }
|
|
288
630
|
}
|
|
289
631
|
|
|
632
|
+
/* Finds the index of a ` <token> ` keyword (` of `, ` by `) at brace/paren/bracket
|
|
633
|
+
depth 0, scanning left to right, skipping string literals. Returns -1 if absent. */
|
|
634
|
+
function indexOfKeywordAtDepthZero(text: string, keyword: string): number {
|
|
635
|
+
let depth = 0
|
|
636
|
+
let i = 0
|
|
637
|
+
while (i < text.length) {
|
|
638
|
+
const char = text.charAt(i)
|
|
639
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
640
|
+
i += 1
|
|
641
|
+
while (i < text.length && text.charAt(i) !== char) {
|
|
642
|
+
if (text.charAt(i) === '\\') {
|
|
643
|
+
i += 1
|
|
644
|
+
}
|
|
645
|
+
i += 1
|
|
646
|
+
}
|
|
647
|
+
} else if (char === '{' || char === '(' || char === '[') {
|
|
648
|
+
depth += 1
|
|
649
|
+
} else if (char === '}' || char === ')' || char === ']') {
|
|
650
|
+
depth -= 1
|
|
651
|
+
} else if (depth === 0 && text.startsWith(keyword, i)) {
|
|
652
|
+
return i
|
|
653
|
+
}
|
|
654
|
+
i += 1
|
|
655
|
+
}
|
|
656
|
+
return -1
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/* Index of a bare keyword (`then`) at brace/paren/bracket depth 0, requiring whitespace
|
|
660
|
+
on both sides — so it isn't matched inside an identifier (`q.then(x)`) and MAY sit
|
|
661
|
+
terminally. Unlike ` of `/` by `, an await `then` can end the head: `{#await p then}`
|
|
662
|
+
is a blocking await with no value binding, which a trailing-space match would miss
|
|
663
|
+
(folding `then` into the promise). Skips string literals. -1 if absent. */
|
|
664
|
+
function keywordAtDepthZero(text: string, word: string): number {
|
|
665
|
+
let depth = 0
|
|
666
|
+
let i = 0
|
|
667
|
+
while (i < text.length) {
|
|
668
|
+
const char = text.charAt(i)
|
|
669
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
670
|
+
i += 1
|
|
671
|
+
while (i < text.length && text.charAt(i) !== char) {
|
|
672
|
+
if (text.charAt(i) === '\\') {
|
|
673
|
+
i += 1
|
|
674
|
+
}
|
|
675
|
+
i += 1
|
|
676
|
+
}
|
|
677
|
+
} else if (char === '{' || char === '(' || char === '[') {
|
|
678
|
+
depth += 1
|
|
679
|
+
} else if (char === '}' || char === ')' || char === ']') {
|
|
680
|
+
depth -= 1
|
|
681
|
+
} else if (
|
|
682
|
+
depth === 0 &&
|
|
683
|
+
text.startsWith(word, i) &&
|
|
684
|
+
i > 0 &&
|
|
685
|
+
/\s/.test(text.charAt(i - 1)) &&
|
|
686
|
+
(i + word.length === text.length || /\s/.test(text.charAt(i + word.length)))
|
|
687
|
+
) {
|
|
688
|
+
return i
|
|
689
|
+
}
|
|
690
|
+
i += 1
|
|
691
|
+
}
|
|
692
|
+
return -1
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/* The depth-0 comma index in a binding (`{id, title}, i` → the comma after `}`),
|
|
696
|
+
so a destructuring pattern's inner commas don't split the binding from its index. */
|
|
697
|
+
function bindingCommaAtDepthZero(text: string): number {
|
|
698
|
+
let depth = 0
|
|
699
|
+
let i = 0
|
|
700
|
+
while (i < text.length) {
|
|
701
|
+
const char = text.charAt(i)
|
|
702
|
+
if (char === '{' || char === '(' || char === '[') {
|
|
703
|
+
depth += 1
|
|
704
|
+
} else if (char === '}' || char === ')' || char === ']') {
|
|
705
|
+
depth -= 1
|
|
706
|
+
} else if (char === ',' && depth === 0) {
|
|
707
|
+
return i
|
|
708
|
+
}
|
|
709
|
+
i += 1
|
|
710
|
+
}
|
|
711
|
+
return -1
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/* Absolute source offset of the trimmed expression beginning at `start` within a
|
|
715
|
+
directive `body` whose first char is at absolute `bodyLoc` — skips the leading
|
|
716
|
+
whitespace `.trim()` drops so the offset points at the expression's first real char
|
|
717
|
+
(the shadow source-map invariant: source text at `loc` equals the emitted code). */
|
|
718
|
+
function exprLoc(bodyLoc: number, body: string, start: number): number {
|
|
719
|
+
const raw = body.slice(start)
|
|
720
|
+
return bodyLoc + start + (raw.length - raw.trimStart().length)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/* Parses `for [await] <binding>[, <index>] of <iterable> [by <key>]`. Offsets are
|
|
724
|
+
tracked against the original `body` (not trimmed slices) so `loc` points at the
|
|
725
|
+
iterable expression's first char — `bodyLoc` is the absolute offset of `body[0]`. */
|
|
726
|
+
function parseForHead(
|
|
727
|
+
body: string,
|
|
728
|
+
bodyLoc: number,
|
|
729
|
+
): {
|
|
730
|
+
items: string
|
|
731
|
+
as: string
|
|
732
|
+
index: string | undefined
|
|
733
|
+
key: string | undefined
|
|
734
|
+
async: boolean
|
|
735
|
+
loc: number
|
|
736
|
+
asLoc: number | undefined
|
|
737
|
+
keyLoc: number | undefined
|
|
738
|
+
indexLoc: number | undefined
|
|
739
|
+
} {
|
|
740
|
+
const leadingSpace = (text: string): number => text.length - text.trimStart().length
|
|
741
|
+
const skipSpace = (i: number): number => {
|
|
742
|
+
let at = i
|
|
743
|
+
while (at < body.length && /\s/.test(body.charAt(at))) {
|
|
744
|
+
at += 1
|
|
745
|
+
}
|
|
746
|
+
return at
|
|
747
|
+
}
|
|
748
|
+
let bindingStart = skipSpace(body.indexOf('for') + 3)
|
|
749
|
+
const isAsync = /^await\b/.test(body.slice(bindingStart))
|
|
750
|
+
if (isAsync) {
|
|
751
|
+
bindingStart = skipSpace(bindingStart + 'await'.length)
|
|
752
|
+
}
|
|
753
|
+
const region = body.slice(bindingStart)
|
|
754
|
+
const ofAt = indexOfKeywordAtDepthZero(region, ' of ')
|
|
755
|
+
if (ofAt === -1) {
|
|
756
|
+
throw new Error('[abide] {#for} requires `<binding> of <iterable>`')
|
|
757
|
+
}
|
|
758
|
+
const left = region.slice(0, ofAt).trim()
|
|
759
|
+
const itemsStart = skipSpace(bindingStart + ofAt + ' of '.length)
|
|
760
|
+
let itemsRegion = body.slice(itemsStart)
|
|
761
|
+
const byAt = indexOfKeywordAtDepthZero(itemsRegion, ' by ')
|
|
762
|
+
const keyRaw = byAt === -1 ? '' : itemsRegion.slice(byAt + ' by '.length)
|
|
763
|
+
const key = byAt === -1 ? undefined : keyRaw.trim()
|
|
764
|
+
const keyLoc =
|
|
765
|
+
byAt === -1 ? undefined : bodyLoc + itemsStart + byAt + ' by '.length + leadingSpace(keyRaw)
|
|
766
|
+
if (byAt !== -1) {
|
|
767
|
+
itemsRegion = itemsRegion.slice(0, byAt)
|
|
768
|
+
}
|
|
769
|
+
const commaAt = bindingCommaAtDepthZero(left)
|
|
770
|
+
const as = (commaAt === -1 ? left : left.slice(0, commaAt)).trim()
|
|
771
|
+
const indexRaw = commaAt === -1 ? '' : left.slice(commaAt + 1)
|
|
772
|
+
const index = commaAt === -1 ? undefined : indexRaw.trim()
|
|
773
|
+
/* `left` is trimmed and starts at `bindingStart` (skipSpace'd), so the binding
|
|
774
|
+
name begins exactly there; the index sits past the comma. */
|
|
775
|
+
return {
|
|
776
|
+
items: itemsRegion.trim(),
|
|
777
|
+
as: as === '' ? '_item' : as,
|
|
778
|
+
index,
|
|
779
|
+
key,
|
|
780
|
+
async: isAsync,
|
|
781
|
+
loc: bodyLoc + itemsStart,
|
|
782
|
+
asLoc: as === '' ? undefined : bodyLoc + bindingStart,
|
|
783
|
+
keyLoc,
|
|
784
|
+
indexLoc:
|
|
785
|
+
commaAt === -1
|
|
786
|
+
? undefined
|
|
787
|
+
: bodyLoc + bindingStart + commaAt + 1 + leadingSpace(indexRaw),
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
290
791
|
/* A `case` node (`<template else>`/`<template case>`/`<template default>`) is valid
|
|
291
792
|
only as a direct child of its `<template if>`/`<template switch>`; a `branch`
|
|
292
793
|
(`then`/`catch`/`finally`) only inside its `<template await>`/`<template try>`.
|
|
@@ -385,7 +886,7 @@ function rejectMisplacedBranchContent(
|
|
|
385
886
|
letting a prop hold any value, functions included (e.g. an `onclick` callback). */
|
|
386
887
|
function toProps(
|
|
387
888
|
attrs: TemplateAttr[],
|
|
388
|
-
): { name: string; code: string; loc?: number; spread?: boolean }[] {
|
|
889
|
+
): { name: string; code: string; loc?: number; nameLoc?: number; spread?: boolean }[] {
|
|
389
890
|
return attrs.map((attr) => {
|
|
390
891
|
/* A `{...expr}` spread carries no name — its keys merge in at runtime
|
|
391
892
|
(`mergeProps`/`spreadProps`); `spread: true` marks it for the back-ends. */
|
|
@@ -396,19 +897,31 @@ function toProps(
|
|
|
396
897
|
/* A bare attribute (`<Toggle on />`) is a boolean flag: coerce it to
|
|
397
898
|
`true` so the prop reads as a boolean, not the empty string a native
|
|
398
899
|
element would serialise. An explicit `on=""` stays the empty string. */
|
|
399
|
-
return {
|
|
900
|
+
return {
|
|
901
|
+
name: attr.name,
|
|
902
|
+
code: attr.bare ? 'true' : JSON.stringify(attr.value),
|
|
903
|
+
nameLoc: attr.nameLoc,
|
|
904
|
+
}
|
|
400
905
|
}
|
|
401
906
|
/* Every non-static kind keeps its `code`/`loc`; only the prop name differs —
|
|
402
|
-
a directive (`event`/`bind`/`attach`) round-trips to its written
|
|
907
|
+
a directive (`event`/`bind`/`class`/`style`/`attach`) round-trips to its written
|
|
908
|
+
name as a passthrough prop. */
|
|
403
909
|
const name =
|
|
404
910
|
attr.kind === 'event'
|
|
405
911
|
? `on${attr.event}`
|
|
406
912
|
: attr.kind === 'bind'
|
|
407
913
|
? `bind:${attr.property}`
|
|
408
|
-
: attr.kind === '
|
|
409
|
-
?
|
|
410
|
-
: attr.
|
|
411
|
-
|
|
914
|
+
: attr.kind === 'class'
|
|
915
|
+
? `class:${attr.name}`
|
|
916
|
+
: attr.kind === 'style'
|
|
917
|
+
? `style:${attr.property}`
|
|
918
|
+
: attr.kind === 'attach'
|
|
919
|
+
? 'attach'
|
|
920
|
+
: attr.name
|
|
921
|
+
/* Only `expression` carries a name offset; event/bind/attach are framework-handled
|
|
922
|
+
passthrough excluded from the strict prop check, so theirs is unused. */
|
|
923
|
+
const nameLoc = attr.kind === 'expression' ? attr.nameLoc : undefined
|
|
924
|
+
return { name, code: attr.code, loc: attr.loc, nameLoc }
|
|
412
925
|
})
|
|
413
926
|
}
|
|
414
927
|
|
|
@@ -441,6 +954,12 @@ function attrName(attr: TemplateAttr): string {
|
|
|
441
954
|
if (attr.kind === 'attach') {
|
|
442
955
|
return 'attach'
|
|
443
956
|
}
|
|
957
|
+
if (attr.kind === 'class') {
|
|
958
|
+
return `class:${attr.name}`
|
|
959
|
+
}
|
|
960
|
+
if (attr.kind === 'style') {
|
|
961
|
+
return `style:${attr.property}`
|
|
962
|
+
}
|
|
444
963
|
/* A spread has no name. `attrName` only feeds `<template>` directive lookups, and a
|
|
445
964
|
spread on a `<template>` is rejected at parse, so this branch is unreachable in
|
|
446
965
|
practice. (Spread itself IS supported — on components as props and on native
|
|
@@ -452,10 +971,39 @@ function attrName(attr: TemplateAttr): string {
|
|
|
452
971
|
}
|
|
453
972
|
|
|
454
973
|
/* Turns a `<template>` directive into a control node (if/each/await + then/catch). */
|
|
455
|
-
|
|
974
|
+
/* The control-flow attribute names that used to drive `<template>` directives — now
|
|
975
|
+
moved to `{#…}` blocks. A `<template>` carrying one is a migration error. */
|
|
976
|
+
const CONTROL_DIRECTIVES = new Set([
|
|
977
|
+
'if',
|
|
978
|
+
'elseif',
|
|
979
|
+
'else',
|
|
980
|
+
'each',
|
|
981
|
+
'await',
|
|
982
|
+
'then',
|
|
983
|
+
'catch',
|
|
984
|
+
'finally',
|
|
985
|
+
'switch',
|
|
986
|
+
'case',
|
|
987
|
+
'default',
|
|
988
|
+
'try',
|
|
989
|
+
])
|
|
990
|
+
|
|
991
|
+
/* A `<template>` is now ONLY a snippet declaration (`name`) or a plain inert
|
|
992
|
+
`<template>` element. Control flow moved to `{#…}` blocks; a directive attribute
|
|
993
|
+
(`if`/`each`/`await`/…) is a migration error pointing at the block form. `name`
|
|
994
|
+
makes the element callable; without it, it stays an inert reusable fragment. */
|
|
995
|
+
function toSnippetOrTemplate(attrs: TemplateAttr[], children: TemplateNode[]): TemplateNode {
|
|
456
996
|
const find = (name: string) => attrs.find((attr) => attrName(attr) === name)
|
|
457
|
-
|
|
458
|
-
|
|
997
|
+
const directive = attrs.find((attr) => CONTROL_DIRECTIVES.has(attrName(attr)))
|
|
998
|
+
if (directive !== undefined) {
|
|
999
|
+
const name = attrName(directive)
|
|
1000
|
+
const block = name === 'elseif' || name === 'else' ? 'if' : name
|
|
1001
|
+
throw new Error(
|
|
1002
|
+
`[abide] <template ${name}> control flow was removed — use the {#${block}…} block instead`,
|
|
1003
|
+
)
|
|
1004
|
+
}
|
|
1005
|
+
/* `<template name="row" args={item}>` declares a snippet — a named builder. `args`
|
|
1006
|
+
(its parameter list) rides the `{…}` expression slot. */
|
|
459
1007
|
const snippet = find('name')
|
|
460
1008
|
if (snippet !== undefined) {
|
|
461
1009
|
const name = attrText(snippet)
|
|
@@ -471,104 +1019,6 @@ function toControlFlow(attrs: TemplateAttr[], children: TemplateNode[]): Templat
|
|
|
471
1019
|
loc: attrLoc(params),
|
|
472
1020
|
}
|
|
473
1021
|
}
|
|
474
|
-
/* `<template
|
|
475
|
-
|
|
476
|
-
if (find('try') !== undefined) {
|
|
477
|
-
return { kind: 'try', children }
|
|
478
|
-
}
|
|
479
|
-
/* `await` alongside `each` is the async-list switch (handled in the each branch
|
|
480
|
-
below), not an await block. */
|
|
481
|
-
const promise = find('await')
|
|
482
|
-
if (promise !== undefined && find('each') === undefined) {
|
|
483
|
-
const promiseCode = attrText(promise)
|
|
484
|
-
if (promiseCode === undefined) {
|
|
485
|
-
throw new Error('[abide] <template await> requires a promise expression')
|
|
486
|
-
}
|
|
487
|
-
/* A `then` attribute ON the await tag is the blocking switch: children become
|
|
488
|
-
the resolved content bound to its value (a `then` *child* is a streaming
|
|
489
|
-
branch, handled separately below when its own tag is parsed). */
|
|
490
|
-
const boundThen = find('then')
|
|
491
|
-
return {
|
|
492
|
-
kind: 'await',
|
|
493
|
-
promise: promiseCode,
|
|
494
|
-
blocking: boundThen !== undefined,
|
|
495
|
-
as: boundThen === undefined ? undefined : attrText(boundThen) || undefined,
|
|
496
|
-
children,
|
|
497
|
-
loc: attrLoc(promise),
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
const thenAttr = find('then')
|
|
501
|
-
if (thenAttr !== undefined) {
|
|
502
|
-
return { kind: 'branch', branch: 'then', as: attrText(thenAttr) || undefined, children }
|
|
503
|
-
}
|
|
504
|
-
const catchAttr = find('catch')
|
|
505
|
-
if (catchAttr !== undefined) {
|
|
506
|
-
return { kind: 'branch', branch: 'catch', as: attrText(catchAttr) || undefined, children }
|
|
507
|
-
}
|
|
508
|
-
/* `<template finally>` renders after settle on BOTH outcomes — outcome-agnostic,
|
|
509
|
-
so it binds no value. */
|
|
510
|
-
if (find('finally') !== undefined) {
|
|
511
|
-
return { kind: 'branch', branch: 'finally', as: undefined, children }
|
|
512
|
-
}
|
|
513
|
-
const subject = find('switch')
|
|
514
|
-
if (subject !== undefined) {
|
|
515
|
-
const subjectCode = attrText(subject)
|
|
516
|
-
if (subjectCode === undefined) {
|
|
517
|
-
throw new Error('[abide] <template switch> requires a subject expression')
|
|
518
|
-
}
|
|
519
|
-
return { kind: 'switch', subject: subjectCode, children, loc: attrLoc(subject) }
|
|
520
|
-
}
|
|
521
|
-
const caseAttr = find('case')
|
|
522
|
-
if (caseAttr !== undefined) {
|
|
523
|
-
const matchCode = attrText(caseAttr)
|
|
524
|
-
if (matchCode === undefined) {
|
|
525
|
-
throw new Error('[abide] <template case> requires a value expression')
|
|
526
|
-
}
|
|
527
|
-
return { kind: 'case', match: matchCode, children, loc: attrLoc(caseAttr) }
|
|
528
|
-
}
|
|
529
|
-
/* `<template elseif={c}>` is a match-less case carrying a condition — a branch of the
|
|
530
|
-
enclosing `<template if>` chain, truthy-tested in source order. */
|
|
531
|
-
const elseif = find('elseif')
|
|
532
|
-
if (elseif !== undefined) {
|
|
533
|
-
const conditionCode = attrText(elseif)
|
|
534
|
-
if (conditionCode === undefined) {
|
|
535
|
-
throw new Error('[abide] <template elseif> requires a condition expression')
|
|
536
|
-
}
|
|
537
|
-
return {
|
|
538
|
-
kind: 'case',
|
|
539
|
-
match: undefined,
|
|
540
|
-
condition: conditionCode,
|
|
541
|
-
children,
|
|
542
|
-
loc: attrLoc(elseif),
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
if (find('default') !== undefined || find('else') !== undefined) {
|
|
546
|
-
return { kind: 'case', match: undefined, children } // default (switch) / else (if)
|
|
547
|
-
}
|
|
548
|
-
const condition = find('if')
|
|
549
|
-
if (condition !== undefined) {
|
|
550
|
-
const conditionCode = attrText(condition)
|
|
551
|
-
if (conditionCode === undefined) {
|
|
552
|
-
throw new Error('[abide] <template if> requires a condition expression')
|
|
553
|
-
}
|
|
554
|
-
return { kind: 'if', condition: conditionCode, children, loc: attrLoc(condition) }
|
|
555
|
-
}
|
|
556
|
-
const items = find('each')
|
|
557
|
-
const itemsCode = items === undefined ? undefined : attrText(items)
|
|
558
|
-
if (itemsCode === undefined) {
|
|
559
|
-
throw new Error('[abide] <template> without a supported directive (if/each)')
|
|
560
|
-
}
|
|
561
|
-
const as = find('as')
|
|
562
|
-
const key = find('key')
|
|
563
|
-
const index = find('index')
|
|
564
|
-
return {
|
|
565
|
-
kind: 'each',
|
|
566
|
-
items: itemsCode,
|
|
567
|
-
as: (as === undefined ? undefined : attrText(as)) ?? '_item',
|
|
568
|
-
key: key === undefined ? undefined : attrText(key),
|
|
569
|
-
index: index === undefined ? undefined : attrText(index),
|
|
570
|
-
async: find('await') !== undefined, // `<template each await>` over an AsyncIterable
|
|
571
|
-
children,
|
|
572
|
-
loc: attrLoc(items),
|
|
573
|
-
}
|
|
1022
|
+
/* A plain inert `<template>` element (e.g. client-side cloning) — keep as an element. */
|
|
1023
|
+
return { kind: 'element', tag: 'template', attrs, children }
|
|
574
1024
|
}
|