@bettercms-ai/convert 0.6.0 → 0.8.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/cli.js CHANGED
@@ -147,195 +147,76 @@ function propsAttribute(props) {
147
147
  return `data-bcms-props="${value}"`;
148
148
  }
149
149
 
150
- // src/helper.ts
151
- import { createHash as createHash2 } from "crypto";
152
- var HELPER_VERSION = 4;
153
- var HEADER = "// @bettercms-ai/convert helper v";
154
- var HELPER_PATH = Object.fromEntries(
155
- Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
156
- );
157
- var CONTENT_DIR = "bcms-content";
158
- var STUB_CONTENT = "{}";
159
- function relativeImport(fromFile, toFile) {
160
- const from = fromFile.split("/").slice(0, -1);
161
- const to = toFile.split("/");
162
- let shared = 0;
163
- while (shared < from.length && shared < to.length - 1 && from[shared] === to[shared]) shared++;
164
- const up = from.length - shared;
165
- const path = [...Array(up).fill(".."), ...to.slice(shared)].join("/");
166
- return up === 0 ? `./${path}` : path;
167
- }
168
- function helperSource(dialect) {
169
- const path = HELPER_PATH[dialect];
170
- if (!path) throw new Error(`The ${dialect} dialect reads its values at publish time, not at build time.`);
171
- const layout = relativeImport(path, `${CONTENT_DIR}/layout.json`);
172
- return `${HEADER}${HELPER_VERSION}
173
- // Values come from bcms-content/*.json, written into the tree before the build. The committed
174
- // stubs are {} \u2014 a build with them renders every fallback below and still succeeds.
175
- import layoutSnapshot from "${layout}";
176
-
177
- const isRecord = (v: unknown): v is Record<string, unknown> =>
178
- typeof v === "object" && v !== null && !Array.isArray(v);
179
-
180
- /** Dotted keys and \`[n]\` indices, positionally \u2014 the editor's own path grammar. */
181
- function readPath(root: unknown, path: string): unknown {
182
- let cursor: unknown = root;
183
- for (const segment of path.split(".")) {
184
- if (!segment) return undefined;
185
- const key = segment.replace(/\\[\\d+\\]/g, "");
186
- if (key) {
187
- if (!isRecord(cursor)) return undefined;
188
- cursor = cursor[key];
150
+ // src/declarations.ts
151
+ var BINDING_ATTR = /data-bcms-(?:layout-)?field\s*=\s*["']([^"']+)["']/g;
152
+ var PROPS_ATTR = /data-bcms-props\s*=\s*["']([^"']+)["']/g;
153
+ var TEMPLATE_SCAN = /:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")/g;
154
+ var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{(?:`[^`]*`|[^}])*\}\}/g;
155
+ var propsDeclarations = (value, at) => value.split(";").flatMap((entry) => {
156
+ const [address, kind] = entry.split("|");
157
+ if (!address) return [];
158
+ const scope = address.startsWith("layout:") ? "layout" : "page";
159
+ return [{ scope, path: stripLayoutPrefix(address), kind: kind ?? "", at, via: "props" }];
160
+ });
161
+ var propsPaths = (value) => propsDeclarations(value, 0).map((d) => ({ scope: d.scope, path: d.path }));
162
+ var TEMPLATE_BINDING = /^:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")$/;
163
+ var shapeOfPath = (path) => path.replace(/\[(?:\d+|\$\{[^}]*\})\]/g, "[*]");
164
+ function declarationsIn(roots, content) {
165
+ const out = [];
166
+ const visit = (node) => {
167
+ if (node.type === "expr") {
168
+ for (const child of node.children) visit(child);
169
+ return;
189
170
  }
190
- for (const index of segment.matchAll(/\\[(\\d+)\\]/g)) {
191
- if (!Array.isArray(cursor)) return undefined;
192
- cursor = cursor[Number(index[1])];
171
+ if (node.type !== "element") return;
172
+ const kind = node.attrs.find((a) => a.name === "data-bcms-kind")?.value ?? "";
173
+ const at = node.attrInsertAt;
174
+ for (const attr of node.attrs) {
175
+ const name = attr.name.replace(/^:/, "");
176
+ if (name === "data-bcms-field" || name === "data-bcms-layout-field") {
177
+ const scope = name === "data-bcms-layout-field" ? "layout" : "page";
178
+ const template = content.slice(attr.range.start, attr.range.end).match(TEMPLATE_BINDING);
179
+ const templated = template?.[1] ?? template?.[2];
180
+ if (templated) out.push({ scope, path: shapeOfPath(stripLayoutPrefix(templated)), kind, at, via: "field" });
181
+ else if (attr.value) out.push({ scope, path: stripLayoutPrefix(attr.value), kind, at, via: "field" });
182
+ } else if (attr.name === "data-bcms-props") {
183
+ out.push(...propsDeclarations(attr.value, at));
184
+ } else if (attr.name === "bcmsBindings") {
185
+ for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
186
+ out.push({ scope: "page", path, kind: "", at, via: "prop" });
187
+ }
188
+ }
193
189
  }
194
- }
195
- return cursor;
196
- }
197
-
198
- /** A stored value as a template renders it: richtext is its html, an image is its url. */
199
- function render(value: unknown): string | undefined {
200
- if (typeof value === "string") return value;
201
- if (typeof value === "number" || typeof value === "boolean") return String(value);
202
- if (isRecord(value)) {
203
- if (typeof value.html === "string") return value.html;
204
- if (typeof value.url === "string") return value.url;
205
- }
206
- return undefined;
207
- }
208
-
209
- /** One field of one page. \`slug\` selects the entry on a dynamic route. */
210
- export function bcms(page: unknown, path: string, fallback: string, slug?: string): string {
211
- const entries = isRecord(page) ? page.entries : undefined;
212
- const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
213
- return render(readPath(root, path)) ?? fallback;
214
- }
215
-
216
- /** One field of the shared layout \u2014 the header and footer every route renders. */
217
- export function bcmsLayout(path: string, fallback: string): string {
218
- return render(readPath(layoutSnapshot, path)) ?? fallback;
219
- }
220
-
221
- /**
222
- * The ROWS of a repeater. \`fallback\` is every row the template shipped with, so a build against
223
- * an empty snapshot still renders the same number of cards it always did.
224
- */
225
- export function bcmsRows<T>(page: unknown, path: string, fallback: T[], slug?: string): T[] {
226
- const entries = isRecord(page) ? page.entries : undefined;
227
- const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
228
- const value = readPath(root, path);
229
- return Array.isArray(value) ? (value as T[]) : fallback;
230
- }
231
-
232
- /**
233
- * ONE SECTION's copy, for a component extracted by \`--componentize\`.
234
- *
235
- * OVERRIDES FIRST, PER KEY: \`overrides[key] ?? group[key]\`. An instance that owns its copy
236
- * (Phase B) carries an override for every prop it declares, so the group underneath it is never
237
- * read; a placement still bound to a page field group (Phase A) carries none, so the group is all
238
- * there is. The interesting case is the one in between \u2014 a section half moved \u2014 where a key the
239
- * instance does not hold has to keep rendering the page's value rather than nothing.
240
- *
241
- * \u{1F534} \`??\`, NOT \`||\`. An empty string is a value an editor typed, and treating it as absent put
242
- * the page's old copy back into a heading somebody had deliberately cleared.
243
- *
244
- * With neither an override nor a group, the answer is \`{}\` and every \`bcms(...)\` below renders
245
- * the fallback the template shipped with \u2014 which is why a section renders correctly against the
246
- * committed \`{}\` stub.
247
- */
248
- export function bcmsSection(page: unknown, bind?: string, overrides?: unknown): Record<string, unknown> {
249
- const group = bind ? readPath(page, bind) : undefined;
250
- const base: Record<string, unknown> = isRecord(group) ? { ...group } : {};
251
- if (!isRecord(overrides)) return base;
252
- for (const key of Object.keys(overrides)) {
253
- const value = overrides[key];
254
- if (value === undefined || value === null) continue;
255
- /**
256
- * \u{1F534} A TABLE PROP IS STORED AS \`{ rows: [...] }\` AND READ AS A LIST. That is the shape the
257
- * dashboard edits a repeater in; handing it to \`bcmsRows\` unchanged is not an array, so the
258
- * helper fell back to the rows the template shipped with and the editor's work never showed.
259
- */
260
- base[key] = isRecord(value) && Array.isArray(value.rows) ? value.rows : value;
261
- }
262
- return base;
263
- }
264
-
265
- /**
266
- * WHICH ADDRESS one bound element declares \u2014 decided at RENDER time, from the placement.
267
- *
268
- * An element carries exactly ONE \`data-bcms-field\`, and which lane it names depends on where its
269
- * copy lives. A placement that owns its copy declares the BLOCK lane \u2014
270
- * \`<blockId>__overrides.<propKey>\` \u2014 because that is the address the editor commits an instance's
271
- * own value to. A placement bound to a page field group declares the PAGE path, which is what the
272
- * page form and the coverage meter already know.
273
- *
274
- * \u{1F534} THE BOUND ADDRESS IS RE-BASED ONTO **THIS** PLACEMENT'S GROUP. The markup was cut from ONE
275
- * page and carries that page's paths (\`hero.title\`); the whole point of \`shapeHash\` is that a
276
- * second page renders the same component bound to a group of its own (\`bind="landingHero"\`).
277
- * Handing the embedded path through unchanged declared \`hero.title\` on a page whose field is
278
- * \`landingHero.title\` \u2014 an address that page does not have, so the editor wrote into nothing and
279
- * the meter counted a binding that was never there. The prefix the markup was cut with comes out,
280
- * the placement's own group goes in.
281
- *
282
- * \u{1F534} THE SAME EXTRACTED FILE SERVES BOTH LANES. The choice is a function of the props, never of
283
- * how the codemod ran: one component renders a Phase A placement on one page and a Phase B
284
- * instance on the next, and a repository does not have to be re-cut when its copy moves.
285
- */
286
- export function bcmsFields(
287
- placement: { blockId?: string; bind?: string; overrides?: unknown },
288
- pathPrefix: string,
289
- ): (path: string) => string {
290
- const id = placement.blockId;
291
- const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
292
- /**
293
- * What replaces the prefix the markup was cut with, or null for "leave every path alone".
294
- *
295
- * Null covers a placement with neither a bind nor an instance \u2014 a shipped fallback, or a page
296
- * nobody has componentized yet \u2014 and a bound placement in a component whose prefix is EMPTY,
297
- * where the plan said the prop key IS the whole page path: there is no group segment to swap,
298
- * so prepending one would invent an address rather than correct it.
299
- */
300
- const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
301
- return (path: string): string => {
302
- // A path outside the group this section was cut from is not one this placement re-addresses:
303
- // it names a key the instance does not hold and a group this placement is not bound to.
304
- // (An EMPTY prefix is a plan saying the prop key IS the page path, and matches everything.)
305
- if (lane === null || !path.startsWith(pathPrefix)) return path;
306
- return lane + path.slice(pathPrefix.length);
190
+ for (const child of node.children) visit(child);
307
191
  };
192
+ for (const root of roots) visit(root);
193
+ return out;
308
194
  }
309
- `;
310
- }
311
- var HISTORICAL = {
312
- "src/bcms-content.ts": {
313
- 1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
314
- 2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
315
- 3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227"
316
- },
317
- "src/lib/bcms-content.ts": {
318
- 1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
319
- 2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
320
- 3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e"
321
- },
322
- "composables/bcms-content.ts": {
323
- 1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
324
- 2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
325
- 3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227"
195
+ var bindingsPaths = (text) => [
196
+ ...[...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]),
197
+ ...[...text.matchAll(/`([^`]+)`/g)].map((match) => shapeOfPath(match[1]))
198
+ ];
199
+ var scopedPath = (scope, path) => `${scope}::${path}`;
200
+ var covers = (declared, target) => declared !== void 0 && (declared.has(scopedPath(target.scope, target.path)) || declared.has(scopedPath(target.scope, shapeOfPath(target.path))));
201
+ var declaredInTree = (roots, content) => new Set(declarationsIn(roots, content).map((d) => scopedPath(d.scope, d.path)));
202
+ function declaredByScan(content) {
203
+ const out = /* @__PURE__ */ new Set();
204
+ const add = (scope, path) => void out.add(scopedPath(scope, path));
205
+ for (const match of content.matchAll(BINDING_ATTR)) {
206
+ add(match[0].includes("layout-field") ? "layout" : "page", stripLayoutPrefix(match[1]));
326
207
  }
327
- };
328
- var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
329
- function isKnownHelper(content, dialect) {
330
- if (content === helperSource(dialect)) return HELPER_VERSION;
331
- const path = HELPER_PATH[dialect];
332
- if (!path) return null;
333
- const digest = sha256(content);
334
- for (const [version, known] of Object.entries(HISTORICAL[path] ?? {})) {
335
- if (known === digest) return Number(version);
208
+ for (const match of content.matchAll(TEMPLATE_SCAN)) {
209
+ add(match[0].includes("layout-field") ? "layout" : "page", shapeOfPath(stripLayoutPrefix(match[1] ?? match[2])));
336
210
  }
337
- return null;
211
+ for (const match of content.matchAll(BINDINGS_SCAN)) for (const path of bindingsPaths(match[0])) add("page", path);
212
+ for (const match of content.matchAll(PROPS_ATTR))
213
+ for (const entry of propsPaths(match[1])) add(entry.scope, entry.path);
214
+ return out;
338
215
  }
216
+ var stripLayoutPrefix = (value) => value.startsWith("layout:") ? value.slice("layout:".length) : value;
217
+
218
+ // src/sites/jsx.ts
219
+ import { parse as parseModule } from "@babel/parser";
339
220
 
340
221
  // src/entities.ts
341
222
  var LONGEST_ENTITY = 32;
@@ -2673,198 +2554,6 @@ function decodeStringEscapes(body) {
2673
2554
  }
2674
2555
  var unwrapJsxStrings = (value) => value.replace(JSX_STRING_CHILD, (_whole, _quote, body) => decodeStringEscapes(body));
2675
2556
 
2676
- // src/locate.ts
2677
- var flat = (value) => value.replace(/\s+/g, " ").trim();
2678
- var stripTags = (value) => value.replace(/<br\s*\/?>/gi, " ").replace(/<[^>]*>/g, "");
2679
- function locate(brief, sources, options) {
2680
- const flattened = sources.map((s) => {
2681
- const readable = unwrapJsxStrings(decodeEntities(s.content));
2682
- return {
2683
- path: s.path,
2684
- content: flat(readable),
2685
- text: options?.stripTags ? flat(stripTags(readable)) : null
2686
- };
2687
- });
2688
- const located = [];
2689
- const unlocated = [];
2690
- for (const page of brief.pages) {
2691
- for (const p of page.paths) {
2692
- const original = p.original === null ? "" : flat(p.original);
2693
- if (!original) {
2694
- unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "no-original" });
2695
- continue;
2696
- }
2697
- const files = flattened.filter((f) => f.content.includes(original) || f.text !== null && f.text.includes(original)).map((f) => f.path);
2698
- if (files.length === 0) {
2699
- unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "not-in-source" });
2700
- continue;
2701
- }
2702
- located.push({ route: page.route, scope: p.scope, path: p.path, kind: p.kind, original, files });
2703
- }
2704
- }
2705
- return { located, unlocated };
2706
- }
2707
-
2708
- // src/loops.ts
2709
- import MagicString from "magic-string";
2710
-
2711
- // src/declarations.ts
2712
- var BINDING_ATTR = /data-bcms-(?:layout-)?field\s*=\s*["']([^"']+)["']/g;
2713
- var PROPS_ATTR = /data-bcms-props\s*=\s*["']([^"']+)["']/g;
2714
- var TEMPLATE_SCAN = /:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")/g;
2715
- var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{[^}]*\}\}/g;
2716
- var propsDeclarations = (value, at) => value.split(";").flatMap((entry) => {
2717
- const [address, kind] = entry.split("|");
2718
- if (!address) return [];
2719
- const scope = address.startsWith("layout:") ? "layout" : "page";
2720
- return [{ scope, path: stripLayoutPrefix(address), kind: kind ?? "", at, via: "props" }];
2721
- });
2722
- var propsPaths = (value) => propsDeclarations(value, 0).map((d) => ({ scope: d.scope, path: d.path }));
2723
- var TEMPLATE_BINDING = /^:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")$/;
2724
- var shapeOfPath = (path) => path.replace(/\[(?:\d+|\$\{[^}]*\})\]/g, "[*]");
2725
- function declarationsIn(roots, content) {
2726
- const out = [];
2727
- const visit = (node) => {
2728
- if (node.type === "expr") {
2729
- for (const child of node.children) visit(child);
2730
- return;
2731
- }
2732
- if (node.type !== "element") return;
2733
- const kind = node.attrs.find((a) => a.name === "data-bcms-kind")?.value ?? "";
2734
- const at = node.attrInsertAt;
2735
- for (const attr of node.attrs) {
2736
- const name = attr.name.replace(/^:/, "");
2737
- if (name === "data-bcms-field" || name === "data-bcms-layout-field") {
2738
- const scope = name === "data-bcms-layout-field" ? "layout" : "page";
2739
- const template = content.slice(attr.range.start, attr.range.end).match(TEMPLATE_BINDING);
2740
- const templated = template?.[1] ?? template?.[2];
2741
- if (templated) out.push({ scope, path: shapeOfPath(stripLayoutPrefix(templated)), kind, at, via: "field" });
2742
- else if (attr.value) out.push({ scope, path: stripLayoutPrefix(attr.value), kind, at, via: "field" });
2743
- } else if (attr.name === "data-bcms-props") {
2744
- out.push(...propsDeclarations(attr.value, at));
2745
- } else if (attr.name === "bcmsBindings") {
2746
- for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
2747
- out.push({ scope: "page", path, kind: "", at, via: "prop" });
2748
- }
2749
- }
2750
- }
2751
- for (const child of node.children) visit(child);
2752
- };
2753
- for (const root of roots) visit(root);
2754
- return out;
2755
- }
2756
- var bindingsPaths = (text) => [...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
2757
- var scopedPath = (scope, path) => `${scope}::${path}`;
2758
- var covers = (declared, target) => declared !== void 0 && (declared.has(scopedPath(target.scope, target.path)) || declared.has(scopedPath(target.scope, shapeOfPath(target.path))));
2759
- var declaredInTree = (roots, content) => new Set(declarationsIn(roots, content).map((d) => scopedPath(d.scope, d.path)));
2760
- function declaredByScan(content) {
2761
- const out = /* @__PURE__ */ new Set();
2762
- const add = (scope, path) => void out.add(scopedPath(scope, path));
2763
- for (const match of content.matchAll(BINDING_ATTR)) {
2764
- add(match[0].includes("layout-field") ? "layout" : "page", stripLayoutPrefix(match[1]));
2765
- }
2766
- for (const match of content.matchAll(TEMPLATE_SCAN)) {
2767
- add(match[0].includes("layout-field") ? "layout" : "page", shapeOfPath(stripLayoutPrefix(match[1] ?? match[2])));
2768
- }
2769
- for (const match of content.matchAll(BINDINGS_SCAN)) for (const path of bindingsPaths(match[0])) add("page", path);
2770
- for (const match of content.matchAll(PROPS_ATTR))
2771
- for (const entry of propsPaths(match[1])) add(entry.scope, entry.path);
2772
- return out;
2773
- }
2774
- var stripLayoutPrefix = (value) => value.startsWith("layout:") ? value.slice("layout:".length) : value;
2775
-
2776
- // src/scope.ts
2777
- function patternNames(node, out) {
2778
- if (!node || typeof node !== "object") return;
2779
- const typed = node;
2780
- switch (typed.type) {
2781
- case "Identifier":
2782
- out.add(String(typed.name));
2783
- return;
2784
- case "ObjectPattern":
2785
- for (const property of typed.properties ?? [])
2786
- patternNames(property.type === "RestElement" ? property.argument : property.value, out);
2787
- return;
2788
- case "ArrayPattern":
2789
- for (const element of typed.elements ?? []) patternNames(element, out);
2790
- return;
2791
- case "AssignmentPattern":
2792
- patternNames(typed.left, out);
2793
- return;
2794
- case "RestElement":
2795
- patternNames(typed.argument, out);
2796
- return;
2797
- default:
2798
- return;
2799
- }
2800
- }
2801
- function statementNames(node, out) {
2802
- if (!node || typeof node !== "object") return;
2803
- const typed = node;
2804
- switch (typed.type) {
2805
- case "ExportNamedDeclaration":
2806
- case "ExportDefaultDeclaration":
2807
- statementNames(typed.declaration, out);
2808
- return;
2809
- case "ImportDeclaration":
2810
- for (const specifier of typed.specifiers ?? []) patternNames(specifier.local, out);
2811
- return;
2812
- case "VariableDeclaration":
2813
- for (const declarator of typed.declarations ?? []) patternNames(declarator.id, out);
2814
- return;
2815
- case "FunctionDeclaration":
2816
- case "ClassDeclaration":
2817
- case "TSTypeAliasDeclaration":
2818
- case "TSInterfaceDeclaration":
2819
- case "TSEnumDeclaration":
2820
- case "TSModuleDeclaration":
2821
- patternNames(typed.id, out);
2822
- return;
2823
- default:
2824
- return;
2825
- }
2826
- }
2827
- var DECLARED = /\b(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g;
2828
- var IMPORTED = /\bimport\s+([^;\n]*?)\s+from\b/g;
2829
- function scanNames(code, out) {
2830
- for (const match of code.matchAll(DECLARED)) out.add(match[1]);
2831
- for (const match of code.matchAll(IMPORTED))
2832
- for (const name of match[1].match(/[A-Za-z_$][\w$]*/g) ?? []) {
2833
- if (name !== "as" && name !== "type") out.add(name);
2834
- }
2835
- }
2836
- function scriptText(nodes, content, out) {
2837
- for (const node of nodes) {
2838
- if (node.type !== "element") continue;
2839
- if (node.raw && node.tag === "script" && node.inner) out.push(content.slice(node.inner.start, node.inner.end));
2840
- scriptText(node.children, content, out);
2841
- }
2842
- }
2843
- function topLevelBindings(dialect, parsed, content) {
2844
- const out = /* @__PURE__ */ new Set();
2845
- const program = parsed.ast?.program;
2846
- if (program) for (const statement of program.body ?? []) statementNames(statement, out);
2847
- else if (parsed.script) scanNames(parsed.script.code, out);
2848
- if (dialect === "html") {
2849
- const scripts = [];
2850
- scriptText(parsed.roots, content, scripts);
2851
- for (const code of scripts) scanNames(code, out);
2852
- }
2853
- return out;
2854
- }
2855
- function allocateNames(taken, wanted) {
2856
- const used = new Set(taken);
2857
- const out = /* @__PURE__ */ new Map();
2858
- for (const { key, name } of wanted) {
2859
- if (out.has(key)) continue;
2860
- let candidate = name;
2861
- for (let n = 1; used.has(candidate); n++) candidate = `${name}_${n}`;
2862
- used.add(candidate);
2863
- out.set(key, candidate);
2864
- }
2865
- return out;
2866
- }
2867
-
2868
2557
  // src/sites/shared.ts
2869
2558
  var isIntrinsic = (tag) => tag !== "" && !tag.includes(".") && tag[0] === tag[0].toLowerCase();
2870
2559
  var IMAGE_SOURCES = {
@@ -2905,7 +2594,7 @@ function renderedText(node) {
2905
2594
  var attrsOf = (node) => Object.fromEntries(node.attrs.map((a) => [a.name, a.value]));
2906
2595
  function soleTextChild(node) {
2907
2596
  const texts = node.children.filter((c) => c.type === "text" && !isBlank(c.value));
2908
- return texts.length === 1 ? texts[0].range : null;
2597
+ return texts.length === 1 ? texts[0] : null;
2909
2598
  }
2910
2599
  function collectSites(roots, literals) {
2911
2600
  const wanted = new Set(literals.map(norm));
@@ -2965,13 +2654,39 @@ function collectSites(roots, literals) {
2965
2654
  scope: "page",
2966
2655
  existingAttrs: attrs,
2967
2656
  mixed: node.children.some((c) => c.type !== "text"),
2968
- textRange: soleTextChild(node),
2657
+ textRange: soleTextChild(node)?.range ?? null,
2969
2658
  literal,
2970
2659
  partial: false,
2971
2660
  ancestors: walk.ancestors,
2972
2661
  siblingIndex: walk.siblingIndex
2973
2662
  });
2974
2663
  }
2664
+ const own = soleTextChild(node);
2665
+ if (own && node.children.some((c) => c.type !== "text")) {
2666
+ const value = norm(own.value);
2667
+ for (const literal of wanted) {
2668
+ if (matched.has(literal) || value !== literal) continue;
2669
+ matched.add(literal);
2670
+ sites.push({
2671
+ file: "",
2672
+ tag: node.tag,
2673
+ attrInsertAt: node.attrInsertAt,
2674
+ // The TEXT NODE's bytes, not the element's: two values inside one element must not be
2675
+ // read as claiming the same span. @see overlapping
2676
+ range: own.range,
2677
+ inner: node.inner,
2678
+ where: "text",
2679
+ scope: "page",
2680
+ existingAttrs: attrs,
2681
+ mixed: true,
2682
+ textRange: own.range,
2683
+ literal,
2684
+ partial: false,
2685
+ ancestors: walk.ancestors,
2686
+ siblingIndex: walk.siblingIndex
2687
+ });
2688
+ }
2689
+ }
2975
2690
  const bindingsAttr = node.attrs.find((a) => a.name === "bcmsBindings")?.range;
2976
2691
  for (const attr of node.attrs) {
2977
2692
  if (attr.expression || !attr.valueRange || BCMS_ATTRS.includes(attr.name)) continue;
@@ -3038,7 +2753,7 @@ function partials(node, literals, walk, out) {
3038
2753
  scope: "page",
3039
2754
  existingAttrs: attrsOf(node),
3040
2755
  mixed: node.children.some((c) => c.type !== "text"),
3041
- textRange: soleTextChild(node),
2756
+ textRange: soleTextChild(node)?.range ?? null,
3042
2757
  literal,
3043
2758
  partial: true,
3044
2759
  ancestors: walk.ancestors,
@@ -3058,264 +2773,32 @@ function wordBounded(haystack, needle) {
3058
2773
  return false;
3059
2774
  }
3060
2775
 
3061
- // src/loops.ts
3062
- function splitNested(leaf) {
3063
- const match = leaf.match(/^([^[\]]+)\[(\d+)\]\.(.+)$/);
3064
- return match ? { array: match[1], index: Number(match[2]), leaf: match[3] } : null;
3065
- }
3066
- var elementEnd = (node) => node.inner ? node.inner.end + node.tag.length + 3 : null;
3067
- function findRows(roots, members) {
3068
- const wanted = new Set(members.map((m) => m.index));
3069
- const search = (node) => {
3070
- if (node.type !== "element") return null;
3071
- for (const child of node.children) {
3072
- const deeper = search(child);
3073
- if (deeper) return deeper;
3074
- }
3075
- const rows = [];
3076
- for (const child of node.children) {
3077
- if (child.type !== "element") continue;
3078
- const end = elementEnd(child);
3079
- if (end === null) continue;
3080
- const inside = members.filter((m) => m.site.range.start >= child.start && m.site.range.end <= end);
3081
- if (inside.length === 0) continue;
3082
- const indices = new Set(inside.map((m) => m.index));
3083
- if (indices.size !== 1) return null;
3084
- rows.push({
3085
- index: inside[0].index,
3086
- range: { start: child.start, end },
3087
- attrInsertAt: child.attrInsertAt,
3088
- // `:key` in vue, `key` in jsx — one existing key, whichever way it is spelled.
3089
- keyAttr: child.attrs.find((a) => a.name.replace(/^:/, "") === "key")?.range ?? null,
3090
- node: child,
3091
- members: inside
3092
- });
3093
- }
3094
- if (rows.length === 0) return null;
3095
- const covered = new Set(rows.flatMap((r) => r.members.map((m) => m.index)));
3096
- if (covered.size !== wanted.size || rows.length !== wanted.size) return null;
3097
- if (rows.some((r) => r.members.length !== members.length / wanted.size)) return null;
3098
- return rows.sort((a, b) => a.range.start - b.range.start);
3099
- };
3100
- for (const root of roots) {
3101
- const found = search(root);
3102
- if (found) return found;
3103
- }
3104
- return null;
3105
- }
3106
- var consecutive = (rows, content) => rows.every((row, at) => at === 0 || content.slice(rows[at - 1].range.end, row.range.start).trim() === "");
3107
- function maskedRow(row, content) {
3108
- const masked = new Map(row.members.map((member) => [member.site.inner.start, member.leaf]));
3109
- return canonical(row.node, content, masked);
3110
- }
3111
- var TRIVIA = "";
3112
- function canonical(node, content, masked) {
3113
- if (node.type === "text") {
3114
- return node.value.trim() === "" ? TRIVIA : content.slice(node.range.start, node.range.end);
3115
- }
3116
- if (node.type === "comment" || node.type === "expr") {
3117
- return content.slice(node.range.start, node.range.end);
3118
- }
3119
- if (node.type !== "element") return "";
3120
- const open = content.slice(node.start, node.inner ? node.inner.start : node.attrInsertAt);
3121
- if (node.inner && masked.has(node.inner.start)) {
3122
- return `${open}\0${masked.get(node.inner.start)}\0</${node.tag}>`;
3123
- }
3124
- const inside = node.children.map((child) => canonical(child, content, masked)).join("");
3125
- return node.inner ? `${open}${inside}</${node.tag}>` : open;
3126
- }
3127
- function sameShape(rows, content) {
3128
- const first = maskedRow(rows[0], content);
3129
- return rows.every((row) => maskedRow(row, content) === first);
3130
- }
3131
- var indented = (body, indent) => body.split("\n").map((line) => line.startsWith(indent) ? `${indent} ${line.slice(indent.length)}` : `${indent} ${line}`).join("\n");
3132
- function indentOf(content, at) {
3133
- const lineStart = content.lastIndexOf("\n", at - 1) + 1;
3134
- const prefix = content.slice(lineStart, at);
3135
- return prefix.trim() === "" ? prefix : "";
3136
- }
3137
- function writeLeaf(source, shift, member, rule, read, row, literal) {
3138
- const site = member.site;
3139
- const expression = `${read}(${row}, ${JSON.stringify(member.leaf)}, "")`;
3140
- if (member.kind === "richtext" && rule.richtext) {
3141
- source.remove(site.inner.start + shift, site.inner.end + shift);
3142
- source.appendLeft(site.attrInsertAt + shift, ` ${rule.richtext(expression)}`);
3143
- } else {
3144
- source.overwrite(site.inner.start + shift, site.inner.end + shift, rule.text(expression));
3145
- }
3146
- const kind = member.kind ? ` data-bcms-kind="${member.kind}"` : "";
3147
- source.appendLeft(site.attrInsertAt + shift, ` ${rule.attr("data-bcms-field", `\`${literal}\``)}${kind}`);
3148
- }
3149
- function emitNested(rule, array, members, prefix, outer, parent, ctx) {
3150
- if (!rule.loop || !rule.text) return null;
3151
- const inner = [];
3152
- for (const member of members) {
3153
- const split = splitNested(member.leaf);
3154
- if (!split || splitNested(split.leaf) !== null) return null;
3155
- inner.push({ ...member, index: split.index, leaf: split.leaf });
3156
- }
3157
- const rows = findRows([parent.node], inner);
3158
- if (!rows || rows.length === 0) return null;
3159
- if (!consecutive(rows, ctx.content)) return null;
3160
- if (!sameShape(rows, ctx.content)) return null;
3161
- const template = rows[0];
3162
- const used = /* @__PURE__ */ new Set([
3163
- ...ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? [],
3164
- outer.row,
3165
- outer.index
3166
- ]);
3167
- const allocated = allocateNames(used, [
3168
- { key: "row", name: "item" },
3169
- { key: "index", name: "j" }
3170
- ]);
3171
- const names = { row: allocated.get("row"), index: allocated.get("index") };
3172
- const fallback = [];
3173
- const writable = inner.every((member) => setLeaf(fallback[member.index] ??= emptyRow(), member.leaf, member.fallback));
3174
- if (!writable) return null;
3175
- const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
3176
- const shift = -template.range.start;
3177
- const declarations = [];
3178
- for (const member of template.members) {
3179
- const literal = `${prefix}.${array}[\${${names.index}}].${member.leaf}`;
3180
- writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
3181
- declarations.push({ path: shapeOfPath(literal), literal });
3182
- }
3183
- const rowsExpr = `${ctx.rows}(${outer.row}, ${JSON.stringify(array)}, ${JSON.stringify(fallback.filter(Boolean))})`;
3184
- if (rule.loop.kind === "attr") {
3185
- source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
3186
- }
3187
- if (rule.loopKey) {
3188
- const key = rule.loopKey(names.index);
3189
- if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
3190
- else if (template.keyAttr.end > template.keyAttr.start)
3191
- source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
3192
- else return null;
3193
- }
3194
- const indent = indentOf(ctx.content, template.range.start);
3195
- const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
3196
- ${indented(source.toString(), indent)}
3197
- ${indent}${rule.loop.close}` : source.toString();
3198
- return { range: { start: template.range.start, end: rows[rows.length - 1].range.end }, text, declarations };
3199
- }
3200
- function emitRepeater(dialect, group, arrayPath, fallbackRows, ctx) {
3201
- const rule = DIALECT_RULES[dialect];
3202
- if (!rule.loop || !rule.text || group.length === 0) return null;
3203
- const rows = findRows(ctx.roots, group);
3204
- if (!rows || rows.length < 2) return null;
3205
- if (!consecutive(rows, ctx.content)) return null;
3206
- if (!sameShape(rows, ctx.content)) return null;
3207
- if (group.some((m) => m.site.where !== "text" || !isIntrinsic(m.site.tag) || !m.site.inner)) return null;
3208
- const template = rows[0];
3209
- const used = new Set(ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? []);
3210
- const allocated = allocateNames(used, [
3211
- { key: "row", name: "card" },
3212
- { key: "index", name: "i" }
3213
- ]);
3214
- const names = { row: allocated.get("row"), index: allocated.get("index") };
3215
- const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
3216
- const shift = -template.range.start;
3217
- const declarations = [];
3218
- const nested = /* @__PURE__ */ new Map();
3219
- const direct = [];
3220
- for (const member of template.members) {
3221
- const inner = splitNested(member.leaf);
3222
- if (inner === null) direct.push(member);
3223
- else nested.set(inner.array, [...nested.get(inner.array) ?? [], member]);
3224
- }
3225
- for (const [array, members] of nested) {
3226
- const plan2 = emitNested(rule, array, members, `${arrayPath}[\${${names.index}}]`, names, template, ctx);
3227
- if (!plan2) return null;
3228
- if (direct.some((m) => m.site.range.start >= plan2.range.start && m.site.range.end <= plan2.range.end)) return null;
3229
- source.overwrite(plan2.range.start + shift, plan2.range.end + shift, plan2.text);
3230
- declarations.push(...plan2.declarations);
3231
- }
3232
- for (const member of direct) {
3233
- const literal = `${arrayPath}[\${${names.index}}].${member.leaf}`;
3234
- writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
3235
- declarations.push({ path: shapeOfPath(literal), literal });
3236
- }
3237
- const args = [ctx.snapshot, JSON.stringify(arrayPath), JSON.stringify(fallbackRows), ...ctx.slug ? [ctx.slug] : []];
3238
- const rowsExpr = `${ctx.rows}(${args.join(", ")})`;
3239
- if (rule.loop.kind === "attr") {
3240
- source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
3241
- }
3242
- if (rule.loopKey) {
3243
- const key = rule.loopKey(names.index);
3244
- if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
3245
- else if (template.keyAttr.end > template.keyAttr.start)
3246
- source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
3247
- else return null;
3248
- }
3249
- const indent = indentOf(ctx.content, template.range.start);
3250
- const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
3251
- ${indented(source.toString(), indent)}
3252
- ${indent}${rule.loop.close}` : source.toString();
3253
- return {
3254
- range: { start: template.range.start, end: rows[rows.length - 1].range.end },
3255
- text,
3256
- declarations,
3257
- bindings: [...new Set(declarations.map((d) => d.path))],
3258
- names
3259
- };
3260
- }
3261
- function setLeaf(row, leaf, value) {
3262
- const segments = leaf.split(".").flatMap((segment) => {
3263
- const name = segment.replace(/\[\d+\]/g, "");
3264
- const indices = [...segment.matchAll(/\[(\d+)\]/g)].map((m) => Number(m[1]));
3265
- return [...name ? [name] : [], ...indices];
3266
- });
3267
- if (segments.some((segment) => typeof segment === "string" && FORBIDDEN.has(segment))) return false;
3268
- let cursor = row;
3269
- for (const [at, segment] of segments.entries()) {
3270
- const key = segment;
3271
- const last = at === segments.length - 1;
3272
- if (last) {
3273
- cursor[key] = value;
3274
- return true;
3275
- }
3276
- const held = Object.prototype.hasOwnProperty.call(cursor, key) ? cursor[key] : void 0;
3277
- if (held === void 0 || typeof held !== "object" || held === null) {
3278
- cursor[key] = typeof segments[at + 1] === "number" ? [] : emptyRow();
3279
- }
3280
- cursor = cursor[key];
3281
- }
3282
- return true;
3283
- }
3284
- var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3285
- var emptyRow = () => /* @__PURE__ */ Object.create(null);
3286
- function splitIndexed(arrayPath, path) {
3287
- if (!path.startsWith(`${arrayPath}[`)) return null;
3288
- const match = path.slice(arrayPath.length).match(/^\[(\d+)\]\.(.+)$/);
3289
- return match ? { index: Number(match[1]), leaf: match[2] } : null;
3290
- }
3291
-
3292
- // src/sites/jsx.ts
3293
- import { parse as parseModule } from "@babel/parser";
3294
- var nameOf = (node) => {
3295
- if (!node) return "";
3296
- if (node.type === "JSXIdentifier") return String(node.name);
3297
- if (node.type === "JSXMemberExpression")
3298
- return `${nameOf(node.object)}.${nameOf(node.property)}`;
3299
- if (node.type === "JSXNamespacedName")
3300
- return `${nameOf(node.namespace)}:${nameOf(node.name)}`;
3301
- return "";
3302
- };
3303
- var SPREAD = "{...}";
3304
- function attrsOf2(open, content) {
3305
- return (open.attributes ?? []).flatMap((attr) => {
3306
- if (attr.type === "JSXSpreadAttribute") {
3307
- const argument = attr.argument;
3308
- return [
3309
- {
3310
- name: SPREAD,
3311
- value: content.slice(argument?.start ?? 0, argument?.end ?? 0),
3312
- valueRange: null,
3313
- expression: true,
3314
- range: { start: attr.start ?? 0, end: attr.end ?? 0 },
3315
- // `{...props}` — the braces ARE the attribute, so the expression is its whole span.
3316
- expressionRange: { start: attr.start ?? 0, end: attr.end ?? 0 }
3317
- }
3318
- ];
2776
+ // src/sites/jsx.ts
2777
+ var nameOf = (node) => {
2778
+ if (!node) return "";
2779
+ if (node.type === "JSXIdentifier") return String(node.name);
2780
+ if (node.type === "JSXMemberExpression")
2781
+ return `${nameOf(node.object)}.${nameOf(node.property)}`;
2782
+ if (node.type === "JSXNamespacedName")
2783
+ return `${nameOf(node.namespace)}:${nameOf(node.name)}`;
2784
+ return "";
2785
+ };
2786
+ var SPREAD = "{...}";
2787
+ function attrsOf2(open, content) {
2788
+ return (open.attributes ?? []).flatMap((attr) => {
2789
+ if (attr.type === "JSXSpreadAttribute") {
2790
+ const argument = attr.argument;
2791
+ return [
2792
+ {
2793
+ name: SPREAD,
2794
+ value: content.slice(argument?.start ?? 0, argument?.end ?? 0),
2795
+ valueRange: null,
2796
+ expression: true,
2797
+ range: { start: attr.start ?? 0, end: attr.end ?? 0 },
2798
+ // `{...props}` — the braces ARE the attribute, so the expression is its whole span.
2799
+ expressionRange: { start: attr.start ?? 0, end: attr.end ?? 0 }
2800
+ }
2801
+ ];
3319
2802
  }
3320
2803
  if (attr.type !== "JSXAttribute") return [];
3321
2804
  const value = attr.value;
@@ -3549,7 +3032,7 @@ function receptionIn(component, propName, offset, content) {
3549
3032
  let receiver = null;
3550
3033
  let rest = null;
3551
3034
  let hasBindings = false;
3552
- let destructured = false;
3035
+ let destructured2 = false;
3553
3036
  for (const node of component ? [component] : []) {
3554
3037
  const params = node.params ?? [];
3555
3038
  const first = params[0];
@@ -3575,7 +3058,7 @@ function receptionIn(component, propName, offset, content) {
3575
3058
  if (key !== propName) continue;
3576
3059
  const value = property.value;
3577
3060
  if (value?.type === "Identifier") names.add(String(value.name));
3578
- destructured = true;
3061
+ destructured2 = true;
3579
3062
  receivable = true;
3580
3063
  receive = patternClose(content, first, offset);
3581
3064
  }
@@ -3585,7 +3068,7 @@ function receptionIn(component, propName, offset, content) {
3585
3068
  viaProps = true;
3586
3069
  }
3587
3070
  }
3588
- return { names, receive, viaProps, typeName, receivable, inline, receiver, rest, hasBindings, destructured };
3071
+ return { names, receive, viaProps, typeName, receivable, inline, receiver, rest, hasBindings, destructured: destructured2 };
3589
3072
  }
3590
3073
  function astroReception(ast, propName, offset, content) {
3591
3074
  const names = /* @__PURE__ */ new Set();
@@ -3759,129 +3242,1493 @@ function tryParse(code) {
3759
3242
  return void 0;
3760
3243
  }
3761
3244
  }
3762
- var bindingsName = (viaProps) => viaProps ? "props.bcmsBindings" : "bcmsBindings";
3763
- var bindingsRead = (prop, viaProps) => `${bindingsName(viaProps)}?.${prop}`;
3764
-
3765
- // src/resolve.ts
3766
- var EXTENSIONS = [".astro", ".tsx", ".jsx", ".ts", ".js", ".svelte", ".vue", ".mjs", ".cjs"];
3767
- function join(fromFile, specifier) {
3768
- const parts = fromFile.split("/").slice(0, -1);
3769
- for (const segment of specifier.split("/")) {
3770
- if (segment === "." || segment === "") continue;
3771
- if (segment === "..") parts.pop();
3772
- else parts.push(segment);
3245
+ var bindingsName = (viaProps) => viaProps ? "props.bcmsBindings" : "bcmsBindings";
3246
+ var bindingsRead = (prop, viaProps) => `${bindingsName(viaProps)}?.${prop}`;
3247
+
3248
+ // src/data.ts
3249
+ function splitGroup(path) {
3250
+ const match = path.match(/^([^[\]]+)\[(\d+)\]\.(.+)$/);
3251
+ return match ? { array: match[1], index: Number(match[2]), leaf: match[3] } : null;
3252
+ }
3253
+ function moduleOf(parsed) {
3254
+ return { ast: parsed.ast ?? null, offset: parsed.script?.offset ?? 0 };
3255
+ }
3256
+ function arrayLiterals(ast) {
3257
+ const out = /* @__PURE__ */ new Map();
3258
+ walkAst(ast, (node) => {
3259
+ if (node.type !== "VariableDeclarator") return;
3260
+ const id = node.id;
3261
+ const init = node.init;
3262
+ if (id?.type !== "Identifier" || init?.type !== "ArrayExpression") return;
3263
+ out.set(String(id.name), init);
3264
+ });
3265
+ return out;
3266
+ }
3267
+ function exportedNames(ast) {
3268
+ const out = /* @__PURE__ */ new Set();
3269
+ walkAst(ast, (node) => {
3270
+ if (node.type !== "ExportNamedDeclaration") return;
3271
+ const declaration2 = node.declaration;
3272
+ if (declaration2?.type !== "VariableDeclaration") return;
3273
+ for (const declarator of declaration2.declarations ?? []) {
3274
+ const id = declarator.id;
3275
+ if (id?.type === "Identifier") out.add(String(id.name));
3276
+ }
3277
+ });
3278
+ return out;
3279
+ }
3280
+ function propertyFor(row, literal) {
3281
+ if (row?.type !== "ObjectExpression") return null;
3282
+ for (const property of row.properties ?? []) {
3283
+ if (property.type !== "ObjectProperty" && property.type !== "Property") continue;
3284
+ const key = property.key;
3285
+ const value = property.value;
3286
+ if (value?.type !== "StringLiteral" || key?.type !== "Identifier") continue;
3287
+ if (norm(String(value.value)) === literal) return String(key.name);
3288
+ }
3289
+ return null;
3290
+ }
3291
+ function iterations(roots, identifier2) {
3292
+ const pattern = new RegExp(`(?<![\\w$])${identifier2}(?![\\w$])\\s*\\.\\s*map\\s*\\(`);
3293
+ const out = [];
3294
+ const visit = (node) => {
3295
+ if (node.type === "expr") {
3296
+ if (pattern.test(node.value)) out.push(node);
3297
+ return;
3298
+ }
3299
+ if (node.type === "element" && !node.raw) for (const child of node.children) visit(child);
3300
+ };
3301
+ for (const root of roots) visit(root);
3302
+ return out;
3303
+ }
3304
+ var CALLBACK = String.raw`\s*\.\s*map\s*\(\s*(?:\(\s*(?:([A-Za-z_$][\w$]*)|(\[[^\]]*\]))\s*(,\s*([A-Za-z_$][\w$]*)\s*)?\)|([A-Za-z_$][\w$]*))\s*=>`;
3305
+ function paramsIn(text, pattern, base) {
3306
+ const match = text.match(pattern);
3307
+ if (!match || match.index === void 0) return null;
3308
+ if (match[5] !== void 0) return { row: match[5], names: null, index: null, addIndexAt: -1 };
3309
+ const bound = match[1] ?? match[2];
3310
+ const within = match[0].lastIndexOf(bound);
3311
+ return {
3312
+ // One of the two, never both: a row is either named or taken apart. @see bindLists for the
3313
+ // second shape — `([question, answer]) =>`, which is how a TUPLE reaches the markup.
3314
+ row: match[1] ?? null,
3315
+ names: match[2] === void 0 ? null : destructured(match[2]),
3316
+ index: match[4] ?? null,
3317
+ addIndexAt: base + match.index + within + bound.length
3318
+ };
3319
+ }
3320
+ function callbackParams(expression, identifier2) {
3321
+ return paramsIn(
3322
+ expression.value,
3323
+ new RegExp(`(?<![\\w$])${identifier2}(?![\\w$])${CALLBACK}`),
3324
+ expression.range.start
3325
+ );
3326
+ }
3327
+ function mapParams(content, at, until) {
3328
+ return paramsIn(content.slice(at, until), new RegExp(`^${CALLBACK}`), at);
3329
+ }
3330
+ function destructured(pattern) {
3331
+ const names = pattern.slice(1, -1).split(",").map((part) => part.trim());
3332
+ return names.every((name) => /^[A-Za-z_$][\w$]*$/.test(name)) ? names : null;
3333
+ }
3334
+ function renders(content, node, row) {
3335
+ if (!node.inner) return null;
3336
+ const inner = content.slice(node.inner.start, node.inner.end).trim();
3337
+ const match = inner.match(/^\{\s*([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)\s*\}$/);
3338
+ return match && match[1] === row ? match[2] : null;
3339
+ }
3340
+ function leafElements(content, expression, row) {
3341
+ const out = /* @__PURE__ */ new Map();
3342
+ const visit = (node) => {
3343
+ if (node.type === "expr") {
3344
+ for (const child of node.children) visit(child);
3345
+ return;
3346
+ }
3347
+ if (node.type !== "element" || node.raw) return;
3348
+ const property = isIntrinsic(node.tag) ? renders(content, node, row) : null;
3349
+ if (property) out.set(property, [...out.get(property) ?? [], node]);
3350
+ for (const child of node.children) visit(child);
3351
+ };
3352
+ for (const child of expression.children) visit(child);
3353
+ return out;
3354
+ }
3355
+ function stringLiterals(ast) {
3356
+ const out = /* @__PURE__ */ new Map();
3357
+ walkAst(ast, (node) => {
3358
+ if (node.type !== "VariableDeclarator") return;
3359
+ const id = node.id;
3360
+ const init = node.init;
3361
+ if (id?.type !== "Identifier" || init?.type !== "StringLiteral") return;
3362
+ out.set(String(id.name), init);
3363
+ });
3364
+ return out;
3365
+ }
3366
+ function scalarUses(content, roots, id) {
3367
+ const mention = new RegExp(`(?<![\\w$])${id}(?![\\w$])`, "g");
3368
+ const alone = new RegExp(`^\\{\\s*${id}\\s*\\}$`);
3369
+ const count = (text) => (text.match(mention) ?? []).length;
3370
+ let total = 0;
3371
+ const hosts = [];
3372
+ const visit = (node) => {
3373
+ if (node.type === "expr") {
3374
+ total += count(node.value);
3375
+ for (const child of node.children) visit(child);
3376
+ return;
3377
+ }
3378
+ if (node.type !== "element" || node.raw) return;
3379
+ for (const attr of node.attrs) {
3380
+ if (attr.expressionRange) total += count(content.slice(attr.expressionRange.start, attr.expressionRange.end));
3381
+ }
3382
+ if (node.inner && isIntrinsic(node.tag) && alone.test(content.slice(node.inner.start, node.inner.end).trim())) {
3383
+ hosts.push(node);
3384
+ }
3385
+ for (const child of node.children) visit(child);
3386
+ };
3387
+ for (const root of roots) visit(root);
3388
+ return { total, hosts };
3389
+ }
3390
+ function bindScalar(ctx, member, literals, offset) {
3391
+ const rule = DIALECT_RULES[ctx.dialect];
3392
+ const named = [...literals].filter(([, init2]) => norm(String(init2.value)) === member.literal);
3393
+ if (named.length !== 1) return { splices: [], refusal: null };
3394
+ const [id, init] = named[0];
3395
+ const uses = scalarUses(ctx.content, ctx.parsed.roots, id);
3396
+ if (uses.hosts.length !== 1 || uses.total !== 1) return { splices: [], refusal: "IN_EXPRESSION" };
3397
+ const host = uses.hosts[0];
3398
+ const at = { start: offset + (init.start ?? 0), end: offset + (init.end ?? 0) };
3399
+ const args = [
3400
+ ctx.snapshotName(member.slug),
3401
+ JSON.stringify(member.path),
3402
+ // The literal EXACTLY as the author wrote it — quotes, escapes and all — so the fallback the
3403
+ // build renders is the byte-for-byte copy the repository renders today.
3404
+ ctx.content.slice(at.start, at.end),
3405
+ ...ctx.slug ? [ctx.slug] : []
3406
+ ];
3407
+ const attributes = [
3408
+ `data-bcms-field="${member.path}"`,
3409
+ ...member.kind ? [`data-bcms-kind="${member.kind}"`] : [],
3410
+ ...member.kind === "richtext" && rule.richtext ? [rule.richtext(id)] : []
3411
+ ];
3412
+ const splices = [
3413
+ { start: at.start, end: at.end, text: `${ctx.read}(${args.join(", ")})` },
3414
+ { start: host.attrInsertAt, end: host.attrInsertAt, text: ` ${attributes.join(" ")}` }
3415
+ ];
3416
+ if (member.kind === "richtext" && rule.richtext && host.inner) {
3417
+ splices.push({ start: host.inner.start, end: host.inner.end, text: "" });
3418
+ }
3419
+ return { splices, refusal: null };
3420
+ }
3421
+ async function bindDataLiterals(ctx, members) {
3422
+ const out = { splices: [], usesRead: false, usesRows: false, usesList: false, usesTuples: false, notes: [], slugs: [], bound: [], declarations: [], refused: [], componentEdits: [], props: [] };
3423
+ const rule = DIALECT_RULES[ctx.dialect];
3424
+ const module = moduleOf(ctx.parsed);
3425
+ if (!rule.text) return out;
3426
+ const groups = /* @__PURE__ */ new Map();
3427
+ const scalars = [];
3428
+ for (const member of members) {
3429
+ const split = splitGroup(member.path);
3430
+ if (!split) {
3431
+ scalars.push(member);
3432
+ continue;
3433
+ }
3434
+ const held = { ...member, array: split.array, index: split.index, leaf: split.leaf };
3435
+ const key = `${member.slug}\0${split.array}`;
3436
+ groups.set(key, [...groups.get(key) ?? [], held]);
3437
+ }
3438
+ const indexes = /* @__PURE__ */ new Set();
3439
+ const strings = stringLiterals(module.ast);
3440
+ for (const member of scalars) {
3441
+ const scalar = bindScalar(ctx, member, strings, module.offset);
3442
+ if (scalar.refusal) out.refused.push({ key: member.key, reason: scalar.refusal });
3443
+ if (scalar.splices.length === 0) continue;
3444
+ out.splices.push(...scalar.splices);
3445
+ out.usesRead = true;
3446
+ out.slugs.push(member.slug);
3447
+ out.bound.push(member.key);
3448
+ }
3449
+ const literals = arrayLiterals(module.ast);
3450
+ for (const all of groups.values()) {
3451
+ const array = all[0].array;
3452
+ const nested = all.filter((member) => /\[\d+\]/.test(member.leaf));
3453
+ for (const member of nested) out.refused.push({ key: member.key, reason: "PROP_DRILLED_DEEP" });
3454
+ const group = all.filter((member) => !nested.includes(member));
3455
+ if (group.length === 0) continue;
3456
+ const refuse = (reason) => {
3457
+ for (const member of group) out.refused.push({ key: member.key, reason });
3458
+ };
3459
+ let identifier2 = null;
3460
+ let keys = null;
3461
+ for (const [name, literal] of literals) {
3462
+ const elements2 = literal.elements ?? [];
3463
+ const mapped = /* @__PURE__ */ new Map();
3464
+ const ok = group.every((member) => {
3465
+ const property = propertyFor(elements2[member.index] ?? void 0, member.literal);
3466
+ if (!property) return false;
3467
+ const held = mapped.get(property);
3468
+ if (held !== void 0 && held !== member.leaf) return false;
3469
+ mapped.set(property, member.leaf);
3470
+ return true;
3471
+ });
3472
+ if (!ok) continue;
3473
+ if (identifier2 !== null) {
3474
+ identifier2 = null;
3475
+ break;
3476
+ }
3477
+ identifier2 = name;
3478
+ keys = mapped;
3479
+ }
3480
+ if (identifier2 === null || keys === null) continue;
3481
+ const loops = iterations(ctx.parsed.roots, identifier2);
3482
+ if (loops.length === 0) continue;
3483
+ if (loops.length > 1) {
3484
+ refuse("REPEATER_AMBIGUOUS");
3485
+ continue;
3486
+ }
3487
+ const loop = loops[0];
3488
+ const params = callbackParams(loop, identifier2);
3489
+ if (!params || params.row === null) {
3490
+ refuse("IN_EXPRESSION");
3491
+ continue;
3492
+ }
3493
+ const rowName = params.row;
3494
+ const elements = leafElements(ctx.content, loop, rowName);
3495
+ const placed = /* @__PURE__ */ new Map();
3496
+ const passed = [];
3497
+ let refusal = null;
3498
+ for (const property of keys.keys()) {
3499
+ const found = elements.get(property) ?? [];
3500
+ if (found.length === 1) {
3501
+ placed.set(property, found[0]);
3502
+ continue;
3503
+ }
3504
+ if (found.length > 1) {
3505
+ refusal = "REPEATER_AMBIGUOUS";
3506
+ continue;
3507
+ }
3508
+ const calls = propCalls(ctx.content, loop, rowName, property);
3509
+ if (ctx.drill && calls.length === 1) passed.push({ property, ...calls[0] });
3510
+ else if (calls.length > 1) refusal = "REPEATER_AMBIGUOUS";
3511
+ else refusal = leafRefusal(loop.value, rowName, property);
3512
+ }
3513
+ if (refusal !== null) {
3514
+ refuse(refusal);
3515
+ continue;
3516
+ }
3517
+ const index = params.index ?? allocateIndex(loop.value, indexes);
3518
+ indexes.add(index);
3519
+ const splices = [];
3520
+ if (rule.loopKey) {
3521
+ const rows = loop.children.filter((child) => child.type === "element");
3522
+ const row = rows.length === 1 ? rows[0] : null;
3523
+ if (row && row.tag !== "" && !row.attrs.some((a) => a.name.replace(/^:/, "") === "key")) {
3524
+ splices.push({ start: row.attrInsertAt, end: row.attrInsertAt, text: ` ${rule.loopKey(index)}` });
3525
+ }
3526
+ }
3527
+ if (params.index === null) {
3528
+ if (params.addIndexAt < 0) {
3529
+ refuse("IN_EXPRESSION");
3530
+ continue;
3531
+ }
3532
+ splices.push({ start: params.addIndexAt, end: params.addIndexAt, text: `, ${index}` });
3533
+ }
3534
+ const drilled = [];
3535
+ const edits = [];
3536
+ const bindings = /* @__PURE__ */ new Map();
3537
+ for (const entry of passed) {
3538
+ const leaf = keys.get(entry.property);
3539
+ const literal = `${array}[\${${index}}].${leaf}`;
3540
+ const outcome = entry.call.attrs.some((a) => a.name === "bcmsBindings") ? { ok: false, reason: "PROP_TARGET_NOT_FOUND" } : await ctx.drill(entry.call.tag, entry.prop, group.find((member) => member.leaf === leaf).kind);
3541
+ if (!outcome.ok) {
3542
+ refusal = outcome.reason;
3543
+ break;
3544
+ }
3545
+ edits.push(...outcome.edits);
3546
+ bindings.set(entry.call.attrInsertAt, [
3547
+ ...bindings.get(entry.call.attrInsertAt) ?? [],
3548
+ `${entry.prop}: \`${literal}\``
3549
+ ]);
3550
+ drilled.push({
3551
+ file: outcome.edits[0].file,
3552
+ prop: entry.prop,
3553
+ path: shapeOfPath(literal),
3554
+ literal: `${entry.prop}: \`${literal}\``
3555
+ });
3556
+ }
3557
+ if (refusal !== null) {
3558
+ refuse(refusal);
3559
+ continue;
3560
+ }
3561
+ for (const [at2, entries] of bindings) {
3562
+ splices.push({ start: at2, end: at2, text: ` bcmsBindings={{ ${entries.join(", ")} }}` });
3563
+ }
3564
+ for (const [property, element] of placed) {
3565
+ const leaf = keys.get(property);
3566
+ const literal = `${array}[\${${index}}].${leaf}`;
3567
+ const kind = group.find((member) => member.leaf === leaf).kind;
3568
+ const attributes = [
3569
+ rule.attr("data-bcms-field", `\`${literal}\``),
3570
+ ...kind ? [`data-bcms-kind="${kind}"`] : [],
3571
+ // A richtext row value is html, so it goes into the dialect's html sink rather than being
3572
+ // printed as characters — the same rule `writeLeaf` follows one level down. @see loops.ts
3573
+ ...kind === "richtext" && rule.richtext ? [rule.richtext(`${rowName}.${property}`)] : []
3574
+ ];
3575
+ splices.push({ start: element.attrInsertAt, end: element.attrInsertAt, text: ` ${attributes.join(" ")}` });
3576
+ if (kind === "richtext" && rule.richtext && element.inner) {
3577
+ splices.push({ start: element.inner.start, end: element.inner.end, text: "" });
3578
+ }
3579
+ out.declarations.push({ path: shapeOfPath(literal), literal });
3580
+ }
3581
+ const at = { start: module.offset + (literals.get(identifier2).start ?? 0), end: module.offset + (literals.get(identifier2).end ?? 0) };
3582
+ const map = [...keys].map(([property, leaf]) => `${property}: ${JSON.stringify(leaf)}`).join(", ");
3583
+ const args = [
3584
+ ctx.snapshotName(group[0].slug),
3585
+ JSON.stringify(array),
3586
+ ctx.content.slice(at.start, at.end),
3587
+ `{ ${map} }`,
3588
+ ...ctx.slug ? [ctx.slug] : []
3589
+ ];
3590
+ splices.push({ start: at.start, end: at.end, text: `${ctx.rowsAs}(${args.join(", ")})` });
3591
+ out.splices.push(...splices);
3592
+ out.usesRows = true;
3593
+ out.componentEdits.push(...edits);
3594
+ out.props.push(...drilled);
3595
+ out.slugs.push(...group.map((member) => member.slug));
3596
+ out.bound.push(...group.map((member) => member.key));
3597
+ }
3598
+ const settled = /* @__PURE__ */ new Set([...out.bound, ...out.refused.map((refusal) => refusal.key)]);
3599
+ bindLists(ctx, members.filter((member) => !settled.has(member.key)), module, out, indexes);
3600
+ return out;
3601
+ }
3602
+ function literalTable(init) {
3603
+ const elements = (init.elements ?? []).filter((e) => e !== null);
3604
+ const first = elements[0];
3605
+ if (!first || elements.length !== (init.elements ?? []).length) return null;
3606
+ if (elements.some((element) => element.type !== first.type)) return null;
3607
+ if (first.type === "StringLiteral") {
3608
+ return { tuple: false, rows: elements.map((element) => [norm(String(element.value))]) };
3609
+ }
3610
+ if (first.type !== "ArrayExpression") return null;
3611
+ const rows = [];
3612
+ for (const element of elements) {
3613
+ const inner = (element.elements ?? []).filter((x) => x !== null);
3614
+ if (inner.length === 0 || inner.some((x) => x.type !== "StringLiteral")) return null;
3615
+ if (inner.length !== (element.elements ?? []).length) return null;
3616
+ rows.push(inner.map((x) => norm(String(x.value))));
3617
+ }
3618
+ if (new Set(rows.map((row) => row.length)).size !== 1) return null;
3619
+ return { tuple: true, rows };
3620
+ }
3621
+ function openingBracket(text, closeAt) {
3622
+ let depth = 0;
3623
+ for (let at = closeAt; at >= 0; at--) {
3624
+ if (text[at] === "]") depth++;
3625
+ else if (text[at] === "[") {
3626
+ depth--;
3627
+ if (depth === 0) return at;
3628
+ }
3629
+ }
3630
+ return -1;
3631
+ }
3632
+ function arrayLiteral(source) {
3633
+ try {
3634
+ const ast = parseProgram(`(${source})`);
3635
+ const expression = ast.program.body[0]?.expression;
3636
+ if (!expression || expression.type !== "ArrayExpression") return null;
3637
+ return (expression.start ?? -1) === 1 && (expression.end ?? -1) === source.length + 1 ? expression : null;
3638
+ } catch {
3639
+ return null;
3640
+ }
3641
+ }
3642
+ function inlineTables(ctx) {
3643
+ const out = /* @__PURE__ */ new Map();
3644
+ const visit = (node) => {
3645
+ if (node.type === "element") {
3646
+ if (!node.raw) for (const child of node.children) visit(child);
3647
+ return;
3648
+ }
3649
+ if (node.type !== "expr") return;
3650
+ for (const match of node.value.matchAll(/\]\s*\.\s*map\s*\(/g)) {
3651
+ const closeAt = match.index;
3652
+ const openAt = openingBracket(node.value, closeAt);
3653
+ if (openAt <= 0 || /[\w$)\]]/.test(node.value[openAt - 1] ?? "")) continue;
3654
+ const init = arrayLiteral(node.value.slice(openAt, closeAt + 1));
3655
+ const table = init ? literalTable(init) : null;
3656
+ if (!table) continue;
3657
+ const at = { start: node.range.start + openAt, end: node.range.start + closeAt + 1 };
3658
+ out.set(at.start, {
3659
+ ...table,
3660
+ at,
3661
+ label: "an inline array literal",
3662
+ render: () => ({ loop: node, params: mapParams(ctx.content, at.end, node.range.end) })
3663
+ });
3664
+ }
3665
+ for (const child of node.children) visit(child);
3666
+ };
3667
+ for (const root of ctx.parsed.roots) visit(root);
3668
+ return [...out.values()];
3669
+ }
3670
+ function readerElements(content, expression, reads) {
3671
+ const wanted = new Set(reads);
3672
+ const out = /* @__PURE__ */ new Map();
3673
+ const visit = (node) => {
3674
+ if (node.type === "expr") {
3675
+ for (const child of node.children) visit(child);
3676
+ return;
3677
+ }
3678
+ if (node.type !== "element" || node.raw) return;
3679
+ const inner = isIntrinsic(node.tag) && node.inner ? content.slice(node.inner.start, node.inner.end).trim() : "";
3680
+ const read = inner.match(/^\{\s*([A-Za-z_$][\w$]*(?:\[\d+\])?)\s*\}$/)?.[1];
3681
+ if (read && wanted.has(read)) out.set(read, [...out.get(read) ?? [], node]);
3682
+ for (const child of node.children) visit(child);
3683
+ };
3684
+ for (const child of expression.children) visit(child);
3685
+ return out;
3686
+ }
3687
+ function readerWraps(expression, reads) {
3688
+ const wanted = new Set(reads);
3689
+ const out = /* @__PURE__ */ new Map();
3690
+ const visit = (node, parent) => {
3691
+ if (node.type === "expr") {
3692
+ const read = node.value.trim().match(/^\{\s*([A-Za-z_$][\w$]*(?:\[\d+\])?)\s*\}$/)?.[1];
3693
+ const beside = parent !== null && isIntrinsic(parent.tag) && parent.children.some((child) => child !== node && !(child.type === "text" && child.value.trim() === ""));
3694
+ if (read && wanted.has(read) && beside) out.set(read, [...out.get(read) ?? [], node.range]);
3695
+ for (const child of node.children) visit(child, parent);
3696
+ return;
3697
+ }
3698
+ if (node.type !== "element" || node.raw) return;
3699
+ for (const child of node.children) visit(child, node);
3700
+ };
3701
+ for (const child of expression.children) visit(child, null);
3702
+ return out;
3703
+ }
3704
+ function pathTemplates(paths, index) {
3705
+ const out = [];
3706
+ for (let position = 0; position < paths[0].length; position++) {
3707
+ const split = paths.map((row) => splitGroup(row[position]));
3708
+ const first = split[0];
3709
+ if (!first) return null;
3710
+ if (split.some((held, row) => !held || held.array !== first.array || held.leaf !== first.leaf || held.index !== row)) {
3711
+ return null;
3712
+ }
3713
+ out.push(`${first.array}[\${${index}}].${first.leaf}`);
3714
+ }
3715
+ return out;
3716
+ }
3717
+ function bindLists(ctx, members, module, out, indexes) {
3718
+ const rule = DIALECT_RULES[ctx.dialect];
3719
+ const escapes = exportedNames(module.ast);
3720
+ const candidates = [];
3721
+ for (const [name, init] of arrayLiterals(module.ast)) {
3722
+ const table = literalTable(init);
3723
+ if (!table) continue;
3724
+ candidates.push({
3725
+ ...table,
3726
+ at: { start: module.offset + (init.start ?? 0), end: module.offset + (init.end ?? 0) },
3727
+ label: `\`${name}\``,
3728
+ render: () => {
3729
+ const loops = iterations(ctx.parsed.roots, name);
3730
+ if (loops.length > 1) return { refuse: "REPEATER_AMBIGUOUS" };
3731
+ if (loops.length === 0) return null;
3732
+ const mentions2 = ctx.content.match(new RegExp(`(?<![\\w$])${name}(?![\\w$])`, "g")) ?? [];
3733
+ if (mentions2.length !== 2 || escapes.has(name)) return { refuse: "IN_EXPRESSION" };
3734
+ return { loop: loops[0], params: callbackParams(loops[0], name) };
3735
+ }
3736
+ });
3737
+ }
3738
+ candidates.push(...inlineTables(ctx));
3739
+ const siblings = /* @__PURE__ */ new Map();
3740
+ const groupOf2 = (member) => `${member.slug}\0${splitGroup(member.path)?.array ?? member.path}`;
3741
+ for (const member of members) {
3742
+ siblings.set(groupOf2(member), [...siblings.get(groupOf2(member)) ?? [], member]);
3743
+ }
3744
+ const fit = (candidate, member) => (siblings.get(groupOf2(member)) ?? [member]).filter(
3745
+ (leaf) => candidate.rows.some((row) => row.includes(leaf.literal))
3746
+ ).length;
3747
+ const best = /* @__PURE__ */ new Map();
3748
+ const bestFit = (member) => {
3749
+ const key = groupOf2(member);
3750
+ if (!best.has(key)) best.set(key, Math.max(0, ...candidates.map((candidate) => fit(candidate, member))));
3751
+ return best.get(key);
3752
+ };
3753
+ const claims = (candidate, member) => candidate.rows.some((row) => row.includes(member.literal)) && fit(candidate, member) === bestFit(member);
3754
+ const holders = (member) => candidates.filter((candidate) => fit(candidate, member) === bestFit(member)).length;
3755
+ for (const candidate of candidates) {
3756
+ const mine = members.filter((member) => claims(candidate, member));
3757
+ if (mine.length === 0 || mine.some((member) => holders(member) > 1)) continue;
3758
+ if (new Set(mine.map((member) => member.slug)).size !== 1) continue;
3759
+ const refuse = (reason) => {
3760
+ for (const member of mine) out.refused.push({ key: member.key, reason });
3761
+ };
3762
+ const paths = candidate.rows.map((row) => row.map(() => ""));
3763
+ const kinds = candidate.rows.map((row) => row.map(() => ""));
3764
+ let clash = false;
3765
+ for (const member of mine) {
3766
+ const spots = candidate.rows.flatMap(
3767
+ (row, r) => row.flatMap((value, p) => value === member.literal ? [[r, p]] : [])
3768
+ );
3769
+ if (spots.length !== 1 || paths[spots[0][0]][spots[0][1]] !== "") clash = true;
3770
+ else {
3771
+ paths[spots[0][0]][spots[0][1]] = member.path;
3772
+ kinds[spots[0][0]][spots[0][1]] = member.kind;
3773
+ }
3774
+ }
3775
+ if (clash || paths.some((row) => row.some((path) => path === ""))) {
3776
+ refuse("IN_EXPRESSION");
3777
+ continue;
3778
+ }
3779
+ const kindAt = kinds[0];
3780
+ if (kinds.some((row) => row.some((kind, position) => kind !== kindAt[position]))) {
3781
+ refuse("IN_EXPRESSION");
3782
+ continue;
3783
+ }
3784
+ const rendered = candidate.render();
3785
+ if (rendered === null) continue;
3786
+ if ("refuse" in rendered) {
3787
+ refuse(rendered.refuse);
3788
+ continue;
3789
+ }
3790
+ const { loop, params } = rendered;
3791
+ const reads = !params ? null : candidate.tuple ? params.names ?? (params.row === null ? null : candidate.rows[0].map((_, p) => `${params.row}[${p}]`)) : params.row === null ? null : [params.row];
3792
+ if (!params || !reads || reads.length !== candidate.rows[0].length) {
3793
+ refuse("IN_EXPRESSION");
3794
+ continue;
3795
+ }
3796
+ const elements = readerElements(ctx.content, loop, reads);
3797
+ const wraps = readerWraps(loop, reads);
3798
+ const hosts = [];
3799
+ let refusal = null;
3800
+ for (const read of reads) {
3801
+ const found = elements.get(read) ?? [];
3802
+ const beside = elements.has(read) ? [] : wraps.get(read) ?? [];
3803
+ if (found.length === 1) hosts.push({ element: found[0] });
3804
+ else if (found.length === 0 && beside.length === 1) hosts.push({ wrap: beside[0] });
3805
+ else refusal = found.length + beside.length > 1 ? "REPEATER_AMBIGUOUS" : "IN_EXPRESSION";
3806
+ }
3807
+ if (refusal !== null) {
3808
+ refuse(refusal);
3809
+ continue;
3810
+ }
3811
+ const index = params.index ?? allocateIndex(loop.value, indexes);
3812
+ indexes.add(index);
3813
+ const splices = [];
3814
+ if (rule.loopKey) {
3815
+ const rows = loop.children.filter((child) => child.type === "element");
3816
+ const row = rows.length === 1 ? rows[0] : null;
3817
+ if (row && row.tag !== "" && !row.attrs.some((a) => a.name.replace(/^:/, "") === "key")) {
3818
+ splices.push({ start: row.attrInsertAt, end: row.attrInsertAt, text: ` ${rule.loopKey(index)}` });
3819
+ }
3820
+ }
3821
+ if (params.index === null) {
3822
+ if (params.addIndexAt < 0) {
3823
+ refuse("IN_EXPRESSION");
3824
+ continue;
3825
+ }
3826
+ splices.push({ start: params.addIndexAt, end: params.addIndexAt, text: `, ${index}` });
3827
+ }
3828
+ const templates = pathTemplates(paths, index);
3829
+ if (!templates) {
3830
+ refuse("IN_EXPRESSION");
3831
+ continue;
3832
+ }
3833
+ const list = JSON.stringify(candidate.tuple ? paths : paths.map((row) => row[0]));
3834
+ for (const [position, host] of hosts.entries()) {
3835
+ const kind = kindAt[position];
3836
+ const attributes = [
3837
+ rule.attr("data-bcms-field", `\`${templates[position]}\``),
3838
+ ...kind ? [`data-bcms-kind="${kind}"`] : [],
3839
+ // A richtext value is html, so it goes into the dialect's html sink rather than being
3840
+ // printed as characters — the same rule one level up. @see bindDataLiterals
3841
+ ...kind === "richtext" && rule.richtext ? [rule.richtext(reads[position])] : []
3842
+ ];
3843
+ if ("element" in host) {
3844
+ splices.push({ start: host.element.attrInsertAt, end: host.element.attrInsertAt, text: ` ${attributes.join(" ")}` });
3845
+ if (kind === "richtext" && rule.richtext && host.element.inner) {
3846
+ splices.push({ start: host.element.inner.start, end: host.element.inner.end, text: "" });
3847
+ }
3848
+ } else {
3849
+ const inner = kind === "richtext" && rule.richtext ? "" : ctx.content.slice(host.wrap.start, host.wrap.end);
3850
+ splices.push({ start: host.wrap.start, end: host.wrap.end, text: `<span ${attributes.join(" ")}>${inner}</span>` });
3851
+ }
3852
+ out.declarations.push({ path: shapeOfPath(templates[position]), literal: templates[position] });
3853
+ }
3854
+ const args = [
3855
+ ctx.snapshotName(mine[0].slug),
3856
+ list,
3857
+ ctx.content.slice(candidate.at.start, candidate.at.end),
3858
+ ...ctx.slug ? [ctx.slug] : []
3859
+ ];
3860
+ splices.push({
3861
+ start: candidate.at.start,
3862
+ end: candidate.at.end,
3863
+ text: `${candidate.tuple ? ctx.tuples : ctx.list}(${args.join(", ")})`
3864
+ });
3865
+ out.splices.push(...splices);
3866
+ if (candidate.tuple) out.usesTuples = true;
3867
+ else out.usesList = true;
3868
+ out.slugs.push(...mine.map((member) => member.slug));
3869
+ out.bound.push(...mine.map((member) => member.key));
3870
+ out.notes.push(
3871
+ `${candidate.label} renders exactly the ${paths.length} row(s) this run wrote paths for: a row ADDED in the CMS does not appear, and a row DELETED falls back to the copy the template shipped with.`
3872
+ );
3873
+ }
3874
+ }
3875
+ function propCalls(content, expression, row, property) {
3876
+ const named = new RegExp(`^\\{\\s*${row}\\s*\\.\\s*${property}\\s*\\}$`);
3877
+ const spread = new RegExp(`^\\{\\s*\\.\\.\\.\\s*${row}\\s*\\}$`);
3878
+ const out = [];
3879
+ const visit = (node) => {
3880
+ if (node.type === "expr") {
3881
+ for (const child of node.children) visit(child);
3882
+ return;
3883
+ }
3884
+ if (node.type !== "element" || node.raw) return;
3885
+ if (node.tag !== "" && !isIntrinsic(node.tag)) {
3886
+ for (const attr of node.attrs) {
3887
+ if (!attr.expressionRange) continue;
3888
+ const text = content.slice(attr.expressionRange.start, attr.expressionRange.end).trim();
3889
+ if (spread.test(text)) out.push({ call: node, prop: property });
3890
+ else if (named.test(text)) out.push({ call: node, prop: attr.name });
3891
+ }
3892
+ }
3893
+ for (const child of node.children) visit(child);
3894
+ };
3895
+ for (const child of expression.children) visit(child);
3896
+ return out;
3897
+ }
3898
+ function leafRefusal(loop, row, property) {
3899
+ const prop = new RegExp(`=\\s*\\{\\s*${row}\\s*\\.\\s*${property}\\s*\\}|\\{\\s*\\.\\.\\.\\s*${row}\\s*\\}`);
3900
+ return prop.test(loop) ? "PROP_TARGET_NOT_FOUND" : "IN_EXPRESSION";
3901
+ }
3902
+ function allocateIndex(loop, taken) {
3903
+ const used = new Set(loop.match(/[A-Za-z_$][\w$]*/g) ?? []);
3904
+ if (!used.has("i") && !taken.has("i")) return "i";
3905
+ for (let n = 1; ; n++) if (!used.has(`i_${n}`) && !taken.has(`i_${n}`)) return `i_${n}`;
3906
+ }
3907
+
3908
+ // src/resolve.ts
3909
+ var EXTENSIONS = [".astro", ".tsx", ".jsx", ".ts", ".js", ".svelte", ".vue", ".mjs", ".cjs"];
3910
+ function join(fromFile, specifier) {
3911
+ const parts = fromFile.split("/").slice(0, -1);
3912
+ for (const segment of specifier.split("/")) {
3913
+ if (segment === "." || segment === "") continue;
3914
+ if (segment === "..") parts.pop();
3915
+ else parts.push(segment);
3916
+ }
3917
+ return parts.join("/");
3918
+ }
3919
+ var firstPresent = (base, files) => [base, ...EXTENSIONS.map((ext) => `${base}${ext}`), ...EXTENSIONS.map((ext) => `${base}/index${ext}`)].find(
3920
+ (candidate) => files.has(candidate)
3921
+ ) ?? null;
3922
+ function aliasesFrom(sources) {
3923
+ const out = /* @__PURE__ */ new Map();
3924
+ for (const source of sources) {
3925
+ if (!isProjectConfig(source.path)) continue;
3926
+ const parsed = readJsonc(source.content);
3927
+ if (parsed === null) continue;
3928
+ const base = parsed.compilerOptions?.baseUrl?.replace(/^\.\/?/, "").replace(/\/$/, "") ?? "";
3929
+ const root = source.path.split("/").slice(0, -1).join("/");
3930
+ for (const [pattern, targets] of Object.entries(parsed.compilerOptions?.paths ?? {})) {
3931
+ out.set(
3932
+ pattern,
3933
+ targets.map((target) => [root, base, target.replace(/^\.\/?/, "")].filter(Boolean).join("/"))
3934
+ );
3935
+ }
3936
+ }
3937
+ return out;
3938
+ }
3939
+ var isProjectConfig = (path) => path.endsWith("tsconfig.json") || path.endsWith("jsconfig.json");
3940
+ function readJsonc(content) {
3941
+ let out = "";
3942
+ let at = 0;
3943
+ while (at < content.length) {
3944
+ const char = content[at];
3945
+ if (char === '"') {
3946
+ const start = at++;
3947
+ while (at < content.length && content[at] !== '"') at += content[at] === "\\" ? 2 : 1;
3948
+ out += content.slice(start, Math.min(at + 1, content.length));
3949
+ at++;
3950
+ continue;
3951
+ }
3952
+ if (char === "/" && content[at + 1] === "/") {
3953
+ while (at < content.length && content[at] !== "\n") at++;
3954
+ continue;
3955
+ }
3956
+ if (char === "/" && content[at + 1] === "*") {
3957
+ const close = content.indexOf("*/", at + 2);
3958
+ at = close < 0 ? content.length : close + 2;
3959
+ continue;
3960
+ }
3961
+ out += char;
3962
+ at++;
3963
+ }
3964
+ try {
3965
+ const parsed = JSON.parse(out.replace(/,(?=\s*[}\]])/g, ""));
3966
+ return isAliasShaped(parsed) ? parsed : null;
3967
+ } catch {
3968
+ return null;
3969
+ }
3970
+ }
3971
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3972
+ function isAliasShaped(value) {
3973
+ if (!isRecord(value)) return false;
3974
+ const options = value.compilerOptions;
3975
+ if (options === void 0) return true;
3976
+ if (!isRecord(options)) return false;
3977
+ if (options.baseUrl !== void 0 && typeof options.baseUrl !== "string") return false;
3978
+ if (options.paths === void 0) return true;
3979
+ if (!isRecord(options.paths)) return false;
3980
+ return Object.values(options.paths).every(
3981
+ (targets) => Array.isArray(targets) && targets.every((target) => typeof target === "string")
3982
+ );
3983
+ }
3984
+ function unreadableConfigs(sources) {
3985
+ return sources.filter((source) => isProjectConfig(source.path) && readJsonc(source.content) === null).map((source) => source.path);
3986
+ }
3987
+ function extendedConfigs(sources) {
3988
+ return sources.filter((source) => {
3989
+ if (!isProjectConfig(source.path)) return false;
3990
+ const parsed = readJsonc(source.content);
3991
+ return parsed !== null && parsed.extends !== void 0 && parsed.compilerOptions?.paths === void 0;
3992
+ }).map((source) => source.path);
3993
+ }
3994
+ function expand(specifier, aliases) {
3995
+ const out = [];
3996
+ for (const [pattern, targets] of aliases) {
3997
+ const star = pattern.indexOf("*");
3998
+ if (star < 0) {
3999
+ if (specifier === pattern) out.push(...targets);
4000
+ continue;
4001
+ }
4002
+ const head = pattern.slice(0, star);
4003
+ const tail = pattern.slice(star + 1);
4004
+ if (!specifier.startsWith(head) || !specifier.endsWith(tail)) continue;
4005
+ const middle = specifier.slice(head.length, specifier.length - tail.length);
4006
+ out.push(...targets.map((target) => target.replace("*", middle)));
4007
+ }
4008
+ return out;
4009
+ }
4010
+ function resolveSpecifier(fromFile, specifier, files, aliases) {
4011
+ if (specifier.startsWith(".")) return firstPresent(join(fromFile, specifier), files);
4012
+ for (const candidate of expand(specifier, aliases)) {
4013
+ const found = firstPresent(candidate.replace(/\/$/, ""), files);
4014
+ if (found) return found;
4015
+ }
4016
+ return null;
4017
+ }
4018
+ function importsIn(parsed, _dialect) {
4019
+ const out = /* @__PURE__ */ new Map();
4020
+ walkAst(parsed.ast, (node) => {
4021
+ if (node.type !== "ImportDeclaration") return;
4022
+ if (node.importKind === "type") return;
4023
+ const specifier = node.source?.value;
4024
+ if (typeof specifier !== "string") return;
4025
+ for (const entry of node.specifiers ?? []) {
4026
+ if (entry.importKind === "type") continue;
4027
+ const local = entry.local?.name;
4028
+ if (typeof local !== "string") continue;
4029
+ const kind = entry.type === "ImportDefaultSpecifier" ? "default" : entry.type === "ImportNamespaceSpecifier" ? "namespace" : "named";
4030
+ const imported = kind === "named" ? entry.imported?.name ?? local : null;
4031
+ out.set(local, { specifier, imported, kind });
4032
+ }
4033
+ });
4034
+ return out;
4035
+ }
4036
+
4037
+ // src/images.ts
4038
+ var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif", ".svg"];
4039
+ var ASSET_COMPONENTS = /* @__PURE__ */ new Set(["Image", "Picture"]);
4040
+ var ASSET_MODULE = "astro:assets";
4041
+ function baseName(url) {
4042
+ const file = url.split(/[?#]/)[0].split("/").pop() ?? "";
4043
+ const base = file.split(".")[0] ?? "";
4044
+ return base === "" ? null : base.toLowerCase();
4045
+ }
4046
+ function assetCandidates(parsed, dialect, original) {
4047
+ const wanted = baseName(original);
4048
+ if (wanted === null || parsed.error) return [];
4049
+ const found = [];
4050
+ for (const [local, from] of importsIn(parsed, dialect)) {
4051
+ const specifier = from.specifier.toLowerCase();
4052
+ if (from.kind !== "default" || !IMAGE_EXTENSIONS.some((ext) => specifier.endsWith(ext))) continue;
4053
+ if (baseName(from.specifier) === wanted) found.push(local);
4054
+ }
4055
+ return found;
4056
+ }
4057
+ function assetImport(parsed, dialect, original) {
4058
+ const found = assetCandidates(parsed, dialect, original);
4059
+ return found.length === 1 ? found[0] : null;
4060
+ }
4061
+ function renderedBy(content, parsed, dialect, binding) {
4062
+ const assets = new Set(
4063
+ [...importsIn(parsed, dialect)].filter(([, from]) => from.specifier === ASSET_MODULE).map(([local]) => local)
4064
+ );
4065
+ const out = [];
4066
+ const visit = (node) => {
4067
+ if (node.type === "expr") {
4068
+ for (const child of node.children) visit(child);
4069
+ return;
4070
+ }
4071
+ if (node.type !== "element" || node.raw) return;
4072
+ const sources = IMAGE_SOURCES[node.tag.toLowerCase()] ?? (assets.has(node.tag) && ASSET_COMPONENTS.has(node.tag) ? ["src"] : []);
4073
+ for (const attr of node.attrs) {
4074
+ if (!sources.includes(attr.name) || !attr.expressionRange) continue;
4075
+ const inner = content.slice(attr.expressionRange.start, attr.expressionRange.end).replace(/^\{|\}$/g, "").trim();
4076
+ if (inner === binding) out.push({ node, attr: attr.name });
4077
+ }
4078
+ for (const child of node.children) visit(child);
4079
+ };
4080
+ for (const root of parsed.roots) visit(root);
4081
+ return out;
4082
+ }
4083
+ function bindImages(ctx, targets) {
4084
+ const out = { splices: [], slugs: [], bound: [], refused: [] };
4085
+ const rule = DIALECT_RULES[ctx.dialect];
4086
+ if (!rule.attr || !rule.text || ctx.parsed.error) return out;
4087
+ const claimed = /* @__PURE__ */ new Set();
4088
+ for (const target of targets) {
4089
+ const binding = assetImport(ctx.parsed, ctx.dialect, target.original);
4090
+ const rendered = binding === null ? [] : renderedBy(ctx.content, ctx.parsed, ctx.dialect, binding);
4091
+ const free = rendered.filter((hit2) => !claimed.has(hit2.node.attrInsertAt));
4092
+ if (free.length !== 1) {
4093
+ out.refused.push({ key: target.key, reason: "IMAGE_ASSET_UNRESOLVED" });
4094
+ continue;
4095
+ }
4096
+ const hit = free[0];
4097
+ claimed.add(hit.node.attrInsertAt);
4098
+ const attr = hit.node.attrs.find((a) => a.name === hit.attr);
4099
+ const args = [
4100
+ ctx.snapshotName(target.slug),
4101
+ JSON.stringify(target.path),
4102
+ binding,
4103
+ ...ctx.slug ? [ctx.slug] : []
4104
+ ];
4105
+ out.splices.push({
4106
+ start: attr.range.start,
4107
+ end: attr.range.end,
4108
+ text: rule.attr(hit.attr, `${ctx.image}(${args.join(", ")})`)
4109
+ });
4110
+ out.splices.push({
4111
+ start: hit.node.attrInsertAt,
4112
+ end: hit.node.attrInsertAt,
4113
+ text: ` data-bcms-field="${target.path}" data-bcms-kind="${target.kind || "image"}"`
4114
+ });
4115
+ out.slugs.push(target.slug);
4116
+ out.bound.push(target.key);
4117
+ }
4118
+ return out;
4119
+ }
4120
+
4121
+ // src/helper.ts
4122
+ import { createHash as createHash2 } from "crypto";
4123
+ var HELPER_VERSION = 6;
4124
+ var HEADER = "// @bettercms-ai/convert helper v";
4125
+ var HELPER_PATH = Object.fromEntries(
4126
+ Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
4127
+ );
4128
+ var CONTENT_DIR = "bcms-content";
4129
+ var STUB_CONTENT = "{}";
4130
+ function relativeImport(fromFile, toFile) {
4131
+ const from = fromFile.split("/").slice(0, -1);
4132
+ const to = toFile.split("/");
4133
+ let shared = 0;
4134
+ while (shared < from.length && shared < to.length - 1 && from[shared] === to[shared]) shared++;
4135
+ const up = from.length - shared;
4136
+ const path = [...Array(up).fill(".."), ...to.slice(shared)].join("/");
4137
+ return up === 0 ? `./${path}` : path;
4138
+ }
4139
+ function helperSource(dialect) {
4140
+ const path = HELPER_PATH[dialect];
4141
+ if (!path) throw new Error(`The ${dialect} dialect reads its values at publish time, not at build time.`);
4142
+ const layout = relativeImport(path, `${CONTENT_DIR}/layout.json`);
4143
+ return `${HEADER}${HELPER_VERSION}
4144
+ // Values come from bcms-content/*.json, written into the tree before the build. The committed
4145
+ // stubs are {} \u2014 a build with them renders every fallback below and still succeeds.
4146
+ import layoutSnapshot from "${layout}";
4147
+
4148
+ const isRecord = (v: unknown): v is Record<string, unknown> =>
4149
+ typeof v === "object" && v !== null && !Array.isArray(v);
4150
+
4151
+ /** Dotted keys and \`[n]\` indices, positionally \u2014 the editor's own path grammar. */
4152
+ function readPath(root: unknown, path: string): unknown {
4153
+ let cursor: unknown = root;
4154
+ for (const segment of path.split(".")) {
4155
+ if (!segment) return undefined;
4156
+ const key = segment.replace(/\\[\\d+\\]/g, "");
4157
+ if (key) {
4158
+ if (!isRecord(cursor)) return undefined;
4159
+ cursor = cursor[key];
4160
+ }
4161
+ for (const index of segment.matchAll(/\\[(\\d+)\\]/g)) {
4162
+ if (!Array.isArray(cursor)) return undefined;
4163
+ cursor = cursor[Number(index[1])];
4164
+ }
4165
+ }
4166
+ return cursor;
4167
+ }
4168
+
4169
+ /** A stored value as a template renders it: richtext is its html, an image is its url. */
4170
+ function render(value: unknown): string | undefined {
4171
+ if (typeof value === "string") return value;
4172
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
4173
+ if (isRecord(value)) {
4174
+ if (typeof value.html === "string") return value.html;
4175
+ if (typeof value.url === "string") return value.url;
4176
+ }
4177
+ return undefined;
4178
+ }
4179
+
4180
+ /** One field of one page. \`slug\` selects the entry on a dynamic route. */
4181
+ export function bcms(page: unknown, path: string, fallback: string, slug?: string): string {
4182
+ const entries = isRecord(page) ? page.entries : undefined;
4183
+ const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
4184
+ return render(readPath(root, path)) ?? fallback;
4185
+ }
4186
+
4187
+ /** One field of the shared layout \u2014 the header and footer every route renders. */
4188
+ export function bcmsLayout(path: string, fallback: string): string {
4189
+ return render(readPath(layoutSnapshot, path)) ?? fallback;
4190
+ }
4191
+
4192
+ /**
4193
+ * The ROWS of a repeater. \`fallback\` is every row the template shipped with, so a build against
4194
+ * an empty snapshot still renders the same number of cards it always did.
4195
+ */
4196
+ export function bcmsRows<T>(page: unknown, path: string, fallback: T[], slug?: string): T[] {
4197
+ const entries = isRecord(page) ? page.entries : undefined;
4198
+ const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
4199
+ const value = readPath(root, path);
4200
+ return Array.isArray(value) ? (value as T[]) : fallback;
4201
+ }
4202
+
4203
+ /**
4204
+ * The rows of a repeater whose template renders its OWN property names.
4205
+ *
4206
+ * \u{1F534} A ROW IS MORE THAN ITS COPY. The template's array carries \`tone\`, \`link\`, an icon \u2014 styling
4207
+ * the brief never heard of, because the CMS holds the four sentences and not the class names. So a
4208
+ * CMS row is merged OVER the row the template shipped with rather than replacing it, and \`keys\`
4209
+ * says which template property each CMS leaf lands on. A row an editor ADDS has no counterpart, so
4210
+ * it inherits row 0's styling \u2014 the only answer that renders at all.
4211
+ */
4212
+ export function bcmsRowsAs<T>(
4213
+ page: unknown,
4214
+ path: string,
4215
+ fallback: T[],
4216
+ keys: Record<string, string>,
4217
+ slug?: string,
4218
+ ): T[] {
4219
+ const rows = bcmsRows<unknown>(page, path, [], slug);
4220
+ if (rows.length === 0) return fallback;
4221
+ return rows.map((row, index) => {
4222
+ const base = { ...(fallback[index] ?? fallback[0] ?? {}) } as Record<string, unknown>;
4223
+ for (const property of Object.keys(keys)) {
4224
+ const value = render(readPath(row, keys[property] as string));
4225
+ if (value !== undefined) base[property] = value;
4226
+ }
4227
+ return base as T;
4228
+ });
4229
+ }
4230
+
4231
+ /**
4232
+ * A list of PRIMITIVES \u2014 \`const tags = ["\u2026", "\u2026"]\` \u2014 with one CMS field per ELEMENT.
4233
+ *
4234
+ * \u{1F534} THE LENGTH IS \`paths\`, NOT THE CMS. Unlike \`bcmsRows\`, which reads the array itself and
4235
+ * renders as many rows as the CMS holds, this maps over the path list the codemod wrote \u2014 so a row
4236
+ * an editor ADDS has no path here and never renders, and a row an editor DELETES leaves its path
4237
+ * resolving to nothing, which falls back to the copy the template shipped with. Every row's TEXT is
4238
+ * editable; the number of rows is the template's. @see the PRIMITIVE_LIST receipt note, which says
4239
+ * this out loud rather than leaving an editor to discover it.
4240
+ */
4241
+ export function bcmsList(page: unknown, paths: string[], fallback: string[], slug?: string): string[] {
4242
+ const entries = isRecord(page) ? page.entries : undefined;
4243
+ const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
4244
+ return paths.map((path, index) => render(readPath(root, path)) ?? fallback[index] ?? "");
4245
+ }
4246
+
4247
+ /**
4248
+ * The same for a list of TUPLES \u2014 \`[["q", "a"], \u2026]\` \u2014 with one field per tuple POSITION, and the
4249
+ * same ceiling: \`paths\` decides how many rows render, and an added or deleted CMS row does not.
4250
+ */
4251
+ export function bcmsTuples(page: unknown, paths: string[][], fallback: string[][], slug?: string): string[][] {
4252
+ return paths.map((row, index) => bcmsList(page, row, fallback[index] ?? [], slug));
4253
+ }
4254
+
4255
+ /** An image field's url, or the asset the template already imports. @see IMAGE_ASSET_UNRESOLVED */
4256
+ export function bcmsImage<T>(page: unknown, path: string, fallback: T, slug?: string): T | string {
4257
+ const entries = isRecord(page) ? page.entries : undefined;
4258
+ const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
4259
+ return render(readPath(root, path)) ?? fallback;
4260
+ }
4261
+
4262
+ /**
4263
+ * ONE SECTION's copy, for a component extracted by \`--componentize\`.
4264
+ *
4265
+ * OVERRIDES FIRST, PER KEY: \`overrides[key] ?? group[key]\`. An instance that owns its copy
4266
+ * (Phase B) carries an override for every prop it declares, so the group underneath it is never
4267
+ * read; a placement still bound to a page field group (Phase A) carries none, so the group is all
4268
+ * there is. The interesting case is the one in between \u2014 a section half moved \u2014 where a key the
4269
+ * instance does not hold has to keep rendering the page's value rather than nothing.
4270
+ *
4271
+ * \u{1F534} \`??\`, NOT \`||\`. An empty string is a value an editor typed, and treating it as absent put
4272
+ * the page's old copy back into a heading somebody had deliberately cleared.
4273
+ *
4274
+ * With neither an override nor a group, the answer is \`{}\` and every \`bcms(...)\` below renders
4275
+ * the fallback the template shipped with \u2014 which is why a section renders correctly against the
4276
+ * committed \`{}\` stub.
4277
+ */
4278
+ export function bcmsSection(page: unknown, bind?: string, overrides?: unknown): Record<string, unknown> {
4279
+ const group = bind ? readPath(page, bind) : undefined;
4280
+ const base: Record<string, unknown> = isRecord(group) ? { ...group } : {};
4281
+ if (!isRecord(overrides)) return base;
4282
+ for (const key of Object.keys(overrides)) {
4283
+ const value = overrides[key];
4284
+ if (value === undefined || value === null) continue;
4285
+ /**
4286
+ * \u{1F534} A TABLE PROP IS STORED AS \`{ rows: [...] }\` AND READ AS A LIST. That is the shape the
4287
+ * dashboard edits a repeater in; handing it to \`bcmsRows\` unchanged is not an array, so the
4288
+ * helper fell back to the rows the template shipped with and the editor's work never showed.
4289
+ */
4290
+ base[key] = isRecord(value) && Array.isArray(value.rows) ? value.rows : value;
4291
+ }
4292
+ return base;
4293
+ }
4294
+
4295
+ /**
4296
+ * WHICH ADDRESS one bound element declares \u2014 decided at RENDER time, from the placement.
4297
+ *
4298
+ * An element carries exactly ONE \`data-bcms-field\`, and which lane it names depends on where its
4299
+ * copy lives. A placement that owns its copy declares the BLOCK lane \u2014
4300
+ * \`<blockId>__overrides.<propKey>\` \u2014 because that is the address the editor commits an instance's
4301
+ * own value to. A placement bound to a page field group declares the PAGE path, which is what the
4302
+ * page form and the coverage meter already know.
4303
+ *
4304
+ * \u{1F534} THE BOUND ADDRESS IS RE-BASED ONTO **THIS** PLACEMENT'S GROUP. The markup was cut from ONE
4305
+ * page and carries that page's paths (\`hero.title\`); the whole point of \`shapeHash\` is that a
4306
+ * second page renders the same component bound to a group of its own (\`bind="landingHero"\`).
4307
+ * Handing the embedded path through unchanged declared \`hero.title\` on a page whose field is
4308
+ * \`landingHero.title\` \u2014 an address that page does not have, so the editor wrote into nothing and
4309
+ * the meter counted a binding that was never there. The prefix the markup was cut with comes out,
4310
+ * the placement's own group goes in.
4311
+ *
4312
+ * \u{1F534} THE SAME EXTRACTED FILE SERVES BOTH LANES. The choice is a function of the props, never of
4313
+ * how the codemod ran: one component renders a Phase A placement on one page and a Phase B
4314
+ * instance on the next, and a repository does not have to be re-cut when its copy moves.
4315
+ */
4316
+ export function bcmsFields(
4317
+ placement: { blockId?: string; bind?: string; overrides?: unknown },
4318
+ pathPrefix: string,
4319
+ ): (path: string) => string {
4320
+ const id = placement.blockId;
4321
+ const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
4322
+ /**
4323
+ * What replaces the prefix the markup was cut with, or null for "leave every path alone".
4324
+ *
4325
+ * Null covers a placement with neither a bind nor an instance \u2014 a shipped fallback, or a page
4326
+ * nobody has componentized yet \u2014 and a bound placement in a component whose prefix is EMPTY,
4327
+ * where the plan said the prop key IS the whole page path: there is no group segment to swap,
4328
+ * so prepending one would invent an address rather than correct it.
4329
+ */
4330
+ const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
4331
+ return (path: string): string => {
4332
+ // A path outside the group this section was cut from is not one this placement re-addresses:
4333
+ // it names a key the instance does not hold and a group this placement is not bound to.
4334
+ // (An EMPTY prefix is a plan saying the prop key IS the page path, and matches everything.)
4335
+ if (lane === null || !path.startsWith(pathPrefix)) return path;
4336
+ return lane + path.slice(pathPrefix.length);
4337
+ };
4338
+ }
4339
+ `;
4340
+ }
4341
+ var HISTORICAL = {
4342
+ "src/bcms-content.ts": {
4343
+ 1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
4344
+ 2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
4345
+ 3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
4346
+ 4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4347
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
4348
+ },
4349
+ "src/lib/bcms-content.ts": {
4350
+ 1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
4351
+ 2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
4352
+ 3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
4353
+ 4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
4354
+ 5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba"
4355
+ },
4356
+ "composables/bcms-content.ts": {
4357
+ 1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
4358
+ 2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
4359
+ 3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
4360
+ 4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4361
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
4362
+ }
4363
+ };
4364
+ var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
4365
+ function isKnownHelper(content, dialect) {
4366
+ if (content === helperSource(dialect)) return HELPER_VERSION;
4367
+ const path = HELPER_PATH[dialect];
4368
+ if (!path) return null;
4369
+ const digest = sha256(content);
4370
+ for (const [version, known] of Object.entries(HISTORICAL[path] ?? {})) {
4371
+ if (known === digest) return Number(version);
4372
+ }
4373
+ return null;
4374
+ }
4375
+
4376
+ // src/locate.ts
4377
+ var flat = (value) => value.replace(/\s+/g, " ").trim();
4378
+ var stripTags = (value) => value.replace(/<br\s*\/?>/gi, " ").replace(/<[^>]*>/g, "");
4379
+ function locate(brief, sources, options) {
4380
+ const flattened = sources.map((s) => {
4381
+ const readable = unwrapJsxStrings(decodeEntities(s.content));
4382
+ return {
4383
+ path: s.path,
4384
+ content: flat(readable),
4385
+ text: options?.stripTags ? flat(stripTags(readable)) : null
4386
+ };
4387
+ });
4388
+ const located = [];
4389
+ const unlocated = [];
4390
+ for (const page of brief.pages) {
4391
+ for (const p of page.paths) {
4392
+ const original = p.original === null ? "" : flat(p.original);
4393
+ if (!original) {
4394
+ unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "no-original" });
4395
+ continue;
4396
+ }
4397
+ const files = flattened.filter((f) => f.content.includes(original) || f.text !== null && f.text.includes(original)).map((f) => f.path);
4398
+ if (files.length === 0) {
4399
+ unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "not-in-source" });
4400
+ continue;
4401
+ }
4402
+ located.push({ route: page.route, scope: p.scope, path: p.path, kind: p.kind, original, files });
4403
+ }
4404
+ }
4405
+ return { located, unlocated };
4406
+ }
4407
+
4408
+ // src/loops.ts
4409
+ import MagicString from "magic-string";
4410
+
4411
+ // src/scope.ts
4412
+ function patternNames(node, out) {
4413
+ if (!node || typeof node !== "object") return;
4414
+ const typed = node;
4415
+ switch (typed.type) {
4416
+ case "Identifier":
4417
+ out.add(String(typed.name));
4418
+ return;
4419
+ case "ObjectPattern":
4420
+ for (const property of typed.properties ?? [])
4421
+ patternNames(property.type === "RestElement" ? property.argument : property.value, out);
4422
+ return;
4423
+ case "ArrayPattern":
4424
+ for (const element of typed.elements ?? []) patternNames(element, out);
4425
+ return;
4426
+ case "AssignmentPattern":
4427
+ patternNames(typed.left, out);
4428
+ return;
4429
+ case "RestElement":
4430
+ patternNames(typed.argument, out);
4431
+ return;
4432
+ default:
4433
+ return;
4434
+ }
4435
+ }
4436
+ function statementNames(node, out) {
4437
+ if (!node || typeof node !== "object") return;
4438
+ const typed = node;
4439
+ switch (typed.type) {
4440
+ case "ExportNamedDeclaration":
4441
+ case "ExportDefaultDeclaration":
4442
+ statementNames(typed.declaration, out);
4443
+ return;
4444
+ case "ImportDeclaration":
4445
+ for (const specifier of typed.specifiers ?? []) patternNames(specifier.local, out);
4446
+ return;
4447
+ case "VariableDeclaration":
4448
+ for (const declarator of typed.declarations ?? []) patternNames(declarator.id, out);
4449
+ return;
4450
+ case "FunctionDeclaration":
4451
+ case "ClassDeclaration":
4452
+ case "TSTypeAliasDeclaration":
4453
+ case "TSInterfaceDeclaration":
4454
+ case "TSEnumDeclaration":
4455
+ case "TSModuleDeclaration":
4456
+ patternNames(typed.id, out);
4457
+ return;
4458
+ default:
4459
+ return;
4460
+ }
4461
+ }
4462
+ var DECLARED = /\b(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g;
4463
+ var IMPORTED = /\bimport\s+([^;\n]*?)\s+from\b/g;
4464
+ function scanNames(code, out) {
4465
+ for (const match of code.matchAll(DECLARED)) out.add(match[1]);
4466
+ for (const match of code.matchAll(IMPORTED))
4467
+ for (const name of match[1].match(/[A-Za-z_$][\w$]*/g) ?? []) {
4468
+ if (name !== "as" && name !== "type") out.add(name);
4469
+ }
4470
+ }
4471
+ function scriptText(nodes, content, out) {
4472
+ for (const node of nodes) {
4473
+ if (node.type !== "element") continue;
4474
+ if (node.raw && node.tag === "script" && node.inner) out.push(content.slice(node.inner.start, node.inner.end));
4475
+ scriptText(node.children, content, out);
4476
+ }
4477
+ }
4478
+ function topLevelBindings(dialect, parsed, content) {
4479
+ const out = /* @__PURE__ */ new Set();
4480
+ const program = parsed.ast?.program;
4481
+ if (program) for (const statement of program.body ?? []) statementNames(statement, out);
4482
+ else if (parsed.script) scanNames(parsed.script.code, out);
4483
+ if (dialect === "html") {
4484
+ const scripts = [];
4485
+ scriptText(parsed.roots, content, scripts);
4486
+ for (const code of scripts) scanNames(code, out);
4487
+ }
4488
+ return out;
4489
+ }
4490
+ function allocateNames(taken, wanted) {
4491
+ const used = new Set(taken);
4492
+ const out = /* @__PURE__ */ new Map();
4493
+ for (const { key, name } of wanted) {
4494
+ if (out.has(key)) continue;
4495
+ let candidate = name;
4496
+ for (let n = 1; used.has(candidate); n++) candidate = `${name}_${n}`;
4497
+ used.add(candidate);
4498
+ out.set(key, candidate);
4499
+ }
4500
+ return out;
4501
+ }
4502
+
4503
+ // src/loops.ts
4504
+ function splitNested(leaf) {
4505
+ const match = leaf.match(/^([^[\]]+)\[(\d+)\]\.(.+)$/);
4506
+ return match ? { array: match[1], index: Number(match[2]), leaf: match[3] } : null;
4507
+ }
4508
+ var elementEnd = (node) => node.inner ? node.inner.end + node.tag.length + 3 : null;
4509
+ function findRows(roots, members) {
4510
+ const wanted = new Set(members.map((m) => m.index));
4511
+ const search = (node) => {
4512
+ if (node.type !== "element") return null;
4513
+ for (const child of node.children) {
4514
+ const deeper = search(child);
4515
+ if (deeper) return deeper;
4516
+ }
4517
+ const rows = [];
4518
+ for (const child of node.children) {
4519
+ if (child.type !== "element") continue;
4520
+ const end = elementEnd(child);
4521
+ if (end === null) continue;
4522
+ const inside = members.filter((m) => m.site.range.start >= child.start && m.site.range.end <= end);
4523
+ if (inside.length === 0) continue;
4524
+ const indices = new Set(inside.map((m) => m.index));
4525
+ if (indices.size !== 1) return null;
4526
+ rows.push({
4527
+ index: inside[0].index,
4528
+ range: { start: child.start, end },
4529
+ attrInsertAt: child.attrInsertAt,
4530
+ // `:key` in vue, `key` in jsx — one existing key, whichever way it is spelled.
4531
+ keyAttr: child.attrs.find((a) => a.name.replace(/^:/, "") === "key")?.range ?? null,
4532
+ node: child,
4533
+ members: inside
4534
+ });
4535
+ }
4536
+ if (rows.length === 0) return null;
4537
+ const covered = new Set(rows.flatMap((r) => r.members.map((m) => m.index)));
4538
+ if (covered.size !== wanted.size || rows.length !== wanted.size) return null;
4539
+ if (rows.some((r) => r.members.length !== members.length / wanted.size)) return null;
4540
+ return rows.sort((a, b) => a.range.start - b.range.start);
4541
+ };
4542
+ for (const root of roots) {
4543
+ const found = search(root);
4544
+ if (found) return found;
4545
+ }
4546
+ return null;
4547
+ }
4548
+ var consecutive = (rows, content) => rows.every((row, at) => at === 0 || content.slice(rows[at - 1].range.end, row.range.start).trim() === "");
4549
+ function maskedRow(row, content) {
4550
+ const masked = new Map(row.members.map((member) => [member.site.inner.start, member.leaf]));
4551
+ return canonical(row.node, content, masked);
4552
+ }
4553
+ var TRIVIA = "";
4554
+ function canonical(node, content, masked) {
4555
+ if (node.type === "text") {
4556
+ return node.value.trim() === "" ? TRIVIA : content.slice(node.range.start, node.range.end);
4557
+ }
4558
+ if (node.type === "comment" || node.type === "expr") {
4559
+ return content.slice(node.range.start, node.range.end);
4560
+ }
4561
+ if (node.type !== "element") return "";
4562
+ const open = content.slice(node.start, node.inner ? node.inner.start : node.attrInsertAt);
4563
+ if (node.inner && masked.has(node.inner.start)) {
4564
+ return `${open}\0${masked.get(node.inner.start)}\0</${node.tag}>`;
4565
+ }
4566
+ const inside = node.children.map((child) => canonical(child, content, masked)).join("");
4567
+ return node.inner ? `${open}${inside}</${node.tag}>` : open;
4568
+ }
4569
+ function sameShape(rows, content) {
4570
+ const first = maskedRow(rows[0], content);
4571
+ return rows.every((row) => maskedRow(row, content) === first);
4572
+ }
4573
+ var indented = (body, indent) => body.split("\n").map((line) => line.startsWith(indent) ? `${indent} ${line.slice(indent.length)}` : `${indent} ${line}`).join("\n");
4574
+ function indentOf(content, at) {
4575
+ const lineStart = content.lastIndexOf("\n", at - 1) + 1;
4576
+ const prefix = content.slice(lineStart, at);
4577
+ return prefix.trim() === "" ? prefix : "";
4578
+ }
4579
+ function writeLeaf(source, shift, member, rule, read, row, literal) {
4580
+ const site = member.site;
4581
+ const expression = `${read}(${row}, ${JSON.stringify(member.leaf)}, "")`;
4582
+ if (member.kind === "richtext" && rule.richtext) {
4583
+ source.remove(site.inner.start + shift, site.inner.end + shift);
4584
+ source.appendLeft(site.attrInsertAt + shift, ` ${rule.richtext(expression)}`);
4585
+ } else {
4586
+ source.overwrite(site.inner.start + shift, site.inner.end + shift, rule.text(expression));
4587
+ }
4588
+ const kind = member.kind ? ` data-bcms-kind="${member.kind}"` : "";
4589
+ source.appendLeft(site.attrInsertAt + shift, ` ${rule.attr("data-bcms-field", `\`${literal}\``)}${kind}`);
4590
+ }
4591
+ function emitNested(rule, array, members, prefix, outer, parent, ctx) {
4592
+ if (!rule.loop || !rule.text) return null;
4593
+ const inner = [];
4594
+ for (const member of members) {
4595
+ const split = splitNested(member.leaf);
4596
+ if (!split || splitNested(split.leaf) !== null) return null;
4597
+ inner.push({ ...member, index: split.index, leaf: split.leaf });
4598
+ }
4599
+ const rows = findRows([parent.node], inner);
4600
+ if (!rows || rows.length === 0) return null;
4601
+ if (!consecutive(rows, ctx.content)) return null;
4602
+ if (!sameShape(rows, ctx.content)) return null;
4603
+ const template = rows[0];
4604
+ const used = /* @__PURE__ */ new Set([
4605
+ ...ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? [],
4606
+ outer.row,
4607
+ outer.index
4608
+ ]);
4609
+ const allocated = allocateNames(used, [
4610
+ { key: "row", name: "item" },
4611
+ { key: "index", name: "j" }
4612
+ ]);
4613
+ const names = { row: allocated.get("row"), index: allocated.get("index") };
4614
+ const fallback = [];
4615
+ const writable = inner.every((member) => setLeaf(fallback[member.index] ??= emptyRow(), member.leaf, member.fallback));
4616
+ if (!writable) return null;
4617
+ const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
4618
+ const shift = -template.range.start;
4619
+ const declarations = [];
4620
+ for (const member of template.members) {
4621
+ const literal = `${prefix}.${array}[\${${names.index}}].${member.leaf}`;
4622
+ writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
4623
+ declarations.push({ path: shapeOfPath(literal), literal });
4624
+ }
4625
+ const rowsExpr = `${ctx.rows}(${outer.row}, ${JSON.stringify(array)}, ${JSON.stringify(fallback.filter(Boolean))})`;
4626
+ if (rule.loop.kind === "attr") {
4627
+ source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
4628
+ }
4629
+ if (rule.loopKey) {
4630
+ const key = rule.loopKey(names.index);
4631
+ if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
4632
+ else if (template.keyAttr.end > template.keyAttr.start)
4633
+ source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
4634
+ else return null;
4635
+ }
4636
+ const indent = indentOf(ctx.content, template.range.start);
4637
+ const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
4638
+ ${indented(source.toString(), indent)}
4639
+ ${indent}${rule.loop.close}` : source.toString();
4640
+ return { range: { start: template.range.start, end: rows[rows.length - 1].range.end }, text, declarations };
4641
+ }
4642
+ function emitRepeater(dialect, group, arrayPath, fallbackRows, ctx) {
4643
+ const rule = DIALECT_RULES[dialect];
4644
+ if (!rule.loop || !rule.text || group.length === 0) return null;
4645
+ const rows = findRows(ctx.roots, group);
4646
+ if (!rows || rows.length < 2) return null;
4647
+ if (!consecutive(rows, ctx.content)) return null;
4648
+ if (!sameShape(rows, ctx.content)) return null;
4649
+ if (group.some((m) => m.site.where !== "text" || !isIntrinsic(m.site.tag) || !m.site.inner)) return null;
4650
+ const template = rows[0];
4651
+ const used = new Set(ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? []);
4652
+ const allocated = allocateNames(used, [
4653
+ { key: "row", name: "card" },
4654
+ { key: "index", name: "i" }
4655
+ ]);
4656
+ const names = { row: allocated.get("row"), index: allocated.get("index") };
4657
+ const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
4658
+ const shift = -template.range.start;
4659
+ const declarations = [];
4660
+ const nested = /* @__PURE__ */ new Map();
4661
+ const direct = [];
4662
+ for (const member of template.members) {
4663
+ const inner = splitNested(member.leaf);
4664
+ if (inner === null) direct.push(member);
4665
+ else nested.set(inner.array, [...nested.get(inner.array) ?? [], member]);
3773
4666
  }
3774
- return parts.join("/");
3775
- }
3776
- var firstPresent = (base, files) => [base, ...EXTENSIONS.map((ext) => `${base}${ext}`), ...EXTENSIONS.map((ext) => `${base}/index${ext}`)].find(
3777
- (candidate) => files.has(candidate)
3778
- ) ?? null;
3779
- function aliasesFrom(sources) {
3780
- const out = /* @__PURE__ */ new Map();
3781
- for (const source of sources) {
3782
- if (!isProjectConfig(source.path)) continue;
3783
- const parsed = readJsonc(source.content);
3784
- if (parsed === null) continue;
3785
- const base = parsed.compilerOptions?.baseUrl?.replace(/^\.\/?/, "").replace(/\/$/, "") ?? "";
3786
- const root = source.path.split("/").slice(0, -1).join("/");
3787
- for (const [pattern, targets] of Object.entries(parsed.compilerOptions?.paths ?? {})) {
3788
- out.set(
3789
- pattern,
3790
- targets.map((target) => [root, base, target.replace(/^\.\/?/, "")].filter(Boolean).join("/"))
3791
- );
3792
- }
4667
+ for (const [array, members] of nested) {
4668
+ const plan2 = emitNested(rule, array, members, `${arrayPath}[\${${names.index}}]`, names, template, ctx);
4669
+ if (!plan2) return null;
4670
+ if (direct.some((m) => m.site.range.start >= plan2.range.start && m.site.range.end <= plan2.range.end)) return null;
4671
+ source.overwrite(plan2.range.start + shift, plan2.range.end + shift, plan2.text);
4672
+ declarations.push(...plan2.declarations);
3793
4673
  }
3794
- return out;
3795
- }
3796
- var isProjectConfig = (path) => path.endsWith("tsconfig.json") || path.endsWith("jsconfig.json");
3797
- function readJsonc(content) {
3798
- let out = "";
3799
- let at = 0;
3800
- while (at < content.length) {
3801
- const char = content[at];
3802
- if (char === '"') {
3803
- const start = at++;
3804
- while (at < content.length && content[at] !== '"') at += content[at] === "\\" ? 2 : 1;
3805
- out += content.slice(start, Math.min(at + 1, content.length));
3806
- at++;
3807
- continue;
3808
- }
3809
- if (char === "/" && content[at + 1] === "/") {
3810
- while (at < content.length && content[at] !== "\n") at++;
3811
- continue;
3812
- }
3813
- if (char === "/" && content[at + 1] === "*") {
3814
- const close = content.indexOf("*/", at + 2);
3815
- at = close < 0 ? content.length : close + 2;
3816
- continue;
3817
- }
3818
- out += char;
3819
- at++;
4674
+ for (const member of direct) {
4675
+ const literal = `${arrayPath}[\${${names.index}}].${member.leaf}`;
4676
+ writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
4677
+ declarations.push({ path: shapeOfPath(literal), literal });
3820
4678
  }
3821
- try {
3822
- const parsed = JSON.parse(out.replace(/,(?=\s*[}\]])/g, ""));
3823
- return isAliasShaped(parsed) ? parsed : null;
3824
- } catch {
3825
- return null;
4679
+ const args = [ctx.snapshot, JSON.stringify(arrayPath), JSON.stringify(fallbackRows), ...ctx.slug ? [ctx.slug] : []];
4680
+ const rowsExpr = `${ctx.rows}(${args.join(", ")})`;
4681
+ if (rule.loop.kind === "attr") {
4682
+ source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
3826
4683
  }
3827
- }
3828
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3829
- function isAliasShaped(value) {
3830
- if (!isRecord(value)) return false;
3831
- const options = value.compilerOptions;
3832
- if (options === void 0) return true;
3833
- if (!isRecord(options)) return false;
3834
- if (options.baseUrl !== void 0 && typeof options.baseUrl !== "string") return false;
3835
- if (options.paths === void 0) return true;
3836
- if (!isRecord(options.paths)) return false;
3837
- return Object.values(options.paths).every(
3838
- (targets) => Array.isArray(targets) && targets.every((target) => typeof target === "string")
3839
- );
3840
- }
3841
- function unreadableConfigs(sources) {
3842
- return sources.filter((source) => isProjectConfig(source.path) && readJsonc(source.content) === null).map((source) => source.path);
3843
- }
3844
- function expand(specifier, aliases) {
3845
- const out = [];
3846
- for (const [pattern, targets] of aliases) {
3847
- const star = pattern.indexOf("*");
3848
- if (star < 0) {
3849
- if (specifier === pattern) out.push(...targets);
3850
- continue;
3851
- }
3852
- const head = pattern.slice(0, star);
3853
- const tail = pattern.slice(star + 1);
3854
- if (!specifier.startsWith(head) || !specifier.endsWith(tail)) continue;
3855
- const middle = specifier.slice(head.length, specifier.length - tail.length);
3856
- out.push(...targets.map((target) => target.replace("*", middle)));
4684
+ if (rule.loopKey) {
4685
+ const key = rule.loopKey(names.index);
4686
+ if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
4687
+ else if (template.keyAttr.end > template.keyAttr.start)
4688
+ source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
4689
+ else return null;
3857
4690
  }
3858
- return out;
4691
+ const indent = indentOf(ctx.content, template.range.start);
4692
+ const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
4693
+ ${indented(source.toString(), indent)}
4694
+ ${indent}${rule.loop.close}` : source.toString();
4695
+ return {
4696
+ range: { start: template.range.start, end: rows[rows.length - 1].range.end },
4697
+ text,
4698
+ declarations,
4699
+ bindings: [...new Set(declarations.map((d) => d.path))],
4700
+ names
4701
+ };
3859
4702
  }
3860
- function resolveSpecifier(fromFile, specifier, files, aliases) {
3861
- if (specifier.startsWith(".")) return firstPresent(join(fromFile, specifier), files);
3862
- for (const candidate of expand(specifier, aliases)) {
3863
- const found = firstPresent(candidate.replace(/\/$/, ""), files);
3864
- if (found) return found;
4703
+ function setLeaf(row, leaf, value) {
4704
+ const segments = leaf.split(".").flatMap((segment) => {
4705
+ const name = segment.replace(/\[\d+\]/g, "");
4706
+ const indices = [...segment.matchAll(/\[(\d+)\]/g)].map((m) => Number(m[1]));
4707
+ return [...name ? [name] : [], ...indices];
4708
+ });
4709
+ if (segments.some((segment) => typeof segment === "string" && FORBIDDEN.has(segment))) return false;
4710
+ let cursor = row;
4711
+ for (const [at, segment] of segments.entries()) {
4712
+ const key = segment;
4713
+ const last = at === segments.length - 1;
4714
+ if (last) {
4715
+ cursor[key] = value;
4716
+ return true;
4717
+ }
4718
+ const held = Object.prototype.hasOwnProperty.call(cursor, key) ? cursor[key] : void 0;
4719
+ if (held === void 0 || typeof held !== "object" || held === null) {
4720
+ cursor[key] = typeof segments[at + 1] === "number" ? [] : emptyRow();
4721
+ }
4722
+ cursor = cursor[key];
3865
4723
  }
3866
- return null;
4724
+ return true;
3867
4725
  }
3868
- function importsIn(parsed, _dialect) {
3869
- const out = /* @__PURE__ */ new Map();
3870
- walkAst(parsed.ast, (node) => {
3871
- if (node.type !== "ImportDeclaration") return;
3872
- if (node.importKind === "type") return;
3873
- const specifier = node.source?.value;
3874
- if (typeof specifier !== "string") return;
3875
- for (const entry of node.specifiers ?? []) {
3876
- if (entry.importKind === "type") continue;
3877
- const local = entry.local?.name;
3878
- if (typeof local !== "string") continue;
3879
- const kind = entry.type === "ImportDefaultSpecifier" ? "default" : entry.type === "ImportNamespaceSpecifier" ? "namespace" : "named";
3880
- const imported = kind === "named" ? entry.imported?.name ?? local : null;
3881
- out.set(local, { specifier, imported, kind });
3882
- }
3883
- });
3884
- return out;
4726
+ var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
4727
+ var emptyRow = () => /* @__PURE__ */ Object.create(null);
4728
+ function splitIndexed(arrayPath, path) {
4729
+ if (!path.startsWith(`${arrayPath}[`)) return null;
4730
+ const match = path.slice(arrayPath.length).match(/^\[(\d+)\]\.(.+)$/);
4731
+ return match ? { index: Number(match[1]), leaf: match[2] } : null;
3885
4732
  }
3886
4733
 
3887
4734
  // src/routes.ts
@@ -4046,8 +4893,8 @@ function declaringElements(roots, content) {
4046
4893
  const name = attr.name.replace(/^:/, "");
4047
4894
  if (name === "bcmsBindings") {
4048
4895
  const own = ownBytes(node, content);
4049
- for (const quoted of content.slice(attr.range.start, attr.range.end).matchAll(/["']([^"']+)["']/g)) {
4050
- out.push({ scope: "page", path: quoted[1], kind, own });
4896
+ for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
4897
+ out.push({ scope: "page", path, kind, own });
4051
4898
  }
4052
4899
  continue;
4053
4900
  }
@@ -4077,14 +4924,18 @@ function declaringElements(roots, content) {
4077
4924
  return out;
4078
4925
  }
4079
4926
  function helperLocals(parsed, dialect, file) {
4080
- const out = { bcms: [], bcmsRows: [], bcmsLayout: [] };
4927
+ const out = { bcms: [], bcmsRows: [], bcmsRowsAs: [], bcmsImage: [], bcmsLayout: [], bcmsList: [], bcmsTuples: [] };
4081
4928
  const helper = HELPER_PATH[dialect];
4082
4929
  if (!helper) return out;
4083
4930
  for (const [local, from] of importsIn(parsed, dialect)) {
4084
4931
  if (!namesFile(file, from.specifier, helper)) continue;
4085
4932
  if (from.imported === "bcms") out.bcms.push(local);
4086
4933
  if (from.imported === "bcmsRows") out.bcmsRows.push(local);
4934
+ if (from.imported === "bcmsRowsAs") out.bcmsRowsAs.push(local);
4935
+ if (from.imported === "bcmsImage") out.bcmsImage.push(local);
4087
4936
  if (from.imported === "bcmsLayout") out.bcmsLayout.push(local);
4937
+ if (from.imported === "bcmsList") out.bcmsList.push(local);
4938
+ if (from.imported === "bcmsTuples") out.bcmsTuples.push(local);
4088
4939
  }
4089
4940
  return out;
4090
4941
  }
@@ -4115,19 +4966,29 @@ function snapshotLocal(parsed, dialect, slug, file) {
4115
4966
  }
4116
4967
  return null;
4117
4968
  }
4969
+ function moduleBytes(roots, content) {
4970
+ let out = "";
4971
+ let at = 0;
4972
+ for (const root of roots) {
4973
+ if (root.type !== "element" || root.start < at) continue;
4974
+ out += content.slice(at, root.start);
4975
+ at = Math.max(at, root.end);
4976
+ }
4977
+ return out + content.slice(at);
4978
+ }
4118
4979
  var identifier = (name) => `(?<![\\w$])${escapeRegExp(name)}(?![\\w$])`;
4119
- function elementReads(own, target, locals, snapshot) {
4980
+ function elementReads(own, target, locals, snapshot, viaRow) {
4120
4981
  const quoted = escapeRegExp(JSON.stringify(target.path));
4121
4982
  if (target.scope === "layout") {
4122
4983
  return locals.bcmsLayout.some((name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${quoted}`).test(own));
4123
4984
  }
4124
4985
  const indexed = splitArrayPath(target.path);
4125
- if (indexed) {
4986
+ if (viaRow && indexed) {
4126
4987
  const leaf = escapeRegExp(JSON.stringify(indexed.leaf));
4127
4988
  return locals.bcms.some((name) => new RegExp(`${identifier(name)}\\s*\\([^)]*,\\s*${leaf}`).test(own));
4128
4989
  }
4129
4990
  if (!snapshot) return false;
4130
- return locals.bcms.some(
4991
+ return [...locals.bcms, ...locals.bcmsImage].some(
4131
4992
  (name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,\\s*${quoted}`).test(own)
4132
4993
  );
4133
4994
  }
@@ -4137,16 +4998,36 @@ function convertedHere(content, parsed, dialect, target, file) {
4137
4998
  const snapshot = snapshotLocal(parsed, dialect, target.slug, file);
4138
4999
  const shape = shapeOfPath(target.path);
4139
5000
  const indexed = splitArrayPath(target.path);
5001
+ let keyed = false;
5002
+ let rows = false;
4140
5003
  if (indexed) {
4141
5004
  const array = escapeRegExp(JSON.stringify(indexed.array));
4142
- const rows = locals.bcmsRows.some(
5005
+ const reads = (names) => names.some(
4143
5006
  (name) => snapshot !== null && new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,\\s*${array}`).test(content)
4144
5007
  );
4145
- if (!rows) return false;
5008
+ keyed = reads(locals.bcmsRowsAs);
5009
+ rows = keyed || reads(locals.bcmsRows);
4146
5010
  }
4147
- return declaringElements(parsed.roots, content).some(
4148
- (element) => element.scope === target.scope && (element.path === target.path || element.path === shape) && elementReads(element.own, target, locals, snapshot)
5011
+ const listed = indexed !== null && snapshot !== null && content.includes(JSON.stringify(target.path)) && [...locals.bcmsList, ...locals.bcmsTuples].some(
5012
+ (name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,`).test(content)
4149
5013
  );
5014
+ let cached = null;
5015
+ const moduleRead = () => {
5016
+ if (indexed || snapshot === null) return false;
5017
+ cached ??= moduleBytes(parsed.roots, content);
5018
+ const quoted = escapeRegExp(JSON.stringify(target.path));
5019
+ return locals.bcms.some(
5020
+ (name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,\\s*${quoted}`).test(cached)
5021
+ );
5022
+ };
5023
+ return declaringElements(parsed.roots, content).some((element) => {
5024
+ if (element.scope !== target.scope) return false;
5025
+ if (element.path === target.path) {
5026
+ return elementReads(element.own, target, locals, snapshot, false) || moduleRead();
5027
+ }
5028
+ if (element.path !== shape) return false;
5029
+ return listed || rows && (keyed || elementReads(element.own, target, locals, snapshot, true));
5030
+ });
4150
5031
  }
4151
5032
 
4152
5033
  // src/rewrite.ts
@@ -4212,7 +5093,7 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
4212
5093
  addDeclaration(site.attrInsertAt, declaration(rewrite));
4213
5094
  continue;
4214
5095
  }
4215
- if (kind === "richtext") {
5096
+ if (kind === "richtext" && !(site.mixed && site.textRange)) {
4216
5097
  if (!isIntrinsic(site.tag)) continue;
4217
5098
  if (rule.richtext && site.inner) {
4218
5099
  source.remove(site.inner.start, site.inner.end);
@@ -4223,11 +5104,13 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
4223
5104
  continue;
4224
5105
  }
4225
5106
  if (site.mixed && site.textRange) {
5107
+ const html = kind === "richtext" && rule.richtext ? rule.richtext(expression) : null;
4226
5108
  const inner = rule.text ? rule.text(expression) : content.slice(site.textRange.start, site.textRange.end).trim();
5109
+ const attributes2 = attrsFor(path, kind, void 0, rewrite.scope);
4227
5110
  source.overwrite(
4228
5111
  site.textRange.start,
4229
5112
  site.textRange.end,
4230
- `<span ${attrsFor(path, kind, void 0, rewrite.scope)}>${inner}</span>`
5113
+ html ? `<span ${attributes2} ${html}></span>` : `<span ${attributes2}>${inner}</span>`
4231
5114
  );
4232
5115
  continue;
4233
5116
  }
@@ -4475,6 +5358,7 @@ function convert3(node, content) {
4475
5358
  }
4476
5359
  ];
4477
5360
  }
5361
+ if (node.type === "fragment") return (node.children ?? []).flatMap((child) => convert3(child, content));
4478
5362
  if (node.type !== "element" && node.type !== "component" && node.type !== "custom-element") return [];
4479
5363
  const tag = node.name ?? "";
4480
5364
  const children = (node.children ?? []).flatMap((child) => convert3(child, content));
@@ -4845,16 +5729,16 @@ function functionBindings(fn) {
4845
5729
  if (body?.type === "BlockStatement") statementNames2(body.body, out);
4846
5730
  return out;
4847
5731
  }
4848
- function stringLiterals(node, out) {
5732
+ function stringLiterals2(node, out) {
4849
5733
  if (!isNode(node)) {
4850
- if (Array.isArray(node)) for (const child of node) stringLiterals(child, out);
5734
+ if (Array.isArray(node)) for (const child of node) stringLiterals2(child, out);
4851
5735
  return;
4852
5736
  }
4853
5737
  if (FUNCTIONS3.has(node.type)) return;
4854
5738
  if (node.type === "StringLiteral" && typeof node.value === "string") out.add(node.value);
4855
5739
  for (const key of Object.keys(node)) {
4856
5740
  if (key === "loc" || key === "leadingComments" || key === "trailingComments") continue;
4857
- stringLiterals(node[key], out);
5741
+ stringLiterals2(node[key], out);
4858
5742
  }
4859
5743
  }
4860
5744
  function usages(ast, local) {
@@ -4871,7 +5755,7 @@ function usages(ast, local) {
4871
5755
  if (FUNCTIONS3.has(node.type)) {
4872
5756
  nextScope = /* @__PURE__ */ new Set([...scope, ...functionBindings(node)]);
4873
5757
  nextLiterals = /* @__PURE__ */ new Set();
4874
- stringLiterals(node.body, nextLiterals);
5758
+ stringLiterals2(node.body, nextLiterals);
4875
5759
  }
4876
5760
  if (node.type === "JSXElement") {
4877
5761
  const name = node.openingElement?.name;
@@ -5201,12 +6085,29 @@ var SOURCE_EXTENSIONS = [
5201
6085
  ".erb",
5202
6086
  ".twig"
5203
6087
  ];
6088
+ var PROSE_EXTENSIONS = [".md", ".mdx", ".njk", ".hbs", ".ejs", ".liquid", ".erb", ".twig"];
6089
+ var TEMPLATE_DIRS = /* @__PURE__ */ new Set([
6090
+ "pages",
6091
+ "content",
6092
+ "src",
6093
+ "app",
6094
+ "routes",
6095
+ "layouts",
6096
+ "components",
6097
+ "templates",
6098
+ "views",
6099
+ "_includes",
6100
+ "_layouts"
6101
+ ]);
5204
6102
  function isSourceCandidate(path) {
5205
6103
  const lower = path.toLowerCase();
5206
6104
  if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
5207
6105
  return false;
5208
6106
  if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
5209
- return SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext));
6107
+ if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
6108
+ if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
6109
+ if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
6110
+ return lower.split("/").slice(0, -1).some((segment) => TEMPLATE_DIRS.has(segment));
5210
6111
  }
5211
6112
 
5212
6113
  // src/carry.ts
@@ -7024,6 +7925,7 @@ var ConvertError = class extends Error {
7024
7925
  var keyOf = (route, scope, path) => scope === "layout" ? `layout::${path}` : `${route}::page::${path}`;
7025
7926
  var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7026
7927
  var identifierFor2 = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
7928
+ var UNPLACED = /* @__PURE__ */ new Set(["NOT_IN_SOURCE", "IN_EXPRESSION"]);
7027
7929
  async function drillProp(ctx, fromFile, component, prop, kind, hop) {
7028
7930
  const miss = { ok: false, reason: "PROP_TARGET_NOT_FOUND", edits: [] };
7029
7931
  if (hop > MAX_HOPS) return { ok: false, reason: "PROP_DRILLED_DEEP", edits: [] };
@@ -7120,7 +8022,7 @@ async function convertSources(briefIn, sources, options) {
7120
8022
  const parsed = await parseOf(source.path);
7121
8023
  declarations.set(
7122
8024
  source.path,
7123
- parsed === null ? /* @__PURE__ */ new Set() : parsed.error ? declaredByScan(source.content) : declaredInTree(parsed.roots, source.content)
8025
+ parsed === null ? isTolerantDialect(source.path) ? declaredByScan(source.content) : /* @__PURE__ */ new Set() : parsed.error ? declaredByScan(source.content) : declaredInTree(parsed.roots, source.content)
7124
8026
  );
7125
8027
  }
7126
8028
  let declaredOnly = 0;
@@ -7152,6 +8054,36 @@ async function convertSources(briefIn, sources, options) {
7152
8054
  const key = keyOf(hit.route, hit.scope ?? "page", hit.path);
7153
8055
  if (!locatedByKey.has(key)) locatedByKey.set(key, hit);
7154
8056
  }
8057
+ const imageUnresolved = /* @__PURE__ */ new Map();
8058
+ for (const miss of unlocated) {
8059
+ const key = keyOf(miss.route, miss.scope ?? "page", miss.path);
8060
+ const target = targets.get(key);
8061
+ if (!target || target.kind !== "image" || !target.fallback || locatedByKey.has(key)) continue;
8062
+ const hosts = [];
8063
+ for (const file of pageFilesFor(target.route, contents.keys())) {
8064
+ const parsed = await parseOf(file);
8065
+ const dialect = dialectOf(file, contents.get(file) ?? "");
8066
+ const bindings = parsed && dialect ? assetCandidates(parsed, dialect, target.fallback) : [];
8067
+ if (bindings.length > 0) hosts.push({ file, bindings });
8068
+ }
8069
+ const only = hosts.length === 1 && hosts[0].bindings.length === 1 ? hosts[0] : null;
8070
+ if (only) {
8071
+ locatedByKey.set(key, {
8072
+ route: target.route,
8073
+ scope: target.scope,
8074
+ path: target.path,
8075
+ kind: target.kind,
8076
+ original: target.literal,
8077
+ files: [only.file]
8078
+ });
8079
+ continue;
8080
+ }
8081
+ const named = hosts.flatMap((host) => host.bindings.map((binding) => `${host.file}:${binding}`));
8082
+ imageUnresolved.set(
8083
+ key,
8084
+ named.length === 0 ? `No asset imported by this route's page has the base name of ${target.fallback}.` : `${named.join(", ")} all match the base name of ${target.fallback}, and nothing says which.`
8085
+ );
8086
+ }
7155
8087
  const sharingPath = /* @__PURE__ */ new Map();
7156
8088
  for (const [key, hit] of locatedByKey) {
7157
8089
  const held = targets.get(key);
@@ -7195,6 +8127,11 @@ async function convertSources(briefIn, sources, options) {
7195
8127
  declaredRow(target, false, byHand);
7196
8128
  continue;
7197
8129
  }
8130
+ const unresolved = imageUnresolved.get(key);
8131
+ if (unresolved !== void 0) {
8132
+ pend2(target, "IMAGE_ASSET_UNRESOLVED", void 0, unresolved);
8133
+ continue;
8134
+ }
7198
8135
  pend2(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
7199
8136
  }
7200
8137
  const byFile = /* @__PURE__ */ new Map();
@@ -7216,6 +8153,7 @@ async function convertSources(briefIn, sources, options) {
7216
8153
  const receiptFiles = [];
7217
8154
  const dialects = /* @__PURE__ */ new Set();
7218
8155
  const dynamicBindings = [];
8156
+ const laneNotes = [];
7219
8157
  const snapshotSlugs = /* @__PURE__ */ new Set();
7220
8158
  let occurrencesLocated = 0;
7221
8159
  let occurrencesRewritten = 0;
@@ -7241,6 +8179,7 @@ async function convertSources(briefIn, sources, options) {
7241
8179
  outFiles.push({ path: file, operation: "modify", content: result.content });
7242
8180
  receiptFiles.push({ path: file, bindings: result.bindings, tier: result.tier });
7243
8181
  dynamicBindings.push(...result.dynamic);
8182
+ laneNotes.push(...result.notes);
7244
8183
  for (const slug of result.snapshots) snapshotSlugs.add(slug);
7245
8184
  }
7246
8185
  }
@@ -7252,7 +8191,14 @@ async function convertSources(briefIn, sources, options) {
7252
8191
  const worst = outcomes.find((o) => o !== "rewritten" && o !== "declared");
7253
8192
  if (worst) {
7254
8193
  const file = hit.files[outcomes.indexOf(worst)];
7255
- pend2(target, worst, file);
8194
+ pend2(
8195
+ target,
8196
+ worst,
8197
+ file,
8198
+ // The reason says the source does not render it and the file name says where it was
8199
+ // found; without the second half the two read as a contradiction. @see unplacedReason
8200
+ worst === "NOT_IN_SOURCE" && file ? `${file} contains this copy, but no element renders it on its own.` : void 0
8201
+ );
7256
8202
  } else if (outcomes.every((o) => o === "declared")) {
7257
8203
  const witness = await witnessed(target);
7258
8204
  const byHand = witness === null ? declaredByHand(target, hit.files) : null;
@@ -7330,13 +8276,25 @@ async function convertSources(briefIn, sources, options) {
7330
8276
  helperUpgraded,
7331
8277
  dynamicBindings,
7332
8278
  canvas: { lane: canvas.lane, ...canvas.files.length > 0 ? { files: canvas.files } : {} },
7333
- notes: unreadableConfigs(sources).map(
7334
- (file) => ({
7335
- code: "TSCONFIG_UNREADABLE",
7336
- file,
7337
- message: `${file} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
7338
- })
7339
- )
8279
+ notes: [
8280
+ ...unreadableConfigs(sources).map(
8281
+ (file) => ({
8282
+ code: "TSCONFIG_UNREADABLE",
8283
+ file,
8284
+ message: `${file} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
8285
+ })
8286
+ ),
8287
+ // An absent alias table looks exactly like a project that has none, and the two have
8288
+ // opposite consequences for every component reached through one. @see extendedConfigs
8289
+ ...extendedConfigs(sources).map(
8290
+ (file) => ({
8291
+ code: "TSCONFIG_EXTENDS_ONLY",
8292
+ file,
8293
+ message: `${file} declares no \`compilerOptions.paths\` of its own and extends another config, which this package does not follow \u2014 so an import through a path alias resolved to nothing and any prop drilled through one is PROP_TARGET_NOT_FOUND.`
8294
+ })
8295
+ ),
8296
+ ...laneNotes
8297
+ ]
7340
8298
  });
7341
8299
  return { files: outFiles.sort((a, b) => a.path.localeCompare(b.path)), receipt };
7342
8300
  }
@@ -7358,12 +8316,17 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
7358
8316
  dialect: dialectOf(file, content),
7359
8317
  dynamic: [],
7360
8318
  componentEdits: [],
7361
- snapshots: []
8319
+ snapshots: [],
8320
+ notes: []
7362
8321
  };
7363
8322
  if (pendingTargets.length === 0) return empty;
7364
8323
  if (!dialect) {
7365
8324
  if (isTolerantDialect(file)) {
7366
- const tolerantLiterals = [...new Set(pendingTargets.map((t) => t.literal))];
8325
+ const already = declaredByScan(content);
8326
+ const remaining = pendingTargets.filter((target) => !covers(already, target));
8327
+ for (const target of pendingTargets) if (covers(already, target)) outcomes.set(target.key, "declared");
8328
+ if (remaining.length === 0) return { ...empty, dialect: null, outcomes };
8329
+ const tolerantLiterals = [...new Set(remaining.map((t) => t.literal))];
7367
8330
  const tolerantSites = findSitesTolerant(content, tolerantLiterals);
7368
8331
  if (tolerantSites.length > 0) {
7369
8332
  return plan(
@@ -7373,7 +8336,7 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
7373
8336
  // argument is behind `!parsed.error` or `tier !== 2`. Passing the writing dialect keeps
7374
8337
  // the parameter honest rather than widening its type for one caller.
7375
8338
  "html",
7376
- pendingTargets,
8339
+ remaining,
7377
8340
  tolerantSites,
7378
8341
  void 0,
7379
8342
  // no parse, no import point: tier 2 declares, it does not read
@@ -7386,7 +8349,7 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
7386
8349
  );
7387
8350
  }
7388
8351
  }
7389
- for (const target of pendingTargets) outcomes.set(target.key, "DIALECT_UNSUPPORTED");
8352
+ for (const target of pendingTargets) if (!outcomes.has(target.key)) outcomes.set(target.key, "DIALECT_UNSUPPORTED");
7390
8353
  return { ...empty, dialect: null, outcomes };
7391
8354
  }
7392
8355
  const literals = [...new Set(pendingTargets.map((t) => t.literal))];
@@ -7415,7 +8378,8 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
7415
8378
  dialect: null,
7416
8379
  dynamic: [],
7417
8380
  componentEdits: [],
7418
- snapshots: []
8381
+ snapshots: [],
8382
+ notes: []
7419
8383
  };
7420
8384
  }
7421
8385
  if (rescued.reason === "unverifiable") {
@@ -7423,8 +8387,8 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
7423
8387
  return { ...empty, tier: 3, outcomes, dialect: null };
7424
8388
  }
7425
8389
  }
7426
- if (sites.length === 0) {
7427
- const reason = tier1.error?.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : tier1.error ? "PARSE_ERROR" : "NOT_IN_SOURCE";
8390
+ if (sites.length === 0 && tier1.error) {
8391
+ const reason = tier1.error.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR";
7428
8392
  for (const target of pendingTargets) outcomes.set(target.key, reason);
7429
8393
  return { ...empty, tier, outcomes, dialect };
7430
8394
  }
@@ -7442,9 +8406,24 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
7442
8406
  tier1.error ? [] : declarationsIn(tier1.roots, content)
7443
8407
  );
7444
8408
  }
8409
+ function unplacedReason(parsed, literal) {
8410
+ if (!parsed || parsed.error) return "NOT_IN_SOURCE";
8411
+ if (parsed.script && norm(parsed.script.code).includes(literal)) return "IN_EXPRESSION";
8412
+ let found = false;
8413
+ const visit = (node) => {
8414
+ if (found) return;
8415
+ if (node.type === "expr") {
8416
+ if (norm(node.value).includes(literal)) found = true;
8417
+ return;
8418
+ }
8419
+ if (node.type === "element" && !node.raw) for (const child of node.children) visit(child);
8420
+ };
8421
+ for (const root of parsed.roots) visit(root);
8422
+ return found ? "IN_EXPRESSION" : "NOT_IN_SOURCE";
8423
+ }
7445
8424
  async function plan(file, content, dialect, targets, sites, importAt, outcomes, tier, parsed, drill, declarations) {
7446
8425
  const writing = tier === 2 ? "html" : dialect;
7447
- const missing = tier !== 2 ? "NOT_IN_SOURCE" : parsed.error?.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR";
8426
+ const missing = (literal) => tier !== 2 ? unplacedReason(parsed, literal) : parsed.error?.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR";
7448
8427
  const taken = parsed.error ? /* @__PURE__ */ new Set() : topLevelBindings(dialect, parsed, content);
7449
8428
  const slugs = [...new Set(targets.map((t) => t.slug))].sort();
7450
8429
  const preset = /* @__PURE__ */ new Map();
@@ -7453,7 +8432,9 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
7453
8432
  for (const [key, held] of [
7454
8433
  ["helper:bcms", locals.bcms],
7455
8434
  ["helper:bcmsRows", locals.bcmsRows],
7456
- ["helper:bcmsLayout", locals.bcmsLayout]
8435
+ ["helper:bcmsLayout", locals.bcmsLayout],
8436
+ ["helper:bcmsRowsAs", locals.bcmsRowsAs],
8437
+ ["helper:bcmsImage", locals.bcmsImage]
7457
8438
  ]) {
7458
8439
  if (held[0]) preset.set(key, held[0]);
7459
8440
  }
@@ -7466,6 +8447,10 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
7466
8447
  { key: "helper:bcms", name: "bcms" },
7467
8448
  { key: "helper:bcmsRows", name: "bcmsRows" },
7468
8449
  { key: "helper:bcmsLayout", name: "bcmsLayout" },
8450
+ { key: "helper:bcmsRowsAs", name: "bcmsRowsAs" },
8451
+ { key: "helper:bcmsImage", name: "bcmsImage" },
8452
+ { key: "helper:bcmsList", name: "bcmsList" },
8453
+ { key: "helper:bcmsTuples", name: "bcmsTuples" },
7469
8454
  // Keyed by the SLUG: two slugs that spell one identifier are still two pages. @see scope.ts
7470
8455
  ...slugs.map((slug) => ({ key: `slug:${slug}`, name: identifierFor2(slug) }))
7471
8456
  ].filter((entry) => !preset.has(entry.key));
@@ -7483,7 +8468,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
7483
8468
  for (const [literal, sharing] of byLiteral) {
7484
8469
  const forLiteral = sites.filter((s) => s.literal === literal);
7485
8470
  if (forLiteral.length === 0) {
7486
- for (const target of sharing) outcomes.set(target.key, missing);
8471
+ for (const target of sharing) outcomes.set(target.key, missing(literal));
7487
8472
  continue;
7488
8473
  }
7489
8474
  const assigned = sharing.length === 1 ? /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]) : resolvePositionally(sharing, forLiteral);
@@ -7497,7 +8482,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
7497
8482
  if (rewritable.length === 0) {
7498
8483
  outcomes.set(
7499
8484
  target.key,
7500
- mine.some((s) => s.partial) ? "SUBSTRING_ONLY" : mine.some((s) => s.where === "script" || s.where === "comment") ? "IN_SCRIPT_OR_COMMENT" : missing
8485
+ mine.some((s) => s.partial) ? "SUBSTRING_ONLY" : mine.some((s) => s.where === "script" || s.where === "comment") ? "IN_SCRIPT_OR_COMMENT" : missing(literal)
7501
8486
  );
7502
8487
  continue;
7503
8488
  }
@@ -7572,7 +8557,11 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
7572
8557
  // `original` has been through `firstBlockText`, so it is the text a reader sees with
7573
8558
  // the markup already stripped. Keeping that as the in-code fallback would silently
7574
8559
  // flatten every bold and link the moment a build ran against a stub snapshot.
7575
- fallback: target.kind === "richtext" && site.inner ? content.slice(site.inner.start, site.inner.end).trim() : target.fallback,
8560
+ //
8561
+ // 🔴 AND ONLY WHEN THE ELEMENT'S SUBTREE IS THE VALUE. A value that is one TEXT NODE of
8562
+ // a mixed element gets a wrapper of its own, so taking the element's inner range as its
8563
+ // fallback swallowed the `<em>` beside it — the wrapper then rendered the sibling twice.
8564
+ fallback: target.kind === "richtext" && site.inner && !(site.mixed && site.textRange) ? content.slice(site.inner.start, site.inner.end).trim() : target.fallback,
7576
8565
  snapshot: snapshotName(target.slug),
7577
8566
  slug: target.slug,
7578
8567
  scope: target.scope,
@@ -7653,10 +8642,65 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
7653
8642
  const collided = new Set(overlapping(kept).map((r) => r.key));
7654
8643
  const applied = kept.filter((r) => !collided.has(r.key));
7655
8644
  for (const key of collided) outcomes.set(key, "AMBIGUOUS_LITERAL");
7656
- if (applied.length === 0) {
7657
- return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [] };
8645
+ const data = await bindDataLiterals(
8646
+ {
8647
+ content,
8648
+ parsed,
8649
+ dialect: writing,
8650
+ rowsAs: helperName("bcmsRowsAs"),
8651
+ list: helperName("bcmsList"),
8652
+ tuples: helperName("bcmsTuples"),
8653
+ read: helperName("bcms"),
8654
+ snapshotName,
8655
+ slug: params?.slug ?? null,
8656
+ // The row's leaf travels to another file exactly as a scalar prop does, and by the same
8657
+ // machinery — the lane decides WHICH props, this decides what may be touched. @see props.ts
8658
+ drill: async (component, prop, kind) => {
8659
+ const drilled = await drillProp(drill, file, component, prop, kind, 1);
8660
+ return drilled.ok ? { ok: true, edits: drilled.edits } : { ok: false, reason: drilled.reason ?? "PROP_TARGET_NOT_FOUND" };
8661
+ }
8662
+ },
8663
+ targets.filter((t) => {
8664
+ const outcome = outcomes.get(t.key);
8665
+ return t.scope === "page" && (outcome === void 0 || UNPLACED.has(outcome));
8666
+ }).map((t) => ({
8667
+ key: t.key,
8668
+ path: t.path,
8669
+ kind: t.kind,
8670
+ literal: t.literal,
8671
+ slug: t.slug
8672
+ }))
8673
+ );
8674
+ for (const key of data.bound) outcomes.set(key, "rewritten");
8675
+ for (const refusal of data.refused) outcomes.set(refusal.key, refusal.reason);
8676
+ for (const declaration2 of data.declarations) {
8677
+ dynamic.push({ file, prop: null, path: declaration2.path, callSites: [{ file, literal: declaration2.literal }] });
8678
+ }
8679
+ componentEdits.push(...data.componentEdits);
8680
+ for (const drilledProp of data.props) {
8681
+ dynamic.push({
8682
+ file: drilledProp.file,
8683
+ prop: drilledProp.prop,
8684
+ path: drilledProp.path,
8685
+ callSites: [{ file, literal: drilledProp.literal }]
8686
+ });
7658
8687
  }
7659
- const readSlugs = [...new Set(applied.filter((r) => r.scope === "page").map((r) => r.slug))].sort();
8688
+ const images = bindImages(
8689
+ { content, parsed, dialect: writing, image: helperName("bcmsImage"), snapshotName, slug: params?.slug ?? null },
8690
+ targets.filter((t) => {
8691
+ const outcome = outcomes.get(t.key);
8692
+ return t.kind === "image" && t.scope === "page" && (outcome === void 0 || UNPLACED.has(outcome));
8693
+ }).map((t) => ({ key: t.key, path: t.path, kind: t.kind, original: t.fallback, slug: t.slug }))
8694
+ );
8695
+ for (const key of images.bound) outcomes.set(key, "rewritten");
8696
+ for (const refusal of images.refused) outcomes.set(refusal.key, refusal.reason);
8697
+ const lanes = [...data.splices, ...images.splices];
8698
+ if (applied.length === 0 && lanes.length === 0) {
8699
+ return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [], notes: [] };
8700
+ }
8701
+ const readSlugs = [
8702
+ .../* @__PURE__ */ new Set([...applied.filter((r) => r.scope === "page").map((r) => r.slug), ...data.slugs, ...images.slugs])
8703
+ ].sort();
7660
8704
  const helper = HELPER_PATH[writing];
7661
8705
  const rewritten = rewriteFile(content, writing, applied, {
7662
8706
  // html reads nothing at build time, so it imports nothing: `injectFields` writes its values
@@ -7677,10 +8721,17 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
7677
8721
  { name: "bcms", alias: helperName("bcms") },
7678
8722
  { name: "bcmsRows", alias: helperName("bcmsRows") }
7679
8723
  ] : [],
7680
- ...applied.some((r) => r.scope === "layout") ? [{ name: "bcmsLayout", alias: helperName("bcmsLayout") }] : []
8724
+ ...applied.some((r) => r.scope === "layout") ? [{ name: "bcmsLayout", alias: helperName("bcmsLayout") }] : [],
8725
+ ...data.usesRows ? [{ name: "bcmsRowsAs", alias: helperName("bcmsRowsAs") }] : [],
8726
+ ...data.usesList ? [{ name: "bcmsList", alias: helperName("bcmsList") }] : [],
8727
+ // `bcmsTuples` reads each row through `bcmsList`, but the emitted helper exports both and the
8728
+ // page imports only the one it calls — an unused import is dead code in someone else's repo.
8729
+ ...data.usesTuples ? [{ name: "bcmsTuples", alias: helperName("bcmsTuples") }] : [],
8730
+ ...data.usesRead ? [{ name: "bcms", alias: helperName("bcms") }] : [],
8731
+ ...images.splices.length > 0 ? [{ name: "bcmsImage", alias: helperName("bcmsImage") }] : []
7681
8732
  ].filter((helper2, at, all) => all.findIndex((h) => h.name === helper2.name) === at).filter((helper2) => !preset.has(`helper:${helper2.name}`)),
7682
8733
  at: importAt
7683
- }, params?.splices ?? []);
8734
+ }, [...params?.splices ?? [], ...lanes]);
7684
8735
  return {
7685
8736
  tier,
7686
8737
  content: rewritten,
@@ -7690,7 +8741,9 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
7690
8741
  dialect: writing,
7691
8742
  dynamic,
7692
8743
  componentEdits,
7693
- snapshots: rule.text ? readSlugs : []
8744
+ snapshots: rule.text ? readSlugs : [],
8745
+ // The lane reports one sentence per list it bound; the FILE is added here, where it is known.
8746
+ notes: data.notes.map((message) => ({ code: "PRIMITIVE_LIST", file, message }))
7694
8747
  };
7695
8748
  }
7696
8749
  function mergeInto(content, site) {