@bettercms-ai/convert 0.9.0 → 0.11.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
@@ -2560,6 +2560,7 @@ var IMAGE_SOURCES = {
2560
2560
  video: ["poster"]
2561
2561
  };
2562
2562
  var isImageSource = (site) => site.where === "attr" && (IMAGE_SOURCES[site.tag.toLowerCase()] ?? []).includes(site.attr ?? "");
2563
+ var ownsSubtree = (kind, site, inline) => kind === "richtext" && (inline === true ? !site.nonHtml : !(site.mixed && site.textRange !== null));
2563
2564
  var norm = (value) => decodeEntities(value.normalize("NFC")).replace(/\s+/g, " ").trim();
2564
2565
  var BCMS_ATTRS = [
2565
2566
  "data-bcms-field",
@@ -2592,6 +2593,7 @@ function openTagEnd(content, start) {
2592
2593
  }
2593
2594
  return content.length;
2594
2595
  }
2596
+ var OPAQUE_TAGS = /* @__PURE__ */ new Set(["pre"]);
2595
2597
  var isBlank = (value) => value.trim() === "";
2596
2598
  function renderedText(node) {
2597
2599
  if (node.type === "text") return node.value;
@@ -2599,6 +2601,11 @@ function renderedText(node) {
2599
2601
  return "";
2600
2602
  }
2601
2603
  var attrsOf = (node) => Object.fromEntries(node.attrs.map((a) => [a.name, a.value]));
2604
+ function nonHtmlSubtree(node) {
2605
+ return node.children.some(
2606
+ (c) => c.type === "expr" || c.type === "element" && (!isIntrinsic(c.tag) || c.tag === "slot" || nonHtmlSubtree(c))
2607
+ );
2608
+ }
2602
2609
  function soleTextChild(node) {
2603
2610
  const texts = node.children.filter((c) => c.type === "text" && !isBlank(c.value));
2604
2611
  return texts.length === 1 ? texts[0] : null;
@@ -2635,10 +2642,12 @@ function collectSites(roots, literals) {
2635
2642
  if (node.type !== "element") return matched;
2636
2643
  const attrs = attrsOf(node);
2637
2644
  let index = 0;
2638
- for (const child of node.children) {
2639
- const childWalk = { ancestors: [...walk.ancestors, node.tag], siblingIndex: index };
2640
- if (child.type === "element") index++;
2641
- for (const hit of visit(child, childWalk)) matched.add(hit);
2645
+ if (!OPAQUE_TAGS.has(node.tag)) {
2646
+ for (const child of node.children) {
2647
+ const childWalk = { ancestors: [...walk.ancestors, node.tag], siblingIndex: index };
2648
+ if (child.type === "element") index++;
2649
+ for (const hit of visit(child, childWalk)) matched.add(hit);
2650
+ }
2642
2651
  }
2643
2652
  if (node.raw) {
2644
2653
  const text2 = norm(renderedTextRaw(node));
@@ -2664,6 +2673,7 @@ function collectSites(roots, literals) {
2664
2673
  textRange: soleTextChild(node)?.range ?? null,
2665
2674
  literal,
2666
2675
  partial: false,
2676
+ ...nonHtmlSubtree(node) ? { nonHtml: true } : {},
2667
2677
  ancestors: walk.ancestors,
2668
2678
  siblingIndex: walk.siblingIndex
2669
2679
  });
@@ -2689,6 +2699,7 @@ function collectSites(roots, literals) {
2689
2699
  textRange: own.range,
2690
2700
  literal,
2691
2701
  partial: false,
2702
+ ...nonHtmlSubtree(node) ? { nonHtml: true } : {},
2692
2703
  ancestors: walk.ancestors,
2693
2704
  siblingIndex: walk.siblingIndex
2694
2705
  });
@@ -2739,10 +2750,12 @@ function partials(node, literals, walk, out) {
2739
2750
  if (node.type !== "element" || node.raw) return false;
2740
2751
  let deeper = false;
2741
2752
  let index = 0;
2742
- for (const child of node.children) {
2743
- const childWalk = { ancestors: [...walk.ancestors, node.tag], siblingIndex: index };
2744
- if (child.type === "element") index++;
2745
- if (partials(child, literals, childWalk, out)) deeper = true;
2753
+ if (!OPAQUE_TAGS.has(node.tag)) {
2754
+ for (const child of node.children) {
2755
+ const childWalk = { ancestors: [...walk.ancestors, node.tag], siblingIndex: index };
2756
+ if (child.type === "element") index++;
2757
+ if (partials(child, literals, childWalk, out)) deeper = true;
2758
+ }
2746
2759
  }
2747
2760
  if (deeper) return true;
2748
2761
  const text = norm(renderedText(node));
@@ -4127,7 +4140,7 @@ function bindImages(ctx, targets) {
4127
4140
 
4128
4141
  // src/helper.ts
4129
4142
  import { createHash as createHash2 } from "crypto";
4130
- var HELPER_VERSION = 6;
4143
+ var HELPER_VERSION = 7;
4131
4144
  var HEADER = "// @bettercms-ai/convert helper v";
4132
4145
  var HELPER_PATH = Object.fromEntries(
4133
4146
  Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
@@ -4320,9 +4333,65 @@ export function bcmsSection(page: unknown, bind?: string, overrides?: unknown):
4320
4333
  * how the codemod ran: one component renders a Phase A placement on one page and a Phase B
4321
4334
  * instance on the next, and a repository does not have to be re-cut when its copy moves.
4322
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
+
4323
4391
  export function bcmsFields(
4324
4392
  placement: { blockId?: string; bind?: string; overrides?: unknown },
4325
4393
  pathPrefix: string,
4394
+ map?: BcmsPathMap,
4326
4395
  ): (path: string) => string {
4327
4396
  const id = placement.blockId;
4328
4397
  const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
@@ -4335,6 +4404,24 @@ export function bcmsFields(
4335
4404
  * so prepending one would invent an address rather than correct it.
4336
4405
  */
4337
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
+ }
4338
4425
  return (path: string): string => {
4339
4426
  // A path outside the group this section was cut from is not one this placement re-addresses:
4340
4427
  // it names a key the instance does not hold and a group this placement is not bound to.
@@ -4351,21 +4438,24 @@ var HISTORICAL = {
4351
4438
  2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
4352
4439
  3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
4353
4440
  4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4354
- 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
4441
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
4442
+ 6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
4355
4443
  },
4356
4444
  "src/lib/bcms-content.ts": {
4357
4445
  1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
4358
4446
  2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
4359
4447
  3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
4360
4448
  4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
4361
- 5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba"
4449
+ 5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba",
4450
+ 6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5"
4362
4451
  },
4363
4452
  "composables/bcms-content.ts": {
4364
4453
  1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
4365
4454
  2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
4366
4455
  3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
4367
4456
  4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
4368
- 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
4457
+ 5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
4458
+ 6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
4369
4459
  }
4370
4460
  };
4371
4461
  var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
@@ -4382,14 +4472,23 @@ function isKnownHelper(content, dialect) {
4382
4472
 
4383
4473
  // src/locate.ts
4384
4474
  var flat = (value) => value.replace(/\s+/g, " ").trim();
4385
- var stripTags = (value) => value.replace(/<br\s*\/?>/gi, " ").replace(/<[^>]*>/g, "");
4475
+ var stripTags = (value, br = " ") => value.replace(/<br\s*\/?>/gi, br).replace(/<[^>]*>/g, "");
4386
4476
  function locate(brief, sources, options) {
4387
4477
  const flattened = sources.map((s) => {
4388
4478
  const readable = unwrapJsxStrings(decodeEntities(s.content));
4389
4479
  return {
4390
4480
  path: s.path,
4391
4481
  content: flat(readable),
4392
- text: options?.stripTags ? flat(stripTags(readable)) : null
4482
+ text: options?.stripTags ? flat(stripTags(readable)) : null,
4483
+ /**
4484
+ * 🔴 A `<br>` IS A SPACE TO A READER AND NOTHING TO `textContent`, and the derive lane reads
4485
+ * the DOM. `<h1>Launch your site<br /><span>fast.</span></h1>` is one INLINE richtext field
4486
+ * whose value is `Launch your sitefast.` — no space, because no text node holds one — while
4487
+ * this prefilter's only reading inserted one, so the file was never opened and every split
4488
+ * heading on every imported site came back NOT_IN_SOURCE. Both readings are searched: this
4489
+ * decides which FILES to open, and `collectSites` still has to match an element exactly.
4490
+ */
4491
+ tight: options?.stripTags ? flat(stripTags(readable, "")) : null
4393
4492
  };
4394
4493
  });
4395
4494
  const located = [];
@@ -4401,7 +4500,9 @@ function locate(brief, sources, options) {
4401
4500
  unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "no-original" });
4402
4501
  continue;
4403
4502
  }
4404
- const files = flattened.filter((f) => f.content.includes(original) || f.text !== null && f.text.includes(original)).map((f) => f.path);
4503
+ const files = flattened.filter(
4504
+ (f) => f.content.includes(original) || f.text !== null && f.text.includes(original) || f.tight !== null && f.tight.includes(original)
4505
+ ).map((f) => f.path);
4405
4506
  if (files.length === 0) {
4406
4507
  unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "not-in-source" });
4407
4508
  continue;
@@ -5100,7 +5201,7 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
5100
5201
  addDeclaration(site.attrInsertAt, declaration(rewrite));
5101
5202
  continue;
5102
5203
  }
5103
- if (kind === "richtext" && !(site.mixed && site.textRange)) {
5204
+ if (ownsSubtree(kind, site, rewrite.inline)) {
5104
5205
  if (!isIntrinsic(site.tag)) continue;
5105
5206
  if (rule.richtext && site.inner) {
5106
5207
  source.remove(site.inner.start, site.inner.end);
@@ -6386,6 +6487,66 @@ function isSourceCandidate(path) {
6386
6487
  return lower.split("/").slice(0, -1).some((segment) => TEMPLATE_DIRS.has(segment));
6387
6488
  }
6388
6489
 
6490
+ // src/xray.ts
6491
+ var SKIP_TAGS = /* @__PURE__ */ new Set(["svg", "iframe", "head", "title"]);
6492
+ var CONTEXT_TAGS = /* @__PURE__ */ new Set([
6493
+ "nav",
6494
+ "header",
6495
+ "footer",
6496
+ "main",
6497
+ "aside",
6498
+ "form",
6499
+ "button",
6500
+ "a",
6501
+ "ul",
6502
+ "ol",
6503
+ "table",
6504
+ "dialog",
6505
+ "details",
6506
+ "figure",
6507
+ "blockquote"
6508
+ ]);
6509
+ var BOUND_ATTRS = ["data-bcms-field", "data-bcms-layout-field", "data-bcms-path", "data-bcms-block"];
6510
+ var isDecorativeGlyph = (text) => text.length <= 2 || /^\d{1,2}\s*[/.)\-–]?$/.test(text) || !new RegExp("\\p{L}", "u").test(text) || /^made with\b/i.test(text) || /^skip to\b/i.test(text);
6511
+ var MAX_BUCKETS = 12;
6512
+ var flat2 = (value) => value.replace(/\s+/g, " ").trim();
6513
+ function measureUnaddressable(html) {
6514
+ const parsed = parse2(html);
6515
+ let visible = 0;
6516
+ let unaddressable = 0;
6517
+ const buckets = /* @__PURE__ */ new Map();
6518
+ const visit = (node, tag, context, bound) => {
6519
+ if (node.type === "text") {
6520
+ const text = flat2(node.value);
6521
+ if (!text || isDecorativeGlyph(text)) return;
6522
+ visible += text.length;
6523
+ if (bound) return;
6524
+ unaddressable += text.length;
6525
+ const key = `${tag} @ ${context}`;
6526
+ const held = buckets.get(key) ?? { tag, context, chars: 0, nodes: 0, samples: [] };
6527
+ held.chars += text.length;
6528
+ held.nodes += 1;
6529
+ if (held.samples.length < 2) held.samples.push(text.slice(0, 50));
6530
+ buckets.set(key, held);
6531
+ return;
6532
+ }
6533
+ if (node.type !== "element" || node.raw || SKIP_TAGS.has(node.tag)) return;
6534
+ const attrs = new Map(node.attrs.map((a) => [a.name, a.value]));
6535
+ if (attrs.has("hidden") || attrs.get("aria-hidden") === "true" || /display\s*:\s*none/.test(attrs.get("style") ?? "")) {
6536
+ return;
6537
+ }
6538
+ const owned = bound || BOUND_ATTRS.some((name) => attrs.has(name));
6539
+ const chain = CONTEXT_TAGS.has(node.tag) ? context === "body" ? node.tag : `${context}>${node.tag}` : context;
6540
+ for (const child of node.children) visit(child, node.tag, chain, owned);
6541
+ };
6542
+ for (const root of parsed.roots) visit(root, "body", "body", false);
6543
+ return {
6544
+ visible,
6545
+ unaddressable,
6546
+ buckets: [...buckets.values()].sort((a, b) => b.chars - a.chars).slice(0, MAX_BUCKETS)
6547
+ };
6548
+ }
6549
+
6389
6550
  // src/declared.ts
6390
6551
  async function readDeclarations(file, content) {
6391
6552
  const dialect = dialectOf(file, content);
@@ -6803,6 +6964,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
6803
6964
  return held;
6804
6965
  };
6805
6966
  const fresh = /* @__PURE__ */ new Set();
6967
+ const freshFiles = /* @__PURE__ */ new Map();
6806
6968
  const bindOf = (section) => instanceOwned(section) ? void 0 : section.groupKey;
6807
6969
  const placementOf = (slug, section) => {
6808
6970
  const bind = bindOf(section);
@@ -7130,6 +7292,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
7130
7292
  }
7131
7293
  extractable.set(resolved.slug, { name: resolved.name, path, kind: "default", imported: null });
7132
7294
  fresh.add(resolved.slug);
7295
+ freshFiles.set(resolved.slug, path);
7133
7296
  addFile(outFiles, contents, {
7134
7297
  path,
7135
7298
  operation: existing === void 0 ? "add" : "modify",
@@ -7215,6 +7378,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
7215
7378
  sections,
7216
7379
  // What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
7217
7380
  components: [...fresh].sort(),
7381
+ componentFiles: [...freshFiles].sort(([a], [b]) => a.localeCompare(b)).map(([slug, path]) => ({ slug, path, export: "default" })),
7218
7382
  planPending,
7219
7383
  notes
7220
7384
  }
@@ -7394,7 +7558,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7394
7558
  for (const section of sections) {
7395
7559
  const paths = fieldPaths(section);
7396
7560
  const literals = new Set(
7397
- briefPaths.filter((p) => (p.scope ?? "page") === "page" && groupOf(p.path) === section.groupKey && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
7561
+ briefPaths.filter((p) => (p.scope ?? "page") === "page" && inSection(section, p.path) && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
7398
7562
  );
7399
7563
  const hits = /* @__PURE__ */ new Map();
7400
7564
  for (const source of sources) {
@@ -7405,7 +7569,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7405
7569
  const found = /* @__PURE__ */ new Map();
7406
7570
  for (const declaration2 of declarationsIn(parsed.roots, source.content)) {
7407
7571
  if (declaration2.scope !== "page") continue;
7408
- if (groupOf(declaration2.path) !== section.groupKey) continue;
7572
+ if (!inSection(section, declaration2.path)) continue;
7409
7573
  const placed2 = elements.get(declaration2.at);
7410
7574
  if (placed2) found.set(declaration2.at, placed2);
7411
7575
  }
@@ -7430,6 +7594,12 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
7430
7594
  return { placed, misses };
7431
7595
  }
7432
7596
  var groupOf = (path) => path.split(/[.[]/)[0] ?? "";
7597
+ var inSection = (section, path) => {
7598
+ if (groupOf(path) === section.groupKey) return true;
7599
+ const rowless = (value) => value.replace(/\[\d+\]/g, "[]");
7600
+ const target = rowless(path);
7601
+ return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
7602
+ };
7433
7603
  var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
7434
7604
  function elementIndex(roots) {
7435
7605
  const out = /* @__PURE__ */ new Map();
@@ -7532,6 +7702,9 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7532
7702
  ...addresses.opaque
7533
7703
  ];
7534
7704
  if (unconvertible.length > 0) return { content: "", free: [], duplicates: [], unconvertible };
7705
+ const flat3 = pathPrefix !== "" && !pathPrefix.endsWith(".");
7706
+ const pathMap = flat3 ? flatPathMap(resolved.section, pathPrefix) : null;
7707
+ if (flat3 && !pathMap) return { content: "", free: [], duplicates: [], unconvertible: [`${pathPrefix}* (a repeater leaf without an override address)`] };
7535
7708
  const cut = content.slice(resolved.root.start, resolved.root.end);
7536
7709
  const reserved = /* @__PURE__ */ new Set([...topLevelBindings(dialect, parsed, content), ...referencedNames2(cut)]);
7537
7710
  const introduced = allocateNames(reserved, [
@@ -7550,7 +7723,7 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7550
7723
  const helpers = [
7551
7724
  .../* @__PURE__ */ new Set([
7552
7725
  ...helperNames.map((h) => alias(h.name, h.local)),
7553
- "bcmsSection",
7726
+ flat3 ? "bcmsSectionPaths" : "bcmsSection",
7554
7727
  ...bound > 0 ? [alias("bcmsFields", fieldsName)] : []
7555
7728
  ])
7556
7729
  ].sort();
@@ -7580,9 +7753,10 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
7580
7753
  const key = /^[A-Za-z_$][\w$]*$/.test(groupKey) ? groupKey : JSON.stringify(groupKey);
7581
7754
  const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
7582
7755
  ` : "";
7583
- const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };` : "";
7584
- const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)});` : "";
7756
+ const copy = !snapshot ? "" : flat3 ? `const ${snapshot} = bcmsSectionPaths(page, bind, overrides, ${JSON.stringify(pathPrefix)}, ${JSON.stringify(pathMap)});` : `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };`;
7757
+ const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
7585
7758
  const declared = [copy, field].filter(Boolean).join("\n");
7759
+ const styles = dialect === "astro" ? carriedStyles(content) : "";
7586
7760
  if (dialect === "astro") {
7587
7761
  return { free: [], duplicates: [], unconvertible: [], content: `---
7588
7762
  ${SECTION_MARKER}
@@ -7602,7 +7776,9 @@ const { blockId, bind, overrides, page } = Astro.props;
7602
7776
  ${carriedDeclarations}${declared}
7603
7777
  ---
7604
7778
  ${markup}
7605
- ` };
7779
+ ${styles ? `
7780
+ ${styles}
7781
+ ` : ""}` };
7606
7782
  }
7607
7783
  return { free: [], duplicates: [], unconvertible: [], content: `${SECTION_MARKER}
7608
7784
  ${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
@@ -7876,6 +8052,9 @@ import { bcmsFields } from "${helper}";`);
7876
8052
  return { files, refused };
7877
8053
  }
7878
8054
  function propPrefix(section) {
8055
+ return exactPrefix(section) ?? flatPrefix(section);
8056
+ }
8057
+ function exactPrefix(section) {
7879
8058
  const prefixes = /* @__PURE__ */ new Set();
7880
8059
  for (const field of section.fields ?? []) {
7881
8060
  const { path, key } = field;
@@ -7886,6 +8065,49 @@ function propPrefix(section) {
7886
8065
  if (prefixes.size > 1) return null;
7887
8066
  return prefixes.size === 1 ? [...prefixes][0] : `${section.groupKey}.`;
7888
8067
  }
8068
+ var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "value";
8069
+ function flatPrefix(section) {
8070
+ const prefixes = /* @__PURE__ */ new Set();
8071
+ let informative = false;
8072
+ for (const field of section.fields ?? []) {
8073
+ const { path, key } = field;
8074
+ if (typeof path !== "string" || typeof key !== "string") continue;
8075
+ informative = true;
8076
+ const prefix = ["-", "_"].map((sep) => `${section.groupKey}${sep}`).find((candidate) => path.startsWith(candidate) && (typeof field.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
8077
+ if (!prefix) return null;
8078
+ prefixes.add(prefix);
8079
+ }
8080
+ return informative && prefixes.size === 1 ? [...prefixes][0] : null;
8081
+ }
8082
+ function flatPathMap(section, prefix) {
8083
+ const scalars = {};
8084
+ const tables = {};
8085
+ for (const field of section.fields ?? []) {
8086
+ const { path, key } = field;
8087
+ if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
8088
+ const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
8089
+ if (!row) {
8090
+ scalars[path] = field.overrideKey ?? key;
8091
+ continue;
8092
+ }
8093
+ const address = typeof field.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field.overrideKey) : null;
8094
+ if (!address) return null;
8095
+ const table = tables[row[1]] ??= { key: address[1], columns: {} };
8096
+ if (table.key !== address[1]) return null;
8097
+ table.columns[row[2]] = address[2];
8098
+ }
8099
+ return { scalars, tables };
8100
+ }
8101
+ function carriedStyles(content) {
8102
+ const blocks = [];
8103
+ for (const match of content.matchAll(/<style\b([^>]*)>([\s\S]*?)<\/style>/g)) {
8104
+ const attributes = match[1] ?? "";
8105
+ if (/\bis:global\b|\bdefine:vars\b/.test(attributes)) continue;
8106
+ if (/\blang\s*=/.test(attributes) || /@(import|use|forward)\b/.test(match[2] ?? "")) continue;
8107
+ blocks.push(match[0]);
8108
+ }
8109
+ return blocks.join("\n");
8110
+ }
7889
8111
  function referencedNames2(markup) {
7890
8112
  const out = /* @__PURE__ */ new Set();
7891
8113
  for (const [name] of markup.matchAll(/[A-Za-z_$][\w$]*/g)) out.add(name);
@@ -8274,7 +8496,8 @@ async function convertSources(briefIn, sources, options) {
8274
8496
  kind: p.kind,
8275
8497
  fallback: p.original ?? "",
8276
8498
  literal: norm(p.original ?? ""),
8277
- locator: p.locator
8499
+ locator: p.locator,
8500
+ ...p.inline ? { inline: true } : {}
8278
8501
  });
8279
8502
  }
8280
8503
  }
@@ -8499,6 +8722,16 @@ async function convertSources(briefIn, sources, options) {
8499
8722
  declaredRow(target, witness !== null, witness ?? byHand);
8500
8723
  } else rewritten++;
8501
8724
  }
8725
+ const templated = dynamicBindings.filter((d) => d.prop === null);
8726
+ for (let i = pending.length - 1; i >= 0; i--) {
8727
+ const row = pending[i];
8728
+ const target = targets.get(keyOf(row.route, row.scope, row.path));
8729
+ if (row.reason !== "NO_ORIGINAL" || !target || target.fallback !== "") continue;
8730
+ const shape = shapeOfPath(target.path);
8731
+ if (!templated.some((d) => d.path === shape && attributable(target, d.file))) continue;
8732
+ pending.splice(i, 1);
8733
+ rewritten++;
8734
+ }
8502
8735
  for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
8503
8736
  const source = new MagicString6(contents.get(file));
8504
8737
  const seen = /* @__PURE__ */ new Set();
@@ -8811,6 +9044,10 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
8811
9044
  }
8812
9045
  let refusal = null;
8813
9046
  for (const site of fresh) {
9047
+ if (target.inline && site.where === "text" && !ownsSubtree(target.kind, site, true)) {
9048
+ refusal = "SUBTREE_NOT_HTML";
9049
+ continue;
9050
+ }
8814
9051
  let bindings;
8815
9052
  let bindingsMerge;
8816
9053
  if (site.where === "prop" && site.attr) {
@@ -8845,7 +9082,8 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
8845
9082
  // 🔴 AND ONLY WHEN THE ELEMENT'S SUBTREE IS THE VALUE. A value that is one TEXT NODE of
8846
9083
  // a mixed element gets a wrapper of its own, so taking the element's inner range as its
8847
9084
  // fallback swallowed the `<em>` beside it — the wrapper then rendered the sibling twice.
8848
- fallback: target.kind === "richtext" && site.inner && !(site.mixed && site.textRange) ? content.slice(site.inner.start, site.inner.end).trim() : target.fallback,
9085
+ ...target.inline ? { inline: true } : {},
9086
+ fallback: site.inner && ownsSubtree(target.kind, site, target.inline) ? content.slice(site.inner.start, site.inner.end).trim() : target.fallback,
8849
9087
  snapshot: snapshotName(target.slug),
8850
9088
  slug: target.slug,
8851
9089
  scope: target.scope,
@@ -9249,10 +9487,12 @@ export {
9249
9487
  flat,
9250
9488
  helperSource,
9251
9489
  importsIn,
9490
+ isDecorativeGlyph,
9252
9491
  isDynamicRoute,
9253
9492
  isKnownHelper,
9254
9493
  isSourceCandidate,
9255
9494
  locate,
9495
+ measureUnaddressable,
9256
9496
  overlapping,
9257
9497
  pageFilesFor,
9258
9498
  parseFile4 as parseFile,