@bettercms-ai/convert 0.12.1 → 0.14.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
@@ -33,9 +33,12 @@ interface BriefPath {
33
33
  * what it said.
34
34
  */
35
35
  scope?: "page" | "layout";
36
- /** `data-bcms-kind` — "text" | "richtext" | "image", as `kindOf` spells it. */
37
- kind: string;
38
- /** The declared field type, which `kind` deliberately loses (richtext vs document). */
36
+ /**
37
+ * `data-bcms-kind`, as `kindOf` spells it: text | richtext | image | document. The props-lane
38
+ * members of the union (url, number, array) never appear on a brief path.
39
+ */
40
+ kind: BindingKind;
41
+ /** The declared field type. It agrees with `kind` now; the injector reads it for LINK_TYPES. */
39
42
  type: string;
40
43
  /**
41
44
  * The richtext value IS the element's whole inline subtree — the field's `config.inline`.
@@ -229,6 +232,12 @@ interface PendingFix {
229
232
  /** The bytes at `line`, trimmed — so the agent can see it is looking at the right place. */
230
233
  snippet: string;
231
234
  action: FixAction;
235
+ /**
236
+ * `declare-richtext` only: WHICH container this is. A `document` is a whole Body — one element
237
+ * holds every block — and binding a paragraph of it instead publishes the entire article into
238
+ * that paragraph. The two are different instructions, not one with a longer value.
239
+ */
240
+ kind?: "richtext" | "document";
232
241
  /** One sentence, present whenever the action alone does not say enough. */
233
242
  why?: string;
234
243
  }
@@ -287,7 +296,14 @@ declare function routeCounts(pending: readonly PendingPath[]): Map<string, numbe
287
296
  * is only `rewritten` when EVERY located occurrence of it was.
288
297
  */
289
298
 
290
- type PendingReason = "NO_ORIGINAL" | "NOT_IN_SOURCE" | "DIALECT_UNSUPPORTED" | "IN_EXPRESSION" | "IN_SCRIPT_OR_COMMENT" | "IN_DATA_FILE" | "AMBIGUOUS_LITERAL" | "KIND_MISMATCH" | "SUBSTRING_ONLY"
299
+ type PendingReason =
300
+ /**
301
+ * A Body the codemod refuses to touch, and the only pending reason that is a DECISION rather
302
+ * than a failure. Its value is a whole Portable Text array and its `original` is only the first
303
+ * block's text, so locating it lands on one paragraph — and publish then writes the entire
304
+ * article into that paragraph, or refuses it outright. The fix names the wrapper to declare.
305
+ */
306
+ "DOCUMENT_DECLARE_ONLY" | "NO_ORIGINAL" | "NOT_IN_SOURCE" | "DIALECT_UNSUPPORTED" | "IN_EXPRESSION" | "IN_SCRIPT_OR_COMMENT" | "IN_DATA_FILE" | "AMBIGUOUS_LITERAL" | "KIND_MISMATCH" | "SUBSTRING_ONLY"
291
307
  /** An INLINE richtext value whose element holds a component, a `<slot>` or an expression. */
292
308
  | "SUBTREE_NOT_HTML" | "PROP_TARGET_NOT_FOUND" | "PROP_DRILLED_DEEP" | "REPEATER_FIXED_LENGTH"
293
309
  /** One array declaration, two loops over it: the row this leaf belongs to has two homes. */
@@ -429,6 +445,16 @@ interface FormsReceipt {
429
445
  pending: PendingForm[];
430
446
  /** Prose a human has to act on — a draft form still 403s every submission. */
431
447
  notes?: string[];
448
+ /**
449
+ * Every form on the site after this run, and the file its `<form>` is in — wired now, or by an
450
+ * earlier run. The server matches the file to the component recorded as that file's owner, so
451
+ * the Forms tab can say which component renders each form. OPTIONAL and ADDITIVE: `wired` stays
452
+ * a number, and a receipt without this list still adds up.
453
+ */
454
+ wiredForms?: {
455
+ id: string;
456
+ file: string;
457
+ }[];
432
458
  }
433
459
  /** Raised when the receipt does not account for every form. Never caught inside this package. */
434
460
  declare class FormInvariantError extends Error {
package/dist/index.js CHANGED
@@ -2560,7 +2560,11 @@ 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
+ var ownsSubtree = (kind, site, inline) => (
2564
+ // A `document` is the same claim at article scale: the element renders EVERY block of the Body,
2565
+ // so its children come from the stored value and nothing else may be bound inside it.
2566
+ (kind === "richtext" || kind === "document") && (inline === true ? !site.nonHtml : !(site.mixed && site.textRange !== null))
2567
+ );
2564
2568
  var norm = (value) => decodeEntities(value.normalize("NFC")).replace(/\s+/g, " ").trim();
2565
2569
  var BCMS_ATTRS = [
2566
2570
  "data-bcms-field",
@@ -5369,7 +5373,7 @@ function actionFor(reason, input, file, inData) {
5369
5373
  return chosen;
5370
5374
  }
5371
5375
  function reasonAction(reason, input, file, inData) {
5372
- const richtext = input.kind === "richtext" || input.inline === true;
5376
+ const richtext = input.kind === "richtext" || input.kind === "document" || input.inline === true;
5373
5377
  switch (reason) {
5374
5378
  case "AMBIGUOUS_LITERAL":
5375
5379
  if (input.routes > 1) {
@@ -5420,6 +5424,11 @@ function reasonAction(reason, input, file, inData) {
5420
5424
  return { action: "wrap-span", why: "Only part of the element's text is this value \u2014 give it its own span." };
5421
5425
  case "SUBTREE_NOT_HTML":
5422
5426
  return { action: "declare-richtext", why: "The value is the element's whole subtree \u2014 bind it as richtext." };
5427
+ case "DOCUMENT_DECLARE_ONLY":
5428
+ return {
5429
+ action: "declare-richtext",
5430
+ why: 'This is a Body: bind the ONE element that renders every block of it \u2014 an article, a main or a div \u2014 with the richtext helper and data-bcms-kind="document". Never the paragraph holding its first block: that is where the whole article would be published.'
5431
+ };
5423
5432
  default:
5424
5433
  return { action: "manual", why: `${reason} \u2014 see the receipt's message for what blocked this path.` };
5425
5434
  }
@@ -5436,7 +5445,10 @@ function fixFor(row, input) {
5436
5445
  ...place.col !== void 0 ? { col: place.col } : {},
5437
5446
  snippet: place.snippet,
5438
5447
  action,
5439
- ...why ? { why } : {}
5448
+ ...why ? { why } : {},
5449
+ // Only `declare-richtext` carries it, and only to say WHICH container is meant: one element
5450
+ // for a rich-text value, the whole Body's wrapper for a document.
5451
+ ...action === "declare-richtext" && (input.kind === "document" || input.kind === "richtext") ? { kind: input.kind } : {}
5440
5452
  };
5441
5453
  }
5442
5454
  function routeCounts(pending) {
@@ -6521,11 +6533,11 @@ async function wireForms(briefIn, sources) {
6521
6533
  }
6522
6534
  if (attrOf(candidate.el, "data-bcms-form")) {
6523
6535
  alreadyWired++;
6524
- onSite.push(form);
6536
+ onSite.push({ form, file: candidate.file });
6525
6537
  continue;
6526
6538
  }
6527
6539
  matched.push({ form, candidate });
6528
- onSite.push(form);
6540
+ onSite.push({ form, file: candidate.file });
6529
6541
  }
6530
6542
  const contents = new Map(sources.map((s) => [s.path, s.content]));
6531
6543
  const byFile = /* @__PURE__ */ new Map();
@@ -6543,7 +6555,7 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
6543
6555
  }
6544
6556
  files.push({ path, operation: "modify", content: source.toString() });
6545
6557
  }
6546
- const notes = onSite.filter((f) => f.status === "draft").map((f) => `publish form ${f.name} in the Forms tab \u2014 a draft form rejects every submission with 403.`);
6558
+ const notes = onSite.filter(({ form }) => form.status === "draft").map(({ form }) => `publish form ${form.name} in the Forms tab \u2014 a draft form rejects every submission with 403.`);
6547
6559
  return {
6548
6560
  files,
6549
6561
  receipt: {
@@ -6560,7 +6572,9 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
6560
6572
  wired: matched.length,
6561
6573
  alreadyWired,
6562
6574
  pending,
6563
- ...notes.length > 0 ? { notes } : {}
6575
+ ...notes.length > 0 ? { notes } : {},
6576
+ // Always, even empty: a run where every form went pending must retire the files an earlier one named.
6577
+ wiredForms: onSite.map(({ form, file }) => ({ id: form.id, file }))
6564
6578
  })
6565
6579
  }
6566
6580
  };
@@ -8787,10 +8801,28 @@ async function convertSources(briefIn, sources, options) {
8787
8801
  }
8788
8802
  pend2(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
8789
8803
  }
8804
+ const documents = /* @__PURE__ */ new Set();
8805
+ for (const [key, hit] of locatedByKey) {
8806
+ const target = targets.get(key);
8807
+ if (!target || target.kind !== "document") continue;
8808
+ documents.add(key);
8809
+ const byHand = declaredByHand(target, hit.files);
8810
+ if (byHand !== null) {
8811
+ declaredOnly++;
8812
+ declaredRow(target, false, byHand);
8813
+ continue;
8814
+ }
8815
+ pend2(
8816
+ target,
8817
+ "DOCUMENT_DECLARE_ONLY",
8818
+ hit.files[0],
8819
+ "A Body is bound by declaring the one element that renders every block of it, never the paragraph that happens to hold its first block."
8820
+ );
8821
+ }
8790
8822
  const byFile = /* @__PURE__ */ new Map();
8791
8823
  for (const [key, hit] of locatedByKey) {
8792
8824
  const target = targets.get(key);
8793
- if (!target) continue;
8825
+ if (!target || documents.has(key)) continue;
8794
8826
  for (const file of hit.files) byFile.set(file, [...byFile.get(file) ?? [], target]);
8795
8827
  }
8796
8828
  const drill = {
@@ -8839,6 +8871,7 @@ async function convertSources(briefIn, sources, options) {
8839
8871
  let rewritten = 0;
8840
8872
  let alreadyDeclared = declaredOnly;
8841
8873
  for (const [key, hit] of locatedByKey) {
8874
+ if (documents.has(key)) continue;
8842
8875
  const target = targets.get(key);
8843
8876
  const outcomes = hit.files.map((file) => results.get(file)?.get(target.key) ?? "NOT_IN_SOURCE");
8844
8877
  const worst = outcomes.find((o) => o !== "rewritten" && o !== "declared");