@setzkasten-cms/astro-admin 1.4.2 → 1.5.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/dist/api-routes/_auth-guard.d.ts +27 -3
- package/dist/api-routes/_auth-guard.js +5 -2
- package/dist/api-routes/_dev-session-secret.d.ts +8 -0
- package/dist/api-routes/_dev-session-secret.js +8 -0
- package/dist/api-routes/_github-token.js +1 -1
- package/dist/api-routes/_role-resolver.js +6 -3
- package/dist/api-routes/_session-secret.d.ts +19 -0
- package/dist/api-routes/_session-secret.js +7 -0
- package/dist/api-routes/_session-signing.d.ts +45 -0
- package/dist/api-routes/_session-signing.js +8 -0
- package/dist/api-routes/_webhook-dispatcher.js +4 -4
- package/dist/api-routes/asset-proxy.js +1 -1
- package/dist/api-routes/auth-callback.js +12 -5
- package/dist/api-routes/auth-logout.d.ts +4 -4
- package/dist/api-routes/auth-logout.js +8 -2
- package/dist/api-routes/auth-session.d.ts +6 -0
- package/dist/api-routes/auth-session.js +19 -19
- package/dist/api-routes/auth-setzkasten-login.js +14 -7
- package/dist/api-routes/catalog-add.js +59 -17
- package/dist/api-routes/catalog-export.js +14 -4
- package/dist/api-routes/config.d.ts +10 -3
- package/dist/api-routes/config.js +26 -4
- package/dist/api-routes/deploy-hook.js +8 -8
- package/dist/api-routes/editors.d.ts +1 -1
- package/dist/api-routes/editors.js +5 -2
- package/dist/api-routes/github-proxy.js +30 -8
- package/dist/api-routes/global-config.js +6 -3
- package/dist/api-routes/history-rollback.js +31 -14
- package/dist/api-routes/history-version.js +8 -6
- package/dist/api-routes/history.js +5 -2
- package/dist/api-routes/icons-local.js +1 -1
- package/dist/api-routes/init-add-section.js +150 -48
- package/dist/api-routes/init-apply.js +56 -42
- package/dist/api-routes/init-migrate.js +43 -36
- package/dist/api-routes/init-scan-page.d.ts +1 -1
- package/dist/api-routes/init-scan-page.js +59 -13
- package/dist/api-routes/init-scan.js +22 -7
- package/dist/api-routes/migrate-to-multi.js +5 -2
- package/dist/api-routes/pages.js +15 -4
- package/dist/api-routes/section-add.js +68 -16
- package/dist/api-routes/section-commit-pending.js +70 -22
- package/dist/api-routes/section-delete.js +49 -14
- package/dist/api-routes/section-duplicate.js +65 -16
- package/dist/api-routes/section-prepare-copy.js +15 -2
- package/dist/api-routes/section-prepare.js +25 -4
- package/dist/api-routes/setup-github-app-bounce.js +15 -1
- package/dist/api-routes/setup-github-app-branches.js +9 -6
- package/dist/api-routes/setup-github-app-callback.js +24 -1
- package/dist/api-routes/setup-github-app-credentials.d.ts +27 -0
- package/dist/api-routes/setup-github-app-credentials.js +43 -0
- package/dist/api-routes/setup-github-app-installed.js +22 -1
- package/dist/api-routes/setup-github-app-repos.js +5 -2
- package/dist/api-routes/setup-github-app.d.ts +4 -0
- package/dist/api-routes/setup-github-app.js +19 -2
- package/dist/api-routes/updater-register.js +7 -1
- package/dist/api-routes/webhooks-status.js +5 -2
- package/dist/api-routes/webhooks-test.js +9 -8
- package/dist/api-routes/webhooks.js +12 -14
- package/dist/api-routes/websites-add.js +5 -2
- package/dist/api-routes/websites-remove.js +5 -2
- package/dist/{chunk-ZQDGGWJP.js → chunk-5KMGSFCZ.js} +2 -2
- package/dist/{chunk-RHJONMLK.js → chunk-CDXCYYQR.js} +222 -5
- package/dist/{chunk-NKDATSPA.js → chunk-DP6RTINQ.js} +1 -1
- package/dist/chunk-KENFINT4.js +76 -0
- package/dist/chunk-ONP6BRZO.js +47 -0
- package/dist/{chunk-INIWFKQ3.js → chunk-Q5HV47DW.js} +33 -19
- package/dist/chunk-QVCW6EF3.js +26 -0
- package/dist/{chunk-K22A4ZBS.js → chunk-UHI6323G.js} +293 -174
- package/dist/{chunk-AM4DZXXM.js → chunk-UJAFZEX2.js} +76 -9
- package/package.json +12 -6
- package/src/api-routes/__tests__/_session-signing.test.ts +114 -0
- package/src/api-routes/__tests__/_session-test-helper.ts +27 -0
- package/src/api-routes/__tests__/add-section-helpers.test.ts +91 -97
- package/src/api-routes/__tests__/auth-guard.test.ts +46 -7
- package/src/api-routes/__tests__/catalog-api.test.ts +14 -6
- package/src/api-routes/__tests__/commit-trailers.test.ts +5 -5
- package/src/api-routes/__tests__/deferred-operations.test.ts +9 -12
- package/src/api-routes/__tests__/deploy-hook.test.ts +3 -8
- package/src/api-routes/__tests__/feature-gate.test.ts +1 -1
- package/src/api-routes/__tests__/github-cache.test.ts +1 -1
- package/src/api-routes/__tests__/github-token.test.ts +1 -1
- package/src/api-routes/__tests__/global-config-theme.test.ts +4 -4
- package/src/api-routes/__tests__/history-rollback.test.ts +6 -3
- package/src/api-routes/__tests__/history.test.ts +9 -6
- package/src/api-routes/__tests__/init-scan-page-resolve-config.test.ts +11 -3
- package/src/api-routes/__tests__/migrate-to-multi.test.ts +5 -1
- package/src/api-routes/__tests__/pages-meta-store.test.ts +10 -5
- package/src/api-routes/__tests__/pages.test.ts +7 -2
- package/src/api-routes/__tests__/patch-page-file.test.ts +71 -19
- package/src/api-routes/__tests__/route-registry.test.ts +11 -18
- package/src/api-routes/__tests__/scan-page-helpers.test.ts +13 -10
- package/src/api-routes/__tests__/section-management.test.ts +28 -28
- package/src/api-routes/__tests__/setup-github-app-callback.test.ts +58 -16
- package/src/api-routes/__tests__/setup-github-app-repos.test.ts +4 -5
- package/src/api-routes/__tests__/setup-github-app.test.ts +27 -7
- package/src/api-routes/__tests__/storage-config-for-request.test.ts +83 -0
- package/src/api-routes/__tests__/updater-register.test.ts +230 -0
- package/src/api-routes/__tests__/webhook-signing.test.ts +1 -1
- package/src/api-routes/__tests__/webhooks.test.ts +19 -7
- package/src/api-routes/__tests__/websites-add.test.ts +2 -1
- package/src/api-routes/__tests__/websites-remove.test.ts +2 -1
- package/src/api-routes/_auth-guard.ts +47 -15
- package/src/api-routes/_commit-trailers.ts +3 -2
- package/src/api-routes/_dev-session-secret.ts +79 -0
- package/src/api-routes/_github-token.ts +1 -1
- package/src/api-routes/_pages-meta-store.ts +2 -2
- package/src/api-routes/_role-resolver.ts +7 -5
- package/src/api-routes/_session-secret.ts +46 -0
- package/src/api-routes/_session-signing.ts +135 -0
- package/src/api-routes/_vercel-origin.ts +2 -6
- package/src/api-routes/_webhook-dispatcher.ts +12 -16
- package/src/api-routes/_website-resolver.ts +3 -10
- package/src/api-routes/auth-callback.ts +9 -5
- package/src/api-routes/auth-login.ts +5 -3
- package/src/api-routes/auth-logout.ts +18 -1
- package/src/api-routes/auth-session.ts +13 -21
- package/src/api-routes/auth-setzkasten-login.ts +12 -9
- package/src/api-routes/catalog-add.ts +89 -31
- package/src/api-routes/catalog-export.ts +30 -10
- package/src/api-routes/config.ts +39 -6
- package/src/api-routes/deploy-hook.ts +13 -11
- package/src/api-routes/editors.ts +33 -22
- package/src/api-routes/github-proxy.ts +25 -11
- package/src/api-routes/global-config.ts +103 -18
- package/src/api-routes/history-rollback.ts +41 -14
- package/src/api-routes/history-version.ts +5 -6
- package/src/api-routes/history.ts +3 -3
- package/src/api-routes/icons-local.ts +2 -2
- package/src/api-routes/init-add-section.ts +218 -88
- package/src/api-routes/init-apply.ts +71 -56
- package/src/api-routes/init-migrate.ts +54 -48
- package/src/api-routes/init-scan-page.ts +77 -30
- package/src/api-routes/init-scan.ts +19 -11
- package/src/api-routes/pages.ts +16 -11
- package/src/api-routes/section-add.ts +98 -27
- package/src/api-routes/section-commit-pending.ts +87 -34
- package/src/api-routes/section-delete.ts +76 -27
- package/src/api-routes/section-duplicate.ts +95 -28
- package/src/api-routes/section-management.ts +3 -7
- package/src/api-routes/section-prepare-copy.ts +29 -8
- package/src/api-routes/section-prepare.ts +38 -10
- package/src/api-routes/setup-github-app-bounce.ts +7 -1
- package/src/api-routes/setup-github-app-branches.ts +6 -7
- package/src/api-routes/setup-github-app-callback.ts +18 -1
- package/src/api-routes/setup-github-app-credentials.ts +55 -0
- package/src/api-routes/setup-github-app-installed.ts +12 -1
- package/src/api-routes/setup-github-app-repos.ts +2 -3
- package/src/api-routes/setup-github-app.ts +14 -5
- package/src/api-routes/updater-check.ts +6 -4
- package/src/api-routes/updater-register.ts +34 -20
- package/src/api-routes/updater-transfer.ts +8 -6
- package/src/api-routes/updater-unbind.ts +14 -10
- package/src/api-routes/webhooks-test.ts +9 -11
- package/src/api-routes/webhooks.ts +15 -19
- package/src/init/__tests__/page-level.test.ts +279 -105
- package/src/init/__tests__/page-list-coverage.test.ts +70 -70
- package/src/init/__tests__/patcher-child-component.test.ts +126 -0
- package/src/init/__tests__/patcher-edge-cases.test.ts +47 -23
- package/src/init/__tests__/patcher-mixed-content-wrapper.test.ts +16 -6
- package/src/init/__tests__/patcher-page-mode.test.ts +30 -20
- package/src/init/__tests__/section-pipeline.test.ts +102 -16
- package/src/init/astro-config-patcher.ts +4 -18
- package/src/init/astro-detector.ts +2 -7
- package/src/init/astro-section-analyzer-v2.ts +475 -193
- package/src/init/field-label-enricher.ts +6 -6
- package/src/init/template-patcher-v2.ts +490 -56
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { parse } from '@astrojs/compiler'
|
|
14
|
-
import {
|
|
14
|
+
import { type InferredField, type InferredSection, inferFields } from '@setzkasten-cms/core/init'
|
|
15
15
|
import type {
|
|
16
16
|
AnalyzerResult,
|
|
17
|
+
FieldPosition,
|
|
18
|
+
InnerFieldInfo,
|
|
17
19
|
RepeatedGroup,
|
|
18
20
|
RepeatedGroupInstance,
|
|
19
|
-
InnerFieldInfo,
|
|
20
|
-
FieldPosition,
|
|
21
21
|
} from './analyzer-types.js'
|
|
22
22
|
import { enrichFieldLabels } from './field-label-enricher.js'
|
|
23
23
|
|
|
@@ -43,7 +43,11 @@ interface AstAttr {
|
|
|
43
43
|
position?: { start: { offset: number } }
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function walkAst(
|
|
46
|
+
function walkAst(
|
|
47
|
+
node: AstNode,
|
|
48
|
+
callback: (node: AstNode, parent: AstNode | null) => void,
|
|
49
|
+
parent: AstNode | null = null,
|
|
50
|
+
): void {
|
|
47
51
|
callback(node, parent)
|
|
48
52
|
if (node.children) {
|
|
49
53
|
for (const child of node.children) {
|
|
@@ -76,7 +80,7 @@ export async function analyzeAstroSection(
|
|
|
76
80
|
const { frontmatter, template, templateOffset } = splitAstroFile(source)
|
|
77
81
|
|
|
78
82
|
// Filter variables: skip prop values, fallbacks, and .map()-only sources
|
|
79
|
-
const variables = extractFrontmatterVariables(frontmatter).filter(name => {
|
|
83
|
+
const variables = extractFrontmatterVariables(frontmatter).filter((name) => {
|
|
80
84
|
const propUsageRegex = new RegExp(`\\w+={${name}}`)
|
|
81
85
|
if (propUsageRegex.test(template)) return false
|
|
82
86
|
const fallbackRegex = new RegExp(`(?:\\?\\?|\\|\\|)\\s*${name}\\b`)
|
|
@@ -101,7 +105,10 @@ export async function analyzeAstroSection(
|
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
// Extract template fields + repeated groups (Phase 1)
|
|
104
|
-
const { fields: templateFields, repeatedGroups } = await extractTemplateFields(
|
|
108
|
+
const { fields: templateFields, repeatedGroups } = await extractTemplateFields(
|
|
109
|
+
template,
|
|
110
|
+
frontmatter,
|
|
111
|
+
)
|
|
105
112
|
|
|
106
113
|
// Fix positions: extractTemplateFields already subtracts WRAPPER_OFFSET internally,
|
|
107
114
|
// so positions are relative to the template string. Add templateOffset for full source.
|
|
@@ -146,17 +153,17 @@ export async function analyzeAstroSection(
|
|
|
146
153
|
;(field as any)._pos = usageMatch.index
|
|
147
154
|
} else {
|
|
148
155
|
const mapUsage = template.indexOf(`${field.key}.map(`)
|
|
149
|
-
;(field as any)._pos = mapUsage !== -1 ? mapUsage :
|
|
156
|
+
;(field as any)._pos = mapUsage !== -1 ? mapUsage : Number.POSITIVE_INFINITY
|
|
150
157
|
}
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
// Merge all fields and sort by template position
|
|
154
161
|
const existingKeys = new Set(variableFields.map((f) => f.key))
|
|
155
|
-
const allFields = [
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
const allFields = [...variableFields, ...templateFields.filter((f) => !existingKeys.has(f.key))]
|
|
163
|
+
allFields.sort(
|
|
164
|
+
(a, b) =>
|
|
165
|
+
((a as any)._pos ?? Number.POSITIVE_INFINITY) - ((b as any)._pos ?? Number.POSITIVE_INFINITY),
|
|
166
|
+
)
|
|
160
167
|
|
|
161
168
|
const fields = allFields.map(({ ...field }) => {
|
|
162
169
|
delete (field as any)._pos
|
|
@@ -188,7 +195,11 @@ export async function analyzeAstroSection(
|
|
|
188
195
|
// Frontmatter utilities (unchanged from v1)
|
|
189
196
|
// ---------------------------------------------------------------------------
|
|
190
197
|
|
|
191
|
-
function splitAstroFile(source: string): {
|
|
198
|
+
function splitAstroFile(source: string): {
|
|
199
|
+
frontmatter: string
|
|
200
|
+
template: string
|
|
201
|
+
templateOffset: number
|
|
202
|
+
} {
|
|
192
203
|
if (!source.startsWith('---')) return { frontmatter: '', template: source, templateOffset: 0 }
|
|
193
204
|
// Find closing --- marker (starts at beginning of a line after the opening ---)
|
|
194
205
|
const endIdx = source.indexOf('\n---', 3)
|
|
@@ -210,11 +221,29 @@ function stripTemplateLiterals(source: string): string {
|
|
|
210
221
|
i++
|
|
211
222
|
let depth = 0
|
|
212
223
|
while (i < source.length) {
|
|
213
|
-
if (source[i] === '\\') {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (source[i] === '
|
|
224
|
+
if (source[i] === '\\') {
|
|
225
|
+
i += 2
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
if (source[i] === '$' && source[i + 1] === '{') {
|
|
229
|
+
depth++
|
|
230
|
+
i += 2
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
if (source[i] === '{') {
|
|
234
|
+
depth++
|
|
235
|
+
i++
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
if (source[i] === '}' && depth > 0) {
|
|
239
|
+
depth--
|
|
240
|
+
i++
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
if (source[i] === '`' && depth === 0) {
|
|
244
|
+
i++
|
|
245
|
+
break
|
|
246
|
+
}
|
|
218
247
|
i++
|
|
219
248
|
}
|
|
220
249
|
result += '``' // placeholder so surrounding code stays parseable
|
|
@@ -237,7 +266,8 @@ function extractFrontmatterVariables(frontmatter: string): string[] {
|
|
|
237
266
|
const rhs = match[2]?.trim() ?? ''
|
|
238
267
|
if (isInternalVariable(name)) continue
|
|
239
268
|
// Skip exported declarations (e.g. "export const prerender = true")
|
|
240
|
-
const charBefore =
|
|
269
|
+
const charBefore =
|
|
270
|
+
match.index > 0 ? stripped.slice(Math.max(0, match.index - 10), match.index) : ''
|
|
241
271
|
if (/export\s*$/.test(charBefore)) continue
|
|
242
272
|
if (/\.\s*map\s*\(/.test(rhs) || /\w+\?\.\w+/.test(rhs)) continue
|
|
243
273
|
if (/^\[/.test(rhs) && /^default/i.test(name)) continue
|
|
@@ -298,10 +328,26 @@ function extractFrontmatterValue(frontmatter: string, varName: string): unknown
|
|
|
298
328
|
|
|
299
329
|
function isInternalVariable(name: string): boolean {
|
|
300
330
|
const skip = new Set([
|
|
301
|
-
'Astro',
|
|
302
|
-
'
|
|
303
|
-
'
|
|
304
|
-
'
|
|
331
|
+
'Astro',
|
|
332
|
+
'props',
|
|
333
|
+
'data',
|
|
334
|
+
'class',
|
|
335
|
+
'className',
|
|
336
|
+
'style',
|
|
337
|
+
'id',
|
|
338
|
+
'slot',
|
|
339
|
+
'Fragment',
|
|
340
|
+
'Component',
|
|
341
|
+
'frontmatter',
|
|
342
|
+
'url',
|
|
343
|
+
'site',
|
|
344
|
+
'generator',
|
|
345
|
+
'redirect',
|
|
346
|
+
'response',
|
|
347
|
+
'request',
|
|
348
|
+
'cookies',
|
|
349
|
+
'params',
|
|
350
|
+
'slots',
|
|
305
351
|
])
|
|
306
352
|
return skip.has(name) || name.startsWith('_')
|
|
307
353
|
}
|
|
@@ -323,7 +369,7 @@ function buildByteToCharMap(source: string): (byteOffset: number) => number {
|
|
|
323
369
|
if (codePoint > 0xffff) charIdx++
|
|
324
370
|
}
|
|
325
371
|
map[byteIdx] = source.length
|
|
326
|
-
return (offset) => offset <= 0 ? 0 : offset >= map.length ? source.length : map[offset]!
|
|
372
|
+
return (offset) => (offset <= 0 ? 0 : offset >= map.length ? source.length : map[offset]!)
|
|
327
373
|
}
|
|
328
374
|
|
|
329
375
|
function convertAstPositions(node: AstNode, b2c: (offset: number) => number): void {
|
|
@@ -346,11 +392,11 @@ function convertAstPositions(node: AstNode, b2c: (offset: number) => number): vo
|
|
|
346
392
|
const WRAPPER_OFFSET = 8
|
|
347
393
|
|
|
348
394
|
function nodeOffset(node: AstNode): number {
|
|
349
|
-
return (node.position?.start?.offset ??
|
|
395
|
+
return (node.position?.start?.offset ?? Number.POSITIVE_INFINITY) - WRAPPER_OFFSET
|
|
350
396
|
}
|
|
351
397
|
|
|
352
398
|
function nodeEnd(node: AstNode): number {
|
|
353
|
-
return (node.position?.end?.offset ??
|
|
399
|
+
return (node.position?.end?.offset ?? Number.POSITIVE_INFINITY) - WRAPPER_OFFSET
|
|
354
400
|
}
|
|
355
401
|
|
|
356
402
|
function getClassValue(node: AstNode): string {
|
|
@@ -368,7 +414,7 @@ function isAriaHidden(node: AstNode): boolean {
|
|
|
368
414
|
return attr?.value === 'true'
|
|
369
415
|
}
|
|
370
416
|
|
|
371
|
-
function extractTextContent(node: AstNode, stripCmsBound
|
|
417
|
+
function extractTextContent(node: AstNode, stripCmsBound = false): string {
|
|
372
418
|
let text = ''
|
|
373
419
|
if (node.type === 'text') {
|
|
374
420
|
text += node.value ?? ''
|
|
@@ -387,8 +433,22 @@ function extractTextContent(node: AstNode, stripCmsBound: boolean = false): stri
|
|
|
387
433
|
|
|
388
434
|
/** Tags that indicate inline formatting (bold, italic, links, code, color spans, etc.) */
|
|
389
435
|
const INLINE_FORMATTING_TAGS = new Set([
|
|
390
|
-
'strong',
|
|
391
|
-
'
|
|
436
|
+
'strong',
|
|
437
|
+
'b',
|
|
438
|
+
'em',
|
|
439
|
+
'i',
|
|
440
|
+
'mark',
|
|
441
|
+
'code',
|
|
442
|
+
'del',
|
|
443
|
+
'ins',
|
|
444
|
+
'sub',
|
|
445
|
+
'sup',
|
|
446
|
+
'a',
|
|
447
|
+
'abbr',
|
|
448
|
+
'cite',
|
|
449
|
+
'u',
|
|
450
|
+
's',
|
|
451
|
+
'small',
|
|
392
452
|
'span', // color/style spans inside text content
|
|
393
453
|
])
|
|
394
454
|
|
|
@@ -406,10 +466,14 @@ function hasInlineFormatting(node: AstNode): boolean {
|
|
|
406
466
|
* Collect all class="..." attributes from an element subtree.
|
|
407
467
|
* Returns them with a structural path (child index path) for cross-instance matching.
|
|
408
468
|
*/
|
|
409
|
-
function collectClassAttrs(
|
|
469
|
+
function collectClassAttrs(
|
|
470
|
+
node: AstNode,
|
|
471
|
+
path = '',
|
|
472
|
+
source?: string,
|
|
473
|
+
): import('./analyzer-types.js').ClassAttrInfo[] {
|
|
410
474
|
const result: import('./analyzer-types.js').ClassAttrInfo[] = []
|
|
411
475
|
if (node.type === 'element' && node.attributes && source) {
|
|
412
|
-
const classAttr = node.attributes.find(a => a.name === 'class' && a.kind === 'quoted')
|
|
476
|
+
const classAttr = node.attributes.find((a) => a.name === 'class' && a.kind === 'quoted')
|
|
413
477
|
if (classAttr && classAttr.value) {
|
|
414
478
|
// The AST position points to the element start, not the attribute.
|
|
415
479
|
// Find the actual `class="` within the opening tag.
|
|
@@ -494,7 +558,10 @@ function serializeNode(node: AstNode): string {
|
|
|
494
558
|
}
|
|
495
559
|
|
|
496
560
|
function camelToLabel(str: string): string {
|
|
497
|
-
return str
|
|
561
|
+
return str
|
|
562
|
+
.replace(/([A-Z])/g, ' $1')
|
|
563
|
+
.replace(/^./, (s) => s.toUpperCase())
|
|
564
|
+
.trim()
|
|
498
565
|
}
|
|
499
566
|
|
|
500
567
|
function inferInnerFieldType(name: string): InferredField['type'] {
|
|
@@ -502,8 +569,16 @@ function inferInnerFieldType(name: string): InferredField['type'] {
|
|
|
502
569
|
if (/icon/.test(n)) return 'icon'
|
|
503
570
|
if (/image|img|photo|avatar|logo|thumbnail|src/.test(n)) return 'image'
|
|
504
571
|
if (/color|colour/.test(n)) return 'color'
|
|
505
|
-
if (
|
|
506
|
-
|
|
572
|
+
if (
|
|
573
|
+
/count|amount|number|quantity|total|rating|score|percent|order|index|size|width|height/.test(n)
|
|
574
|
+
)
|
|
575
|
+
return 'number'
|
|
576
|
+
if (
|
|
577
|
+
/^is[A-Z]/.test(name) ||
|
|
578
|
+
/^has[A-Z]/.test(name) ||
|
|
579
|
+
/enabled|disabled|visible|hidden|active|checked|selected|highlight|accent|featured/.test(n)
|
|
580
|
+
)
|
|
581
|
+
return 'boolean'
|
|
507
582
|
return 'text'
|
|
508
583
|
}
|
|
509
584
|
|
|
@@ -511,7 +586,11 @@ function inferInnerFieldType(name: string): InferredField['type'] {
|
|
|
511
586
|
// Inline array extraction (unchanged from v1)
|
|
512
587
|
// ---------------------------------------------------------------------------
|
|
513
588
|
|
|
514
|
-
function extractInlineArrayValues(
|
|
589
|
+
function extractInlineArrayValues(
|
|
590
|
+
arrayContent: string,
|
|
591
|
+
isObjectArray: boolean,
|
|
592
|
+
isStringArray: boolean,
|
|
593
|
+
): unknown[] {
|
|
515
594
|
if (isStringArray || !isObjectArray) {
|
|
516
595
|
const strings: string[] = []
|
|
517
596
|
const strRegex = /['"]([^'"]+)['"]/g
|
|
@@ -528,7 +607,9 @@ function extractInlineArrayValues(arrayContent: string, isObjectArray: boolean,
|
|
|
528
607
|
const obj = parseObjectLiteral(objStr)
|
|
529
608
|
if (Object.keys(obj).length > 0) objects.push(obj)
|
|
530
609
|
i = end + 1
|
|
531
|
-
} else {
|
|
610
|
+
} else {
|
|
611
|
+
i++
|
|
612
|
+
}
|
|
532
613
|
}
|
|
533
614
|
return objects
|
|
534
615
|
}
|
|
@@ -558,7 +639,10 @@ function parseObjectLiteral(objStr: string): Record<string, unknown> {
|
|
|
558
639
|
while (i < objStr.length && /[\s,]/.test(objStr[i]!)) i++
|
|
559
640
|
if (i >= objStr.length) break
|
|
560
641
|
const keyMatch = objStr.slice(i).match(/^(\w+)\s*:\s*/)
|
|
561
|
-
if (!keyMatch) {
|
|
642
|
+
if (!keyMatch) {
|
|
643
|
+
i++
|
|
644
|
+
continue
|
|
645
|
+
}
|
|
562
646
|
const key = keyMatch[1]!
|
|
563
647
|
i += keyMatch[0].length
|
|
564
648
|
const ch = objStr[i]
|
|
@@ -587,11 +671,16 @@ function parseObjectLiteral(objStr: string): Record<string, unknown> {
|
|
|
587
671
|
i = end + 1
|
|
588
672
|
} else if (ch && /\d/.test(ch)) {
|
|
589
673
|
const numMatch = objStr.slice(i).match(/^(\d+(?:\.\d+)?)/)
|
|
590
|
-
if (numMatch) {
|
|
674
|
+
if (numMatch) {
|
|
675
|
+
obj[key] = Number(numMatch[1])
|
|
676
|
+
i += numMatch[0].length
|
|
677
|
+
}
|
|
591
678
|
} else if (objStr.slice(i, i + 4) === 'true') {
|
|
592
|
-
obj[key] = true
|
|
679
|
+
obj[key] = true
|
|
680
|
+
i += 4
|
|
593
681
|
} else if (objStr.slice(i, i + 5) === 'false') {
|
|
594
|
-
obj[key] = false
|
|
682
|
+
obj[key] = false
|
|
683
|
+
i += 5
|
|
595
684
|
} else {
|
|
596
685
|
while (i < objStr.length && objStr[i] !== ',') i++
|
|
597
686
|
}
|
|
@@ -608,7 +697,7 @@ interface ExtractResult {
|
|
|
608
697
|
repeatedGroups: RepeatedGroup[]
|
|
609
698
|
}
|
|
610
699
|
|
|
611
|
-
async function extractTemplateFields(template: string, frontmatter
|
|
700
|
+
async function extractTemplateFields(template: string, frontmatter = ''): Promise<ExtractResult> {
|
|
612
701
|
const fields: Array<InferredField & { _pos: number }> = []
|
|
613
702
|
const usedKeys = new Set<string>()
|
|
614
703
|
const repeatedGroups: RepeatedGroup[] = []
|
|
@@ -632,11 +721,12 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
632
721
|
if (typeof field.defaultValue === 'string' && field.defaultValue.length >= 3) {
|
|
633
722
|
finalPos = template.indexOf(field.defaultValue)
|
|
634
723
|
} else if (Array.isArray(field.defaultValue) && field.defaultValue.length > 0) {
|
|
635
|
-
const firstItem =
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
724
|
+
const firstItem =
|
|
725
|
+
typeof field.defaultValue[0] === 'string'
|
|
726
|
+
? field.defaultValue[0]
|
|
727
|
+
: typeof field.defaultValue[0] === 'object' && field.defaultValue[0]
|
|
728
|
+
? Object.values(field.defaultValue[0])[0]
|
|
729
|
+
: null
|
|
640
730
|
if (typeof firstItem === 'string' && firstItem.length >= 2) {
|
|
641
731
|
finalPos = template.indexOf(firstItem)
|
|
642
732
|
}
|
|
@@ -648,7 +738,7 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
648
738
|
const keyMatch = keyRegex.exec(template)
|
|
649
739
|
if (keyMatch) finalPos = keyMatch.index
|
|
650
740
|
}
|
|
651
|
-
fields.push({ ...field, _pos: finalPos === -1 ?
|
|
741
|
+
fields.push({ ...field, _pos: finalPos === -1 ? Number.POSITIVE_INFINITY : finalPos })
|
|
652
742
|
}
|
|
653
743
|
}
|
|
654
744
|
|
|
@@ -706,7 +796,9 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
706
796
|
repeatedElementGroups.push({ tag, instances: siblings })
|
|
707
797
|
for (const inst of siblings) {
|
|
708
798
|
const start = nodeOffset(inst)
|
|
709
|
-
const end = inst.position?.end?.offset
|
|
799
|
+
const end = inst.position?.end?.offset
|
|
800
|
+
? inst.position.end.offset - WRAPPER_OFFSET
|
|
801
|
+
: start + 1
|
|
710
802
|
repeatedElementRanges.push({ start, end })
|
|
711
803
|
}
|
|
712
804
|
}
|
|
@@ -716,7 +808,7 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
716
808
|
// Detects only tbody rows (not thead) so column headers are excluded.
|
|
717
809
|
walkAst(ast, (node) => {
|
|
718
810
|
if (node.type !== 'element' || node.name !== 'tbody') return
|
|
719
|
-
const trRows = (node.children ?? []).filter(c => c.type === 'element' && c.name === 'tr')
|
|
811
|
+
const trRows = (node.children ?? []).filter((c) => c.type === 'element' && c.name === 'tr')
|
|
720
812
|
if (trRows.length < 2) return
|
|
721
813
|
repeatedElementGroups.push({ tag: 'tr', instances: trRows })
|
|
722
814
|
for (const row of trRows) {
|
|
@@ -754,22 +846,38 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
754
846
|
while ((m = itemRe.exec(arrayFallbackMatch[1]!)) !== null) {
|
|
755
847
|
items.push(m[1] ?? m[2] ?? m[3] ?? '')
|
|
756
848
|
}
|
|
757
|
-
const hasFormatting = items.some(s => /<[a-z]/.test(s))
|
|
758
|
-
addField(
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
849
|
+
const hasFormatting = items.some((s) => /<[a-z]/.test(s))
|
|
850
|
+
addField(
|
|
851
|
+
{
|
|
852
|
+
key: fieldKey,
|
|
853
|
+
type: 'array',
|
|
854
|
+
label: camelToLabel(fieldKey),
|
|
855
|
+
confidence: 'high',
|
|
856
|
+
defaultValue: items.length > 0 ? items : undefined,
|
|
857
|
+
options: {
|
|
858
|
+
arrayItem: { type: 'text', ...(hasFormatting ? { formatting: true } : {}) },
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
nodeOffset(node),
|
|
862
|
+
)
|
|
763
863
|
} else {
|
|
764
864
|
// String fallback or no fallback
|
|
765
865
|
const strFallback = exprCode.match(/\?\?\s*(?:`([\s\S]*?)`|'([^']*)'|"([^"]*)")/)
|
|
766
|
-
const defaultValue = strFallback
|
|
866
|
+
const defaultValue = strFallback
|
|
867
|
+
? (strFallback[1] ?? strFallback[2] ?? strFallback[3] ?? '').trim()
|
|
868
|
+
: undefined
|
|
767
869
|
const hasHtml = defaultValue ? /<[a-z]/.test(defaultValue) : false
|
|
768
|
-
addField(
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
870
|
+
addField(
|
|
871
|
+
{
|
|
872
|
+
key: fieldKey,
|
|
873
|
+
type: 'text',
|
|
874
|
+
label: camelToLabel(fieldKey),
|
|
875
|
+
confidence: 'high',
|
|
876
|
+
...(defaultValue ? { defaultValue } : {}),
|
|
877
|
+
...(hasHtml ? { options: { formatting: true } } : {}),
|
|
878
|
+
},
|
|
879
|
+
nodeOffset(node),
|
|
880
|
+
)
|
|
773
881
|
}
|
|
774
882
|
cmsBoundOffsets.add(nodeOffset(node))
|
|
775
883
|
}
|
|
@@ -779,21 +887,31 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
779
887
|
if (node.type !== 'element' && node.type !== 'component') return
|
|
780
888
|
for (const attr of node.attributes ?? []) {
|
|
781
889
|
if (attr.kind !== 'expression') continue
|
|
782
|
-
const cmsAttrMatch = attr.value.match(
|
|
890
|
+
const cmsAttrMatch = attr.value.match(
|
|
891
|
+
/^\s*\w+\?\.\s*(\w+)\s*\?\?\s*(?:`([\s\S]*?)`|'([^']*)'|"([^"]*)")/,
|
|
892
|
+
)
|
|
783
893
|
if (cmsAttrMatch) {
|
|
784
894
|
const fieldKey = cmsAttrMatch[1]!
|
|
785
895
|
const fallback = (cmsAttrMatch[2] ?? cmsAttrMatch[3] ?? cmsAttrMatch[4] ?? '').trim()
|
|
786
|
-
const pos = attr.position?.start?.offset
|
|
896
|
+
const pos = attr.position?.start?.offset
|
|
897
|
+
? attr.position.start.offset - WRAPPER_OFFSET
|
|
898
|
+
: nodeOffset(node)
|
|
787
899
|
// formatting: true when fallback contains HTML tags,
|
|
788
900
|
// OR when the attribute is set:html without a fallback (content is always HTML then)
|
|
789
901
|
const isSetHtml = attr.name === 'set:html'
|
|
790
902
|
const fallbackHasHtml = fallback ? /<[a-z]/.test(fallback) : false
|
|
791
903
|
const hasHtml = fallbackHasHtml || (isSetHtml && !fallback)
|
|
792
|
-
addField(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
904
|
+
addField(
|
|
905
|
+
{
|
|
906
|
+
key: fieldKey,
|
|
907
|
+
type: 'text',
|
|
908
|
+
label: camelToLabel(fieldKey),
|
|
909
|
+
confidence: 'high',
|
|
910
|
+
...(fallback ? { defaultValue: fallback } : {}),
|
|
911
|
+
...(hasHtml ? { options: { formatting: true } } : {}),
|
|
912
|
+
},
|
|
913
|
+
pos,
|
|
914
|
+
)
|
|
797
915
|
cmsBoundOffsets.add(nodeOffset(node))
|
|
798
916
|
}
|
|
799
917
|
}
|
|
@@ -817,14 +935,25 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
817
935
|
if (!/uppercase|tracking-widest/.test(classVal)) return
|
|
818
936
|
const text = extractTextContent(node, true).trim()
|
|
819
937
|
if (text.length >= 2 && text.length <= 80) {
|
|
820
|
-
addField(
|
|
938
|
+
addField(
|
|
939
|
+
{
|
|
940
|
+
key: 'overline',
|
|
941
|
+
type: 'text',
|
|
942
|
+
label: 'Overline',
|
|
943
|
+
confidence: 'medium',
|
|
944
|
+
defaultValue: text,
|
|
945
|
+
},
|
|
946
|
+
nodeOffset(node),
|
|
947
|
+
)
|
|
821
948
|
}
|
|
822
949
|
})
|
|
823
950
|
|
|
824
951
|
// ── 2. HEADINGS (h1-h6) ───────────────────────────────────────────────
|
|
825
952
|
// Start from the count of already-registered heading* keys (from Section 0)
|
|
826
953
|
// so new headings get non-colliding keys (heading3, heading4, ...).
|
|
827
|
-
let headingCount = Array.from(usedKeys).filter(
|
|
954
|
+
let headingCount = Array.from(usedKeys).filter(
|
|
955
|
+
(k) => k === 'heading' || /^heading\d+$/.test(k),
|
|
956
|
+
).length
|
|
828
957
|
walkAst(ast, (node) => {
|
|
829
958
|
if (node.type !== 'element') return
|
|
830
959
|
if (!/^h[1-6]$/.test(node.name ?? '')) return
|
|
@@ -834,18 +963,26 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
834
963
|
headingCount++
|
|
835
964
|
const headingOpts: Record<string, unknown> = { required: true }
|
|
836
965
|
if (hasInlineFormatting(node)) headingOpts.formatting = true
|
|
837
|
-
addField(
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
966
|
+
addField(
|
|
967
|
+
{
|
|
968
|
+
key: numberedKey('heading', headingCount),
|
|
969
|
+
type: 'text',
|
|
970
|
+
label: numberedLabel('Heading', headingCount),
|
|
971
|
+
confidence: 'high',
|
|
972
|
+
defaultValue: text,
|
|
973
|
+
options: headingOpts,
|
|
974
|
+
},
|
|
975
|
+
nodeOffset(node),
|
|
976
|
+
)
|
|
842
977
|
}
|
|
843
978
|
})
|
|
844
979
|
|
|
845
980
|
// ── 3. PARAGRAPHS / DESCRIPTION TEXT ───────────────────────────────────
|
|
846
981
|
// Start from the count of already-registered description* keys (from Section 0)
|
|
847
982
|
// so new description fields get non-colliding keys (description2, description3, ...).
|
|
848
|
-
let descCount = Array.from(usedKeys).filter(
|
|
983
|
+
let descCount = Array.from(usedKeys).filter(
|
|
984
|
+
(k) => k === 'description' || /^description\d+$/.test(k),
|
|
985
|
+
).length
|
|
849
986
|
walkAst(ast, (node) => {
|
|
850
987
|
if (node.type !== 'element') return
|
|
851
988
|
if (node.name !== 'p' && node.name !== 'div') return
|
|
@@ -853,19 +990,28 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
853
990
|
const classVal = getClassValue(node)
|
|
854
991
|
const serialized = serializeNode(node)
|
|
855
992
|
if (/uppercase|tracking-widest/.test(classVal) && serialized.length < 250) return
|
|
856
|
-
if (/text-2xl|text-3xl|text-\[11px\]|text-\[10px\]|text-\[9px\]|text-\[8px\]/.test(classVal))
|
|
993
|
+
if (/text-2xl|text-3xl|text-\[11px\]|text-\[10px\]|text-\[9px\]|text-\[8px\]/.test(classVal))
|
|
994
|
+
return
|
|
857
995
|
if (node.name === 'div' && (containsElement(node, 'a') || containsElement(node, 'button'))) {
|
|
858
996
|
// Allow mixed-text divs (callouts with inline links) — skip only pure-element
|
|
859
997
|
// containers like nav/card wrappers that have no direct text nodes.
|
|
860
998
|
const hasMixedText = (node.children ?? []).some(
|
|
861
|
-
c => c.type === 'text' && (c.value ?? '').trim().length > 0,
|
|
999
|
+
(c) => c.type === 'text' && (c.value ?? '').trim().length > 0,
|
|
862
1000
|
)
|
|
863
1001
|
if (!hasMixedText) return
|
|
864
1002
|
}
|
|
865
|
-
if (
|
|
1003
|
+
if (
|
|
1004
|
+
node.name === 'div' &&
|
|
1005
|
+
['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].some((h) => containsElement(node, h))
|
|
1006
|
+
)
|
|
1007
|
+
return
|
|
866
1008
|
if (node.name === 'div') {
|
|
867
|
-
const contentChildren = (node.children ?? []).filter(
|
|
868
|
-
|
|
1009
|
+
const contentChildren = (node.children ?? []).filter(
|
|
1010
|
+
(c) => c.type !== 'text' || (c.value ?? '').trim().length > 0,
|
|
1011
|
+
)
|
|
1012
|
+
const hasOnlyElementChildren =
|
|
1013
|
+
contentChildren.length > 0 &&
|
|
1014
|
+
contentChildren.every((c) => c.type === 'element' || c.type === 'component')
|
|
869
1015
|
if (hasOnlyElementChildren) return
|
|
870
1016
|
}
|
|
871
1017
|
const text = extractTextContent(node, true).replace(/\s+/g, ' ').trim()
|
|
@@ -873,11 +1019,17 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
873
1019
|
descCount++
|
|
874
1020
|
const descOpts: Record<string, unknown> = { multiline: true }
|
|
875
1021
|
if (hasInlineFormatting(node)) descOpts.formatting = true
|
|
876
|
-
addField(
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1022
|
+
addField(
|
|
1023
|
+
{
|
|
1024
|
+
key: numberedKey('description', descCount),
|
|
1025
|
+
type: 'text',
|
|
1026
|
+
label: numberedLabel('Beschreibung', descCount),
|
|
1027
|
+
confidence: 'medium',
|
|
1028
|
+
defaultValue: text,
|
|
1029
|
+
options: descOpts,
|
|
1030
|
+
},
|
|
1031
|
+
nodeOffset(node),
|
|
1032
|
+
)
|
|
881
1033
|
})
|
|
882
1034
|
|
|
883
1035
|
// ── 4. RICH TEXT (set:html) ────────────────────────────────────────────
|
|
@@ -893,11 +1045,17 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
893
1045
|
const strMatch = expr.match(/['"]([^'"]+)['"]/)
|
|
894
1046
|
const fallbackMatch = expr.match(/\?\?\s*['"]([^'"]+)['"]/)
|
|
895
1047
|
const value = fallbackMatch?.[1] ?? strMatch?.[1] ?? ''
|
|
896
|
-
addField(
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1048
|
+
addField(
|
|
1049
|
+
{
|
|
1050
|
+
key: numberedKey('richText', richCount),
|
|
1051
|
+
type: 'text',
|
|
1052
|
+
label: numberedLabel('Rich Text', richCount),
|
|
1053
|
+
confidence: 'high',
|
|
1054
|
+
defaultValue: value,
|
|
1055
|
+
options: { multiline: true, formatting: true },
|
|
1056
|
+
},
|
|
1057
|
+
nodeOffset(node),
|
|
1058
|
+
)
|
|
901
1059
|
}
|
|
902
1060
|
})
|
|
903
1061
|
|
|
@@ -912,11 +1070,16 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
912
1070
|
const text = extractTextContent(node, true).replace(/\s+/g, ' ').trim()
|
|
913
1071
|
if (text.length >= 2 && text.length <= 60) {
|
|
914
1072
|
ctaCount++
|
|
915
|
-
addField(
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1073
|
+
addField(
|
|
1074
|
+
{
|
|
1075
|
+
key: numberedKey('ctaText', ctaCount),
|
|
1076
|
+
type: 'text',
|
|
1077
|
+
label: numberedLabel('Button Text', ctaCount),
|
|
1078
|
+
confidence: 'medium',
|
|
1079
|
+
defaultValue: text,
|
|
1080
|
+
},
|
|
1081
|
+
nodeOffset(node),
|
|
1082
|
+
)
|
|
920
1083
|
}
|
|
921
1084
|
})
|
|
922
1085
|
|
|
@@ -940,11 +1103,16 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
940
1103
|
if (!href) return
|
|
941
1104
|
if (href.startsWith('#') || href.startsWith('javascript:')) return
|
|
942
1105
|
linkCount++
|
|
943
|
-
addField(
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1106
|
+
addField(
|
|
1107
|
+
{
|
|
1108
|
+
key: numberedKey('ctaLink', linkCount),
|
|
1109
|
+
type: 'text',
|
|
1110
|
+
label: numberedLabel('Button Link', linkCount),
|
|
1111
|
+
confidence: 'medium',
|
|
1112
|
+
defaultValue: href,
|
|
1113
|
+
},
|
|
1114
|
+
nodeOffset(node),
|
|
1115
|
+
)
|
|
948
1116
|
})
|
|
949
1117
|
|
|
950
1118
|
// ── 7. IMAGES ──────────────────────────────────────────────────────────
|
|
@@ -965,22 +1133,32 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
965
1133
|
}
|
|
966
1134
|
if (src) {
|
|
967
1135
|
imgCount++
|
|
968
|
-
addField(
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1136
|
+
addField(
|
|
1137
|
+
{
|
|
1138
|
+
key: numberedKey('image', imgCount),
|
|
1139
|
+
type: 'image',
|
|
1140
|
+
label: numberedLabel('Bild', imgCount),
|
|
1141
|
+
confidence: 'high',
|
|
1142
|
+
defaultValue: { path: src.trim(), alt: '' },
|
|
1143
|
+
},
|
|
1144
|
+
nodeOffset(node),
|
|
1145
|
+
)
|
|
973
1146
|
}
|
|
974
1147
|
}
|
|
975
1148
|
if (tagName === 'img' || tagName === 'Image') {
|
|
976
1149
|
const altAttr = getAttr(node, 'alt')
|
|
977
1150
|
if (altAttr && altAttr.kind === 'quoted' && altAttr.value.trim().length >= 2) {
|
|
978
1151
|
altCount++
|
|
979
|
-
addField(
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1152
|
+
addField(
|
|
1153
|
+
{
|
|
1154
|
+
key: numberedKey('imageAlt', altCount),
|
|
1155
|
+
type: 'text',
|
|
1156
|
+
label: numberedLabel('Bild Alt-Text', altCount),
|
|
1157
|
+
confidence: 'medium',
|
|
1158
|
+
defaultValue: altAttr.value.trim(),
|
|
1159
|
+
},
|
|
1160
|
+
nodeOffset(node),
|
|
1161
|
+
)
|
|
984
1162
|
}
|
|
985
1163
|
}
|
|
986
1164
|
})
|
|
@@ -994,10 +1172,15 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
994
1172
|
if (svgSource.length < 100) return
|
|
995
1173
|
if (parentNode && (parentNode.name === 'a' || parentNode.name === 'button')) return
|
|
996
1174
|
iconCount++
|
|
997
|
-
addField(
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1175
|
+
addField(
|
|
1176
|
+
{
|
|
1177
|
+
key: numberedKey('icon', iconCount),
|
|
1178
|
+
type: 'icon',
|
|
1179
|
+
label: numberedLabel('Icon', iconCount),
|
|
1180
|
+
confidence: 'low',
|
|
1181
|
+
},
|
|
1182
|
+
nodeOffset(node),
|
|
1183
|
+
)
|
|
1001
1184
|
})
|
|
1002
1185
|
let foundIconProp = false
|
|
1003
1186
|
walkAst(ast, (node) => {
|
|
@@ -1016,14 +1199,18 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1016
1199
|
walkAst(ast, (node) => {
|
|
1017
1200
|
if (node.type !== 'expression') return
|
|
1018
1201
|
const exprCode = (node.children ?? []).map((c) => c.value ?? '').join('')
|
|
1019
|
-
const inlineArrayMatch = exprCode.match(
|
|
1202
|
+
const inlineArrayMatch = exprCode.match(
|
|
1203
|
+
/^\s*\[([\s\S]*?)\]\s*\.map\s*\(\s*\(?\s*(\{[^}]*\}|\w+)/,
|
|
1204
|
+
)
|
|
1020
1205
|
if (!inlineArrayMatch) return
|
|
1021
1206
|
arrayCount++
|
|
1022
1207
|
const arrayContent = inlineArrayMatch[1]!
|
|
1023
1208
|
const callbackParam = inlineArrayMatch[2]!
|
|
1024
1209
|
let objectKeys: string[] = []
|
|
1025
1210
|
if (callbackParam.startsWith('{')) {
|
|
1026
|
-
objectKeys = callbackParam
|
|
1211
|
+
objectKeys = callbackParam
|
|
1212
|
+
.replace(/[{}]/g, '')
|
|
1213
|
+
.split(',')
|
|
1027
1214
|
.map((p: string) => p.trim().split(':')[0]!.trim())
|
|
1028
1215
|
.filter((p: string) => p && !p.startsWith('...'))
|
|
1029
1216
|
} else {
|
|
@@ -1042,25 +1229,39 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1042
1229
|
}
|
|
1043
1230
|
}
|
|
1044
1231
|
const isObjectArray = objectKeys.length > 0
|
|
1045
|
-
const isStringArray =
|
|
1232
|
+
const isStringArray =
|
|
1233
|
+
/^\s*'[^']*'\s*,/.test(arrayContent) || /^\s*"[^"]*"\s*,/.test(arrayContent)
|
|
1046
1234
|
const arrayDefaultValue = extractInlineArrayValues(arrayContent, isObjectArray, isStringArray)
|
|
1047
1235
|
if (isObjectArray && !isStringArray) {
|
|
1048
1236
|
const innerFields: InferredField[] = objectKeys.map((prop: string) => ({
|
|
1049
|
-
key: prop,
|
|
1237
|
+
key: prop,
|
|
1238
|
+
type: inferInnerFieldType(prop),
|
|
1239
|
+
label: camelToLabel(prop),
|
|
1240
|
+
confidence: 'medium' as const,
|
|
1050
1241
|
}))
|
|
1051
|
-
addField(
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1242
|
+
addField(
|
|
1243
|
+
{
|
|
1244
|
+
key: numberedKey('items', arrayCount),
|
|
1245
|
+
type: 'array',
|
|
1246
|
+
label: numberedLabel('Liste', arrayCount),
|
|
1247
|
+
confidence: 'high',
|
|
1248
|
+
defaultValue: arrayDefaultValue,
|
|
1249
|
+
options: { arrayItem: { type: 'object', fields: innerFields } },
|
|
1250
|
+
},
|
|
1251
|
+
nodeOffset(node),
|
|
1252
|
+
)
|
|
1057
1253
|
} else {
|
|
1058
|
-
addField(
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1254
|
+
addField(
|
|
1255
|
+
{
|
|
1256
|
+
key: numberedKey('items', arrayCount),
|
|
1257
|
+
type: 'array',
|
|
1258
|
+
label: numberedLabel('Liste', arrayCount),
|
|
1259
|
+
confidence: 'high',
|
|
1260
|
+
defaultValue: arrayDefaultValue,
|
|
1261
|
+
options: { arrayItem: { type: 'text' } },
|
|
1262
|
+
},
|
|
1263
|
+
nodeOffset(node),
|
|
1264
|
+
)
|
|
1064
1265
|
}
|
|
1065
1266
|
})
|
|
1066
1267
|
|
|
@@ -1079,7 +1280,7 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1079
1280
|
walkAst(node, (n) => {
|
|
1080
1281
|
if (hasMap) return
|
|
1081
1282
|
if (n.type === 'expression') {
|
|
1082
|
-
const code = (n.children ?? []).map(c => c.value ?? '').join('')
|
|
1283
|
+
const code = (n.children ?? []).map((c) => c.value ?? '').join('')
|
|
1083
1284
|
if (/\.map\s*\(/.test(code)) hasMap = true
|
|
1084
1285
|
}
|
|
1085
1286
|
})
|
|
@@ -1137,12 +1338,19 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1137
1338
|
}
|
|
1138
1339
|
if (listItems.length < 1) return
|
|
1139
1340
|
staticListCount++
|
|
1140
|
-
addField(
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1341
|
+
addField(
|
|
1342
|
+
{
|
|
1343
|
+
key: numberedKey('items', arrayCount + staticListCount),
|
|
1344
|
+
type: 'array',
|
|
1345
|
+
label: numberedLabel('Liste', arrayCount + staticListCount),
|
|
1346
|
+
confidence: 'high',
|
|
1347
|
+
defaultValue: listItems,
|
|
1348
|
+
options: {
|
|
1349
|
+
arrayItem: { type: 'text', ...(listHasFormatting ? { formatting: true } : {}) },
|
|
1350
|
+
},
|
|
1351
|
+
},
|
|
1352
|
+
nodeOffset(node),
|
|
1353
|
+
)
|
|
1146
1354
|
})
|
|
1147
1355
|
|
|
1148
1356
|
// ── 10. COMPONENT PROPS (content-bearing) ─────────────────────────────
|
|
@@ -1160,28 +1368,53 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1160
1368
|
if (attr.kind === 'quoted') {
|
|
1161
1369
|
const propName = attr.name
|
|
1162
1370
|
const propValue = attr.value
|
|
1163
|
-
if (
|
|
1164
|
-
|
|
1371
|
+
if (
|
|
1372
|
+
/^(class|className|id|style|type|role|width|height|viewBox|fill|stroke|xmlns|d|cx|cy|r|rx|ry|x|y|x1|y1|x2|y2)$/.test(
|
|
1373
|
+
propName,
|
|
1374
|
+
)
|
|
1375
|
+
)
|
|
1376
|
+
continue
|
|
1377
|
+
if (
|
|
1378
|
+
/^(lang|language|filename|file|format|variant|size|loading|decoding|transition|client:.*)$/.test(
|
|
1379
|
+
propName,
|
|
1380
|
+
)
|
|
1381
|
+
)
|
|
1382
|
+
continue
|
|
1165
1383
|
if (/^(aria-|data-)/.test(propName)) continue
|
|
1166
1384
|
if (propValue.length < 2) continue
|
|
1167
1385
|
if (propValue === 'true' || propValue === 'false' || /^\d+$/.test(propValue)) continue
|
|
1168
1386
|
if (propValue.includes('/') && !propValue.includes(' ')) continue
|
|
1169
|
-
addField(
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1387
|
+
addField(
|
|
1388
|
+
{
|
|
1389
|
+
key: propName,
|
|
1390
|
+
type: propName === 'icon' ? 'icon' : propName === 'src' ? 'image' : 'text',
|
|
1391
|
+
label: camelToLabel(propName),
|
|
1392
|
+
confidence: 'medium',
|
|
1393
|
+
defaultValue: propValue,
|
|
1394
|
+
},
|
|
1395
|
+
nodeOffset(node),
|
|
1396
|
+
)
|
|
1174
1397
|
} else if (attr.kind === 'expression') {
|
|
1175
1398
|
const propName = attr.name
|
|
1176
|
-
if (
|
|
1399
|
+
if (
|
|
1400
|
+
/^(class|className|id|style|type|role|lang|language|filename|file|format|variant|size|loading|decoding)$/.test(
|
|
1401
|
+
propName,
|
|
1402
|
+
)
|
|
1403
|
+
)
|
|
1404
|
+
continue
|
|
1177
1405
|
if (/^\s*\w+\s*$/.test(attr.value)) continue
|
|
1178
1406
|
const fallbackMatch = attr.value.match(/\?\?\s*['"]([^'"]+)['"]/)
|
|
1179
1407
|
if (fallbackMatch) {
|
|
1180
|
-
addField(
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1408
|
+
addField(
|
|
1409
|
+
{
|
|
1410
|
+
key: propName,
|
|
1411
|
+
type: propName === 'icon' ? 'icon' : propName === 'src' ? 'image' : 'text',
|
|
1412
|
+
label: camelToLabel(propName),
|
|
1413
|
+
confidence: 'medium',
|
|
1414
|
+
defaultValue: fallbackMatch[1]!,
|
|
1415
|
+
},
|
|
1416
|
+
nodeOffset(node),
|
|
1417
|
+
)
|
|
1185
1418
|
}
|
|
1186
1419
|
}
|
|
1187
1420
|
}
|
|
@@ -1209,7 +1442,8 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1209
1442
|
if (/^(class|className|id|style|type|role)$/.test(name)) continue
|
|
1210
1443
|
if (/^(aria-|data-)/.test(name)) continue
|
|
1211
1444
|
if (attr.kind === 'quoted') {
|
|
1212
|
-
allProps.add(name)
|
|
1445
|
+
allProps.add(name)
|
|
1446
|
+
item[name] = attr.value
|
|
1213
1447
|
} else if (attr.kind === 'expression') {
|
|
1214
1448
|
allProps.add(name)
|
|
1215
1449
|
const expr = attr.value.trim()
|
|
@@ -1224,20 +1458,45 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1224
1458
|
if (Object.keys(item).length > 0) instanceValues.push(item)
|
|
1225
1459
|
}
|
|
1226
1460
|
if (allProps.size > 0) {
|
|
1227
|
-
const cardKey =
|
|
1461
|
+
const cardKey =
|
|
1462
|
+
compName
|
|
1463
|
+
.replace(/([A-Z])/g, (_m: string, c: string, i: number) =>
|
|
1464
|
+
i === 0 ? c.toLowerCase() : '_' + c.toLowerCase(),
|
|
1465
|
+
)
|
|
1466
|
+
.replace(/_/g, '') + 's'
|
|
1228
1467
|
if (!usedKeys.has(cardKey)) {
|
|
1229
1468
|
const innerFields: InferredField[] = [...allProps].map((p) => {
|
|
1230
|
-
const isArray = instanceValues.some(iv => Array.isArray(iv[p]))
|
|
1469
|
+
const isArray = instanceValues.some((iv) => Array.isArray(iv[p]))
|
|
1231
1470
|
if (isArray) {
|
|
1232
|
-
return {
|
|
1471
|
+
return {
|
|
1472
|
+
key: p,
|
|
1473
|
+
type: 'array' as const,
|
|
1474
|
+
label: camelToLabel(p),
|
|
1475
|
+
confidence: 'medium' as const,
|
|
1476
|
+
options: { arrayItem: { type: 'text' as const } },
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return {
|
|
1480
|
+
key: p,
|
|
1481
|
+
type: inferInnerFieldType(p),
|
|
1482
|
+
label: camelToLabel(p),
|
|
1483
|
+
confidence: 'medium' as const,
|
|
1233
1484
|
}
|
|
1234
|
-
return { key: p, type: inferInnerFieldType(p), label: camelToLabel(p), confidence: 'medium' as const }
|
|
1235
1485
|
})
|
|
1236
|
-
addField(
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1486
|
+
addField(
|
|
1487
|
+
{
|
|
1488
|
+
key: cardKey,
|
|
1489
|
+
type: 'array',
|
|
1490
|
+
label: `${compName} Liste`,
|
|
1491
|
+
confidence: 'medium',
|
|
1492
|
+
defaultValue: instanceValues.length > 0 ? instanceValues : undefined,
|
|
1493
|
+
options: {
|
|
1494
|
+
arrayItem: { type: 'object', fields: innerFields },
|
|
1495
|
+
sourceComponent: compName,
|
|
1496
|
+
},
|
|
1497
|
+
},
|
|
1498
|
+
nodeOffset(instances[0]!),
|
|
1499
|
+
)
|
|
1241
1500
|
}
|
|
1242
1501
|
}
|
|
1243
1502
|
}
|
|
@@ -1249,7 +1508,7 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1249
1508
|
const instanceCount = group.instances.length
|
|
1250
1509
|
|
|
1251
1510
|
// Build instance bounds
|
|
1252
|
-
const instanceBounds: RepeatedGroupInstance[] = group.instances.map(inst => ({
|
|
1511
|
+
const instanceBounds: RepeatedGroupInstance[] = group.instances.map((inst) => ({
|
|
1253
1512
|
start: nodeOffset(inst),
|
|
1254
1513
|
end: nodeEnd(inst),
|
|
1255
1514
|
}))
|
|
@@ -1267,7 +1526,7 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1267
1526
|
|
|
1268
1527
|
// Phase 1b: Use MAJORITY structure as canonical (most common tag sequence)
|
|
1269
1528
|
// This avoids using an instance with optional elements as the template.
|
|
1270
|
-
const signatures = instanceFingerprints.map(fp => fp.map(i => i.tag).join(','))
|
|
1529
|
+
const signatures = instanceFingerprints.map((fp) => fp.map((i) => i.tag).join(','))
|
|
1271
1530
|
const sigCounts = new Map<string, number>()
|
|
1272
1531
|
for (const sig of signatures) sigCounts.set(sig, (sigCounts.get(sig) ?? 0) + 1)
|
|
1273
1532
|
const mostCommonSig = [...sigCounts.entries()].sort((a, b) => b[1] - a[1])[0]![0]
|
|
@@ -1351,7 +1610,8 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1351
1610
|
// For links, also extract link text as a separate field
|
|
1352
1611
|
if (isLink) {
|
|
1353
1612
|
typeCounts['linkText'] = (typeCounts['linkText'] ?? 0) + 1
|
|
1354
|
-
const ltKey =
|
|
1613
|
+
const ltKey =
|
|
1614
|
+
typeCounts['linkText'] === 1 ? 'linkText' : `linkText${typeCounts['linkText']}`
|
|
1355
1615
|
const ltPositions: Array<FieldPosition | null> = []
|
|
1356
1616
|
const ltDefaults: unknown[] = []
|
|
1357
1617
|
|
|
@@ -1397,8 +1657,14 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1397
1657
|
const tag = item.tag
|
|
1398
1658
|
const isArray = tag === '__array__'
|
|
1399
1659
|
const isLink = tag === 'a'
|
|
1400
|
-
|
|
1401
|
-
|
|
1660
|
+
const fieldType: 'text' | 'array' | 'link' = isArray ? 'array' : isLink ? 'link' : 'text'
|
|
1661
|
+
const keyBase = /^h[1-6]$/.test(tag)
|
|
1662
|
+
? 'heading'
|
|
1663
|
+
: isLink
|
|
1664
|
+
? 'link'
|
|
1665
|
+
: isArray
|
|
1666
|
+
? 'list'
|
|
1667
|
+
: 'text'
|
|
1402
1668
|
typeCounts[keyBase] = (typeCounts[keyBase] ?? 0) + 1
|
|
1403
1669
|
const key = typeCounts[keyBase] === 1 ? keyBase : `${keyBase}${typeCounts[keyBase]}`
|
|
1404
1670
|
|
|
@@ -1419,12 +1685,20 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1419
1685
|
|
|
1420
1686
|
if (isLink) {
|
|
1421
1687
|
typeCounts['linkText'] = (typeCounts['linkText'] ?? 0) + 1
|
|
1422
|
-
const ltKey =
|
|
1688
|
+
const ltKey =
|
|
1689
|
+
typeCounts['linkText'] === 1 ? 'linkText' : `linkText${typeCounts['linkText']}`
|
|
1423
1690
|
const ltPositions: Array<FieldPosition | null> = new Array(instanceCount).fill(null)
|
|
1424
1691
|
const ltDefaults: unknown[] = new Array(instanceCount).fill(null)
|
|
1425
1692
|
ltPositions[ii] = positions[ii] ?? null
|
|
1426
1693
|
ltDefaults[ii] = item.text || null
|
|
1427
|
-
innerFields.push({
|
|
1694
|
+
innerFields.push({
|
|
1695
|
+
key: ltKey,
|
|
1696
|
+
type: 'text',
|
|
1697
|
+
tag: 'a',
|
|
1698
|
+
required: false,
|
|
1699
|
+
positions: ltPositions,
|
|
1700
|
+
defaultValues: ltDefaults,
|
|
1701
|
+
})
|
|
1428
1702
|
}
|
|
1429
1703
|
}
|
|
1430
1704
|
}
|
|
@@ -1451,7 +1725,7 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1451
1725
|
}
|
|
1452
1726
|
|
|
1453
1727
|
// Build inner field definitions for the field schema
|
|
1454
|
-
const innerFieldDefs: InferredField[] = innerFields.map(f => {
|
|
1728
|
+
const innerFieldDefs: InferredField[] = innerFields.map((f) => {
|
|
1455
1729
|
if (f.type === 'array') {
|
|
1456
1730
|
return {
|
|
1457
1731
|
key: f.key,
|
|
@@ -1463,32 +1737,36 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1463
1737
|
}
|
|
1464
1738
|
return {
|
|
1465
1739
|
key: f.key,
|
|
1466
|
-
type: f.type === 'link' ? 'text' as const : 'text' as const,
|
|
1740
|
+
type: f.type === 'link' ? ('text' as const) : ('text' as const),
|
|
1467
1741
|
label: f.label ?? camelToLabel(f.key),
|
|
1468
1742
|
confidence: 'medium' as const,
|
|
1469
1743
|
}
|
|
1470
1744
|
})
|
|
1471
1745
|
|
|
1472
1746
|
// Add as top-level field
|
|
1473
|
-
const groupLabel =
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1747
|
+
const groupLabel =
|
|
1748
|
+
group.tag === 'tr'
|
|
1749
|
+
? 'Tabellen-Zeilen'
|
|
1750
|
+
: `${group.tag.charAt(0).toUpperCase() + group.tag.slice(1)} Liste`
|
|
1751
|
+
addField(
|
|
1752
|
+
{
|
|
1753
|
+
key: fieldKey,
|
|
1754
|
+
type: 'array',
|
|
1755
|
+
label: groupLabel,
|
|
1756
|
+
confidence: 'high',
|
|
1757
|
+
defaultValue,
|
|
1758
|
+
options: {
|
|
1759
|
+
arrayItem: { type: 'object', fields: innerFieldDefs },
|
|
1760
|
+
_repeatedTag: group.tag,
|
|
1761
|
+
_instanceCount: instanceCount,
|
|
1762
|
+
} as any,
|
|
1763
|
+
},
|
|
1764
|
+
instanceBounds[0]!.start,
|
|
1765
|
+
)
|
|
1488
1766
|
|
|
1489
1767
|
// Collect class attributes per instance (AST-based, for dynamic class detection in patcher)
|
|
1490
1768
|
// Positions are relative to `template` here; posAdjust is applied after extractTemplateFields returns
|
|
1491
|
-
const classAttrs = group.instances.map(inst => collectClassAttrs(inst, '', template))
|
|
1769
|
+
const classAttrs = group.instances.map((inst) => collectClassAttrs(inst, '', template))
|
|
1492
1770
|
|
|
1493
1771
|
// Store RepeatedGroup for the patcher
|
|
1494
1772
|
repeatedGroups.push({
|
|
@@ -1510,7 +1788,7 @@ async function extractTemplateFields(template: string, frontmatter: string = '')
|
|
|
1510
1788
|
// ---------------------------------------------------------------------------
|
|
1511
1789
|
|
|
1512
1790
|
interface ContentItem {
|
|
1513
|
-
tag: string
|
|
1791
|
+
tag: string // h2, p, span, a, __array__
|
|
1514
1792
|
depth: number
|
|
1515
1793
|
node: AstNode
|
|
1516
1794
|
text: string
|
|
@@ -1590,7 +1868,7 @@ function walkContentNodes(
|
|
|
1590
1868
|
walkAst(child, (expr) => {
|
|
1591
1869
|
if (mapSource) return
|
|
1592
1870
|
if (expr.type !== 'expression') return
|
|
1593
|
-
const code = (expr.children ?? []).map(c => c.value ?? '').join('')
|
|
1871
|
+
const code = (expr.children ?? []).map((c) => c.value ?? '').join('')
|
|
1594
1872
|
const mapMatch = code.match(/(\w+)\.map\s*\(/)
|
|
1595
1873
|
if (mapMatch) mapSource = mapMatch[1]
|
|
1596
1874
|
})
|
|
@@ -1633,7 +1911,13 @@ function findMatchingItem(
|
|
|
1633
1911
|
allCanonical: ContentItem[],
|
|
1634
1912
|
consumed: Set<number>,
|
|
1635
1913
|
): ContentItem | null {
|
|
1636
|
-
const result = findMatchingItemCore(
|
|
1914
|
+
const result = findMatchingItemCore(
|
|
1915
|
+
targetFp,
|
|
1916
|
+
canonicalItem,
|
|
1917
|
+
canonicalIndex,
|
|
1918
|
+
allCanonical,
|
|
1919
|
+
consumed,
|
|
1920
|
+
)
|
|
1637
1921
|
if (result) consumed.add(result.idx)
|
|
1638
1922
|
return result?.item ?? null
|
|
1639
1923
|
}
|
|
@@ -1651,9 +1935,7 @@ function findMatchingItemPeek(
|
|
|
1651
1935
|
// For peek, we look for the already-consumed link node (it was consumed by the link field)
|
|
1652
1936
|
// So we search WITHOUT the consumed filter
|
|
1653
1937
|
const tag = canonicalItem.tag
|
|
1654
|
-
const candidates = targetFp
|
|
1655
|
-
.map((item, idx) => ({ item, idx }))
|
|
1656
|
-
.filter(c => c.item.tag === tag)
|
|
1938
|
+
const candidates = targetFp.map((item, idx) => ({ item, idx })).filter((c) => c.item.tag === tag)
|
|
1657
1939
|
if (candidates.length === 0) return null
|
|
1658
1940
|
if (candidates.length === 1) return candidates[0]!.item
|
|
1659
1941
|
const canonicalRelPos = canonicalIndex / Math.max(allCanonical.length - 1, 1)
|
|
@@ -1675,7 +1957,7 @@ function findMatchingItemCore(
|
|
|
1675
1957
|
const tag = canonicalItem.tag
|
|
1676
1958
|
const candidates = targetFp
|
|
1677
1959
|
.map((item, idx) => ({ item, idx }))
|
|
1678
|
-
.filter(c => c.item.tag === tag && !consumed.has(c.idx))
|
|
1960
|
+
.filter((c) => c.item.tag === tag && !consumed.has(c.idx))
|
|
1679
1961
|
|
|
1680
1962
|
if (candidates.length === 0) return null
|
|
1681
1963
|
if (candidates.length === 1) return candidates[0]!
|