@setzkasten-cms/astro-admin 1.4.0 → 1.4.1

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.
Files changed (153) hide show
  1. package/dist/api-routes/_auth-guard.d.ts +47 -0
  2. package/dist/api-routes/_auth-guard.js +18 -0
  3. package/dist/api-routes/_commit-trailers.d.ts +8 -0
  4. package/dist/api-routes/_commit-trailers.js +8 -0
  5. package/dist/api-routes/_feature-gate.d.ts +23 -0
  6. package/dist/api-routes/_feature-gate.js +7 -0
  7. package/dist/api-routes/_github-cache.d.ts +4 -0
  8. package/dist/api-routes/_github-cache.js +8 -0
  9. package/dist/api-routes/_github-token.d.ts +27 -0
  10. package/dist/api-routes/_github-token.js +8 -0
  11. package/dist/api-routes/_license-tier.d.ts +22 -0
  12. package/dist/api-routes/_license-tier.js +6 -0
  13. package/dist/api-routes/_pages-meta-store.d.ts +32 -0
  14. package/dist/api-routes/_pages-meta-store.js +9 -0
  15. package/dist/api-routes/_role-resolver.d.ts +15 -0
  16. package/dist/api-routes/_role-resolver.js +13 -0
  17. package/dist/api-routes/_session-cookie.d.ts +18 -0
  18. package/dist/api-routes/_session-cookie.js +6 -0
  19. package/dist/api-routes/_storage-config.d.ts +60 -0
  20. package/dist/api-routes/_storage-config.js +10 -0
  21. package/dist/api-routes/_vercel-origin.d.ts +16 -0
  22. package/dist/api-routes/_vercel-origin.js +6 -0
  23. package/dist/api-routes/_webhook-dispatcher.d.ts +13 -0
  24. package/dist/api-routes/_webhook-dispatcher.js +97 -0
  25. package/dist/api-routes/_webhook-signing.d.ts +11 -0
  26. package/dist/api-routes/_webhook-signing.js +6 -0
  27. package/dist/api-routes/_webhook-status-store.d.ts +19 -0
  28. package/dist/api-routes/_webhook-status-store.js +10 -0
  29. package/dist/api-routes/_website-resolver.d.ts +49 -0
  30. package/dist/api-routes/_website-resolver.js +14 -0
  31. package/dist/api-routes/_websites-store.d.ts +30 -0
  32. package/dist/api-routes/_websites-store.js +11 -0
  33. package/dist/api-routes/asset-proxy.d.ts +12 -0
  34. package/dist/api-routes/asset-proxy.js +67 -0
  35. package/dist/api-routes/auth-callback.d.ts +9 -0
  36. package/dist/api-routes/auth-callback.js +68 -0
  37. package/dist/api-routes/auth-login.d.ts +11 -0
  38. package/dist/api-routes/auth-login.js +27 -0
  39. package/dist/api-routes/auth-logout.d.ts +10 -0
  40. package/dist/api-routes/auth-logout.js +13 -0
  41. package/dist/api-routes/auth-session.d.ts +9 -0
  42. package/dist/api-routes/auth-session.js +31 -0
  43. package/dist/api-routes/auth-setzkasten-login.d.ts +18 -0
  44. package/dist/api-routes/auth-setzkasten-login.js +74 -0
  45. package/dist/api-routes/catalog-add.d.ts +14 -0
  46. package/dist/api-routes/catalog-add.js +153 -0
  47. package/dist/api-routes/catalog-export.d.ts +13 -0
  48. package/dist/api-routes/catalog-export.js +71 -0
  49. package/dist/api-routes/catalog-helpers.d.ts +41 -0
  50. package/dist/api-routes/catalog-helpers.js +11 -0
  51. package/dist/api-routes/catalog-list.d.ts +11 -0
  52. package/dist/api-routes/catalog-list.js +12 -0
  53. package/dist/api-routes/config.d.ts +12 -0
  54. package/dist/api-routes/config.js +43 -0
  55. package/dist/api-routes/deploy-hook.d.ts +14 -0
  56. package/dist/api-routes/deploy-hook.js +52 -0
  57. package/dist/api-routes/editors.d.ts +29 -0
  58. package/dist/api-routes/editors.js +18 -0
  59. package/dist/api-routes/github-proxy.d.ts +12 -0
  60. package/dist/api-routes/github-proxy.js +82 -0
  61. package/dist/api-routes/global-config.d.ts +20 -0
  62. package/dist/api-routes/global-config.js +19 -0
  63. package/dist/api-routes/history-rollback.d.ts +22 -0
  64. package/dist/api-routes/history-rollback.js +111 -0
  65. package/dist/api-routes/history-version.d.ts +11 -0
  66. package/dist/api-routes/history-version.js +57 -0
  67. package/dist/api-routes/history.d.ts +13 -0
  68. package/dist/api-routes/history.js +85 -0
  69. package/dist/api-routes/icons-local.d.ts +28 -0
  70. package/dist/api-routes/icons-local.js +115 -0
  71. package/dist/api-routes/init-add-section.d.ts +23 -0
  72. package/dist/api-routes/init-add-section.js +396 -0
  73. package/dist/api-routes/init-apply.d.ts +11 -0
  74. package/dist/api-routes/init-apply.js +266 -0
  75. package/dist/api-routes/init-migrate.d.ts +16 -0
  76. package/dist/api-routes/init-migrate.js +205 -0
  77. package/dist/api-routes/init-scan-page.d.ts +39 -0
  78. package/dist/api-routes/init-scan-page.js +260 -0
  79. package/dist/api-routes/init-scan.d.ts +11 -0
  80. package/dist/api-routes/init-scan.js +128 -0
  81. package/dist/api-routes/migrate-to-multi.d.ts +26 -0
  82. package/dist/api-routes/migrate-to-multi.js +188 -0
  83. package/dist/api-routes/pages.d.ts +39 -0
  84. package/dist/api-routes/pages.js +88 -0
  85. package/dist/api-routes/section-add.d.ts +18 -0
  86. package/dist/api-routes/section-add.js +173 -0
  87. package/dist/api-routes/section-commit-pending.d.ts +18 -0
  88. package/dist/api-routes/section-commit-pending.js +207 -0
  89. package/dist/api-routes/section-delete.d.ts +15 -0
  90. package/dist/api-routes/section-delete.js +149 -0
  91. package/dist/api-routes/section-duplicate.d.ts +15 -0
  92. package/dist/api-routes/section-duplicate.js +143 -0
  93. package/dist/api-routes/section-management.d.ts +41 -0
  94. package/dist/api-routes/section-management.js +14 -0
  95. package/dist/api-routes/section-prepare-copy.d.ts +25 -0
  96. package/dist/api-routes/section-prepare-copy.js +69 -0
  97. package/dist/api-routes/section-prepare.d.ts +18 -0
  98. package/dist/api-routes/section-prepare.js +104 -0
  99. package/dist/api-routes/setup-github-app-bounce.d.ts +13 -0
  100. package/dist/api-routes/setup-github-app-bounce.js +45 -0
  101. package/dist/api-routes/setup-github-app-branches.d.ts +14 -0
  102. package/dist/api-routes/setup-github-app-branches.js +58 -0
  103. package/dist/api-routes/setup-github-app-callback.d.ts +15 -0
  104. package/dist/api-routes/setup-github-app-callback.js +45 -0
  105. package/dist/api-routes/setup-github-app-installed.d.ts +15 -0
  106. package/dist/api-routes/setup-github-app-installed.js +33 -0
  107. package/dist/api-routes/setup-github-app-repos.d.ts +17 -0
  108. package/dist/api-routes/setup-github-app-repos.js +41 -0
  109. package/dist/api-routes/setup-github-app.d.ts +15 -0
  110. package/dist/api-routes/setup-github-app.js +41 -0
  111. package/dist/api-routes/updater-check.d.ts +10 -0
  112. package/dist/api-routes/updater-check.js +37 -0
  113. package/dist/api-routes/updater-register.d.ts +14 -0
  114. package/dist/api-routes/updater-register.js +71 -0
  115. package/dist/api-routes/updater-transfer.d.ts +11 -0
  116. package/dist/api-routes/updater-transfer.js +37 -0
  117. package/dist/api-routes/updater-unbind.d.ts +17 -0
  118. package/dist/api-routes/updater-unbind.js +35 -0
  119. package/dist/api-routes/webhooks-status.d.ts +12 -0
  120. package/dist/api-routes/webhooks-status.js +22 -0
  121. package/dist/api-routes/webhooks-test.d.ts +13 -0
  122. package/dist/api-routes/webhooks-test.js +124 -0
  123. package/dist/api-routes/webhooks.d.ts +6 -0
  124. package/dist/api-routes/webhooks.js +148 -0
  125. package/dist/api-routes/websites-add.d.ts +15 -0
  126. package/dist/api-routes/websites-add.js +92 -0
  127. package/dist/api-routes/websites-list.d.ts +12 -0
  128. package/dist/api-routes/websites-list.js +35 -0
  129. package/dist/api-routes/websites-remove.d.ts +15 -0
  130. package/dist/api-routes/websites-remove.js +69 -0
  131. package/dist/chunk-35S35OIV.js +80 -0
  132. package/dist/chunk-45ARVNT3.js +25 -0
  133. package/dist/chunk-5PIMDP4N.js +25 -0
  134. package/dist/chunk-5ZFTG4BW.js +10 -0
  135. package/dist/chunk-6UIKVKED.js +51 -0
  136. package/dist/chunk-737TIZRU.js +9 -0
  137. package/dist/chunk-AM4DZXXM.js +120 -0
  138. package/dist/chunk-FXNOTESI.js +87 -0
  139. package/dist/chunk-GHNK2GFE.js +48 -0
  140. package/dist/chunk-GRG3LNKH.js +37 -0
  141. package/dist/chunk-INIWFKQ3.js +236 -0
  142. package/dist/chunk-JHY6XTLL.js +24 -0
  143. package/dist/chunk-K22A4ZBS.js +1574 -0
  144. package/dist/chunk-KH22FJO5.js +17 -0
  145. package/dist/chunk-NKDATSPA.js +43 -0
  146. package/dist/chunk-RHJONMLK.js +1267 -0
  147. package/dist/chunk-TJNJKPUL.js +11 -0
  148. package/dist/chunk-V6IMPVF3.js +120 -0
  149. package/dist/chunk-W3QHY5GW.js +19 -0
  150. package/dist/chunk-ZQDGGWJP.js +43 -0
  151. package/package.json +249 -53
  152. package/src/api-routes/__tests__/route-registry.test.ts +7 -1
  153. package/tsconfig.json +0 -9
@@ -0,0 +1,1574 @@
1
+ // src/init/astro-detector.ts
2
+ function findAstroPages(files, projectRoot) {
3
+ const pagesDir = projectRoot ? `${projectRoot}/src/pages/` : "src/pages/";
4
+ const pages = [];
5
+ for (const file of files) {
6
+ if (file.type !== "blob") continue;
7
+ if (!file.path.startsWith(pagesDir)) continue;
8
+ if (!file.path.endsWith(".astro")) continue;
9
+ const relativePath = file.path.slice(pagesDir.length);
10
+ if (relativePath.startsWith("api/")) continue;
11
+ if (relativePath.includes("[")) continue;
12
+ if (relativePath.includes("admin")) continue;
13
+ if (relativePath === "preview.astro") continue;
14
+ const name = relativePath.replace(/\.astro$/, "");
15
+ const isIndex = name === "index" || name.endsWith("/index");
16
+ const pageKey = isIndex ? name === "index" ? "index" : name.replace(/\/index$/, "") : name;
17
+ pages.push({
18
+ filePath: file.path,
19
+ pageKey,
20
+ label: pageKey === "index" ? "Startseite" : pageKey
21
+ });
22
+ }
23
+ pages.sort((a, b) => {
24
+ if (a.pageKey === "index") return -1;
25
+ if (b.pageKey === "index") return 1;
26
+ return a.pageKey.localeCompare(b.pageKey);
27
+ });
28
+ return pages;
29
+ }
30
+ function extractSectionImports(pageSource, pagePath, allFiles, projectRoot) {
31
+ const imports = [];
32
+ const importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g;
33
+ let match;
34
+ while ((match = importRegex.exec(pageSource)) !== null) {
35
+ const componentName = match[1];
36
+ const importPath = match[2];
37
+ const isSectionPath = importPath.includes("section");
38
+ const isSectionName = componentName.endsWith("Section");
39
+ if (!isSectionPath && !isSectionName) continue;
40
+ if (!importPath.endsWith(".astro") && !importPath.match(/\/[A-Z]/)) continue;
41
+ const sectionKey = deriveSectionKey(componentName);
42
+ const resolvedPath = resolveImportPath(importPath, pagePath, allFiles, projectRoot);
43
+ imports.push({
44
+ componentName,
45
+ importPath,
46
+ resolvedPath,
47
+ sectionKey
48
+ });
49
+ }
50
+ return imports;
51
+ }
52
+ function extractLayoutImport(pageSource, pagePath, allFiles, projectRoot) {
53
+ const importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g;
54
+ let match;
55
+ while ((match = importRegex.exec(pageSource)) !== null) {
56
+ const componentName = match[1];
57
+ const importPath = match[2];
58
+ if (!importPath.includes("layout") && !componentName.includes("Layout")) continue;
59
+ if (!importPath.endsWith(".astro") && !importPath.match(/\/[A-Z]/)) continue;
60
+ const resolvedPath = resolveImportPath(importPath, pagePath, allFiles, projectRoot);
61
+ return { componentName, importPath, resolvedPath };
62
+ }
63
+ return null;
64
+ }
65
+ function deriveSectionKey(componentName) {
66
+ const stripped = componentName.replace(/Section$/, "");
67
+ return stripped.charAt(0).toLowerCase() + stripped.slice(1);
68
+ }
69
+ function resolveImportPath(importPath, fromFile, allFiles, projectRoot) {
70
+ if (importPath.startsWith("@/") || importPath.startsWith("~/")) {
71
+ const aliasPath = importPath.replace(/^[@~]\//, "");
72
+ const candidates = [
73
+ `${projectRoot}/src/${aliasPath}`,
74
+ `${projectRoot}/src/${aliasPath}.astro`
75
+ ];
76
+ for (const candidate of candidates) {
77
+ if (allFiles.some((f) => f.path === candidate)) return candidate;
78
+ }
79
+ return null;
80
+ }
81
+ if (importPath.startsWith(".")) {
82
+ const fromDir = fromFile.substring(0, fromFile.lastIndexOf("/"));
83
+ const parts = importPath.split("/");
84
+ let currentDir = fromDir;
85
+ for (const part of parts) {
86
+ if (part === ".") continue;
87
+ if (part === "..") {
88
+ currentDir = currentDir.substring(0, currentDir.lastIndexOf("/"));
89
+ } else {
90
+ currentDir = `${currentDir}/${part}`;
91
+ }
92
+ }
93
+ const resolved = currentDir;
94
+ const candidates = [resolved, `${resolved}.astro`];
95
+ for (const candidate of candidates) {
96
+ if (allFiles.some((f) => f.path === candidate)) return candidate;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+
102
+ // src/init/astro-section-analyzer-v2.ts
103
+ import { parse } from "@astrojs/compiler";
104
+ import { inferFields } from "@setzkasten-cms/core/init";
105
+
106
+ // src/init/field-label-enricher.ts
107
+ function enrichFieldLabels(fields) {
108
+ for (const field of fields) {
109
+ const label = inferLabel(field);
110
+ if (label) field.label = label;
111
+ }
112
+ }
113
+ function inferLabel(field) {
114
+ if (/^h[1-6]$/.test(field.tag)) return "\xDCberschrift";
115
+ if (field.type === "array") return "Liste";
116
+ if (field.tag === "a" && field.type === "link") return "Link";
117
+ if (field.tag === "a" && field.type === "text") return "Link-Text";
118
+ const rawValues = field.defaultValues.filter((v) => typeof v === "string");
119
+ const cleanValues = rawValues.map((v) => stripSpecialChars(v));
120
+ if (rawValues.length === 0) return void 0;
121
+ if (rawValues.every((v) => v.trim().length > 0 && v.trim().length < 20 && !v.includes(" "))) {
122
+ return "Label";
123
+ }
124
+ if (rawValues.some((v) => /[€$£¥₹]/.test(v)) && rawValues.every((v) => v.length < 25)) {
125
+ return "Preis";
126
+ }
127
+ if (rawValues.every((v) => /\d/.test(v) && v.length < 25)) {
128
+ return "Preis";
129
+ }
130
+ if (cleanValues.every((v) => v.length > 40)) {
131
+ return "Beschreibung";
132
+ }
133
+ if (field.tag === "p" && cleanValues.every((v) => v.length > 15)) {
134
+ return "Beschreibung";
135
+ }
136
+ return void 0;
137
+ }
138
+ function stripSpecialChars(str) {
139
+ return str.replace(/[^a-zA-Z0-9äöüÄÖÜß\s]/g, "").trim();
140
+ }
141
+
142
+ // src/init/astro-section-analyzer-v2.ts
143
+ function walkAst(node, callback, parent = null) {
144
+ callback(node, parent);
145
+ if (node.children) {
146
+ for (const child of node.children) {
147
+ walkAst(child, callback, node);
148
+ }
149
+ }
150
+ }
151
+ async function analyzeAstroSection(source, sectionKey, componentName, componentPath, options) {
152
+ const isPageMode = options?.mode === "page";
153
+ const alreadyIntegrated = source.includes("data-sk-field") || source.includes("getSection(") || source.includes("setzkasten:content");
154
+ const { frontmatter, template, templateOffset } = splitAstroFile(source);
155
+ const variables = extractFrontmatterVariables(frontmatter).filter((name) => {
156
+ const propUsageRegex = new RegExp(`\\w+={${name}}`);
157
+ if (propUsageRegex.test(template)) return false;
158
+ const fallbackRegex = new RegExp(`(?:\\?\\?|\\|\\|)\\s*${name}\\b`);
159
+ if (fallbackRegex.test(frontmatter) || fallbackRegex.test(template)) return false;
160
+ if (!isPageMode) {
161
+ const hasMapUsage = new RegExp(`\\b${name}\\.map\\s*\\(`).test(template);
162
+ const hasNonMapUsage = new RegExp(`\\b${name}\\b(?!\\.map)`).test(template);
163
+ if (hasMapUsage && !hasNonMapUsage) return false;
164
+ }
165
+ return true;
166
+ });
167
+ const variableFields = inferFields(variables, template);
168
+ for (const field of variableFields) {
169
+ if (!field.defaultValue) {
170
+ field.defaultValue = extractFrontmatterValue(frontmatter, field.key);
171
+ }
172
+ }
173
+ const { fields: templateFields, repeatedGroups } = await extractTemplateFields(template, frontmatter);
174
+ const posAdjust = templateOffset;
175
+ for (const group of repeatedGroups) {
176
+ for (const inst of group.instances) {
177
+ inst.start += posAdjust;
178
+ inst.end += posAdjust;
179
+ }
180
+ for (const field of group.fields) {
181
+ for (let i = 0; i < field.positions.length; i++) {
182
+ const pos = field.positions[i];
183
+ if (pos) {
184
+ pos.offset += posAdjust;
185
+ }
186
+ }
187
+ }
188
+ }
189
+ for (const group of repeatedGroups) {
190
+ if (group.classAttrs) {
191
+ for (const instAttrs of group.classAttrs) {
192
+ for (const a of instAttrs) {
193
+ a.sourceOffset += posAdjust;
194
+ }
195
+ }
196
+ }
197
+ }
198
+ for (const group of repeatedGroups) {
199
+ enrichFieldLabels(group.fields);
200
+ }
201
+ for (const field of variableFields) {
202
+ const usageRegex = new RegExp(`\\{\\s*(?:\\w+\\.)?${field.key}(?:[.\\s}(])`, "s");
203
+ const usageMatch = usageRegex.exec(template);
204
+ if (usageMatch) {
205
+ ;
206
+ field._pos = usageMatch.index;
207
+ } else {
208
+ const mapUsage = template.indexOf(`${field.key}.map(`);
209
+ field._pos = mapUsage !== -1 ? mapUsage : Infinity;
210
+ }
211
+ }
212
+ const existingKeys = new Set(variableFields.map((f) => f.key));
213
+ const allFields = [
214
+ ...variableFields,
215
+ ...templateFields.filter((f) => !existingKeys.has(f.key))
216
+ ];
217
+ allFields.sort((a, b) => (a._pos ?? Infinity) - (b._pos ?? Infinity));
218
+ const fields = allFields.map(({ ...field }) => {
219
+ delete field._pos;
220
+ return field;
221
+ });
222
+ const analyzerResult = {
223
+ sectionKey,
224
+ componentName,
225
+ componentPath,
226
+ alreadyIntegrated,
227
+ fields,
228
+ repeatedGroups,
229
+ frontmatter,
230
+ template
231
+ };
232
+ return {
233
+ key: sectionKey,
234
+ componentName,
235
+ componentPath,
236
+ fields,
237
+ alreadyIntegrated,
238
+ _analyzerResult: analyzerResult
239
+ };
240
+ }
241
+ function splitAstroFile(source) {
242
+ if (!source.startsWith("---")) return { frontmatter: "", template: source, templateOffset: 0 };
243
+ const endIdx = source.indexOf("\n---", 3);
244
+ if (endIdx === -1) return { frontmatter: "", template: source, templateOffset: 0 };
245
+ const frontmatter = source.slice(4, endIdx);
246
+ let templateStart = endIdx + 4;
247
+ while (templateStart < source.length && source[templateStart] === " ") templateStart++;
248
+ if (templateStart < source.length && source[templateStart] === "\n") templateStart++;
249
+ return { frontmatter, template: source.slice(templateStart), templateOffset: templateStart };
250
+ }
251
+ function stripTemplateLiterals(source) {
252
+ let result = "";
253
+ let i = 0;
254
+ while (i < source.length) {
255
+ if (source[i] === "`") {
256
+ i++;
257
+ let depth = 0;
258
+ while (i < source.length) {
259
+ if (source[i] === "\\") {
260
+ i += 2;
261
+ continue;
262
+ }
263
+ if (source[i] === "$" && source[i + 1] === "{") {
264
+ depth++;
265
+ i += 2;
266
+ continue;
267
+ }
268
+ if (source[i] === "{") {
269
+ depth++;
270
+ i++;
271
+ continue;
272
+ }
273
+ if (source[i] === "}" && depth > 0) {
274
+ depth--;
275
+ i++;
276
+ continue;
277
+ }
278
+ if (source[i] === "`" && depth === 0) {
279
+ i++;
280
+ break;
281
+ }
282
+ i++;
283
+ }
284
+ result += "``";
285
+ } else {
286
+ result += source[i];
287
+ i++;
288
+ }
289
+ }
290
+ return result;
291
+ }
292
+ function extractFrontmatterVariables(frontmatter) {
293
+ const stripped = stripTemplateLiterals(frontmatter);
294
+ const variables = [];
295
+ const constRegex = /(?:const|let)\s+(\w+)\s*=\s*(.*)/g;
296
+ let match;
297
+ while ((match = constRegex.exec(stripped)) !== null) {
298
+ const name = match[1];
299
+ const rhs = match[2]?.trim() ?? "";
300
+ if (isInternalVariable(name)) continue;
301
+ const charBefore = match.index > 0 ? stripped.slice(Math.max(0, match.index - 10), match.index) : "";
302
+ if (/export\s*$/.test(charBefore)) continue;
303
+ if (/\.\s*map\s*\(/.test(rhs) || /\w+\?\.\w+/.test(rhs)) continue;
304
+ if (/^\[/.test(rhs) && /^default/i.test(name)) continue;
305
+ if (/^\(|^\w+\s*=>/.test(rhs) && /=>/.test(rhs)) continue;
306
+ if (/\bget(?:Page|Pages|Section|CollectionEntry)\s*\(/.test(rhs)) continue;
307
+ variables.push(name);
308
+ }
309
+ const propsRegex = /const\s+\{\s*([^}]+)\}\s*=\s*Astro\.props/;
310
+ const propsMatch = frontmatter.match(propsRegex);
311
+ if (propsMatch) {
312
+ const props = propsMatch[1].split(",").map((p) => p.trim().split(":")[0].split("=")[0].trim()).filter((p) => p && !isInternalVariable(p));
313
+ variables.push(...props);
314
+ }
315
+ const interfaceRegex = /interface\s+Props\s*\{([^}]+)\}/s;
316
+ const interfaceMatch = frontmatter.match(interfaceRegex);
317
+ if (interfaceMatch) {
318
+ const fields = interfaceMatch[1].split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("//")).map((line) => line.match(/^(\w+)\??:/)?.[1]).filter((name) => !!name && !isInternalVariable(name));
319
+ variables.push(...fields);
320
+ }
321
+ return [...new Set(variables)];
322
+ }
323
+ function extractFrontmatterValue(frontmatter, varName) {
324
+ const arrayStartRegex = new RegExp(`(?:const|let)\\s+${varName}\\s*=\\s*\\[`);
325
+ const arrayStartMatch = arrayStartRegex.exec(frontmatter);
326
+ if (arrayStartMatch) {
327
+ const startIdx = arrayStartMatch.index + arrayStartMatch[0].length;
328
+ let depth = 1;
329
+ let endIdx = startIdx;
330
+ for (let i = startIdx; i < frontmatter.length && depth > 0; i++) {
331
+ if (frontmatter[i] === "[") depth++;
332
+ else if (frontmatter[i] === "]") depth--;
333
+ endIdx = i;
334
+ }
335
+ const content = frontmatter.slice(startIdx, endIdx);
336
+ const isObjectArray = /\{/.test(content);
337
+ const isStringArray = /^\s*['"]/.test(content.trim());
338
+ return extractInlineArrayValues(content, isObjectArray, isStringArray);
339
+ }
340
+ const strRegex = new RegExp(`(?:const|let)\\s+${varName}\\s*=\\s*['"]([^'"]+)['"]`);
341
+ const strMatch = frontmatter.match(strRegex);
342
+ if (strMatch) return strMatch[1];
343
+ const numRegex = new RegExp(`(?:const|let)\\s+${varName}\\s*=\\s*(\\d+(?:\\.\\d+)?)`);
344
+ const numMatch = frontmatter.match(numRegex);
345
+ if (numMatch) return Number(numMatch[1]);
346
+ return void 0;
347
+ }
348
+ function isInternalVariable(name) {
349
+ const skip = /* @__PURE__ */ new Set([
350
+ "Astro",
351
+ "props",
352
+ "data",
353
+ "class",
354
+ "className",
355
+ "style",
356
+ "id",
357
+ "slot",
358
+ "Fragment",
359
+ "Component",
360
+ "frontmatter",
361
+ "url",
362
+ "site",
363
+ "generator",
364
+ "redirect",
365
+ "response",
366
+ "request",
367
+ "cookies",
368
+ "params",
369
+ "slots"
370
+ ]);
371
+ return skip.has(name) || name.startsWith("_");
372
+ }
373
+ function buildByteToCharMap(source) {
374
+ const buf = Buffer.from(source, "utf-8");
375
+ if (buf.length === source.length) return (offset) => offset;
376
+ const map = new Array(buf.length + 1);
377
+ let byteIdx = 0;
378
+ for (let charIdx = 0; charIdx < source.length; charIdx++) {
379
+ const codePoint = source.codePointAt(charIdx);
380
+ const charByteLen = codePoint <= 127 ? 1 : codePoint <= 2047 ? 2 : codePoint <= 65535 ? 3 : 4;
381
+ for (let b = 0; b < charByteLen; b++) map[byteIdx + b] = charIdx;
382
+ byteIdx += charByteLen;
383
+ if (codePoint > 65535) charIdx++;
384
+ }
385
+ map[byteIdx] = source.length;
386
+ return (offset) => offset <= 0 ? 0 : offset >= map.length ? source.length : map[offset];
387
+ }
388
+ function convertAstPositions(node, b2c) {
389
+ if (node.position?.start) node.position.start.offset = b2c(node.position.start.offset);
390
+ if (node.position?.end) node.position.end.offset = b2c(node.position.end.offset);
391
+ if (node.attributes) {
392
+ for (const attr of node.attributes) {
393
+ if (attr.position?.start) attr.position.start.offset = b2c(attr.position.start.offset);
394
+ }
395
+ }
396
+ if (node.children) {
397
+ for (const child of node.children) convertAstPositions(child, b2c);
398
+ }
399
+ }
400
+ var WRAPPER_OFFSET = 8;
401
+ function nodeOffset(node) {
402
+ return (node.position?.start?.offset ?? Infinity) - WRAPPER_OFFSET;
403
+ }
404
+ function nodeEnd(node) {
405
+ return (node.position?.end?.offset ?? Infinity) - WRAPPER_OFFSET;
406
+ }
407
+ function getClassValue(node) {
408
+ if (!node.attributes) return "";
409
+ const classAttr = node.attributes.find((a) => a.name === "class" || a.name === "className");
410
+ return classAttr?.value ?? "";
411
+ }
412
+ function getAttr(node, name) {
413
+ return node.attributes?.find((a) => a.name === name);
414
+ }
415
+ function isAriaHidden(node) {
416
+ const attr = getAttr(node, "aria-hidden");
417
+ return attr?.value === "true";
418
+ }
419
+ function extractTextContent(node, stripCmsBound = false) {
420
+ let text = "";
421
+ if (node.type === "text") {
422
+ text += node.value ?? "";
423
+ } else if (node.type === "expression") {
424
+ const exprCode = (node.children ?? []).map((c) => c.value ?? "").join("");
425
+ if (stripCmsBound && /^\s*\(?\s*\w+\?\.\s*\w+/.test(exprCode)) return "";
426
+ const fallbackMatch = exprCode.match(/\?\?\s*['"]([^'"]+)['"]/);
427
+ if (fallbackMatch) text += fallbackMatch[1];
428
+ } else if (node.children) {
429
+ for (const child of node.children) {
430
+ text += extractTextContent(child, stripCmsBound);
431
+ }
432
+ }
433
+ return text;
434
+ }
435
+ var INLINE_FORMATTING_TAGS = /* @__PURE__ */ new Set([
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",
452
+ "span"
453
+ // color/style spans inside text content
454
+ ]);
455
+ function hasInlineFormatting(node) {
456
+ if (!node.children) return false;
457
+ for (const child of node.children) {
458
+ if (child.type === "element" && INLINE_FORMATTING_TAGS.has(child.name ?? "")) return true;
459
+ if (hasInlineFormatting(child)) return true;
460
+ }
461
+ return false;
462
+ }
463
+ function collectClassAttrs(node, path = "", source) {
464
+ const result = [];
465
+ if (node.type === "element" && node.attributes && source) {
466
+ const classAttr = node.attributes.find((a) => a.name === "class" && a.kind === "quoted");
467
+ if (classAttr && classAttr.value) {
468
+ const elemStart = nodeOffset(node);
469
+ const tagEndGuess = source.indexOf(">", elemStart);
470
+ if (tagEndGuess !== -1) {
471
+ const classIdx = source.indexOf('class="', elemStart);
472
+ if (classIdx !== -1 && classIdx < tagEndGuess) {
473
+ const sourceLength = 7 + classAttr.value.length + 1;
474
+ result.push({
475
+ path,
476
+ value: classAttr.value,
477
+ sourceOffset: classIdx,
478
+ sourceLength
479
+ });
480
+ }
481
+ }
482
+ }
483
+ }
484
+ if (node.children) {
485
+ const tagCounts = {};
486
+ for (const child of node.children) {
487
+ if (child.type === "element" && child.name) {
488
+ const tag = child.name;
489
+ const nth = tagCounts[tag] ?? 0;
490
+ tagCounts[tag] = nth + 1;
491
+ const childPath = path ? `${path}/${tag}:${nth}` : `${tag}:${nth}`;
492
+ result.push(...collectClassAttrs(child, childPath, source));
493
+ } else if (child.type === "expression" && child.children) {
494
+ for (const exprChild of child.children) {
495
+ if (exprChild.type === "element" && exprChild.name) {
496
+ const tag = exprChild.name;
497
+ const nth = tagCounts[tag] ?? 0;
498
+ tagCounts[tag] = nth + 1;
499
+ const childPath = path ? `${path}/${tag}:${nth}` : `${tag}:${nth}`;
500
+ result.push(...collectClassAttrs(exprChild, childPath, source));
501
+ }
502
+ }
503
+ }
504
+ }
505
+ }
506
+ return result;
507
+ }
508
+ function containsElement(node, tagName) {
509
+ if (node.type === "element" && node.name === tagName) return true;
510
+ if (node.children) {
511
+ for (const child of node.children) {
512
+ if (containsElement(child, tagName)) return true;
513
+ }
514
+ }
515
+ return false;
516
+ }
517
+ function serializeNode(node) {
518
+ if (node.type === "text") return node.value ?? "";
519
+ if (node.type === "expression") {
520
+ const inner = (node.children ?? []).map((c) => c.value ?? "").join("");
521
+ return `{${inner}}`;
522
+ }
523
+ let result = "";
524
+ if (node.type === "element" || node.type === "component") {
525
+ result += `<${node.name ?? ""}`;
526
+ for (const attr of node.attributes ?? []) {
527
+ if (attr.kind === "quoted") result += ` ${attr.name}="${attr.value}"`;
528
+ else if (attr.kind === "expression") result += ` ${attr.name}={${attr.value}}`;
529
+ else if (attr.kind === "empty") result += ` ${attr.name}`;
530
+ else result += ` ${attr.name}="${attr.value}"`;
531
+ }
532
+ result += ">";
533
+ }
534
+ for (const child of node.children ?? []) {
535
+ result += serializeNode(child);
536
+ }
537
+ if ((node.type === "element" || node.type === "component") && node.name) {
538
+ result += `</${node.name}>`;
539
+ }
540
+ return result;
541
+ }
542
+ function camelToLabel(str) {
543
+ return str.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
544
+ }
545
+ function inferInnerFieldType(name) {
546
+ const n = name.toLowerCase();
547
+ if (/icon/.test(n)) return "icon";
548
+ if (/image|img|photo|avatar|logo|thumbnail|src/.test(n)) return "image";
549
+ if (/color|colour/.test(n)) return "color";
550
+ if (/count|amount|number|quantity|total|rating|score|percent|order|index|size|width|height/.test(n)) return "number";
551
+ if (/^is[A-Z]/.test(name) || /^has[A-Z]/.test(name) || /enabled|disabled|visible|hidden|active|checked|selected|highlight|accent|featured/.test(n)) return "boolean";
552
+ return "text";
553
+ }
554
+ function extractInlineArrayValues(arrayContent, isObjectArray, isStringArray) {
555
+ if (isStringArray || !isObjectArray) {
556
+ const strings = [];
557
+ const strRegex = /['"]([^'"]+)['"]/g;
558
+ let sm;
559
+ while ((sm = strRegex.exec(arrayContent)) !== null) strings.push(sm[1]);
560
+ return strings;
561
+ }
562
+ const objects = [];
563
+ let i = 0;
564
+ while (i < arrayContent.length) {
565
+ if (arrayContent[i] === "{") {
566
+ const end = findMatchingBracket(arrayContent, i, "{", "}");
567
+ const objStr = arrayContent.slice(i + 1, end);
568
+ const obj = parseObjectLiteral(objStr);
569
+ if (Object.keys(obj).length > 0) objects.push(obj);
570
+ i = end + 1;
571
+ } else {
572
+ i++;
573
+ }
574
+ }
575
+ return objects;
576
+ }
577
+ function findMatchingBracket(source, start, open, close) {
578
+ let depth = 1;
579
+ let i = start + 1;
580
+ while (i < source.length && depth > 0) {
581
+ const ch = source[i];
582
+ if (ch === "'" || ch === '"') {
583
+ i++;
584
+ while (i < source.length && source[i] !== ch) i++;
585
+ } else if (ch === open) {
586
+ depth++;
587
+ } else if (ch === close) {
588
+ depth--;
589
+ }
590
+ i++;
591
+ }
592
+ return i - 1;
593
+ }
594
+ function parseObjectLiteral(objStr) {
595
+ const obj = {};
596
+ let i = 0;
597
+ while (i < objStr.length) {
598
+ while (i < objStr.length && /[\s,]/.test(objStr[i])) i++;
599
+ if (i >= objStr.length) break;
600
+ const keyMatch = objStr.slice(i).match(/^(\w+)\s*:\s*/);
601
+ if (!keyMatch) {
602
+ i++;
603
+ continue;
604
+ }
605
+ const key = keyMatch[1];
606
+ i += keyMatch[0].length;
607
+ const ch = objStr[i];
608
+ if (ch === "'" || ch === '"') {
609
+ let j = i + 1;
610
+ while (j < objStr.length && objStr[j] !== ch) j++;
611
+ obj[key] = objStr.slice(i + 1, j);
612
+ i = j + 1;
613
+ } else if (ch === "[") {
614
+ const end = findMatchingBracket(objStr, i, "[", "]");
615
+ const innerContent = objStr.slice(i + 1, end);
616
+ const hasObjects = /\{/.test(innerContent);
617
+ if (hasObjects) {
618
+ obj[key] = extractInlineArrayValues(innerContent, true, false);
619
+ } else {
620
+ const items = [];
621
+ const strRegex = /['"]([^'"]+)['"]/g;
622
+ let sm;
623
+ while ((sm = strRegex.exec(innerContent)) !== null) items.push(sm[1]);
624
+ obj[key] = items;
625
+ }
626
+ i = end + 1;
627
+ } else if (ch === "{") {
628
+ const end = findMatchingBracket(objStr, i, "{", "}");
629
+ obj[key] = parseObjectLiteral(objStr.slice(i + 1, end));
630
+ i = end + 1;
631
+ } else if (ch && /\d/.test(ch)) {
632
+ const numMatch = objStr.slice(i).match(/^(\d+(?:\.\d+)?)/);
633
+ if (numMatch) {
634
+ obj[key] = Number(numMatch[1]);
635
+ i += numMatch[0].length;
636
+ }
637
+ } else if (objStr.slice(i, i + 4) === "true") {
638
+ obj[key] = true;
639
+ i += 4;
640
+ } else if (objStr.slice(i, i + 5) === "false") {
641
+ obj[key] = false;
642
+ i += 5;
643
+ } else {
644
+ while (i < objStr.length && objStr[i] !== ",") i++;
645
+ }
646
+ }
647
+ return obj;
648
+ }
649
+ async function extractTemplateFields(template, frontmatter = "") {
650
+ const fields = [];
651
+ const usedKeys = /* @__PURE__ */ new Set();
652
+ const repeatedGroups = [];
653
+ let ast;
654
+ try {
655
+ const wrappedSource = `---
656
+ ---
657
+ ${template}`;
658
+ const result = await parse(wrappedSource);
659
+ ast = result.ast;
660
+ convertAstPositions(ast, buildByteToCharMap(wrappedSource));
661
+ } catch (err) {
662
+ console.error("[setzkasten] section-analyzer: parse() failed:", err);
663
+ return { fields, repeatedGroups };
664
+ }
665
+ function addField(field, pos) {
666
+ if (!usedKeys.has(field.key)) {
667
+ usedKeys.add(field.key);
668
+ let finalPos = pos ?? -1;
669
+ if (finalPos === -1) {
670
+ if (typeof field.defaultValue === "string" && field.defaultValue.length >= 3) {
671
+ finalPos = template.indexOf(field.defaultValue);
672
+ } else if (Array.isArray(field.defaultValue) && field.defaultValue.length > 0) {
673
+ const firstItem = typeof field.defaultValue[0] === "string" ? field.defaultValue[0] : typeof field.defaultValue[0] === "object" && field.defaultValue[0] ? Object.values(field.defaultValue[0])[0] : null;
674
+ if (typeof firstItem === "string" && firstItem.length >= 2) {
675
+ finalPos = template.indexOf(firstItem);
676
+ }
677
+ }
678
+ }
679
+ if (finalPos === -1) finalPos = template.indexOf(`?.${field.key}`);
680
+ if (finalPos === -1) {
681
+ const keyRegex = new RegExp(`(?<![\\w-])${field.key}(?![\\w-])`);
682
+ const keyMatch = keyRegex.exec(template);
683
+ if (keyMatch) finalPos = keyMatch.index;
684
+ }
685
+ fields.push({ ...field, _pos: finalPos === -1 ? Infinity : finalPos });
686
+ }
687
+ }
688
+ function numberedKey(base, count) {
689
+ return count === 1 ? base : `${base}${count}`;
690
+ }
691
+ function numberedLabel(base, count) {
692
+ return count === 1 ? base : `${base} ${count}`;
693
+ }
694
+ const ariaHiddenRanges = [];
695
+ walkAst(ast, (node) => {
696
+ if (node.type !== "element" && node.type !== "component") return;
697
+ const isHidden = isAriaHidden(node) || getAttr(node, "role")?.value === "img";
698
+ if (isHidden) {
699
+ const start = nodeOffset(node);
700
+ const end = node.position?.end?.offset ? node.position.end.offset - WRAPPER_OFFSET : start + 1;
701
+ ariaHiddenRanges.push({ start, end });
702
+ }
703
+ });
704
+ const mapExpressionRanges = [];
705
+ walkAst(ast, (node) => {
706
+ if (node.type !== "expression") return;
707
+ const exprCode = (node.children ?? []).map((c) => c.value ?? "").join("");
708
+ if (/\.map\s*\(/.test(exprCode)) {
709
+ const start = nodeOffset(node);
710
+ const end = node.position?.end?.offset ? node.position.end.offset - WRAPPER_OFFSET : start + 1;
711
+ mapExpressionRanges.push({ start, end });
712
+ }
713
+ });
714
+ const SEMANTIC_REPEATED_TAGS = /* @__PURE__ */ new Set(["article", "aside", "figure", "details", "blockquote"]);
715
+ const repeatedElementGroups = [];
716
+ const repeatedElementRanges = [];
717
+ walkAst(ast, (node) => {
718
+ if (node.type !== "element" && node.type !== "fragment") return;
719
+ if (!node.children || node.children.length < 2) return;
720
+ const childrenByTag = /* @__PURE__ */ new Map();
721
+ for (const child of node.children) {
722
+ if (child.type !== "element") continue;
723
+ const tag = child.name ?? "";
724
+ if (!tag) continue;
725
+ if (!childrenByTag.has(tag)) childrenByTag.set(tag, []);
726
+ childrenByTag.get(tag).push(child);
727
+ }
728
+ for (const [tag, siblings] of childrenByTag) {
729
+ if (siblings.length < 2) continue;
730
+ if (!SEMANTIC_REPEATED_TAGS.has(tag)) continue;
731
+ repeatedElementGroups.push({ tag, instances: siblings });
732
+ for (const inst of siblings) {
733
+ const start = nodeOffset(inst);
734
+ const end = inst.position?.end?.offset ? inst.position.end.offset - WRAPPER_OFFSET : start + 1;
735
+ repeatedElementRanges.push({ start, end });
736
+ }
737
+ }
738
+ });
739
+ walkAst(ast, (node) => {
740
+ if (node.type !== "element" || node.name !== "tbody") return;
741
+ const trRows = (node.children ?? []).filter((c) => c.type === "element" && c.name === "tr");
742
+ if (trRows.length < 2) return;
743
+ repeatedElementGroups.push({ tag: "tr", instances: trRows });
744
+ for (const row of trRows) {
745
+ const start = nodeOffset(row);
746
+ const end = row.position?.end?.offset ? row.position.end.offset - WRAPPER_OFFSET : start + 1;
747
+ repeatedElementRanges.push({ start, end });
748
+ }
749
+ });
750
+ function isInAriaHidden(offset) {
751
+ return ariaHiddenRanges.some((r) => offset >= r.start && offset < r.end);
752
+ }
753
+ function isInMapExpression(offset) {
754
+ return mapExpressionRanges.some((r) => offset >= r.start && offset < r.end);
755
+ }
756
+ function isInRepeatedElement(offset) {
757
+ return repeatedElementRanges.some((r) => offset >= r.start && offset < r.end);
758
+ }
759
+ const cmsBoundOffsets = /* @__PURE__ */ new Set();
760
+ walkAst(ast, (node) => {
761
+ if (node.type !== "expression") return;
762
+ const exprCode = (node.children ?? []).map((c) => c.value ?? "").join("");
763
+ const cmsVarMatch = exprCode.match(/^\s*\(?\s*\w+\?\.\s*(\w+)/);
764
+ if (cmsVarMatch) {
765
+ const fieldKey = cmsVarMatch[1];
766
+ const arrayFallbackMatch = exprCode.match(/\?\?\s*\[([^\]]*)\]\s*\)\.map\(/);
767
+ if (arrayFallbackMatch) {
768
+ const items = [];
769
+ const itemRe = /`([^`]*)`|'([^']*)'|"([^"]*)"/g;
770
+ let m;
771
+ while ((m = itemRe.exec(arrayFallbackMatch[1])) !== null) {
772
+ items.push(m[1] ?? m[2] ?? m[3] ?? "");
773
+ }
774
+ const hasFormatting = items.some((s) => /<[a-z]/.test(s));
775
+ addField({
776
+ key: fieldKey,
777
+ type: "array",
778
+ label: camelToLabel(fieldKey),
779
+ confidence: "high",
780
+ defaultValue: items.length > 0 ? items : void 0,
781
+ options: { arrayItem: { type: "text", ...hasFormatting ? { formatting: true } : {} } }
782
+ }, nodeOffset(node));
783
+ } else {
784
+ const strFallback = exprCode.match(/\?\?\s*(?:`([\s\S]*?)`|'([^']*)'|"([^"]*)")/);
785
+ const defaultValue = strFallback ? (strFallback[1] ?? strFallback[2] ?? strFallback[3] ?? "").trim() : void 0;
786
+ const hasHtml = defaultValue ? /<[a-z]/.test(defaultValue) : false;
787
+ addField({
788
+ key: fieldKey,
789
+ type: "text",
790
+ label: camelToLabel(fieldKey),
791
+ confidence: "high",
792
+ ...defaultValue ? { defaultValue } : {},
793
+ ...hasHtml ? { options: { formatting: true } } : {}
794
+ }, nodeOffset(node));
795
+ }
796
+ cmsBoundOffsets.add(nodeOffset(node));
797
+ }
798
+ });
799
+ walkAst(ast, (node) => {
800
+ if (node.type !== "element" && node.type !== "component") return;
801
+ for (const attr of node.attributes ?? []) {
802
+ if (attr.kind !== "expression") continue;
803
+ const cmsAttrMatch = attr.value.match(/^\s*\w+\?\.\s*(\w+)\s*\?\?\s*(?:`([\s\S]*?)`|'([^']*)'|"([^"]*)")/);
804
+ if (cmsAttrMatch) {
805
+ const fieldKey = cmsAttrMatch[1];
806
+ const fallback = (cmsAttrMatch[2] ?? cmsAttrMatch[3] ?? cmsAttrMatch[4] ?? "").trim();
807
+ const pos = attr.position?.start?.offset ? attr.position.start.offset - WRAPPER_OFFSET : nodeOffset(node);
808
+ const isSetHtml = attr.name === "set:html";
809
+ const fallbackHasHtml = fallback ? /<[a-z]/.test(fallback) : false;
810
+ const hasHtml = fallbackHasHtml || isSetHtml && !fallback;
811
+ addField({
812
+ key: fieldKey,
813
+ type: "text",
814
+ label: camelToLabel(fieldKey),
815
+ confidence: "high",
816
+ ...fallback ? { defaultValue: fallback } : {},
817
+ ...hasHtml ? { options: { formatting: true } } : {}
818
+ }, pos);
819
+ cmsBoundOffsets.add(nodeOffset(node));
820
+ }
821
+ }
822
+ });
823
+ function shouldSkipForContent(node) {
824
+ const offset = nodeOffset(node);
825
+ return isInAriaHidden(offset) || cmsBoundOffsets.has(offset) || isInRepeatedElement(offset);
826
+ }
827
+ function shouldSkipMapChild(node) {
828
+ return isInMapExpression(nodeOffset(node));
829
+ }
830
+ walkAst(ast, (node) => {
831
+ if (node.type !== "element") return;
832
+ if (node.name !== "p" && node.name !== "span") return;
833
+ if (shouldSkipForContent(node)) return;
834
+ const classVal = getClassValue(node);
835
+ if (!/uppercase|tracking-widest/.test(classVal)) return;
836
+ const text = extractTextContent(node, true).trim();
837
+ if (text.length >= 2 && text.length <= 80) {
838
+ addField({ key: "overline", type: "text", label: "Overline", confidence: "medium", defaultValue: text }, nodeOffset(node));
839
+ }
840
+ });
841
+ let headingCount = Array.from(usedKeys).filter((k) => k === "heading" || /^heading\d+$/.test(k)).length;
842
+ walkAst(ast, (node) => {
843
+ if (node.type !== "element") return;
844
+ if (!/^h[1-6]$/.test(node.name ?? "")) return;
845
+ if (shouldSkipForContent(node)) return;
846
+ const text = extractTextContent(node, true).replace(/\s+/g, " ").trim();
847
+ if (text.length >= 2) {
848
+ headingCount++;
849
+ const headingOpts = { required: true };
850
+ if (hasInlineFormatting(node)) headingOpts.formatting = true;
851
+ addField({
852
+ key: numberedKey("heading", headingCount),
853
+ type: "text",
854
+ label: numberedLabel("Heading", headingCount),
855
+ confidence: "high",
856
+ defaultValue: text,
857
+ options: headingOpts
858
+ }, nodeOffset(node));
859
+ }
860
+ });
861
+ let descCount = Array.from(usedKeys).filter((k) => k === "description" || /^description\d+$/.test(k)).length;
862
+ walkAst(ast, (node) => {
863
+ if (node.type !== "element") return;
864
+ if (node.name !== "p" && node.name !== "div") return;
865
+ if (shouldSkipForContent(node)) return;
866
+ const classVal = getClassValue(node);
867
+ const serialized = serializeNode(node);
868
+ if (/uppercase|tracking-widest/.test(classVal) && serialized.length < 250) return;
869
+ if (/text-2xl|text-3xl|text-\[11px\]|text-\[10px\]|text-\[9px\]|text-\[8px\]/.test(classVal)) return;
870
+ if (node.name === "div" && (containsElement(node, "a") || containsElement(node, "button"))) {
871
+ const hasMixedText = (node.children ?? []).some(
872
+ (c) => c.type === "text" && (c.value ?? "").trim().length > 0
873
+ );
874
+ if (!hasMixedText) return;
875
+ }
876
+ if (node.name === "div" && ["h1", "h2", "h3", "h4", "h5", "h6"].some((h) => containsElement(node, h))) return;
877
+ if (node.name === "div") {
878
+ const contentChildren = (node.children ?? []).filter((c) => c.type !== "text" || (c.value ?? "").trim().length > 0);
879
+ const hasOnlyElementChildren = contentChildren.length > 0 && contentChildren.every((c) => c.type === "element" || c.type === "component");
880
+ if (hasOnlyElementChildren) return;
881
+ }
882
+ const text = extractTextContent(node, true).replace(/\s+/g, " ").trim();
883
+ if (text.length < 15) return;
884
+ descCount++;
885
+ const descOpts = { multiline: true };
886
+ if (hasInlineFormatting(node)) descOpts.formatting = true;
887
+ addField({
888
+ key: numberedKey("description", descCount),
889
+ type: "text",
890
+ label: numberedLabel("Beschreibung", descCount),
891
+ confidence: "medium",
892
+ defaultValue: text,
893
+ options: descOpts
894
+ }, nodeOffset(node));
895
+ });
896
+ let richCount = 0;
897
+ walkAst(ast, (node) => {
898
+ if (node.type !== "element" && node.type !== "component") return;
899
+ const setHtmlAttr = node.attributes?.find((a) => a.name === "set:html");
900
+ if (!setHtmlAttr || setHtmlAttr.kind !== "expression") return;
901
+ const expr = setHtmlAttr.value.trim();
902
+ if (/^\w+\?\.\w+/.test(expr)) return;
903
+ if (expr.startsWith("'") || expr.startsWith('"') || expr.includes("??")) {
904
+ richCount++;
905
+ const strMatch = expr.match(/['"]([^'"]+)['"]/);
906
+ const fallbackMatch = expr.match(/\?\?\s*['"]([^'"]+)['"]/);
907
+ const value = fallbackMatch?.[1] ?? strMatch?.[1] ?? "";
908
+ addField({
909
+ key: numberedKey("richText", richCount),
910
+ type: "text",
911
+ label: numberedLabel("Rich Text", richCount),
912
+ confidence: "high",
913
+ defaultValue: value,
914
+ options: { multiline: true, formatting: true }
915
+ }, nodeOffset(node));
916
+ }
917
+ });
918
+ let ctaCount = 0;
919
+ walkAst(ast, (node) => {
920
+ if (node.type !== "element") return;
921
+ if (node.name !== "a" && node.name !== "button") return;
922
+ if (shouldSkipForContent(node)) return;
923
+ const classVal = getClassValue(node);
924
+ if (!/rounded|px-|py-|font-semibold|bg-/.test(classVal)) return;
925
+ const text = extractTextContent(node, true).replace(/\s+/g, " ").trim();
926
+ if (text.length >= 2 && text.length <= 60) {
927
+ ctaCount++;
928
+ addField({
929
+ key: numberedKey("ctaText", ctaCount),
930
+ type: "text",
931
+ label: numberedLabel("Button Text", ctaCount),
932
+ confidence: "medium",
933
+ defaultValue: text
934
+ }, nodeOffset(node));
935
+ }
936
+ });
937
+ let linkCount = 0;
938
+ walkAst(ast, (node) => {
939
+ if (node.type !== "element" || node.name !== "a") return;
940
+ if (shouldSkipForContent(node)) return;
941
+ const classVal = getClassValue(node);
942
+ if (!/rounded|px-|py-|font-semibold|bg-/.test(classVal)) return;
943
+ const hrefAttr = getAttr(node, "href");
944
+ if (!hrefAttr) return;
945
+ let href;
946
+ if (hrefAttr.kind === "quoted") {
947
+ href = hrefAttr.value;
948
+ } else if (hrefAttr.kind === "expression") {
949
+ if (/^\s*\w+\?\.\w+/.test(hrefAttr.value)) return;
950
+ const fallback = hrefAttr.value.match(/\?\?\s*['"]([^'"]+)['"]/);
951
+ if (fallback) href = fallback[1];
952
+ }
953
+ if (!href) return;
954
+ if (href.startsWith("#") || href.startsWith("javascript:")) return;
955
+ linkCount++;
956
+ addField({
957
+ key: numberedKey("ctaLink", linkCount),
958
+ type: "text",
959
+ label: numberedLabel("Button Link", linkCount),
960
+ confidence: "medium",
961
+ defaultValue: href
962
+ }, nodeOffset(node));
963
+ });
964
+ let imgCount = 0;
965
+ let altCount = 0;
966
+ walkAst(ast, (node) => {
967
+ if (node.type !== "element" && node.type !== "component") return;
968
+ const tagName = node.name ?? "";
969
+ if (tagName !== "img" && tagName !== "Image" && tagName !== "picture") return;
970
+ if (shouldSkipForContent(node)) return;
971
+ const srcAttr = getAttr(node, "src");
972
+ if (srcAttr) {
973
+ let src;
974
+ if (srcAttr.kind === "quoted") src = srcAttr.value;
975
+ else if (srcAttr.kind === "expression") {
976
+ const strMatch = srcAttr.value.match(/['"]([^'"]+)['"]/);
977
+ if (strMatch) src = strMatch[1];
978
+ }
979
+ if (src) {
980
+ imgCount++;
981
+ addField({
982
+ key: numberedKey("image", imgCount),
983
+ type: "image",
984
+ label: numberedLabel("Bild", imgCount),
985
+ confidence: "high",
986
+ defaultValue: { path: src.trim(), alt: "" }
987
+ }, nodeOffset(node));
988
+ }
989
+ }
990
+ if (tagName === "img" || tagName === "Image") {
991
+ const altAttr = getAttr(node, "alt");
992
+ if (altAttr && altAttr.kind === "quoted" && altAttr.value.trim().length >= 2) {
993
+ altCount++;
994
+ addField({
995
+ key: numberedKey("imageAlt", altCount),
996
+ type: "text",
997
+ label: numberedLabel("Bild Alt-Text", altCount),
998
+ confidence: "medium",
999
+ defaultValue: altAttr.value.trim()
1000
+ }, nodeOffset(node));
1001
+ }
1002
+ }
1003
+ });
1004
+ let iconCount = 0;
1005
+ walkAst(ast, (node, parentNode) => {
1006
+ if (node.type !== "element" || node.name !== "svg") return;
1007
+ if (shouldSkipForContent(node) || shouldSkipMapChild(node)) return;
1008
+ const svgSource = serializeNode(node);
1009
+ if (svgSource.length < 100) return;
1010
+ if (parentNode && (parentNode.name === "a" || parentNode.name === "button")) return;
1011
+ iconCount++;
1012
+ addField({
1013
+ key: numberedKey("icon", iconCount),
1014
+ type: "icon",
1015
+ label: numberedLabel("Icon", iconCount),
1016
+ confidence: "low"
1017
+ }, nodeOffset(node));
1018
+ });
1019
+ let foundIconProp = false;
1020
+ walkAst(ast, (node) => {
1021
+ if (foundIconProp) return;
1022
+ if (node.type !== "component") return;
1023
+ if (shouldSkipMapChild(node)) return;
1024
+ const iconAttr = getAttr(node, "icon");
1025
+ if (iconAttr) {
1026
+ addField({ key: "icon", type: "icon", label: "Icon", confidence: "high" }, nodeOffset(node));
1027
+ foundIconProp = true;
1028
+ }
1029
+ });
1030
+ let arrayCount = 0;
1031
+ walkAst(ast, (node) => {
1032
+ if (node.type !== "expression") return;
1033
+ const exprCode = (node.children ?? []).map((c) => c.value ?? "").join("");
1034
+ const inlineArrayMatch = exprCode.match(/^\s*\[([\s\S]*?)\]\s*\.map\s*\(\s*\(?\s*(\{[^}]*\}|\w+)/);
1035
+ if (!inlineArrayMatch) return;
1036
+ arrayCount++;
1037
+ const arrayContent = inlineArrayMatch[1];
1038
+ const callbackParam = inlineArrayMatch[2];
1039
+ let objectKeys = [];
1040
+ if (callbackParam.startsWith("{")) {
1041
+ objectKeys = callbackParam.replace(/[{}]/g, "").split(",").map((p) => p.trim().split(":")[0].trim()).filter((p) => p && !p.startsWith("..."));
1042
+ } else {
1043
+ const firstObjMatch = arrayContent.match(/\{\s*([\s\S]*?)\}/);
1044
+ if (firstObjMatch) {
1045
+ const keyRegex = /(\w+)\s*:/g;
1046
+ let km;
1047
+ while ((km = keyRegex.exec(firstObjMatch[1])) !== null) objectKeys.push(km[1]);
1048
+ }
1049
+ if (objectKeys.length === 0) {
1050
+ const accessRegex = new RegExp(`${callbackParam}\\.(\\w+)`, "g");
1051
+ let am;
1052
+ const accessedProps = /* @__PURE__ */ new Set();
1053
+ while ((am = accessRegex.exec(exprCode)) !== null) accessedProps.add(am[1]);
1054
+ objectKeys = [...accessedProps];
1055
+ }
1056
+ }
1057
+ const isObjectArray = objectKeys.length > 0;
1058
+ const isStringArray = /^\s*'[^']*'\s*,/.test(arrayContent) || /^\s*"[^"]*"\s*,/.test(arrayContent);
1059
+ const arrayDefaultValue = extractInlineArrayValues(arrayContent, isObjectArray, isStringArray);
1060
+ if (isObjectArray && !isStringArray) {
1061
+ const innerFields = objectKeys.map((prop) => ({
1062
+ key: prop,
1063
+ type: inferInnerFieldType(prop),
1064
+ label: camelToLabel(prop),
1065
+ confidence: "medium"
1066
+ }));
1067
+ addField({
1068
+ key: numberedKey("items", arrayCount),
1069
+ type: "array",
1070
+ label: numberedLabel("Liste", arrayCount),
1071
+ confidence: "high",
1072
+ defaultValue: arrayDefaultValue,
1073
+ options: { arrayItem: { type: "object", fields: innerFields } }
1074
+ }, nodeOffset(node));
1075
+ } else {
1076
+ addField({
1077
+ key: numberedKey("items", arrayCount),
1078
+ type: "array",
1079
+ label: numberedLabel("Liste", arrayCount),
1080
+ confidence: "high",
1081
+ defaultValue: arrayDefaultValue,
1082
+ options: { arrayItem: { type: "text" } }
1083
+ }, nodeOffset(node));
1084
+ }
1085
+ });
1086
+ let staticListCount = 0;
1087
+ walkAst(ast, (node) => {
1088
+ if (node.type !== "element") return;
1089
+ if (node.name !== "ul" && node.name !== "ol") return;
1090
+ if (shouldSkipForContent(node)) return;
1091
+ if (shouldSkipMapChild(node)) return;
1092
+ let hasMap = false;
1093
+ walkAst(node, (n) => {
1094
+ if (hasMap) return;
1095
+ if (n.type === "expression") {
1096
+ const code = (n.children ?? []).map((c) => c.value ?? "").join("");
1097
+ if (/\.map\s*\(/.test(code)) hasMap = true;
1098
+ }
1099
+ });
1100
+ if (hasMap) return;
1101
+ let hasNestedList = false;
1102
+ for (const li of node.children ?? []) {
1103
+ if (li.type !== "element" || li.name !== "li") continue;
1104
+ for (const child of li.children ?? []) {
1105
+ if (child.type === "element" && (child.name === "ul" || child.name === "ol")) {
1106
+ hasNestedList = true;
1107
+ break;
1108
+ }
1109
+ }
1110
+ if (hasNestedList) break;
1111
+ }
1112
+ if (hasNestedList) return;
1113
+ const FORMATTING_TAGS = /* @__PURE__ */ new Set(["strong", "em", "b", "i", "code", "a", "s", "u"]);
1114
+ let listHasFormatting = false;
1115
+ const listItems = [];
1116
+ for (const li of node.children ?? []) {
1117
+ if (li.type !== "element" || li.name !== "li") continue;
1118
+ let liHasFormatting = false;
1119
+ walkAst(li, (n) => {
1120
+ if (liHasFormatting) return;
1121
+ if (n.type === "element" && FORMATTING_TAGS.has(n.name ?? "")) liHasFormatting = true;
1122
+ });
1123
+ if (liHasFormatting) {
1124
+ listHasFormatting = true;
1125
+ let contentHTML = null;
1126
+ const liSrcStart = li.position?.start?.offset;
1127
+ const liSrcEnd = li.position?.end?.offset;
1128
+ if (liSrcStart != null && liSrcEnd != null) {
1129
+ const liSrc = template.slice(liSrcStart, liSrcEnd);
1130
+ const spanRegex = /<span[^>]*>([\s\S]*?)<\/span>/g;
1131
+ let spanMatch;
1132
+ while ((spanMatch = spanRegex.exec(liSrc)) !== null) {
1133
+ const inner = spanMatch[1];
1134
+ if (!inner.trim()) continue;
1135
+ contentHTML = inner.replace(/\s+/g, " ").trim();
1136
+ break;
1137
+ }
1138
+ }
1139
+ listItems.push(contentHTML ?? extractTextContent(li, true).replace(/\s+/g, " ").trim());
1140
+ } else {
1141
+ const t = extractTextContent(li, true).replace(/\s+/g, " ").trim();
1142
+ if (t.length >= 1) listItems.push(t);
1143
+ }
1144
+ }
1145
+ if (listItems.length < 1) return;
1146
+ staticListCount++;
1147
+ addField({
1148
+ key: numberedKey("items", arrayCount + staticListCount),
1149
+ type: "array",
1150
+ label: numberedLabel("Liste", arrayCount + staticListCount),
1151
+ confidence: "high",
1152
+ defaultValue: listItems,
1153
+ options: { arrayItem: { type: "text", ...listHasFormatting ? { formatting: true } : {} } }
1154
+ }, nodeOffset(node));
1155
+ });
1156
+ const componentCounts = /* @__PURE__ */ new Map();
1157
+ walkAst(ast, (node) => {
1158
+ if (node.type !== "component" || shouldSkipMapChild(node)) return;
1159
+ componentCounts.set(node.name ?? "", (componentCounts.get(node.name ?? "") ?? 0) + 1);
1160
+ });
1161
+ walkAst(ast, (node) => {
1162
+ if (node.type !== "component") return;
1163
+ if (shouldSkipMapChild(node)) return;
1164
+ if ((componentCounts.get(node.name ?? "") ?? 0) >= 2) return;
1165
+ for (const attr of node.attributes ?? []) {
1166
+ if (attr.kind === "quoted") {
1167
+ const propName = attr.name;
1168
+ const propValue = attr.value;
1169
+ if (/^(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(propName)) continue;
1170
+ if (/^(lang|language|filename|file|format|variant|size|loading|decoding|transition|client:.*)$/.test(propName)) continue;
1171
+ if (/^(aria-|data-)/.test(propName)) continue;
1172
+ if (propValue.length < 2) continue;
1173
+ if (propValue === "true" || propValue === "false" || /^\d+$/.test(propValue)) continue;
1174
+ if (propValue.includes("/") && !propValue.includes(" ")) continue;
1175
+ addField({
1176
+ key: propName,
1177
+ type: propName === "icon" ? "icon" : propName === "src" ? "image" : "text",
1178
+ label: camelToLabel(propName),
1179
+ confidence: "medium",
1180
+ defaultValue: propValue
1181
+ }, nodeOffset(node));
1182
+ } else if (attr.kind === "expression") {
1183
+ const propName = attr.name;
1184
+ if (/^(class|className|id|style|type|role|lang|language|filename|file|format|variant|size|loading|decoding)$/.test(propName)) continue;
1185
+ if (/^\s*\w+\s*$/.test(attr.value)) continue;
1186
+ const fallbackMatch = attr.value.match(/\?\?\s*['"]([^'"]+)['"]/);
1187
+ if (fallbackMatch) {
1188
+ addField({
1189
+ key: propName,
1190
+ type: propName === "icon" ? "icon" : propName === "src" ? "image" : "text",
1191
+ label: camelToLabel(propName),
1192
+ confidence: "medium",
1193
+ defaultValue: fallbackMatch[1]
1194
+ }, nodeOffset(node));
1195
+ }
1196
+ }
1197
+ }
1198
+ });
1199
+ const componentInstances = /* @__PURE__ */ new Map();
1200
+ walkAst(ast, (node) => {
1201
+ if (node.type !== "component") return;
1202
+ if (shouldSkipMapChild(node)) return;
1203
+ const name = node.name ?? "";
1204
+ if (!name) return;
1205
+ if (!componentInstances.has(name)) componentInstances.set(name, []);
1206
+ componentInstances.get(name).push(node);
1207
+ });
1208
+ for (const [compName, instances] of componentInstances) {
1209
+ if (instances.length < 2) continue;
1210
+ const allProps = /* @__PURE__ */ new Set();
1211
+ const instanceValues = [];
1212
+ for (const inst of instances) {
1213
+ const item = {};
1214
+ for (const attr of inst.attributes ?? []) {
1215
+ const name = attr.name;
1216
+ if (/^(class|className|id|style|type|role)$/.test(name)) continue;
1217
+ if (/^(aria-|data-)/.test(name)) continue;
1218
+ if (attr.kind === "quoted") {
1219
+ allProps.add(name);
1220
+ item[name] = attr.value;
1221
+ } else if (attr.kind === "expression") {
1222
+ allProps.add(name);
1223
+ const expr = attr.value.trim();
1224
+ if (expr === "true") item[name] = true;
1225
+ else if (expr === "false") item[name] = false;
1226
+ else if (/^\w+$/.test(expr) && frontmatter) {
1227
+ const extractedValue = extractFrontmatterValue(frontmatter, expr);
1228
+ if (extractedValue !== void 0) item[name] = extractedValue;
1229
+ }
1230
+ }
1231
+ }
1232
+ if (Object.keys(item).length > 0) instanceValues.push(item);
1233
+ }
1234
+ if (allProps.size > 0) {
1235
+ const cardKey = compName.replace(/([A-Z])/g, (_m, c, i) => i === 0 ? c.toLowerCase() : "_" + c.toLowerCase()).replace(/_/g, "") + "s";
1236
+ if (!usedKeys.has(cardKey)) {
1237
+ const innerFields = [...allProps].map((p) => {
1238
+ const isArray = instanceValues.some((iv) => Array.isArray(iv[p]));
1239
+ if (isArray) {
1240
+ return { key: p, type: "array", label: camelToLabel(p), confidence: "medium", options: { arrayItem: { type: "text" } } };
1241
+ }
1242
+ return { key: p, type: inferInnerFieldType(p), label: camelToLabel(p), confidence: "medium" };
1243
+ });
1244
+ addField({
1245
+ key: cardKey,
1246
+ type: "array",
1247
+ label: `${compName} Liste`,
1248
+ confidence: "medium",
1249
+ defaultValue: instanceValues.length > 0 ? instanceValues : void 0,
1250
+ options: { arrayItem: { type: "object", fields: innerFields } }
1251
+ }, nodeOffset(instances[0]));
1252
+ }
1253
+ }
1254
+ }
1255
+ for (const group of repeatedElementGroups) {
1256
+ const instanceCount = group.instances.length;
1257
+ const instanceBounds = group.instances.map((inst) => ({
1258
+ start: nodeOffset(inst),
1259
+ end: nodeEnd(inst)
1260
+ }));
1261
+ const instanceFingerprints = [];
1262
+ for (const inst of group.instances) {
1263
+ const items = [];
1264
+ walkContentNodes(inst, 0, items, frontmatter);
1265
+ instanceFingerprints.push(items);
1266
+ }
1267
+ if (instanceFingerprints.length === 0) continue;
1268
+ const signatures = instanceFingerprints.map((fp) => fp.map((i) => i.tag).join(","));
1269
+ const sigCounts = /* @__PURE__ */ new Map();
1270
+ for (const sig of signatures) sigCounts.set(sig, (sigCounts.get(sig) ?? 0) + 1);
1271
+ const mostCommonSig = [...sigCounts.entries()].sort((a, b) => b[1] - a[1])[0][0];
1272
+ const canonicalIdx = signatures.indexOf(mostCommonSig);
1273
+ const canonical = instanceFingerprints[canonicalIdx];
1274
+ const consumedPerInstance = instanceFingerprints.map(() => /* @__PURE__ */ new Set());
1275
+ const innerFields = [];
1276
+ const typeCounts = {};
1277
+ for (let ci = 0; ci < canonical.length; ci++) {
1278
+ const cItem = canonical[ci];
1279
+ const tag = cItem.tag;
1280
+ const isHeading = /^h[1-6]$/.test(tag);
1281
+ const isLink = tag === "a";
1282
+ const isArray = tag === "__array__";
1283
+ let fieldType;
1284
+ let keyBase;
1285
+ if (isHeading) {
1286
+ fieldType = "text";
1287
+ keyBase = "heading";
1288
+ } else if (isLink) {
1289
+ fieldType = "link";
1290
+ keyBase = "link";
1291
+ } else if (isArray) {
1292
+ fieldType = "array";
1293
+ keyBase = "list";
1294
+ } else {
1295
+ fieldType = "text";
1296
+ keyBase = "text";
1297
+ }
1298
+ typeCounts[keyBase] = (typeCounts[keyBase] ?? 0) + 1;
1299
+ const key = typeCounts[keyBase] === 1 ? keyBase : `${keyBase}${typeCounts[keyBase]}`;
1300
+ const positions = [];
1301
+ const defaultValues = [];
1302
+ let presentCount = 0;
1303
+ for (let ii = 0; ii < instanceCount; ii++) {
1304
+ const fp = instanceFingerprints[ii];
1305
+ const consumed = consumedPerInstance[ii];
1306
+ const match = findMatchingItem(fp, cItem, ci, canonical, consumed);
1307
+ if (match) {
1308
+ presentCount++;
1309
+ positions.push({
1310
+ offset: nodeOffset(match.node),
1311
+ length: nodeEnd(match.node) - nodeOffset(match.node),
1312
+ source: match.exprSource
1313
+ });
1314
+ if (isArray && match.exprSource) {
1315
+ defaultValues.push(extractFrontmatterValue(frontmatter, match.exprSource) ?? []);
1316
+ } else if (isLink) {
1317
+ defaultValues.push(match.hrefValue || null);
1318
+ } else {
1319
+ defaultValues.push(match.text || match.hrefValue || null);
1320
+ }
1321
+ } else {
1322
+ positions.push(null);
1323
+ defaultValues.push(null);
1324
+ }
1325
+ }
1326
+ innerFields.push({
1327
+ key,
1328
+ type: fieldType,
1329
+ tag,
1330
+ required: presentCount === instanceCount,
1331
+ positions,
1332
+ defaultValues
1333
+ });
1334
+ if (isLink) {
1335
+ typeCounts["linkText"] = (typeCounts["linkText"] ?? 0) + 1;
1336
+ const ltKey = typeCounts["linkText"] === 1 ? "linkText" : `linkText${typeCounts["linkText"]}`;
1337
+ const ltPositions = [];
1338
+ const ltDefaults = [];
1339
+ for (let ii = 0; ii < instanceCount; ii++) {
1340
+ const fp = instanceFingerprints[ii];
1341
+ const consumed = consumedPerInstance[ii];
1342
+ const match = findMatchingItemPeek(fp, cItem, ci, canonical, consumed);
1343
+ if (match) {
1344
+ ltPositions.push({
1345
+ offset: nodeOffset(match.node),
1346
+ length: nodeEnd(match.node) - nodeOffset(match.node)
1347
+ });
1348
+ ltDefaults.push(match.text || null);
1349
+ } else {
1350
+ ltPositions.push(null);
1351
+ ltDefaults.push(null);
1352
+ }
1353
+ }
1354
+ innerFields.push({
1355
+ key: ltKey,
1356
+ type: "text",
1357
+ tag: "a",
1358
+ required: presentCount === instanceCount,
1359
+ positions: ltPositions,
1360
+ defaultValues: ltDefaults
1361
+ });
1362
+ }
1363
+ }
1364
+ for (let ii = 0; ii < instanceCount; ii++) {
1365
+ if (ii === canonicalIdx) continue;
1366
+ const fp = instanceFingerprints[ii];
1367
+ const consumed = consumedPerInstance[ii];
1368
+ for (let fi = 0; fi < fp.length; fi++) {
1369
+ if (consumed.has(fi)) continue;
1370
+ const item = fp[fi];
1371
+ const tag = item.tag;
1372
+ const isArray = tag === "__array__";
1373
+ const isLink = tag === "a";
1374
+ let fieldType = isArray ? "array" : isLink ? "link" : "text";
1375
+ let keyBase = /^h[1-6]$/.test(tag) ? "heading" : isLink ? "link" : isArray ? "list" : "text";
1376
+ typeCounts[keyBase] = (typeCounts[keyBase] ?? 0) + 1;
1377
+ const key = typeCounts[keyBase] === 1 ? keyBase : `${keyBase}${typeCounts[keyBase]}`;
1378
+ const positions = new Array(instanceCount).fill(null);
1379
+ const defaultValues = new Array(instanceCount).fill(null);
1380
+ positions[ii] = {
1381
+ offset: nodeOffset(item.node),
1382
+ length: nodeEnd(item.node) - nodeOffset(item.node),
1383
+ source: item.exprSource
1384
+ };
1385
+ if (isArray && item.exprSource) {
1386
+ defaultValues[ii] = extractFrontmatterValue(frontmatter, item.exprSource) ?? [];
1387
+ } else {
1388
+ defaultValues[ii] = item.text || item.hrefValue || null;
1389
+ }
1390
+ innerFields.push({ key, type: fieldType, tag, required: false, positions, defaultValues });
1391
+ if (isLink) {
1392
+ typeCounts["linkText"] = (typeCounts["linkText"] ?? 0) + 1;
1393
+ const ltKey = typeCounts["linkText"] === 1 ? "linkText" : `linkText${typeCounts["linkText"]}`;
1394
+ const ltPositions = new Array(instanceCount).fill(null);
1395
+ const ltDefaults = new Array(instanceCount).fill(null);
1396
+ ltPositions[ii] = positions[ii] ?? null;
1397
+ ltDefaults[ii] = item.text || null;
1398
+ innerFields.push({ key: ltKey, type: "text", tag: "a", required: false, positions: ltPositions, defaultValues: ltDefaults });
1399
+ }
1400
+ }
1401
+ }
1402
+ if (innerFields.length === 0) continue;
1403
+ arrayCount++;
1404
+ let fieldKey = numberedKey("items", arrayCount);
1405
+ while (usedKeys.has(fieldKey)) {
1406
+ arrayCount++;
1407
+ fieldKey = numberedKey("items", arrayCount);
1408
+ }
1409
+ const defaultValue = [];
1410
+ for (let ii = 0; ii < instanceCount; ii++) {
1411
+ const item = {};
1412
+ for (const f of innerFields) {
1413
+ if (f.defaultValues[ii] != null) {
1414
+ item[f.key] = f.defaultValues[ii];
1415
+ }
1416
+ }
1417
+ defaultValue.push(item);
1418
+ }
1419
+ const innerFieldDefs = innerFields.map((f) => {
1420
+ if (f.type === "array") {
1421
+ return {
1422
+ key: f.key,
1423
+ type: "array",
1424
+ label: f.label ?? camelToLabel(f.key),
1425
+ confidence: "medium",
1426
+ options: { arrayItem: { type: "text" } }
1427
+ };
1428
+ }
1429
+ return {
1430
+ key: f.key,
1431
+ type: f.type === "link" ? "text" : "text",
1432
+ label: f.label ?? camelToLabel(f.key),
1433
+ confidence: "medium"
1434
+ };
1435
+ });
1436
+ const groupLabel = group.tag === "tr" ? "Tabellen-Zeilen" : `${group.tag.charAt(0).toUpperCase() + group.tag.slice(1)} Liste`;
1437
+ addField({
1438
+ key: fieldKey,
1439
+ type: "array",
1440
+ label: groupLabel,
1441
+ confidence: "high",
1442
+ defaultValue,
1443
+ options: {
1444
+ arrayItem: { type: "object", fields: innerFieldDefs },
1445
+ _repeatedTag: group.tag,
1446
+ _instanceCount: instanceCount
1447
+ }
1448
+ }, instanceBounds[0].start);
1449
+ const classAttrs = group.instances.map((inst) => collectClassAttrs(inst, "", template));
1450
+ repeatedGroups.push({
1451
+ tag: group.tag,
1452
+ fieldKey,
1453
+ instances: instanceBounds,
1454
+ templateIndex: 0,
1455
+ fields: innerFields,
1456
+ classAttrs
1457
+ });
1458
+ }
1459
+ fields.sort((a, b) => a._pos - b._pos);
1460
+ return { fields: fields.map(({ _pos, ...field }) => field), repeatedGroups };
1461
+ }
1462
+ function walkContentNodes(root, depth, items, frontmatter) {
1463
+ for (const child of root.children ?? []) {
1464
+ if (child.type === "element" && isAriaHidden(child)) continue;
1465
+ if (child.type === "element" && /^h[1-6]$/.test(child.name ?? "")) {
1466
+ const text = extractTextContent(child, true).replace(/\s+/g, " ").trim();
1467
+ if (text.length >= 1) {
1468
+ items.push({ tag: child.name, depth, node: child, text });
1469
+ }
1470
+ continue;
1471
+ }
1472
+ if (child.type === "element" && child.name === "p") {
1473
+ const text = extractTextContent(child, true).replace(/\s+/g, " ").trim();
1474
+ if (text.length >= 1) {
1475
+ items.push({ tag: "p", depth, node: child, text });
1476
+ }
1477
+ continue;
1478
+ }
1479
+ if (child.type === "element" && child.name === "span") {
1480
+ const text = extractTextContent(child, true).replace(/\s+/g, " ").trim();
1481
+ if (text.length >= 1) {
1482
+ items.push({ tag: "span", depth, node: child, text });
1483
+ }
1484
+ continue;
1485
+ }
1486
+ if (child.type === "element" && child.name === "a") {
1487
+ const text = extractTextContent(child, true).replace(/\s+/g, " ").trim();
1488
+ const href = getAttr(child, "href");
1489
+ let hrefValue;
1490
+ if (href?.kind === "quoted") hrefValue = href.value;
1491
+ else if (href?.kind === "expression") {
1492
+ const fb = href.value.match(/['"]([^'"]+)['"]/);
1493
+ if (fb) hrefValue = fb[1];
1494
+ }
1495
+ if (text.length >= 1 || hrefValue) {
1496
+ items.push({ tag: "a", depth, node: child, text, hrefValue });
1497
+ }
1498
+ continue;
1499
+ }
1500
+ if (child.type === "element" && child.name === "td") {
1501
+ const text = extractTextContent(child, true).replace(/\s+/g, " ").trim();
1502
+ if (text.length >= 1) {
1503
+ items.push({ tag: "td", depth, node: child, text });
1504
+ }
1505
+ continue;
1506
+ }
1507
+ if (child.type === "element" && (child.name === "ul" || child.name === "ol")) {
1508
+ let mapSource;
1509
+ walkAst(child, (expr) => {
1510
+ if (mapSource) return;
1511
+ if (expr.type !== "expression") return;
1512
+ const code = (expr.children ?? []).map((c) => c.value ?? "").join("");
1513
+ const mapMatch = code.match(/(\w+)\.map\s*\(/);
1514
+ if (mapMatch) mapSource = mapMatch[1];
1515
+ });
1516
+ if (mapSource) {
1517
+ items.push({ tag: "__array__", depth, node: child, text: "", exprSource: mapSource });
1518
+ } else {
1519
+ const listTexts = [];
1520
+ for (const li of child.children ?? []) {
1521
+ if (li.type === "element" && li.name === "li") {
1522
+ const t = extractTextContent(li, true).trim();
1523
+ if (t.length >= 1) listTexts.push(t);
1524
+ }
1525
+ }
1526
+ if (listTexts.length > 0) {
1527
+ items.push({ tag: "__array__", depth, node: child, text: listTexts.join("\n") });
1528
+ }
1529
+ }
1530
+ continue;
1531
+ }
1532
+ if (child.type === "element") {
1533
+ walkContentNodes(child, depth + 1, items, frontmatter);
1534
+ }
1535
+ }
1536
+ }
1537
+ function findMatchingItem(targetFp, canonicalItem, canonicalIndex, allCanonical, consumed) {
1538
+ const result = findMatchingItemCore(targetFp, canonicalItem, canonicalIndex, allCanonical, consumed);
1539
+ if (result) consumed.add(result.idx);
1540
+ return result?.item ?? null;
1541
+ }
1542
+ function findMatchingItemPeek(targetFp, canonicalItem, canonicalIndex, allCanonical, consumed) {
1543
+ const tag = canonicalItem.tag;
1544
+ const candidates = targetFp.map((item, idx) => ({ item, idx })).filter((c) => c.item.tag === tag);
1545
+ if (candidates.length === 0) return null;
1546
+ if (candidates.length === 1) return candidates[0].item;
1547
+ const canonicalRelPos = canonicalIndex / Math.max(allCanonical.length - 1, 1);
1548
+ candidates.sort((a, b) => {
1549
+ const relA = a.idx / Math.max(targetFp.length - 1, 1);
1550
+ const relB = b.idx / Math.max(targetFp.length - 1, 1);
1551
+ return Math.abs(relA - canonicalRelPos) - Math.abs(relB - canonicalRelPos);
1552
+ });
1553
+ return candidates[0].item;
1554
+ }
1555
+ function findMatchingItemCore(targetFp, canonicalItem, canonicalIndex, allCanonical, consumed) {
1556
+ const tag = canonicalItem.tag;
1557
+ const candidates = targetFp.map((item, idx) => ({ item, idx })).filter((c) => c.item.tag === tag && !consumed.has(c.idx));
1558
+ if (candidates.length === 0) return null;
1559
+ if (candidates.length === 1) return candidates[0];
1560
+ const canonicalRelPos = canonicalIndex / Math.max(allCanonical.length - 1, 1);
1561
+ candidates.sort((a, b) => {
1562
+ const relA = a.idx / Math.max(targetFp.length - 1, 1);
1563
+ const relB = b.idx / Math.max(targetFp.length - 1, 1);
1564
+ return Math.abs(relA - canonicalRelPos) - Math.abs(relB - canonicalRelPos);
1565
+ });
1566
+ return candidates[0];
1567
+ }
1568
+
1569
+ export {
1570
+ findAstroPages,
1571
+ extractSectionImports,
1572
+ extractLayoutImport,
1573
+ analyzeAstroSection
1574
+ };