@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/cli.js CHANGED
@@ -4142,7 +4142,7 @@ function bindImages(ctx, targets) {
4142
4142
 
4143
4143
  // src/helper.ts
4144
4144
  import { createHash as createHash2 } from "crypto";
4145
- var HELPER_VERSION = 6;
4145
+ var HELPER_VERSION = 7;
4146
4146
  var HEADER = "// @bettercms-ai/convert helper v";
4147
4147
  var HELPER_PATH = Object.fromEntries(
4148
4148
  Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
@@ -4335,9 +4335,65 @@ export function bcmsSection(page: unknown, bind?: string, overrides?: unknown):
4335
4335
  * how the codemod ran: one component renders a Phase A placement on one page and a Phase B
4336
4336
  * instance on the next, and a repository does not have to be re-cut when its copy moves.
4337
4337
  */
4338
+ /** How a flat field family maps page paths to an instance's override keys. @see bcmsSectionPaths */
4339
+ export type BcmsPathMap = {
4340
+ scalars?: Record<string, string>;
4341
+ tables?: Record<string, { key: string; columns: Record<string, string> }>;
4342
+ };
4343
+
4344
+ /**
4345
+ * The snapshot for a section whose page fields are a FLAT family (\`wrap-h2-latest\`, not \`wrap.h2_latest\`).
4346
+ *
4347
+ * \`bcmsSection\` returns a group object read through a dotted path, which a flat family does not have: its
4348
+ * copy lives in top-level keys of the page snapshot. So this answers in the page's own spelling \u2014 the
4349
+ * markup reads \`bcms(snapshot, "wrap-h2-latest", \u2026)\` verbatim \u2014 with every mapped path set explicitly:
4350
+ *
4351
+ * - the instance's override for that key when it holds one (a table's rows re-spelled with the page's
4352
+ * column names, \`{ rows }\` unwrapped as in \`bcmsSection\`);
4353
+ * - else, for a placement bound to a group, the page value at the path re-based onto that group;
4354
+ * - else NOTHING \u2014 a placement that owns its copy never falls back to the page's stale copy, exactly as
4355
+ * \`bcmsSection\` synthesizes no group for it.
4356
+ *
4357
+ * Every other page key passes through, so reads outside the family (a shared link) keep working.
4358
+ */
4359
+ export function bcmsSectionPaths(
4360
+ page: unknown,
4361
+ bind: string | undefined,
4362
+ overrides: unknown,
4363
+ prefix: string,
4364
+ map: BcmsPathMap,
4365
+ ): Record<string, unknown> {
4366
+ const out: Record<string, unknown> = isRecord(page) ? { ...page } : {};
4367
+ const own = isRecord(overrides) ? overrides : {};
4368
+ const separator = prefix.slice(-1);
4369
+ const fromPage = (path: string): unknown =>
4370
+ bind ? readPath(page, bind + separator + path.slice(prefix.length)) : undefined;
4371
+ for (const [path, key] of Object.entries(map.scalars ?? {})) {
4372
+ const value = own[key];
4373
+ out[path] = value !== undefined && value !== null ? value : fromPage(path);
4374
+ }
4375
+ for (const [path, table] of Object.entries(map.tables ?? {})) {
4376
+ const held = own[table.key];
4377
+ const rows = isRecord(held) && Array.isArray(held.rows) ? held.rows : held;
4378
+ if (!Array.isArray(rows)) {
4379
+ out[path] = fromPage(path);
4380
+ continue;
4381
+ }
4382
+ const pageColumn = new Map(Object.entries(table.columns).map(([column, key]) => [key, column]));
4383
+ out[path] = rows.map((row) => {
4384
+ if (!isRecord(row)) return row;
4385
+ const spelled: Record<string, unknown> = {};
4386
+ for (const [key, value] of Object.entries(row)) spelled[pageColumn.get(key) ?? key] = value;
4387
+ return spelled;
4388
+ });
4389
+ }
4390
+ return out;
4391
+ }
4392
+
4338
4393
  export function bcmsFields(
4339
4394
  placement: { blockId?: string; bind?: string; overrides?: unknown },
4340
4395
  pathPrefix: string,
4396
+ map?: BcmsPathMap,
4341
4397
  ): (path: string) => string {
4342
4398
  const id = placement.blockId;
4343
4399
  const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
@@ -4350,6 +4406,24 @@ export function bcmsFields(
4350
4406
  * so prepending one would invent an address rather than correct it.
4351
4407
  */
4352
4408
  const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
4409
+ if (map) {
4410
+ /**
4411
+ * A FLAT family: the address is not "prefix swapped". The instance lane names the override key the
4412
+ * plan allocated (\`h2_latest\`, \`group_webgl[0].a_webgl\`); the bind lane keeps the page's own
4413
+ * separator (\`landing-h2-latest\`, never \`landing.h2-latest\`).
4414
+ */
4415
+ const separator = pathPrefix.slice(-1);
4416
+ return (path: string): string => {
4417
+ if (!path.startsWith(pathPrefix) || !(own || placement.bind)) return path;
4418
+ if (!own) return placement.bind + separator + path.slice(pathPrefix.length);
4419
+ const scalar = map.scalars?.[path];
4420
+ if (scalar !== undefined) return id + "__overrides." + scalar;
4421
+ const row = /^([^[]+)\\[(\\d+)\\]\\.(.+)$/.exec(path);
4422
+ const table = row ? map.tables?.[row[1]] : undefined;
4423
+ const column = row && table ? table.columns[row[3]] : undefined;
4424
+ return table && column !== undefined ? id + "__overrides." + table.key + "[" + row![2] + "]." + column : path;
4425
+ };
4426
+ }
4353
4427
  return (path: string): string => {
4354
4428
  // A path outside the group this section was cut from is not one this placement re-addresses:
4355
4429
  // it names a key the instance does not hold and a group this placement is not bound to.
@@ -4366,21 +4440,24 @@ var HISTORICAL = {
4366
4440
  2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
4367
4441
  3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
4368
4442
  4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4369
- 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
4443
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
4444
+ 6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
4370
4445
  },
4371
4446
  "src/lib/bcms-content.ts": {
4372
4447
  1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
4373
4448
  2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
4374
4449
  3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
4375
4450
  4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
4376
- 5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba"
4451
+ 5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba",
4452
+ 6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5"
4377
4453
  },
4378
4454
  "composables/bcms-content.ts": {
4379
4455
  1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
4380
4456
  2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
4381
4457
  3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
4382
4458
  4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4383
- 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
4459
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
4460
+ 6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
4384
4461
  }
4385
4462
  };
4386
4463
  var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
@@ -5235,6 +5312,138 @@ function assertReceipt(receipt) {
5235
5312
  return receipt;
5236
5313
  }
5237
5314
 
5315
+ // src/fix.ts
5316
+ var MAX_SNIPPET = 160;
5317
+ var MAX_WORDS = 12;
5318
+ var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5319
+ function findLiteral(content, literal) {
5320
+ const words = literal.split(/\s+/).filter(Boolean).slice(0, MAX_WORDS);
5321
+ if (words.length === 0) return null;
5322
+ const pattern = new RegExp(words.map(escapeRegExp2).join("(?:\\s|<[^>]*>)+"), "u");
5323
+ const match = pattern.exec(content);
5324
+ return match ? { index: match.index, length: match[0].length } : null;
5325
+ }
5326
+ function placeOf(content, index) {
5327
+ const before = content.slice(0, index);
5328
+ const line = before.split("\n").length;
5329
+ const start = before.lastIndexOf("\n") + 1;
5330
+ const end = content.indexOf("\n", index);
5331
+ return {
5332
+ line,
5333
+ col: index - start + 1,
5334
+ snippet: content.slice(start, end < 0 ? content.length : end).trim().slice(0, MAX_SNIPPET)
5335
+ };
5336
+ }
5337
+ function fileHead(content) {
5338
+ return { line: 1, snippet: (content ?? "").split("\n", 1)[0].trim().slice(0, MAX_SNIPPET) };
5339
+ }
5340
+ var isMarkdown = (file) => /\.mdx?$/i.test(file);
5341
+ var isDataModule = (file) => /\.(json|ya?ml|[mc]?[jt]s)$/i.test(file) && !/\.d\.ts$/i.test(file);
5342
+ function inDataRegion(content, index) {
5343
+ if (content.startsWith("---")) {
5344
+ const close = content.indexOf("\n---", 3);
5345
+ if (close >= 0 && index < close) return true;
5346
+ }
5347
+ for (const match of content.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)) {
5348
+ const start = match.index + match[0].indexOf(">") + 1;
5349
+ if (index >= start && index < start + match[1].length) return true;
5350
+ }
5351
+ return false;
5352
+ }
5353
+ var ELEMENT_ACTIONS = /* @__PURE__ */ new Set([
5354
+ "wrap-span",
5355
+ "declare-attr",
5356
+ "declare-richtext"
5357
+ ]);
5358
+ function actionFor(reason, input, file, inData) {
5359
+ const chosen = reasonAction(reason, input, file, inData);
5360
+ if (inData && ELEMENT_ACTIONS.has(chosen.action)) {
5361
+ return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
5362
+ }
5363
+ return chosen;
5364
+ }
5365
+ function reasonAction(reason, input, file, inData) {
5366
+ const richtext = input.kind === "richtext" || input.inline === true;
5367
+ switch (reason) {
5368
+ case "AMBIGUOUS_LITERAL":
5369
+ if (input.routes > 1) {
5370
+ return {
5371
+ action: "manual",
5372
+ 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.`
5373
+ };
5374
+ }
5375
+ return {
5376
+ action: "declare-attr",
5377
+ 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."
5378
+ };
5379
+ case "REPEATER_AMBIGUOUS":
5380
+ case "REPEATER_FIXED_LENGTH":
5381
+ return {
5382
+ action: "declare-attr",
5383
+ why: "Declare this row's leaf on the element itself, or rewrite the loop to read the CMS array."
5384
+ };
5385
+ case "IN_EXPRESSION":
5386
+ if (inData) {
5387
+ return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
5388
+ }
5389
+ 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." };
5390
+ case "DIALECT_UNSUPPORTED":
5391
+ if (isMarkdown(file)) {
5392
+ return {
5393
+ action: "bind-data",
5394
+ why: "This is a content-collection body: bind the element that renders it and make the entry the source."
5395
+ };
5396
+ }
5397
+ 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.` };
5398
+ case "NOT_IN_SOURCE":
5399
+ case "NO_ORIGINAL":
5400
+ return isDataModule(file) || inData ? { action: "bind-data", why: "The template computes this from data \u2014 bind the data field instead of the text." } : {
5401
+ action: "manual",
5402
+ why: "The built page renders this and no source file holds it verbatim; find what formats it."
5403
+ };
5404
+ case "IN_DATA_FILE":
5405
+ case "IMAGE_ASSET_UNRESOLVED":
5406
+ return { action: "bind-data", why: "Bind the data field this value comes from." };
5407
+ case "PROP_TARGET_NOT_FOUND":
5408
+ case "PROP_DRILLED_DEEP":
5409
+ return {
5410
+ action: "wrap-span",
5411
+ why: "The component renders this in a position no attribute reaches \u2014 wrap it in a span of its own."
5412
+ };
5413
+ case "SUBSTRING_ONLY":
5414
+ return { action: "wrap-span", why: "Only part of the element's text is this value \u2014 give it its own span." };
5415
+ case "SUBTREE_NOT_HTML":
5416
+ return { action: "declare-richtext", why: "The value is the element's whole subtree \u2014 bind it as richtext." };
5417
+ default:
5418
+ return { action: "manual", why: `${reason} \u2014 see the receipt's message for what blocked this path.` };
5419
+ }
5420
+ }
5421
+ function fixFor(row, input) {
5422
+ const file = row.file ?? input.candidates[0] ?? "";
5423
+ const content = input.contents.get(file);
5424
+ const at = content ? findLiteral(content, input.literal) : null;
5425
+ const place = at ? placeOf(content, at.index) : { ...fileHead(content), col: void 0 };
5426
+ const { action, why } = actionFor(row.reason, input, file, at !== null && inDataRegion(content, at.index));
5427
+ return {
5428
+ file,
5429
+ line: row.line ?? place.line,
5430
+ ...place.col !== void 0 ? { col: place.col } : {},
5431
+ snippet: place.snippet,
5432
+ action,
5433
+ ...why ? { why } : {}
5434
+ };
5435
+ }
5436
+ function routeCounts(pending) {
5437
+ const routes = /* @__PURE__ */ new Map();
5438
+ for (const row of pending) {
5439
+ const id = `${row.scope}|${row.path}`;
5440
+ const held = routes.get(id) ?? /* @__PURE__ */ new Set();
5441
+ held.add(row.route);
5442
+ routes.set(id, held);
5443
+ }
5444
+ return new Map([...routes].map(([id, set]) => [id, set.size]));
5445
+ }
5446
+
5238
5447
  // src/sites/html.ts
5239
5448
  import { parse as parseHtml } from "parse5";
5240
5449
  var RAW_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template"]);
@@ -6351,7 +6560,9 @@ var SKIP_DIRS = [
6351
6560
  ".vercel/",
6352
6561
  ".cache/",
6353
6562
  "coverage/",
6354
- "vendor/"
6563
+ "vendor/",
6564
+ "__tests__/",
6565
+ "__mocks__/"
6355
6566
  ];
6356
6567
  var SKIP_FILES = ["package-lock.json", "bun.lock", "bun.lockb", "yarn.lock", "pnpm-lock.yaml"];
6357
6568
  var SOURCE_EXTENSIONS = [
@@ -6389,11 +6600,13 @@ var TEMPLATE_DIRS = /* @__PURE__ */ new Set([
6389
6600
  "_includes",
6390
6601
  "_layouts"
6391
6602
  ]);
6603
+ var isAboutTheSite = (lower) => /\.(test|spec)\.[^./]+$/.test(lower) || /^[^/]*\.config\.[^./]+$/.test(lower);
6392
6604
  function isSourceCandidate(path) {
6393
6605
  const lower = path.toLowerCase();
6394
6606
  if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
6395
6607
  return false;
6396
6608
  if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
6609
+ if (isAboutTheSite(lower)) return false;
6397
6610
  if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
6398
6611
  if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
6399
6612
  if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
@@ -6869,6 +7082,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
6869
7082
  return held;
6870
7083
  };
6871
7084
  const fresh = /* @__PURE__ */ new Set();
7085
+ const freshFiles = /* @__PURE__ */ new Map();
6872
7086
  const bindOf = (section) => instanceOwned(section) ? void 0 : section.groupKey;
6873
7087
  const placementOf = (slug, section) => {
6874
7088
  const bind = bindOf(section);
@@ -7196,6 +7410,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
7196
7410
  }
7197
7411
  extractable.set(resolved.slug, { name: resolved.name, path, kind: "default", imported: null });
7198
7412
  fresh.add(resolved.slug);
7413
+ freshFiles.set(resolved.slug, path);
7199
7414
  addFile(outFiles, contents, {
7200
7415
  path,
7201
7416
  operation: existing === void 0 ? "add" : "modify",
@@ -7281,6 +7496,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
7281
7496
  sections,
7282
7497
  // What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
7283
7498
  components: [...fresh].sort(),
7499
+ componentFiles: [...freshFiles].sort(([a], [b]) => a.localeCompare(b)).map(([slug, path]) => ({ slug, path, export: "default" })),
7284
7500
  planPending,
7285
7501
  notes
7286
7502
  }
@@ -7460,7 +7676,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7460
7676
  for (const section of sections) {
7461
7677
  const paths = fieldPaths(section);
7462
7678
  const literals = new Set(
7463
- briefPaths.filter((p) => (p.scope ?? "page") === "page" && groupOf(p.path) === section.groupKey && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
7679
+ briefPaths.filter((p) => (p.scope ?? "page") === "page" && inSection(section, p.path) && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
7464
7680
  );
7465
7681
  const hits = /* @__PURE__ */ new Map();
7466
7682
  for (const source of sources) {
@@ -7471,7 +7687,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7471
7687
  const found = /* @__PURE__ */ new Map();
7472
7688
  for (const declaration2 of declarationsIn(parsed.roots, source.content)) {
7473
7689
  if (declaration2.scope !== "page") continue;
7474
- if (groupOf(declaration2.path) !== section.groupKey) continue;
7690
+ if (!inSection(section, declaration2.path)) continue;
7475
7691
  const placed2 = elements.get(declaration2.at);
7476
7692
  if (placed2) found.set(declaration2.at, placed2);
7477
7693
  }
@@ -7496,6 +7712,12 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7496
7712
  return { placed, misses };
7497
7713
  }
7498
7714
  var groupOf = (path) => path.split(/[.[]/)[0] ?? "";
7715
+ var inSection = (section, path) => {
7716
+ if (groupOf(path) === section.groupKey) return true;
7717
+ const rowless = (value) => value.replace(/\[\d+\]/g, "[]");
7718
+ const target = rowless(path);
7719
+ return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
7720
+ };
7499
7721
  var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
7500
7722
  function elementIndex(roots) {
7501
7723
  const out = /* @__PURE__ */ new Map();
@@ -7598,6 +7820,9 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7598
7820
  ...addresses.opaque
7599
7821
  ];
7600
7822
  if (unconvertible.length > 0) return { content: "", free: [], duplicates: [], unconvertible };
7823
+ const flat3 = pathPrefix !== "" && !pathPrefix.endsWith(".");
7824
+ const pathMap = flat3 ? flatPathMap(resolved.section, pathPrefix) : null;
7825
+ if (flat3 && !pathMap) return { content: "", free: [], duplicates: [], unconvertible: [`${pathPrefix}* (a repeater leaf without an override address)`] };
7601
7826
  const cut = content.slice(resolved.root.start, resolved.root.end);
7602
7827
  const reserved = /* @__PURE__ */ new Set([...topLevelBindings(dialect, parsed, content), ...referencedNames2(cut)]);
7603
7828
  const introduced = allocateNames(reserved, [
@@ -7616,7 +7841,7 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7616
7841
  const helpers = [
7617
7842
  .../* @__PURE__ */ new Set([
7618
7843
  ...helperNames.map((h) => alias(h.name, h.local)),
7619
- "bcmsSection",
7844
+ flat3 ? "bcmsSectionPaths" : "bcmsSection",
7620
7845
  ...bound > 0 ? [alias("bcmsFields", fieldsName)] : []
7621
7846
  ])
7622
7847
  ].sort();
@@ -7646,9 +7871,10 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7646
7871
  const key = /^[A-Za-z_$][\w$]*$/.test(groupKey) ? groupKey : JSON.stringify(groupKey);
7647
7872
  const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
7648
7873
  ` : "";
7649
- const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };` : "";
7650
- const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)});` : "";
7874
+ const copy = !snapshot ? "" : flat3 ? `const ${snapshot} = bcmsSectionPaths(page, bind, overrides, ${JSON.stringify(pathPrefix)}, ${JSON.stringify(pathMap)});` : `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };`;
7875
+ const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
7651
7876
  const declared = [copy, field].filter(Boolean).join("\n");
7877
+ const styles = dialect === "astro" ? carriedStyles(content) : "";
7652
7878
  if (dialect === "astro") {
7653
7879
  return { free: [], duplicates: [], unconvertible: [], content: `---
7654
7880
  ${SECTION_MARKER}
@@ -7668,7 +7894,9 @@ const { blockId, bind, overrides, page } = Astro.props;
7668
7894
  ${carriedDeclarations}${declared}
7669
7895
  ---
7670
7896
  ${markup}
7671
- ` };
7897
+ ${styles ? `
7898
+ ${styles}
7899
+ ` : ""}` };
7672
7900
  }
7673
7901
  return { free: [], duplicates: [], unconvertible: [], content: `${SECTION_MARKER}
7674
7902
  ${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
@@ -7942,6 +8170,9 @@ import { bcmsFields } from "${helper}";`);
7942
8170
  return { files, refused };
7943
8171
  }
7944
8172
  function propPrefix(section) {
8173
+ return exactPrefix(section) ?? flatPrefix(section);
8174
+ }
8175
+ function exactPrefix(section) {
7945
8176
  const prefixes = /* @__PURE__ */ new Set();
7946
8177
  for (const field of section.fields ?? []) {
7947
8178
  const { path, key } = field;
@@ -7952,6 +8183,49 @@ function propPrefix(section) {
7952
8183
  if (prefixes.size > 1) return null;
7953
8184
  return prefixes.size === 1 ? [...prefixes][0] : `${section.groupKey}.`;
7954
8185
  }
8186
+ var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "value";
8187
+ function flatPrefix(section) {
8188
+ const prefixes = /* @__PURE__ */ new Set();
8189
+ let informative = false;
8190
+ for (const field of section.fields ?? []) {
8191
+ const { path, key } = field;
8192
+ if (typeof path !== "string" || typeof key !== "string") continue;
8193
+ informative = true;
8194
+ const prefix = ["-", "_"].map((sep2) => `${section.groupKey}${sep2}`).find((candidate) => path.startsWith(candidate) && (typeof field.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
8195
+ if (!prefix) return null;
8196
+ prefixes.add(prefix);
8197
+ }
8198
+ return informative && prefixes.size === 1 ? [...prefixes][0] : null;
8199
+ }
8200
+ function flatPathMap(section, prefix) {
8201
+ const scalars = {};
8202
+ const tables = {};
8203
+ for (const field of section.fields ?? []) {
8204
+ const { path, key } = field;
8205
+ if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
8206
+ const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
8207
+ if (!row) {
8208
+ scalars[path] = field.overrideKey ?? key;
8209
+ continue;
8210
+ }
8211
+ const address = typeof field.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field.overrideKey) : null;
8212
+ if (!address) return null;
8213
+ const table = tables[row[1]] ??= { key: address[1], columns: {} };
8214
+ if (table.key !== address[1]) return null;
8215
+ table.columns[row[2]] = address[2];
8216
+ }
8217
+ return { scalars, tables };
8218
+ }
8219
+ function carriedStyles(content) {
8220
+ const blocks = [];
8221
+ for (const match of content.matchAll(/<style\b([^>]*)>([\s\S]*?)<\/style>/g)) {
8222
+ const attributes = match[1] ?? "";
8223
+ if (/\bis:global\b|\bdefine:vars\b/.test(attributes)) continue;
8224
+ if (/\blang\s*=/.test(attributes) || /@(import|use|forward)\b/.test(match[2] ?? "")) continue;
8225
+ blocks.push(match[0]);
8226
+ }
8227
+ return blocks.join("\n");
8228
+ }
7955
8229
  function referencedNames2(markup) {
7956
8230
  const out = /* @__PURE__ */ new Set();
7957
8231
  for (const [name] of markup.matchAll(/[A-Za-z_$][\w$]*/g)) out.add(name);
@@ -8273,7 +8547,7 @@ var ConvertError = class extends Error {
8273
8547
  code;
8274
8548
  };
8275
8549
  var keyOf = (route, scope, path) => scope === "layout" ? `layout::${path}` : `${route}::page::${path}`;
8276
- var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8550
+ var escapeRegExp3 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8277
8551
  var identifierFor2 = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
8278
8552
  var UNPLACED = /* @__PURE__ */ new Set(["NOT_IN_SOURCE", "IN_EXPRESSION"]);
8279
8553
  async function drillProp(ctx, fromFile, component, prop, kind, hop) {
@@ -8620,6 +8894,33 @@ async function convertSources(briefIn, sources, options) {
8620
8894
  else outFiles.push({ path, operation: "modify", content });
8621
8895
  }
8622
8896
  pending.push(...canvas.pending);
8897
+ for (const file of outFiles) if (file.operation === "modify") latest.set(file.path, file.content);
8898
+ const pageFilesOrTemplate = (route, files) => {
8899
+ const all = [...files];
8900
+ const exact = pageFilesFor(route, all);
8901
+ if (exact.length > 0) return exact;
8902
+ const wanted = route.split("/").filter(Boolean);
8903
+ return all.filter((file) => {
8904
+ if (!isDynamicRoute(file)) return false;
8905
+ const pattern = routeOfFile(file)?.split("/").filter(Boolean);
8906
+ if (!pattern) return false;
8907
+ const rest = pattern.some((segment) => segment.startsWith("[..."));
8908
+ if (rest ? wanted.length < pattern.length : wanted.length !== pattern.length) return false;
8909
+ return pattern.every((segment, i) => segment.startsWith("[") || segment === wanted[i]);
8910
+ }).sort();
8911
+ };
8912
+ const routes = routeCounts(pending);
8913
+ for (const row of pending) {
8914
+ const target = targets.get(keyOf(row.route, row.scope, row.path));
8915
+ row.fix = fixFor(row, {
8916
+ literal: target?.literal ?? "",
8917
+ kind: row.kind,
8918
+ ...target?.inline ? { inline: true } : {},
8919
+ candidates: locatedByKey.get(keyOf(row.route, row.scope, row.path))?.files ?? pageFilesOrTemplate(row.route, latest.keys()),
8920
+ contents: latest,
8921
+ routes: routes.get(`${row.scope}|${row.path}`) ?? 1
8922
+ });
8923
+ }
8623
8924
  const receipt = assertReceipt({
8624
8925
  briefDigest: digest,
8625
8926
  paths: {
@@ -8832,13 +9133,13 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
8832
9133
  for (const target of sharing) outcomes.set(target.key, missing(literal));
8833
9134
  continue;
8834
9135
  }
8835
- const assigned = sharing.length === 1 ? /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]) : resolvePositionally(sharing, forLiteral);
8836
- if (!assigned) {
8837
- for (const target of sharing) outcomes.set(target.key, "AMBIGUOUS_LITERAL");
8838
- continue;
8839
- }
9136
+ const resolved = sharing.length === 1 ? { assigned: /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]), ambiguous: /* @__PURE__ */ new Set() } : resolvePositionally(sharing, forLiteral);
8840
9137
  for (const target of sharing) {
8841
- const mine = assigned.get(target.key) ?? [];
9138
+ if (resolved.ambiguous.has(target.key)) {
9139
+ outcomes.set(target.key, "AMBIGUOUS_LITERAL");
9140
+ continue;
9141
+ }
9142
+ const mine = resolved.assigned.get(target.key) ?? [];
8842
9143
  const rewritable = mine.filter((s) => s.where !== "script" && s.where !== "comment" && !s.partial);
8843
9144
  if (rewritable.length === 0) {
8844
9145
  outcomes.set(
@@ -9183,19 +9484,25 @@ function repeatGroups(targets) {
9183
9484
  }
9184
9485
  function resolvePositionally(targets, sites) {
9185
9486
  const assigned = /* @__PURE__ */ new Map();
9186
- const claimed = /* @__PURE__ */ new Set();
9487
+ const ambiguous = /* @__PURE__ */ new Set();
9488
+ const picked = /* @__PURE__ */ new Map();
9489
+ const wanters = /* @__PURE__ */ new Map();
9187
9490
  for (const target of targets) {
9188
9491
  const locator = target.locator;
9189
- if (!locator) return null;
9190
- const candidates = sites.filter(
9191
- (s) => s.siblingIndex === locator.siblingIndex && isSubsequence(locator.ancestors, s.ancestors)
9192
- );
9193
- const site = locator.repeat ? candidates[locator.repeat.index] : candidates.length === 1 ? candidates[0] : void 0;
9194
- if (!site || claimed.has(site)) return null;
9195
- claimed.add(site);
9196
- assigned.set(target.key, [site]);
9492
+ const candidates = locator ? sites.filter((s) => s.siblingIndex === locator.siblingIndex && isSubsequence(locator.ancestors, s.ancestors)) : [];
9493
+ const site = locator?.repeat ? candidates[locator.repeat.index] : candidates.length === 1 ? candidates[0] : void 0;
9494
+ if (!site) {
9495
+ ambiguous.add(target.key);
9496
+ continue;
9497
+ }
9498
+ picked.set(target.key, site);
9499
+ wanters.set(site, (wanters.get(site) ?? 0) + 1);
9500
+ }
9501
+ for (const [key, site] of picked) {
9502
+ if ((wanters.get(site) ?? 0) > 1) ambiguous.add(key);
9503
+ else assigned.set(key, [site]);
9197
9504
  }
9198
- return assigned;
9505
+ return { assigned, ambiguous };
9199
9506
  }
9200
9507
  function isSubsequence(wanted, chain) {
9201
9508
  let at = 0;
@@ -9218,7 +9525,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
9218
9525
  if (elements.length !== targets.length) return { ok: false, reason: "unverifiable" };
9219
9526
  const locals = helperLocals(reparsed, dialect, file);
9220
9527
  const reads = DIALECT_RULES[dialect].text !== null;
9221
- const boundary = (name) => `(?<![\\w$])${escapeRegExp2(name)}(?![\\w$])`;
9528
+ const boundary = (name) => `(?<![\\w$])${escapeRegExp3(name)}(?![\\w$])`;
9222
9529
  const claimed = /* @__PURE__ */ new Set();
9223
9530
  for (const element of elements) {
9224
9531
  const candidates = targets.filter(
@@ -9226,7 +9533,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
9226
9533
  );
9227
9534
  const match = candidates.find((target) => {
9228
9535
  if (!reads) return true;
9229
- const quoted = escapeRegExp2(JSON.stringify(target.path));
9536
+ const quoted = escapeRegExp3(JSON.stringify(target.path));
9230
9537
  if (target.scope === "layout") {
9231
9538
  return locals.bcmsLayout.some(
9232
9539
  (name) => new RegExp(`${boundary(name)}\\s*\\(\\s*${quoted}`).test(element.own)
@@ -9288,7 +9595,7 @@ function emitStubs2(dialects, slugs, contents, out) {
9288
9595
  // package.json
9289
9596
  var package_default = {
9290
9597
  name: "@bettercms-ai/convert",
9291
- version: "0.10.0",
9598
+ version: "0.12.0",
9292
9599
  type: "module",
9293
9600
  description: "Deterministic codemod that turns an imported site's text-matched bindings into declared ones \u2014 parser offsets in, one magic-string pass out, plus a receipt of everything it could not do.",
9294
9601
  bin: {