@bettercms-ai/convert 0.25.0 → 0.26.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.d.ts CHANGED
@@ -589,6 +589,8 @@ interface AttrNode {
589
589
  * that has an expression says where it starts, rather than leaving each reader to scan for it.
590
590
  */
591
591
  expressionRange: Range | null;
592
+ /** The digits of `price={49}` — a number is copy a reader sees, so it is matched like a string. */
593
+ number?: string;
592
594
  }
593
595
  type Node = {
594
596
  type: "element";
@@ -955,7 +957,7 @@ declare function attrsFor(path: string, kind: string, props?: AttrBinding[], sco
955
957
  declare function propsAttribute(props: AttrBinding[]): string;
956
958
 
957
959
  /** Bumped whenever the emitted source changes. `isKnownHelper` upgrades anything older. */
958
- declare const HELPER_VERSION = 13;
960
+ declare const HELPER_VERSION = 14;
959
961
  /** Where each dialect's helper lives. Null = this dialect reads nothing at build time. */
960
962
  declare const HELPER_PATH: Record<Dialect, string | null>;
961
963
  /** The directory the snapshots live in, at the repository root. */
package/dist/index.js CHANGED
@@ -2711,8 +2711,9 @@ function collectSites(roots, literals) {
2711
2711
  }
2712
2712
  const bindingsAttr = node.attrs.find((a) => a.name === "bcmsBindings")?.range;
2713
2713
  for (const attr of node.attrs) {
2714
- if (attr.expression || !attr.valueRange || BCMS_ATTRS.includes(attr.name)) continue;
2715
- const value = norm(attr.value);
2714
+ const digits = attr.expression && attr.expressionRange ? attr.number : void 0;
2715
+ if (attr.expression && digits === void 0 || !digits && !attr.valueRange || BCMS_ATTRS.includes(attr.name)) continue;
2716
+ const value = norm(digits ?? attr.value);
2716
2717
  for (const literal of wanted) {
2717
2718
  if (value !== literal) continue;
2718
2719
  matched.add(literal);
@@ -2720,7 +2721,7 @@ function collectSites(roots, literals) {
2720
2721
  file: "",
2721
2722
  tag: node.tag,
2722
2723
  attrInsertAt: node.attrInsertAt,
2723
- range: attr.valueRange,
2724
+ range: digits === void 0 ? attr.valueRange : attr.expressionRange,
2724
2725
  inner: node.inner,
2725
2726
  // 🔴 A COMPONENT'S ATTRIBUTE IS A PROP, and a prop is a different conversion: the value
2726
2727
  // is rendered in another file, by an element only that file knows. @see props.ts
@@ -2828,6 +2829,8 @@ function attrsOf2(open, content) {
2828
2829
  const value = attr.value;
2829
2830
  const range = { start: attr.start ?? 0, end: attr.end ?? 0 };
2830
2831
  const literal = value?.type === "StringLiteral";
2832
+ const inside = value?.type === "JSXExpressionContainer" ? value.expression : null;
2833
+ const numeric = inside?.type === "NumericLiteral" ? String(inside.extra.raw) : "";
2831
2834
  return [
2832
2835
  {
2833
2836
  name: nameOf(attr.name),
@@ -2836,7 +2839,8 @@ function attrsOf2(open, content) {
2836
2839
  valueRange: literal ? { start: (value.start ?? 0) + 1, end: (value.end ?? 0) - 1 } : null,
2837
2840
  expression: !literal,
2838
2841
  range,
2839
- expressionRange: value?.type === "JSXExpressionContainer" ? { start: value.start ?? 0, end: value.end ?? 0 } : null
2842
+ expressionRange: value?.type === "JSXExpressionContainer" ? { start: value.start ?? 0, end: value.end ?? 0 } : null,
2843
+ ...numeric ? { number: numeric } : {}
2840
2844
  }
2841
2845
  ];
2842
2846
  });
@@ -3312,14 +3316,21 @@ function exportedNames(ast) {
3312
3316
  });
3313
3317
  return out;
3314
3318
  }
3319
+ function printed(node) {
3320
+ if (node?.type === "StringLiteral") return [String(node.value)];
3321
+ if (node?.type === "NumericLiteral") return [String(node.value), String(node.extra?.raw ?? "")];
3322
+ const quasis = node?.type === "TemplateLiteral" ? node.quasis ?? [] : [];
3323
+ const expressions = node?.expressions ?? [];
3324
+ return quasis.length === 1 && expressions.length === 0 ? [String(quasis[0].value.cooked ?? "")] : [];
3325
+ }
3315
3326
  function propertyFor(row, literal) {
3316
3327
  if (row?.type !== "ObjectExpression") return null;
3317
3328
  for (const property of row.properties ?? []) {
3318
3329
  if (property.type !== "ObjectProperty" && property.type !== "Property") continue;
3319
3330
  const key = property.key;
3320
3331
  const value = property.value;
3321
- if (value?.type !== "StringLiteral" || key?.type !== "Identifier") continue;
3322
- if (norm(String(value.value)) === literal) return String(key.name);
3332
+ if (key?.type !== "Identifier") continue;
3333
+ if (printed(value).some((text) => norm(text) === literal)) return String(key.name);
3323
3334
  }
3324
3335
  return null;
3325
3336
  }
@@ -3393,7 +3404,7 @@ function stringLiterals(ast) {
3393
3404
  if (node.type !== "VariableDeclarator") return;
3394
3405
  const id = node.id;
3395
3406
  const init = node.init;
3396
- if (id?.type !== "Identifier" || init?.type !== "StringLiteral") return;
3407
+ if (id?.type !== "Identifier" || !init || printed(init).length === 0) return;
3397
3408
  out.set(String(id.name), init);
3398
3409
  });
3399
3410
  return out;
@@ -3424,7 +3435,7 @@ function scalarUses(content, roots, id) {
3424
3435
  }
3425
3436
  function bindScalar(ctx, member, literals, offset) {
3426
3437
  const rule = DIALECT_RULES[ctx.dialect];
3427
- const named = [...literals].filter(([, init2]) => norm(String(init2.value)) === member.literal);
3438
+ const named = [...literals].filter(([, init2]) => printed(init2).some((text) => norm(text) === member.literal));
3428
3439
  if (named.length !== 1) return { splices: [], refusal: null };
3429
3440
  const [id, init] = named[0];
3430
3441
  const uses = scalarUses(ctx.content, ctx.parsed.roots, id);
@@ -3639,16 +3650,17 @@ function literalTable(init) {
3639
3650
  const first = elements[0];
3640
3651
  if (!first || elements.length !== (init.elements ?? []).length) return null;
3641
3652
  if (elements.some((element) => element.type !== first.type)) return null;
3642
- if (first.type === "StringLiteral") {
3643
- return { tuple: false, rows: elements.map((element) => [norm(String(element.value))]) };
3653
+ if (printed(first).length > 0) {
3654
+ if (elements.some((element) => printed(element).length === 0)) return null;
3655
+ return { tuple: false, rows: elements.map((element) => [norm(printed(element)[0])]) };
3644
3656
  }
3645
3657
  if (first.type !== "ArrayExpression") return null;
3646
3658
  const rows = [];
3647
3659
  for (const element of elements) {
3648
3660
  const inner = (element.elements ?? []).filter((x) => x !== null);
3649
- if (inner.length === 0 || inner.some((x) => x.type !== "StringLiteral")) return null;
3661
+ if (inner.length === 0 || inner.some((x) => printed(x).length === 0)) return null;
3650
3662
  if (inner.length !== (element.elements ?? []).length) return null;
3651
- rows.push(inner.map((x) => norm(String(x.value))));
3663
+ rows.push(inner.map((x) => norm(printed(x)[0])));
3652
3664
  }
3653
3665
  if (new Set(rows.map((row) => row.length)).size !== 1) return null;
3654
3666
  return { tuple: true, rows };
@@ -4187,7 +4199,7 @@ async function bindImages(ctx, targets) {
4187
4199
 
4188
4200
  // src/helper.ts
4189
4201
  import { createHash as createHash2 } from "crypto";
4190
- var HELPER_VERSION = 13;
4202
+ var HELPER_VERSION = 14;
4191
4203
  var HEADER = "// @bettercms-ai/convert helper v";
4192
4204
  var HELPER_PATH = Object.fromEntries(
4193
4205
  Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
@@ -4261,11 +4273,11 @@ function render(value: unknown): string | undefined {
4261
4273
  *
4262
4274
  * Nowhere else does anything change: with no variable (a build, \`astro dev\`, a browser, an edge
4263
4275
  * runtime) every snapshot is the one the bundle imported, exactly as before. A file that cannot be
4264
- * read keeps the last good copy; it is checked at most once a second.
4276
+ * read keeps the last good copy. Its modification time is asked on EVERY read (a stat, microseconds;
4277
+ * the file is parsed again only when it changed), so a publish shows on the very next request.
4265
4278
  */
4266
- type BcmsLiveContent = { stamp: number; bySlug: Map<string, unknown>; layout: unknown };
4279
+ type BcmsLiveContent = { helper: string; stamp: number; bySlug: Map<string, unknown>; entries: Record<string, unknown>; layout: unknown };
4267
4280
  let bcmsLive: BcmsLiveContent | null = null;
4268
- let bcmsLiveCheckedAt = 0;
4269
4281
 
4270
4282
  function liveContent(): BcmsLiveContent | null {
4271
4283
  const proc = (globalThis as {
@@ -4273,9 +4285,6 @@ function liveContent(): BcmsLiveContent | null {
4273
4285
  }).process;
4274
4286
  const file = proc?.env?.BCMS_CONTENT_FILE;
4275
4287
  if (!file || typeof proc?.getBuiltinModule !== "function") return null;
4276
- const now = Date.now();
4277
- if (now - bcmsLiveCheckedAt < 1000) return bcmsLive;
4278
- bcmsLiveCheckedAt = now;
4279
4288
  try {
4280
4289
  const fs = proc.getBuiltinModule("node:fs") as {
4281
4290
  statSync(path: string): { mtimeMs: number };
@@ -4289,13 +4298,30 @@ function liveContent(): BcmsLiveContent | null {
4289
4298
  for (const rows of Object.values(collections)) {
4290
4299
  if (!Array.isArray(rows)) continue;
4291
4300
  for (const row of rows) {
4292
- if (isRecord(row) && typeof row.slug === "string" && !bySlug.has(row.slug)) bySlug.set(row.slug, row);
4301
+ // A published row carries its values under \`data\` (the delivery API's shape); the snapshot a
4302
+ // template reads is the values themselves. Read flat, as before, where a row has none.
4303
+ if (isRecord(row) && typeof row.slug === "string" && !bySlug.has(row.slug)) {
4304
+ bySlug.set(row.slug, isRecord(row.data) ? { ...row.data, slug: row.slug } : row);
4305
+ }
4293
4306
  }
4294
4307
  }
4308
+ // A dynamic route reads \`page.entries[slug]\`: every published row, by its slug.
4309
+ const entries: Record<string, unknown> = Object.fromEntries(bySlug);
4310
+ for (const row of bySlug.values()) if (isRecord(row) && !("entries" in row)) row.entries = entries;
4295
4311
  const pages = isRecord(snapshot) && Array.isArray(snapshot.pages) ? snapshot.pages : [];
4296
4312
  const withLayout = pages.find((page: unknown) => isRecord(page) && isRecord(page.layout));
4297
- const layout = isRecord(withLayout) ? withLayout.layout : isRecord(snapshot) ? snapshot.layout : undefined;
4298
- bcmsLive = { stamp, bySlug, layout };
4313
+ // The site's Layout first; a page's own (its overrides resolved in) only where there is no other.
4314
+ const delivered = isRecord(snapshot) && isRecord(snapshot.layout) ? snapshot.layout : isRecord(withLayout) ? withLayout.layout : undefined;
4315
+ // A template addresses a Layout value as \`<section id>:<field id>\`; the published Layout holds
4316
+ // each Section's values under \`sections.<slug>.fields\`. Both spellings are answered.
4317
+ const layout: Record<string, unknown> | undefined = isRecord(delivered) ? { ...delivered } : undefined;
4318
+ for (const section of layout && isRecord(delivered) && isRecord(delivered.sections) ? Object.values(delivered.sections) : []) {
4319
+ if (!isRecord(section) || typeof section.id !== "string" || !isRecord(section.fields)) continue;
4320
+ for (const [field, value] of Object.entries(section.fields)) layout![section.id + ":" + field] = value;
4321
+ }
4322
+ // \`helper\` names this reader in the built bundle: the platform reads it to know the app shows a
4323
+ // publish on its next request, and an image BetterCMS copied beside it (@see bcmsImage).
4324
+ bcmsLive = { helper: "bcms-helper@${HELPER_VERSION}", stamp, bySlug, entries, layout };
4299
4325
  } catch {
4300
4326
  // Unreadable, or caught mid-write: keep serving the last good copy.
4301
4327
  }
@@ -4307,7 +4333,10 @@ function live(page: unknown): unknown {
4307
4333
  const content = liveContent();
4308
4334
  if (!content) return page;
4309
4335
  if (page === layoutSnapshot) return isRecord(content.layout) ? content.layout : page;
4310
- if (isRecord(page) && typeof page.slug === "string") return content.bySlug.get(page.slug) ?? page;
4336
+ if (isRecord(page) && typeof page.slug === "string") {
4337
+ // A collection's page has no row of its own: its entries are the published ones all the same.
4338
+ return content.bySlug.get(page.slug) ?? ("entries" in page ? page : { ...page, entries: content.entries });
4339
+ }
4311
4340
  return page;
4312
4341
  }
4313
4342
 
@@ -4321,7 +4350,10 @@ export function bcms(page: unknown, path: string, fallback: string, slug?: strin
4321
4350
 
4322
4351
  /** One field of the shared layout \u2014 the header and footer every route renders. */
4323
4352
  export function bcmsLayout(path: string, fallback: string): string {
4324
- return render(readPath(live(layoutSnapshot), path)) ?? fallback;
4353
+ const layout = live(layoutSnapshot);
4354
+ // The whole address first: a field id may carry a dot of its own (\`derived.nav-cta\`).
4355
+ const whole = isRecord(layout) ? layout[path] : undefined;
4356
+ return render(whole !== undefined ? whole : readPath(layout, path)) ?? fallback;
4325
4357
  }
4326
4358
 
4327
4359
  /**
@@ -4398,13 +4430,24 @@ export function bcmsTuples(page: unknown, paths: string[][], fallback: string[][
4398
4430
  * release write (\`writeImage\`) puts the editor's image on the \`data-bcms-field\` element, which
4399
4431
  * is where every published value lands. A plain string fallback (\`<img src="/hero.png">\`) has
4400
4432
  * no such rule and takes the CMS url directly.
4433
+ *
4434
+ * \u{1F534} ON A HOSTED SERVER THE EDITOR'S IMAGE IS A LOCAL FILE. A page the app renders per request has no
4435
+ * release write: what this returns is the page. BetterCMS copies a replaced image beside the app and
4436
+ * names the copy on the published value (\`local\`, with its size), and it is returned in the shape
4437
+ * of an import (\`{ src, width, height, format }\`), so \`<Image>\` optimises it like the site's own.
4438
+ * Absent (a build, or the site's own picture) the import stands.
4401
4439
  */
4402
4440
  export function bcmsImage<T>(page: unknown, path: string, fallback: T, slug?: string): T | string {
4403
4441
  page = live(page);
4404
- if (fallback !== null && typeof fallback === "object") return fallback;
4405
4442
  const entries = isRecord(page) ? page.entries : undefined;
4406
4443
  const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
4407
- return render(readPath(root, path)) ?? fallback;
4444
+ const value = readPath(root, path);
4445
+ if (fallback !== null && typeof fallback === "object") {
4446
+ if (!isRecord(value) || typeof value.local !== "string" || typeof value.width !== "number" || typeof value.height !== "number") return fallback;
4447
+ const format = /\\.([a-z0-9]+)$/i.exec(value.local)?.[1]?.toLowerCase() ?? "";
4448
+ return { src: value.local, width: value.width, height: value.height, format: format === "jpg" ? "jpeg" : format } as unknown as T;
4449
+ }
4450
+ return render(value) ?? fallback;
4408
4451
  }
4409
4452
 
4410
4453
  /**
@@ -4737,7 +4780,8 @@ var HISTORICAL = {
4737
4780
  9: "d6441db2ff4578c3c5d4b7d461f3374b89247ef6ba9e0d535791e840d803e4f3",
4738
4781
  10: "ffcb59862972b15a67c5951e9f3b94ce5b8b50345e6aac2bfa5789cfc3136910",
4739
4782
  11: "ceb2a932d4c2761a5045ff35032e0b45130a0b5034547ff37579c72c9ce3594e",
4740
- 12: "10f71724484c84af12aa3eb90db44bf6acbf71fcf8ddff5b6361c1190ba16528"
4783
+ 12: "10f71724484c84af12aa3eb90db44bf6acbf71fcf8ddff5b6361c1190ba16528",
4784
+ 13: "cd8bdd74dbc944f2331503bdf8e995cb7f74f0d5b307d08aee4690e1695ae798"
4741
4785
  },
4742
4786
  "src/lib/bcms-content.ts": {
4743
4787
  1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
@@ -4751,7 +4795,8 @@ var HISTORICAL = {
4751
4795
  9: "eee9fb1635ae7d6f8f33676a1f390600ca24a62ee992775252273c73a011c494",
4752
4796
  10: "35bb7c0e30f5c41fe68e52259ec4764de6de2734064fa0e1521f3338eef0a09e",
4753
4797
  11: "818784f2088150d64d91b42eac944dc9503f104d32a0e7a18c753d2982a5adc0",
4754
- 12: "79dafc525a541feae55ada5dbe2ea966cac82acd741a4b41d61fe9e0cdfaa208"
4798
+ 12: "79dafc525a541feae55ada5dbe2ea966cac82acd741a4b41d61fe9e0cdfaa208",
4799
+ 13: "06ee70c9f0bb25e03f3930d2250da8dae172ade457f27ebba20382b6482ad3f4"
4755
4800
  },
4756
4801
  "composables/bcms-content.ts": {
4757
4802
  1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
@@ -4765,7 +4810,8 @@ var HISTORICAL = {
4765
4810
  9: "d6441db2ff4578c3c5d4b7d461f3374b89247ef6ba9e0d535791e840d803e4f3",
4766
4811
  10: "ffcb59862972b15a67c5951e9f3b94ce5b8b50345e6aac2bfa5789cfc3136910",
4767
4812
  11: "ceb2a932d4c2761a5045ff35032e0b45130a0b5034547ff37579c72c9ce3594e",
4768
- 12: "10f71724484c84af12aa3eb90db44bf6acbf71fcf8ddff5b6361c1190ba16528"
4813
+ 12: "10f71724484c84af12aa3eb90db44bf6acbf71fcf8ddff5b6361c1190ba16528",
4814
+ 13: "cd8bdd74dbc944f2331503bdf8e995cb7f74f0d5b307d08aee4690e1695ae798"
4769
4815
  }
4770
4816
  };
4771
4817
  var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
@@ -5465,7 +5511,8 @@ function overlapping(rewrites) {
5465
5511
  for (const a of rewrites) {
5466
5512
  for (const b of rewrites) {
5467
5513
  if (a.raw !== void 0 || b.raw !== void 0) continue;
5468
- if (a !== b && a.key !== b.key && a.site.where === "text" && b.site.where === "text" && overlaps(a.site.range, b.site.range)) {
5514
+ if (a === b || a.key === b.key) continue;
5515
+ if (collide(a, b)) {
5469
5516
  out.add(a);
5470
5517
  out.add(b);
5471
5518
  }
@@ -5473,6 +5520,16 @@ function overlapping(rewrites) {
5473
5520
  }
5474
5521
  return [...out];
5475
5522
  }
5523
+ var writes = ({ site }) => [site.range, site.attrRange, site.where === "text" ? site.inner : null].filter((r) => r != null);
5524
+ function collide(a, b) {
5525
+ const inside = (at, spans) => spans.some((r) => r.start < at && at < r.end);
5526
+ const [wa, wb] = [writes(a), writes(b)];
5527
+ if (wa.some((x) => wb.some((y) => overlaps(x, y)))) return true;
5528
+ if (inside(a.site.attrInsertAt, wb) || inside(b.site.attrInsertAt, wa)) return true;
5529
+ const [fa, fb] = [field(a), field(b)];
5530
+ return a.site.attrInsertAt === b.site.attrInsertAt && !!fa && !!fb && fa !== fb;
5531
+ }
5532
+ var field = (r) => r.site.where === "prop" || r.site.where === "text" && !ownsSubtree(r.kind, r.site, r.inline) && r.site.mixed && r.site.textRange ? void 0 : declaration(r).plain[0];
5476
5533
  function declaration(rewrite) {
5477
5534
  const { site, path, kind, scope } = rewrite;
5478
5535
  if (site.where === "attr" && kind !== "image") {
@@ -5487,8 +5544,8 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
5487
5544
  const bindings = /* @__PURE__ */ new Map();
5488
5545
  const addDeclaration = (at, add) => {
5489
5546
  const held = attributes.get(at) ?? { plain: [], props: [] };
5490
- held.plain.push(...add.plain);
5491
- held.props.push(...add.props);
5547
+ held.plain.push(...add.plain.filter((text) => !held.plain.includes(text)));
5548
+ held.props.push(...add.props.filter((p) => !held.props.some((q) => JSON.stringify(q) === JSON.stringify(p))));
5492
5549
  attributes.set(at, held);
5493
5550
  };
5494
5551
  for (const rewrite of rewrites) {
@@ -5919,7 +5976,8 @@ function attrsOf4(node, content) {
5919
5976
  valueRange: quoted && rawStart >= 0 && attr.value !== "" ? { start: rawStart + (unquoted ? 0 : 1), end: rawStart + raw.length - (unquoted ? 0 : 1) } : null,
5920
5977
  expression: !quoted,
5921
5978
  range,
5922
- expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null
5979
+ expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null,
5980
+ ...attr.kind === "expression" && /^\d+(\.\d+)?$/.test(attr.value.trim()) ? { number: attr.value.trim() } : {}
5923
5981
  }
5924
5982
  ];
5925
5983
  });
@@ -6038,6 +6096,9 @@ function attrsOf5(node) {
6038
6096
  return [{ name: `\0${name}`, value: "", valueRange: null, expression: true, expressionRange: null, range }];
6039
6097
  }
6040
6098
  const literal = literalValue(attr);
6099
+ const tag = attr.value;
6100
+ const raw = tag?.type === "ExpressionTag" ? tag.expression : void 0;
6101
+ const digits = typeof raw?.value === "number" ? String(raw.raw ?? "") : "";
6041
6102
  return [
6042
6103
  {
6043
6104
  name,
@@ -6045,7 +6106,8 @@ function attrsOf5(node) {
6045
6106
  valueRange: literal && literal.value !== "" ? literal.range : null,
6046
6107
  expression: literal === null,
6047
6108
  // Not read: svelte sections are DIALECT_UNSUPPORTED. @see componentize.ts
6048
- expressionRange: null,
6109
+ expressionRange: digits ? { start: tag.start ?? 0, end: tag.end ?? 0 } : null,
6110
+ ...digits ? { number: digits } : {},
6049
6111
  range
6050
6112
  }
6051
6113
  ];
@@ -6150,13 +6212,15 @@ var spanOf = (loc) => ({
6150
6212
  function attrsOf6(node) {
6151
6213
  return (node.props ?? []).flatMap((prop) => {
6152
6214
  if (prop.type === DIRECTIVE && prop.name === "bind" && prop.arg?.content) {
6215
+ const digits = /^\d+(\.\d+)?$/.test(String(prop.exp?.content ?? "").trim()) ? String(prop.exp.content).trim() : "";
6153
6216
  return [
6154
6217
  {
6155
6218
  name: prop.arg.content,
6156
6219
  value: "",
6157
6220
  valueRange: null,
6158
6221
  expression: true,
6159
- expressionRange: null,
6222
+ expressionRange: digits ? spanOf(prop.exp?.loc) : null,
6223
+ ...digits ? { number: digits } : {},
6160
6224
  range: spanOf(prop.loc)
6161
6225
  }
6162
6226
  ];
@@ -6644,8 +6708,8 @@ function readPlan(value) {
6644
6708
  check.optionalString(section.sectionType, `${at2}.sectionType`);
6645
6709
  check.optionalString(section.copy, `${at2}.copy`);
6646
6710
  if (section.fields !== void 0) {
6647
- for (const [f, field] of check.array(section.fields, `${at2}.fields`).entries()) {
6648
- const shape = check.record(field, `${at2}.fields[${f}]`);
6711
+ for (const [f, field2] of check.array(section.fields, `${at2}.fields`).entries()) {
6712
+ const shape = check.record(field2, `${at2}.fields[${f}]`);
6649
6713
  check.optionalString(shape.key, `${at2}.fields[${f}].key`);
6650
6714
  check.optionalString(shape.path, `${at2}.fields[${f}].path`);
6651
6715
  }
@@ -6964,15 +7028,15 @@ function wire(source, candidate, form) {
6964
7028
  source.remove(start, attr.range.end);
6965
7029
  }
6966
7030
  }
6967
- for (const field of form.fields) {
6968
- const control = candidate.controls.get(field.key);
7031
+ for (const field2 of form.fields) {
7032
+ const control = candidate.controls.get(field2.key);
6969
7033
  if (candidate.foreign.has(control)) continue;
6970
7034
  const host = candidate.wrapper.get(control) ?? control;
6971
- if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
7035
+ if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field2.key)}"`);
6972
7036
  const name = attrOf(control, "name");
6973
- if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field.key)}"`);
6974
- else if (!name.expression && name.value !== field.key && name.valueRange) {
6975
- source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field.key));
7037
+ if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field2.key)}"`);
7038
+ else if (!name.expression && name.value !== field2.key && name.valueRange) {
7039
+ source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field2.key));
6976
7040
  }
6977
7041
  }
6978
7042
  if (el.inner && !hasMessage(el)) {
@@ -7016,7 +7080,7 @@ async function wireForms(briefIn, sources, options_ = {}) {
7016
7080
  candidates.push(candidateOf(source.path, dialect, el, imported, landmark, source.content, parsed.importAt));
7017
7081
  }
7018
7082
  }
7019
- const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
7083
+ const holds = (c, f) => f.fields.filter((field2) => c.controls.has(field2.key)).length;
7020
7084
  const byId = (c, f) => !attrOf(c.el, "data-bcms-form")?.expression && attrOf(c.el, "data-bcms-form")?.value === f.id;
7021
7085
  const programAction = (c) => !!attrOf(c.el, "action")?.expression && calledName(exprOf(c, "action"), "bcmsFormAction") === null;
7022
7086
  const wiredTo = (c, f) => BY_NAME.includes(c.dialect) ? calledName(exprOf(c, "data-bcms-form"), "bcmsFormId") === f.name || byId(c, f) && programAction(c) : byId(c, f);
@@ -7138,9 +7202,9 @@ async function wireForms(briefIn, sources, options_ = {}) {
7138
7202
  pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
7139
7203
  continue;
7140
7204
  }
7141
- const misnamed = form.fields.find((field) => {
7142
- const attr = attrOf(candidate.controls.get(field.key), "name");
7143
- return !!attr && attr.value !== field.key && (attr.expression || !attr.valueRange);
7205
+ const misnamed = form.fields.find((field2) => {
7206
+ const attr = attrOf(candidate.controls.get(field2.key), "name");
7207
+ return !!attr && attr.value !== field2.key && (attr.expression || !attr.valueRange);
7144
7208
  });
7145
7209
  if (misnamed) {
7146
7210
  pend2(form, "FIELD_UNMATCHED", candidate.file, `the control for ${misnamed.key} posts under an expression, so its name cannot be set to the CMS key.`);
@@ -7220,7 +7284,7 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
7220
7284
  );
7221
7285
  }
7222
7286
  for (const { form, candidate } of matched) {
7223
- const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
7287
+ const keys = form.fields.filter((field2) => candidate.foreign.has(candidate.controls.get(field2.key))).map((field2) => field2.key);
7224
7288
  if (keys.length > 0) {
7225
7289
  notes.push(
7226
7290
  `form ${form.name}: ${keys.join(", ")} matched a control inside an imported component, so no data-bcms-form-field marker was written for ${keys.length === 1 ? "it" : "them"}.`
@@ -8487,7 +8551,7 @@ var inSection = (section, path) => {
8487
8551
  const target = rowless(path);
8488
8552
  return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
8489
8553
  };
8490
- var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
8554
+ var fieldPaths = (section) => (section.fields ?? []).map((field2) => field2.path ?? `${section.groupKey}.${field2.key ?? ""}`);
8491
8555
  function elementIndex(roots) {
8492
8556
  const out = /* @__PURE__ */ new Map();
8493
8557
  const visit = (node, chain) => {
@@ -8645,8 +8709,8 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
8645
8709
  const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
8646
8710
  ` : "";
8647
8711
  const copy = !snapshot ? "" : flat3 ? `const ${snapshot} = bcmsSectionPaths(page, bind, overrides, ${JSON.stringify(pathPrefix)}, ${JSON.stringify(pathMap)}, bindPaths);` : `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides, bindPaths) };`;
8648
- const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, bindPaths, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
8649
- const declared = [copy, field].filter(Boolean).join("\n");
8712
+ const field2 = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, bindPaths, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
8713
+ const declared = [copy, field2].filter(Boolean).join("\n");
8650
8714
  const styles = dialect === "astro" ? carriedStyles(content) : "";
8651
8715
  if (dialect === "astro") {
8652
8716
  return { free: [], duplicates: [], unconvertible: [], content: `---
@@ -8959,8 +9023,8 @@ function propPrefix(section) {
8959
9023
  }
8960
9024
  function exactPrefix(section) {
8961
9025
  const prefixes = /* @__PURE__ */ new Set();
8962
- for (const field of section.fields ?? []) {
8963
- const { path, key } = field;
9026
+ for (const field2 of section.fields ?? []) {
9027
+ const { path, key } = field2;
8964
9028
  if (typeof path !== "string" || typeof key !== "string") continue;
8965
9029
  if (!path.endsWith(key)) return null;
8966
9030
  prefixes.add(path.slice(0, path.length - key.length));
@@ -8972,11 +9036,11 @@ var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a
8972
9036
  function flatPrefix(section) {
8973
9037
  const prefixes = /* @__PURE__ */ new Set();
8974
9038
  let informative = false;
8975
- for (const field of section.fields ?? []) {
8976
- const { path, key } = field;
9039
+ for (const field2 of section.fields ?? []) {
9040
+ const { path, key } = field2;
8977
9041
  if (typeof path !== "string" || typeof key !== "string") continue;
8978
9042
  informative = true;
8979
- const prefix = ["-", "_"].map((sep) => `${section.groupKey}${sep}`).find((candidate) => path.startsWith(candidate) && (typeof field.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
9043
+ const prefix = ["-", "_"].map((sep) => `${section.groupKey}${sep}`).find((candidate) => path.startsWith(candidate) && (typeof field2.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
8980
9044
  if (!prefix) return null;
8981
9045
  prefixes.add(prefix);
8982
9046
  }
@@ -8985,15 +9049,15 @@ function flatPrefix(section) {
8985
9049
  function flatPathMap(section, prefix) {
8986
9050
  const scalars = {};
8987
9051
  const tables = {};
8988
- for (const field of section.fields ?? []) {
8989
- const { path, key } = field;
9052
+ for (const field2 of section.fields ?? []) {
9053
+ const { path, key } = field2;
8990
9054
  if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
8991
9055
  const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
8992
9056
  if (!row) {
8993
- scalars[path] = field.overrideKey ?? key;
9057
+ scalars[path] = field2.overrideKey ?? key;
8994
9058
  continue;
8995
9059
  }
8996
- const address = typeof field.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field.overrideKey) : null;
9060
+ const address = typeof field2.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field2.overrideKey) : null;
8997
9061
  if (!address) return null;
8998
9062
  const table = tables[row[1]] ??= { key: address[1], columns: {} };
8999
9063
  if (table.key !== address[1]) return null;