@bettercms-ai/convert 0.25.0 → 0.25.1

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";
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 };
@@ -5465,7 +5477,8 @@ function overlapping(rewrites) {
5465
5477
  for (const a of rewrites) {
5466
5478
  for (const b of rewrites) {
5467
5479
  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)) {
5480
+ if (a === b || a.key === b.key) continue;
5481
+ if (collide(a, b)) {
5469
5482
  out.add(a);
5470
5483
  out.add(b);
5471
5484
  }
@@ -5473,6 +5486,16 @@ function overlapping(rewrites) {
5473
5486
  }
5474
5487
  return [...out];
5475
5488
  }
5489
+ var writes = ({ site }) => [site.range, site.attrRange, site.where === "text" ? site.inner : null].filter((r) => r != null);
5490
+ function collide(a, b) {
5491
+ const inside = (at, spans) => spans.some((r) => r.start < at && at < r.end);
5492
+ const [wa, wb] = [writes(a), writes(b)];
5493
+ if (wa.some((x) => wb.some((y) => overlaps(x, y)))) return true;
5494
+ if (inside(a.site.attrInsertAt, wb) || inside(b.site.attrInsertAt, wa)) return true;
5495
+ const [fa, fb] = [field(a), field(b)];
5496
+ return a.site.attrInsertAt === b.site.attrInsertAt && !!fa && !!fb && fa !== fb;
5497
+ }
5498
+ 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
5499
  function declaration(rewrite) {
5477
5500
  const { site, path, kind, scope } = rewrite;
5478
5501
  if (site.where === "attr" && kind !== "image") {
@@ -5487,8 +5510,8 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
5487
5510
  const bindings = /* @__PURE__ */ new Map();
5488
5511
  const addDeclaration = (at, add) => {
5489
5512
  const held = attributes.get(at) ?? { plain: [], props: [] };
5490
- held.plain.push(...add.plain);
5491
- held.props.push(...add.props);
5513
+ held.plain.push(...add.plain.filter((text) => !held.plain.includes(text)));
5514
+ held.props.push(...add.props.filter((p) => !held.props.some((q) => JSON.stringify(q) === JSON.stringify(p))));
5492
5515
  attributes.set(at, held);
5493
5516
  };
5494
5517
  for (const rewrite of rewrites) {
@@ -5919,7 +5942,8 @@ function attrsOf4(node, content) {
5919
5942
  valueRange: quoted && rawStart >= 0 && attr.value !== "" ? { start: rawStart + (unquoted ? 0 : 1), end: rawStart + raw.length - (unquoted ? 0 : 1) } : null,
5920
5943
  expression: !quoted,
5921
5944
  range,
5922
- expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null
5945
+ expressionRange: !quoted && open >= 0 && close > open ? { start: open, end: close + 1 } : null,
5946
+ ...attr.kind === "expression" && /^\d+(\.\d+)?$/.test(attr.value.trim()) ? { number: attr.value.trim() } : {}
5923
5947
  }
5924
5948
  ];
5925
5949
  });
@@ -6038,6 +6062,9 @@ function attrsOf5(node) {
6038
6062
  return [{ name: `\0${name}`, value: "", valueRange: null, expression: true, expressionRange: null, range }];
6039
6063
  }
6040
6064
  const literal = literalValue(attr);
6065
+ const tag = attr.value;
6066
+ const raw = tag?.type === "ExpressionTag" ? tag.expression : void 0;
6067
+ const digits = typeof raw?.value === "number" ? String(raw.raw ?? "") : "";
6041
6068
  return [
6042
6069
  {
6043
6070
  name,
@@ -6045,7 +6072,8 @@ function attrsOf5(node) {
6045
6072
  valueRange: literal && literal.value !== "" ? literal.range : null,
6046
6073
  expression: literal === null,
6047
6074
  // Not read: svelte sections are DIALECT_UNSUPPORTED. @see componentize.ts
6048
- expressionRange: null,
6075
+ expressionRange: digits ? { start: tag.start ?? 0, end: tag.end ?? 0 } : null,
6076
+ ...digits ? { number: digits } : {},
6049
6077
  range
6050
6078
  }
6051
6079
  ];
@@ -6150,13 +6178,15 @@ var spanOf = (loc) => ({
6150
6178
  function attrsOf6(node) {
6151
6179
  return (node.props ?? []).flatMap((prop) => {
6152
6180
  if (prop.type === DIRECTIVE && prop.name === "bind" && prop.arg?.content) {
6181
+ const digits = /^\d+(\.\d+)?$/.test(String(prop.exp?.content ?? "").trim()) ? String(prop.exp.content).trim() : "";
6153
6182
  return [
6154
6183
  {
6155
6184
  name: prop.arg.content,
6156
6185
  value: "",
6157
6186
  valueRange: null,
6158
6187
  expression: true,
6159
- expressionRange: null,
6188
+ expressionRange: digits ? spanOf(prop.exp?.loc) : null,
6189
+ ...digits ? { number: digits } : {},
6160
6190
  range: spanOf(prop.loc)
6161
6191
  }
6162
6192
  ];
@@ -6644,8 +6674,8 @@ function readPlan(value) {
6644
6674
  check.optionalString(section.sectionType, `${at2}.sectionType`);
6645
6675
  check.optionalString(section.copy, `${at2}.copy`);
6646
6676
  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}]`);
6677
+ for (const [f, field2] of check.array(section.fields, `${at2}.fields`).entries()) {
6678
+ const shape = check.record(field2, `${at2}.fields[${f}]`);
6649
6679
  check.optionalString(shape.key, `${at2}.fields[${f}].key`);
6650
6680
  check.optionalString(shape.path, `${at2}.fields[${f}].path`);
6651
6681
  }
@@ -6964,15 +6994,15 @@ function wire(source, candidate, form) {
6964
6994
  source.remove(start, attr.range.end);
6965
6995
  }
6966
6996
  }
6967
- for (const field of form.fields) {
6968
- const control = candidate.controls.get(field.key);
6997
+ for (const field2 of form.fields) {
6998
+ const control = candidate.controls.get(field2.key);
6969
6999
  if (candidate.foreign.has(control)) continue;
6970
7000
  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)}"`);
7001
+ if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field2.key)}"`);
6972
7002
  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));
7003
+ if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field2.key)}"`);
7004
+ else if (!name.expression && name.value !== field2.key && name.valueRange) {
7005
+ source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field2.key));
6976
7006
  }
6977
7007
  }
6978
7008
  if (el.inner && !hasMessage(el)) {
@@ -7016,7 +7046,7 @@ async function wireForms(briefIn, sources, options_ = {}) {
7016
7046
  candidates.push(candidateOf(source.path, dialect, el, imported, landmark, source.content, parsed.importAt));
7017
7047
  }
7018
7048
  }
7019
- const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
7049
+ const holds = (c, f) => f.fields.filter((field2) => c.controls.has(field2.key)).length;
7020
7050
  const byId = (c, f) => !attrOf(c.el, "data-bcms-form")?.expression && attrOf(c.el, "data-bcms-form")?.value === f.id;
7021
7051
  const programAction = (c) => !!attrOf(c.el, "action")?.expression && calledName(exprOf(c, "action"), "bcmsFormAction") === null;
7022
7052
  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 +7168,9 @@ async function wireForms(briefIn, sources, options_ = {}) {
7138
7168
  pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
7139
7169
  continue;
7140
7170
  }
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);
7171
+ const misnamed = form.fields.find((field2) => {
7172
+ const attr = attrOf(candidate.controls.get(field2.key), "name");
7173
+ return !!attr && attr.value !== field2.key && (attr.expression || !attr.valueRange);
7144
7174
  });
7145
7175
  if (misnamed) {
7146
7176
  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 +7250,7 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
7220
7250
  );
7221
7251
  }
7222
7252
  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);
7253
+ const keys = form.fields.filter((field2) => candidate.foreign.has(candidate.controls.get(field2.key))).map((field2) => field2.key);
7224
7254
  if (keys.length > 0) {
7225
7255
  notes.push(
7226
7256
  `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 +8517,7 @@ var inSection = (section, path) => {
8487
8517
  const target = rowless(path);
8488
8518
  return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
8489
8519
  };
8490
- var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
8520
+ var fieldPaths = (section) => (section.fields ?? []).map((field2) => field2.path ?? `${section.groupKey}.${field2.key ?? ""}`);
8491
8521
  function elementIndex(roots) {
8492
8522
  const out = /* @__PURE__ */ new Map();
8493
8523
  const visit = (node, chain) => {
@@ -8645,8 +8675,8 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
8645
8675
  const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
8646
8676
  ` : "";
8647
8677
  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");
8678
+ const field2 = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, bindPaths, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
8679
+ const declared = [copy, field2].filter(Boolean).join("\n");
8650
8680
  const styles = dialect === "astro" ? carriedStyles(content) : "";
8651
8681
  if (dialect === "astro") {
8652
8682
  return { free: [], duplicates: [], unconvertible: [], content: `---
@@ -8959,8 +8989,8 @@ function propPrefix(section) {
8959
8989
  }
8960
8990
  function exactPrefix(section) {
8961
8991
  const prefixes = /* @__PURE__ */ new Set();
8962
- for (const field of section.fields ?? []) {
8963
- const { path, key } = field;
8992
+ for (const field2 of section.fields ?? []) {
8993
+ const { path, key } = field2;
8964
8994
  if (typeof path !== "string" || typeof key !== "string") continue;
8965
8995
  if (!path.endsWith(key)) return null;
8966
8996
  prefixes.add(path.slice(0, path.length - key.length));
@@ -8972,11 +9002,11 @@ var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a
8972
9002
  function flatPrefix(section) {
8973
9003
  const prefixes = /* @__PURE__ */ new Set();
8974
9004
  let informative = false;
8975
- for (const field of section.fields ?? []) {
8976
- const { path, key } = field;
9005
+ for (const field2 of section.fields ?? []) {
9006
+ const { path, key } = field2;
8977
9007
  if (typeof path !== "string" || typeof key !== "string") continue;
8978
9008
  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));
9009
+ const prefix = ["-", "_"].map((sep) => `${section.groupKey}${sep}`).find((candidate) => path.startsWith(candidate) && (typeof field2.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
8980
9010
  if (!prefix) return null;
8981
9011
  prefixes.add(prefix);
8982
9012
  }
@@ -8985,15 +9015,15 @@ function flatPrefix(section) {
8985
9015
  function flatPathMap(section, prefix) {
8986
9016
  const scalars = {};
8987
9017
  const tables = {};
8988
- for (const field of section.fields ?? []) {
8989
- const { path, key } = field;
9018
+ for (const field2 of section.fields ?? []) {
9019
+ const { path, key } = field2;
8990
9020
  if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
8991
9021
  const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
8992
9022
  if (!row) {
8993
- scalars[path] = field.overrideKey ?? key;
9023
+ scalars[path] = field2.overrideKey ?? key;
8994
9024
  continue;
8995
9025
  }
8996
- const address = typeof field.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field.overrideKey) : null;
9026
+ const address = typeof field2.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field2.overrideKey) : null;
8997
9027
  if (!address) return null;
8998
9028
  const table = tables[row[1]] ??= { key: address[1], columns: {} };
8999
9029
  if (table.key !== address[1]) return null;