@bettercms-ai/convert 0.10.0 → 0.12.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/index.js CHANGED
@@ -4140,7 +4140,7 @@ function bindImages(ctx, targets) {
4140
4140
 
4141
4141
  // src/helper.ts
4142
4142
  import { createHash as createHash2 } from "crypto";
4143
- var HELPER_VERSION = 6;
4143
+ var HELPER_VERSION = 7;
4144
4144
  var HEADER = "// @bettercms-ai/convert helper v";
4145
4145
  var HELPER_PATH = Object.fromEntries(
4146
4146
  Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
@@ -4333,9 +4333,65 @@ export function bcmsSection(page: unknown, bind?: string, overrides?: unknown):
4333
4333
  * how the codemod ran: one component renders a Phase A placement on one page and a Phase B
4334
4334
  * instance on the next, and a repository does not have to be re-cut when its copy moves.
4335
4335
  */
4336
+ /** How a flat field family maps page paths to an instance's override keys. @see bcmsSectionPaths */
4337
+ export type BcmsPathMap = {
4338
+ scalars?: Record<string, string>;
4339
+ tables?: Record<string, { key: string; columns: Record<string, string> }>;
4340
+ };
4341
+
4342
+ /**
4343
+ * The snapshot for a section whose page fields are a FLAT family (\`wrap-h2-latest\`, not \`wrap.h2_latest\`).
4344
+ *
4345
+ * \`bcmsSection\` returns a group object read through a dotted path, which a flat family does not have: its
4346
+ * copy lives in top-level keys of the page snapshot. So this answers in the page's own spelling \u2014 the
4347
+ * markup reads \`bcms(snapshot, "wrap-h2-latest", \u2026)\` verbatim \u2014 with every mapped path set explicitly:
4348
+ *
4349
+ * - the instance's override for that key when it holds one (a table's rows re-spelled with the page's
4350
+ * column names, \`{ rows }\` unwrapped as in \`bcmsSection\`);
4351
+ * - else, for a placement bound to a group, the page value at the path re-based onto that group;
4352
+ * - else NOTHING \u2014 a placement that owns its copy never falls back to the page's stale copy, exactly as
4353
+ * \`bcmsSection\` synthesizes no group for it.
4354
+ *
4355
+ * Every other page key passes through, so reads outside the family (a shared link) keep working.
4356
+ */
4357
+ export function bcmsSectionPaths(
4358
+ page: unknown,
4359
+ bind: string | undefined,
4360
+ overrides: unknown,
4361
+ prefix: string,
4362
+ map: BcmsPathMap,
4363
+ ): Record<string, unknown> {
4364
+ const out: Record<string, unknown> = isRecord(page) ? { ...page } : {};
4365
+ const own = isRecord(overrides) ? overrides : {};
4366
+ const separator = prefix.slice(-1);
4367
+ const fromPage = (path: string): unknown =>
4368
+ bind ? readPath(page, bind + separator + path.slice(prefix.length)) : undefined;
4369
+ for (const [path, key] of Object.entries(map.scalars ?? {})) {
4370
+ const value = own[key];
4371
+ out[path] = value !== undefined && value !== null ? value : fromPage(path);
4372
+ }
4373
+ for (const [path, table] of Object.entries(map.tables ?? {})) {
4374
+ const held = own[table.key];
4375
+ const rows = isRecord(held) && Array.isArray(held.rows) ? held.rows : held;
4376
+ if (!Array.isArray(rows)) {
4377
+ out[path] = fromPage(path);
4378
+ continue;
4379
+ }
4380
+ const pageColumn = new Map(Object.entries(table.columns).map(([column, key]) => [key, column]));
4381
+ out[path] = rows.map((row) => {
4382
+ if (!isRecord(row)) return row;
4383
+ const spelled: Record<string, unknown> = {};
4384
+ for (const [key, value] of Object.entries(row)) spelled[pageColumn.get(key) ?? key] = value;
4385
+ return spelled;
4386
+ });
4387
+ }
4388
+ return out;
4389
+ }
4390
+
4336
4391
  export function bcmsFields(
4337
4392
  placement: { blockId?: string; bind?: string; overrides?: unknown },
4338
4393
  pathPrefix: string,
4394
+ map?: BcmsPathMap,
4339
4395
  ): (path: string) => string {
4340
4396
  const id = placement.blockId;
4341
4397
  const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
@@ -4348,6 +4404,24 @@ export function bcmsFields(
4348
4404
  * so prepending one would invent an address rather than correct it.
4349
4405
  */
4350
4406
  const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
4407
+ if (map) {
4408
+ /**
4409
+ * A FLAT family: the address is not "prefix swapped". The instance lane names the override key the
4410
+ * plan allocated (\`h2_latest\`, \`group_webgl[0].a_webgl\`); the bind lane keeps the page's own
4411
+ * separator (\`landing-h2-latest\`, never \`landing.h2-latest\`).
4412
+ */
4413
+ const separator = pathPrefix.slice(-1);
4414
+ return (path: string): string => {
4415
+ if (!path.startsWith(pathPrefix) || !(own || placement.bind)) return path;
4416
+ if (!own) return placement.bind + separator + path.slice(pathPrefix.length);
4417
+ const scalar = map.scalars?.[path];
4418
+ if (scalar !== undefined) return id + "__overrides." + scalar;
4419
+ const row = /^([^[]+)\\[(\\d+)\\]\\.(.+)$/.exec(path);
4420
+ const table = row ? map.tables?.[row[1]] : undefined;
4421
+ const column = row && table ? table.columns[row[3]] : undefined;
4422
+ return table && column !== undefined ? id + "__overrides." + table.key + "[" + row![2] + "]." + column : path;
4423
+ };
4424
+ }
4351
4425
  return (path: string): string => {
4352
4426
  // A path outside the group this section was cut from is not one this placement re-addresses:
4353
4427
  // it names a key the instance does not hold and a group this placement is not bound to.
@@ -4364,21 +4438,24 @@ var HISTORICAL = {
4364
4438
  2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
4365
4439
  3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
4366
4440
  4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4367
- 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
4441
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
4442
+ 6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
4368
4443
  },
4369
4444
  "src/lib/bcms-content.ts": {
4370
4445
  1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
4371
4446
  2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
4372
4447
  3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
4373
4448
  4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
4374
- 5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba"
4449
+ 5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba",
4450
+ 6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5"
4375
4451
  },
4376
4452
  "composables/bcms-content.ts": {
4377
4453
  1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
4378
4454
  2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
4379
4455
  3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
4380
4456
  4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4381
- 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
4457
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
4458
+ 6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
4382
4459
  }
4383
4460
  };
4384
4461
  var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
@@ -5241,6 +5318,138 @@ function coverageOf(receipt) {
5241
5318
  };
5242
5319
  }
5243
5320
 
5321
+ // src/fix.ts
5322
+ var MAX_SNIPPET = 160;
5323
+ var MAX_WORDS = 12;
5324
+ var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5325
+ function findLiteral(content, literal) {
5326
+ const words = literal.split(/\s+/).filter(Boolean).slice(0, MAX_WORDS);
5327
+ if (words.length === 0) return null;
5328
+ const pattern = new RegExp(words.map(escapeRegExp2).join("(?:\\s|<[^>]*>)+"), "u");
5329
+ const match = pattern.exec(content);
5330
+ return match ? { index: match.index, length: match[0].length } : null;
5331
+ }
5332
+ function placeOf(content, index) {
5333
+ const before = content.slice(0, index);
5334
+ const line = before.split("\n").length;
5335
+ const start = before.lastIndexOf("\n") + 1;
5336
+ const end = content.indexOf("\n", index);
5337
+ return {
5338
+ line,
5339
+ col: index - start + 1,
5340
+ snippet: content.slice(start, end < 0 ? content.length : end).trim().slice(0, MAX_SNIPPET)
5341
+ };
5342
+ }
5343
+ function fileHead(content) {
5344
+ return { line: 1, snippet: (content ?? "").split("\n", 1)[0].trim().slice(0, MAX_SNIPPET) };
5345
+ }
5346
+ var isMarkdown = (file) => /\.mdx?$/i.test(file);
5347
+ var isDataModule = (file) => /\.(json|ya?ml|[mc]?[jt]s)$/i.test(file) && !/\.d\.ts$/i.test(file);
5348
+ function inDataRegion(content, index) {
5349
+ if (content.startsWith("---")) {
5350
+ const close = content.indexOf("\n---", 3);
5351
+ if (close >= 0 && index < close) return true;
5352
+ }
5353
+ for (const match of content.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)) {
5354
+ const start = match.index + match[0].indexOf(">") + 1;
5355
+ if (index >= start && index < start + match[1].length) return true;
5356
+ }
5357
+ return false;
5358
+ }
5359
+ var ELEMENT_ACTIONS = /* @__PURE__ */ new Set([
5360
+ "wrap-span",
5361
+ "declare-attr",
5362
+ "declare-richtext"
5363
+ ]);
5364
+ function actionFor(reason, input, file, inData) {
5365
+ const chosen = reasonAction(reason, input, file, inData);
5366
+ if (inData && ELEMENT_ACTIONS.has(chosen.action)) {
5367
+ return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
5368
+ }
5369
+ return chosen;
5370
+ }
5371
+ function reasonAction(reason, input, file, inData) {
5372
+ const richtext = input.kind === "richtext" || input.inline === true;
5373
+ switch (reason) {
5374
+ case "AMBIGUOUS_LITERAL":
5375
+ if (input.routes > 1) {
5376
+ return {
5377
+ action: "manual",
5378
+ why: `${file} renders this value on ${input.routes} routes, so it is shared chrome rather than page copy: promote it to the project Layout (a Layout field is one value for the whole site) and rerun, or give each route its own element.`
5379
+ };
5380
+ }
5381
+ return {
5382
+ action: "declare-attr",
5383
+ why: input.candidates.length > 1 ? `This copy appears in ${input.candidates.join(", ")}; declare the one this route renders.` : "Two paths want this element and nothing says which \u2014 declare the right one by hand."
5384
+ };
5385
+ case "REPEATER_AMBIGUOUS":
5386
+ case "REPEATER_FIXED_LENGTH":
5387
+ return {
5388
+ action: "declare-attr",
5389
+ why: "Declare this row's leaf on the element itself, or rewrite the loop to read the CMS array."
5390
+ };
5391
+ case "IN_EXPRESSION":
5392
+ if (inData) {
5393
+ return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
5394
+ }
5395
+ return richtext ? { action: "declare-richtext", why: "The container is fed markup \u2014 bind it with the richtext helper." } : { action: "bind-expression", why: "Replace the computed value with the helper's read for this path." };
5396
+ case "DIALECT_UNSUPPORTED":
5397
+ if (isMarkdown(file)) {
5398
+ return {
5399
+ action: "bind-data",
5400
+ why: "This is a content-collection body: bind the element that renders it and make the entry the source."
5401
+ };
5402
+ }
5403
+ return isDataModule(file) ? { action: "bind-data", why: `${file} holds this copy as data \u2014 bind the element that renders it.` } : { action: "manual", why: `No parser here reads ${file}, so the declaration has to be written by hand.` };
5404
+ case "NOT_IN_SOURCE":
5405
+ case "NO_ORIGINAL":
5406
+ return isDataModule(file) || inData ? { action: "bind-data", why: "The template computes this from data \u2014 bind the data field instead of the text." } : {
5407
+ action: "manual",
5408
+ why: "The built page renders this and no source file holds it verbatim; find what formats it."
5409
+ };
5410
+ case "IN_DATA_FILE":
5411
+ case "IMAGE_ASSET_UNRESOLVED":
5412
+ return { action: "bind-data", why: "Bind the data field this value comes from." };
5413
+ case "PROP_TARGET_NOT_FOUND":
5414
+ case "PROP_DRILLED_DEEP":
5415
+ return {
5416
+ action: "wrap-span",
5417
+ why: "The component renders this in a position no attribute reaches \u2014 wrap it in a span of its own."
5418
+ };
5419
+ case "SUBSTRING_ONLY":
5420
+ return { action: "wrap-span", why: "Only part of the element's text is this value \u2014 give it its own span." };
5421
+ case "SUBTREE_NOT_HTML":
5422
+ return { action: "declare-richtext", why: "The value is the element's whole subtree \u2014 bind it as richtext." };
5423
+ default:
5424
+ return { action: "manual", why: `${reason} \u2014 see the receipt's message for what blocked this path.` };
5425
+ }
5426
+ }
5427
+ function fixFor(row, input) {
5428
+ const file = row.file ?? input.candidates[0] ?? "";
5429
+ const content = input.contents.get(file);
5430
+ const at = content ? findLiteral(content, input.literal) : null;
5431
+ const place = at ? placeOf(content, at.index) : { ...fileHead(content), col: void 0 };
5432
+ const { action, why } = actionFor(row.reason, input, file, at !== null && inDataRegion(content, at.index));
5433
+ return {
5434
+ file,
5435
+ line: row.line ?? place.line,
5436
+ ...place.col !== void 0 ? { col: place.col } : {},
5437
+ snippet: place.snippet,
5438
+ action,
5439
+ ...why ? { why } : {}
5440
+ };
5441
+ }
5442
+ function routeCounts(pending) {
5443
+ const routes = /* @__PURE__ */ new Map();
5444
+ for (const row of pending) {
5445
+ const id = `${row.scope}|${row.path}`;
5446
+ const held = routes.get(id) ?? /* @__PURE__ */ new Set();
5447
+ held.add(row.route);
5448
+ routes.set(id, held);
5449
+ }
5450
+ return new Map([...routes].map(([id, set]) => [id, set.size]));
5451
+ }
5452
+
5244
5453
  // src/sites/html.ts
5245
5454
  import { parse as parseHtml } from "parse5";
5246
5455
  var RAW_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template"]);
@@ -6361,7 +6570,9 @@ var SKIP_DIRS = [
6361
6570
  ".vercel/",
6362
6571
  ".cache/",
6363
6572
  "coverage/",
6364
- "vendor/"
6573
+ "vendor/",
6574
+ "__tests__/",
6575
+ "__mocks__/"
6365
6576
  ];
6366
6577
  var SKIP_FILES = ["package-lock.json", "bun.lock", "bun.lockb", "yarn.lock", "pnpm-lock.yaml"];
6367
6578
  var SOURCE_EXTENSIONS = [
@@ -6399,11 +6610,13 @@ var TEMPLATE_DIRS = /* @__PURE__ */ new Set([
6399
6610
  "_includes",
6400
6611
  "_layouts"
6401
6612
  ]);
6613
+ var isAboutTheSite = (lower) => /\.(test|spec)\.[^./]+$/.test(lower) || /^[^/]*\.config\.[^./]+$/.test(lower);
6402
6614
  function isSourceCandidate(path) {
6403
6615
  const lower = path.toLowerCase();
6404
6616
  if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
6405
6617
  return false;
6406
6618
  if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
6619
+ if (isAboutTheSite(lower)) return false;
6407
6620
  if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
6408
6621
  if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
6409
6622
  if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
@@ -6887,6 +7100,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
6887
7100
  return held;
6888
7101
  };
6889
7102
  const fresh = /* @__PURE__ */ new Set();
7103
+ const freshFiles = /* @__PURE__ */ new Map();
6890
7104
  const bindOf = (section) => instanceOwned(section) ? void 0 : section.groupKey;
6891
7105
  const placementOf = (slug, section) => {
6892
7106
  const bind = bindOf(section);
@@ -7214,6 +7428,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
7214
7428
  }
7215
7429
  extractable.set(resolved.slug, { name: resolved.name, path, kind: "default", imported: null });
7216
7430
  fresh.add(resolved.slug);
7431
+ freshFiles.set(resolved.slug, path);
7217
7432
  addFile(outFiles, contents, {
7218
7433
  path,
7219
7434
  operation: existing === void 0 ? "add" : "modify",
@@ -7299,6 +7514,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
7299
7514
  sections,
7300
7515
  // What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
7301
7516
  components: [...fresh].sort(),
7517
+ componentFiles: [...freshFiles].sort(([a], [b]) => a.localeCompare(b)).map(([slug, path]) => ({ slug, path, export: "default" })),
7302
7518
  planPending,
7303
7519
  notes
7304
7520
  }
@@ -7478,7 +7694,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7478
7694
  for (const section of sections) {
7479
7695
  const paths = fieldPaths(section);
7480
7696
  const literals = new Set(
7481
- briefPaths.filter((p) => (p.scope ?? "page") === "page" && groupOf(p.path) === section.groupKey && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
7697
+ briefPaths.filter((p) => (p.scope ?? "page") === "page" && inSection(section, p.path) && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
7482
7698
  );
7483
7699
  const hits = /* @__PURE__ */ new Map();
7484
7700
  for (const source of sources) {
@@ -7489,7 +7705,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7489
7705
  const found = /* @__PURE__ */ new Map();
7490
7706
  for (const declaration2 of declarationsIn(parsed.roots, source.content)) {
7491
7707
  if (declaration2.scope !== "page") continue;
7492
- if (groupOf(declaration2.path) !== section.groupKey) continue;
7708
+ if (!inSection(section, declaration2.path)) continue;
7493
7709
  const placed2 = elements.get(declaration2.at);
7494
7710
  if (placed2) found.set(declaration2.at, placed2);
7495
7711
  }
@@ -7514,6 +7730,12 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7514
7730
  return { placed, misses };
7515
7731
  }
7516
7732
  var groupOf = (path) => path.split(/[.[]/)[0] ?? "";
7733
+ var inSection = (section, path) => {
7734
+ if (groupOf(path) === section.groupKey) return true;
7735
+ const rowless = (value) => value.replace(/\[\d+\]/g, "[]");
7736
+ const target = rowless(path);
7737
+ return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
7738
+ };
7517
7739
  var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
7518
7740
  function elementIndex(roots) {
7519
7741
  const out = /* @__PURE__ */ new Map();
@@ -7616,6 +7838,9 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7616
7838
  ...addresses.opaque
7617
7839
  ];
7618
7840
  if (unconvertible.length > 0) return { content: "", free: [], duplicates: [], unconvertible };
7841
+ const flat3 = pathPrefix !== "" && !pathPrefix.endsWith(".");
7842
+ const pathMap = flat3 ? flatPathMap(resolved.section, pathPrefix) : null;
7843
+ if (flat3 && !pathMap) return { content: "", free: [], duplicates: [], unconvertible: [`${pathPrefix}* (a repeater leaf without an override address)`] };
7619
7844
  const cut = content.slice(resolved.root.start, resolved.root.end);
7620
7845
  const reserved = /* @__PURE__ */ new Set([...topLevelBindings(dialect, parsed, content), ...referencedNames2(cut)]);
7621
7846
  const introduced = allocateNames(reserved, [
@@ -7634,7 +7859,7 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7634
7859
  const helpers = [
7635
7860
  .../* @__PURE__ */ new Set([
7636
7861
  ...helperNames.map((h) => alias(h.name, h.local)),
7637
- "bcmsSection",
7862
+ flat3 ? "bcmsSectionPaths" : "bcmsSection",
7638
7863
  ...bound > 0 ? [alias("bcmsFields", fieldsName)] : []
7639
7864
  ])
7640
7865
  ].sort();
@@ -7664,9 +7889,10 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7664
7889
  const key = /^[A-Za-z_$][\w$]*$/.test(groupKey) ? groupKey : JSON.stringify(groupKey);
7665
7890
  const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
7666
7891
  ` : "";
7667
- const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };` : "";
7668
- const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)});` : "";
7892
+ const copy = !snapshot ? "" : flat3 ? `const ${snapshot} = bcmsSectionPaths(page, bind, overrides, ${JSON.stringify(pathPrefix)}, ${JSON.stringify(pathMap)});` : `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };`;
7893
+ const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
7669
7894
  const declared = [copy, field].filter(Boolean).join("\n");
7895
+ const styles = dialect === "astro" ? carriedStyles(content) : "";
7670
7896
  if (dialect === "astro") {
7671
7897
  return { free: [], duplicates: [], unconvertible: [], content: `---
7672
7898
  ${SECTION_MARKER}
@@ -7686,7 +7912,9 @@ const { blockId, bind, overrides, page } = Astro.props;
7686
7912
  ${carriedDeclarations}${declared}
7687
7913
  ---
7688
7914
  ${markup}
7689
- ` };
7915
+ ${styles ? `
7916
+ ${styles}
7917
+ ` : ""}` };
7690
7918
  }
7691
7919
  return { free: [], duplicates: [], unconvertible: [], content: `${SECTION_MARKER}
7692
7920
  ${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
@@ -7960,6 +8188,9 @@ import { bcmsFields } from "${helper}";`);
7960
8188
  return { files, refused };
7961
8189
  }
7962
8190
  function propPrefix(section) {
8191
+ return exactPrefix(section) ?? flatPrefix(section);
8192
+ }
8193
+ function exactPrefix(section) {
7963
8194
  const prefixes = /* @__PURE__ */ new Set();
7964
8195
  for (const field of section.fields ?? []) {
7965
8196
  const { path, key } = field;
@@ -7970,6 +8201,49 @@ function propPrefix(section) {
7970
8201
  if (prefixes.size > 1) return null;
7971
8202
  return prefixes.size === 1 ? [...prefixes][0] : `${section.groupKey}.`;
7972
8203
  }
8204
+ var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "value";
8205
+ function flatPrefix(section) {
8206
+ const prefixes = /* @__PURE__ */ new Set();
8207
+ let informative = false;
8208
+ for (const field of section.fields ?? []) {
8209
+ const { path, key } = field;
8210
+ if (typeof path !== "string" || typeof key !== "string") continue;
8211
+ informative = true;
8212
+ const prefix = ["-", "_"].map((sep) => `${section.groupKey}${sep}`).find((candidate) => path.startsWith(candidate) && (typeof field.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
8213
+ if (!prefix) return null;
8214
+ prefixes.add(prefix);
8215
+ }
8216
+ return informative && prefixes.size === 1 ? [...prefixes][0] : null;
8217
+ }
8218
+ function flatPathMap(section, prefix) {
8219
+ const scalars = {};
8220
+ const tables = {};
8221
+ for (const field of section.fields ?? []) {
8222
+ const { path, key } = field;
8223
+ if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
8224
+ const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
8225
+ if (!row) {
8226
+ scalars[path] = field.overrideKey ?? key;
8227
+ continue;
8228
+ }
8229
+ const address = typeof field.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field.overrideKey) : null;
8230
+ if (!address) return null;
8231
+ const table = tables[row[1]] ??= { key: address[1], columns: {} };
8232
+ if (table.key !== address[1]) return null;
8233
+ table.columns[row[2]] = address[2];
8234
+ }
8235
+ return { scalars, tables };
8236
+ }
8237
+ function carriedStyles(content) {
8238
+ const blocks = [];
8239
+ for (const match of content.matchAll(/<style\b([^>]*)>([\s\S]*?)<\/style>/g)) {
8240
+ const attributes = match[1] ?? "";
8241
+ if (/\bis:global\b|\bdefine:vars\b/.test(attributes)) continue;
8242
+ if (/\blang\s*=/.test(attributes) || /@(import|use|forward)\b/.test(match[2] ?? "")) continue;
8243
+ blocks.push(match[0]);
8244
+ }
8245
+ return blocks.join("\n");
8246
+ }
7973
8247
  function referencedNames2(markup) {
7974
8248
  const out = /* @__PURE__ */ new Set();
7975
8249
  for (const [name] of markup.matchAll(/[A-Za-z_$][\w$]*/g)) out.add(name);
@@ -8291,7 +8565,7 @@ var ConvertError = class extends Error {
8291
8565
  code;
8292
8566
  };
8293
8567
  var keyOf = (route, scope, path) => scope === "layout" ? `layout::${path}` : `${route}::page::${path}`;
8294
- var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8568
+ var escapeRegExp3 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8295
8569
  var identifierFor2 = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
8296
8570
  var UNPLACED = /* @__PURE__ */ new Set(["NOT_IN_SOURCE", "IN_EXPRESSION"]);
8297
8571
  async function drillProp(ctx, fromFile, component, prop, kind, hop) {
@@ -8638,6 +8912,33 @@ async function convertSources(briefIn, sources, options) {
8638
8912
  else outFiles.push({ path, operation: "modify", content });
8639
8913
  }
8640
8914
  pending.push(...canvas.pending);
8915
+ for (const file of outFiles) if (file.operation === "modify") latest.set(file.path, file.content);
8916
+ const pageFilesOrTemplate = (route, files) => {
8917
+ const all = [...files];
8918
+ const exact = pageFilesFor(route, all);
8919
+ if (exact.length > 0) return exact;
8920
+ const wanted = route.split("/").filter(Boolean);
8921
+ return all.filter((file) => {
8922
+ if (!isDynamicRoute(file)) return false;
8923
+ const pattern = routeOfFile(file)?.split("/").filter(Boolean);
8924
+ if (!pattern) return false;
8925
+ const rest = pattern.some((segment) => segment.startsWith("[..."));
8926
+ if (rest ? wanted.length < pattern.length : wanted.length !== pattern.length) return false;
8927
+ return pattern.every((segment, i) => segment.startsWith("[") || segment === wanted[i]);
8928
+ }).sort();
8929
+ };
8930
+ const routes = routeCounts(pending);
8931
+ for (const row of pending) {
8932
+ const target = targets.get(keyOf(row.route, row.scope, row.path));
8933
+ row.fix = fixFor(row, {
8934
+ literal: target?.literal ?? "",
8935
+ kind: row.kind,
8936
+ ...target?.inline ? { inline: true } : {},
8937
+ candidates: locatedByKey.get(keyOf(row.route, row.scope, row.path))?.files ?? pageFilesOrTemplate(row.route, latest.keys()),
8938
+ contents: latest,
8939
+ routes: routes.get(`${row.scope}|${row.path}`) ?? 1
8940
+ });
8941
+ }
8641
8942
  const receipt = assertReceipt({
8642
8943
  briefDigest: digest,
8643
8944
  paths: {
@@ -8850,13 +9151,13 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
8850
9151
  for (const target of sharing) outcomes.set(target.key, missing(literal));
8851
9152
  continue;
8852
9153
  }
8853
- const assigned = sharing.length === 1 ? /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]) : resolvePositionally(sharing, forLiteral);
8854
- if (!assigned) {
8855
- for (const target of sharing) outcomes.set(target.key, "AMBIGUOUS_LITERAL");
8856
- continue;
8857
- }
9154
+ const resolved = sharing.length === 1 ? { assigned: /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]), ambiguous: /* @__PURE__ */ new Set() } : resolvePositionally(sharing, forLiteral);
8858
9155
  for (const target of sharing) {
8859
- const mine = assigned.get(target.key) ?? [];
9156
+ if (resolved.ambiguous.has(target.key)) {
9157
+ outcomes.set(target.key, "AMBIGUOUS_LITERAL");
9158
+ continue;
9159
+ }
9160
+ const mine = resolved.assigned.get(target.key) ?? [];
8860
9161
  const rewritable = mine.filter((s) => s.where !== "script" && s.where !== "comment" && !s.partial);
8861
9162
  if (rewritable.length === 0) {
8862
9163
  outcomes.set(
@@ -9201,19 +9502,25 @@ function repeatGroups(targets) {
9201
9502
  }
9202
9503
  function resolvePositionally(targets, sites) {
9203
9504
  const assigned = /* @__PURE__ */ new Map();
9204
- const claimed = /* @__PURE__ */ new Set();
9505
+ const ambiguous = /* @__PURE__ */ new Set();
9506
+ const picked = /* @__PURE__ */ new Map();
9507
+ const wanters = /* @__PURE__ */ new Map();
9205
9508
  for (const target of targets) {
9206
9509
  const locator = target.locator;
9207
- if (!locator) return null;
9208
- const candidates = sites.filter(
9209
- (s) => s.siblingIndex === locator.siblingIndex && isSubsequence(locator.ancestors, s.ancestors)
9210
- );
9211
- const site = locator.repeat ? candidates[locator.repeat.index] : candidates.length === 1 ? candidates[0] : void 0;
9212
- if (!site || claimed.has(site)) return null;
9213
- claimed.add(site);
9214
- assigned.set(target.key, [site]);
9510
+ const candidates = locator ? sites.filter((s) => s.siblingIndex === locator.siblingIndex && isSubsequence(locator.ancestors, s.ancestors)) : [];
9511
+ const site = locator?.repeat ? candidates[locator.repeat.index] : candidates.length === 1 ? candidates[0] : void 0;
9512
+ if (!site) {
9513
+ ambiguous.add(target.key);
9514
+ continue;
9515
+ }
9516
+ picked.set(target.key, site);
9517
+ wanters.set(site, (wanters.get(site) ?? 0) + 1);
9518
+ }
9519
+ for (const [key, site] of picked) {
9520
+ if ((wanters.get(site) ?? 0) > 1) ambiguous.add(key);
9521
+ else assigned.set(key, [site]);
9215
9522
  }
9216
- return assigned;
9523
+ return { assigned, ambiguous };
9217
9524
  }
9218
9525
  function isSubsequence(wanted, chain) {
9219
9526
  let at = 0;
@@ -9236,7 +9543,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
9236
9543
  if (elements.length !== targets.length) return { ok: false, reason: "unverifiable" };
9237
9544
  const locals = helperLocals(reparsed, dialect, file);
9238
9545
  const reads = DIALECT_RULES[dialect].text !== null;
9239
- const boundary = (name) => `(?<![\\w$])${escapeRegExp2(name)}(?![\\w$])`;
9546
+ const boundary = (name) => `(?<![\\w$])${escapeRegExp3(name)}(?![\\w$])`;
9240
9547
  const claimed = /* @__PURE__ */ new Set();
9241
9548
  for (const element of elements) {
9242
9549
  const candidates = targets.filter(
@@ -9244,7 +9551,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
9244
9551
  );
9245
9552
  const match = candidates.find((target) => {
9246
9553
  if (!reads) return true;
9247
- const quoted = escapeRegExp2(JSON.stringify(target.path));
9554
+ const quoted = escapeRegExp3(JSON.stringify(target.path));
9248
9555
  if (target.scope === "layout") {
9249
9556
  return locals.bcmsLayout.some(
9250
9557
  (name) => new RegExp(`${boundary(name)}\\s*\\(\\s*${quoted}`).test(element.own)
@@ -9344,8 +9651,10 @@ export {
9344
9651
  dialectOf,
9345
9652
  exportedFunction,
9346
9653
  extendedConfigs,
9654
+ findLiteral,
9347
9655
  findSites,
9348
9656
  findSitesTolerant,
9657
+ fixFor,
9349
9658
  flat,
9350
9659
  helperSource,
9351
9660
  importsIn,
@@ -9367,6 +9676,7 @@ export {
9367
9676
  relativeImport,
9368
9677
  resolveSpecifier,
9369
9678
  rewriteFile,
9679
+ routeCounts,
9370
9680
  routeOfFile,
9371
9681
  stripTags,
9372
9682
  unreadableConfigs,