@bettercms-ai/convert 0.16.1 → 0.17.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
@@ -114,6 +114,13 @@ interface BriefForm {
114
114
  key: string;
115
115
  type: string;
116
116
  }[];
117
+ /**
118
+ * Where the built form sat — footer, header, nav, main, aside, dialog or body. Breaks the tie
119
+ * between several source `<form>`s posting exactly these fields. @see forms.ts
120
+ */
121
+ landmark?: string;
122
+ /** The owner's answer to FORM_AMBIGUOUS: the file this form is in. Applied like `--pin`. */
123
+ pin?: string;
117
124
  }
118
125
 
119
126
  /**
@@ -431,6 +438,8 @@ interface PendingForm {
431
438
  file?: string;
432
439
  /** What exactly could not be done. Informative; the reason is the contract. */
433
440
  message?: string;
441
+ /** FORM_AMBIGUOUS only: the files that each post every field — what a person picks from. */
442
+ candidates?: string[];
434
443
  }
435
444
  interface FormsReceipt {
436
445
  /** A `<form>` this run pointed at the CMS: action, method, id, field markers and the script. */
package/dist/index.js CHANGED
@@ -6387,6 +6387,7 @@ function scriptFor(dialect) {
6387
6387
  var messageFor = (dialect) => dialect === "jsx" ? `<p className="bcms-form-msg" hidden></p>` : `<p class="bcms-form-msg" hidden></p>`;
6388
6388
  var escapeAttr = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
6389
6389
  var attrOf = (el, name) => el.attrs.find((a) => a.name.toLowerCase() === name);
6390
+ var DECLARED_BINDING_ATTR = /^data-bcms-(field|kind|props)$/i;
6390
6391
  function* elementsIn(nodes) {
6391
6392
  for (const node of nodes) {
6392
6393
  if (node.type === "expr") {
@@ -6401,8 +6402,28 @@ function* elementsIn(nodes) {
6401
6402
  var childElements = (node) => node.children.flatMap(
6402
6403
  (child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
6403
6404
  );
6405
+ var LANDMARK_TAGS = /* @__PURE__ */ new Set(["footer", "header", "nav", "main", "aside", "dialog"]);
6406
+ function landmarkOf(chain) {
6407
+ for (let i = chain.length - 1; i >= 0; i--) {
6408
+ const el = chain[i];
6409
+ if (attrOf(el, "role")?.value === "dialog") return "dialog";
6410
+ if (LANDMARK_TAGS.has(el.tag.toLowerCase())) return el.tag.toLowerCase();
6411
+ }
6412
+ return "body";
6413
+ }
6414
+ function* formsIn(nodes, chain = []) {
6415
+ for (const node of nodes) {
6416
+ if (node.type === "expr") {
6417
+ yield* formsIn(node.children, chain);
6418
+ continue;
6419
+ }
6420
+ if (node.type !== "element") continue;
6421
+ if (node.tag.toLowerCase() === "form") yield { el: node, landmark: landmarkOf(chain) };
6422
+ yield* formsIn(node.children, [...chain, node]);
6423
+ }
6424
+ }
6404
6425
  var isComponentTag = (dialect, tag) => dialect !== "html" && /^[A-Z]/.test(tag);
6405
- function candidateOf(file, dialect, form, imported) {
6426
+ function candidateOf(file, dialect, form, imported, landmark) {
6406
6427
  const controls = /* @__PURE__ */ new Map();
6407
6428
  const wrapper = /* @__PURE__ */ new Map();
6408
6429
  const foreign = /* @__PURE__ */ new Set();
@@ -6439,7 +6460,7 @@ function candidateOf(file, dialect, form, imported) {
6439
6460
  const id = attrOf(control, "id")?.value;
6440
6461
  if (id && !controls.has(id)) controls.set(id, control);
6441
6462
  }
6442
- return { file, dialect, el: form, controls, wrapper, foreign };
6463
+ return { file, dialect, el: form, controls, wrapper, foreign, landmark };
6443
6464
  }
6444
6465
  async function importedControls(source, parsed, dialect, project, cache) {
6445
6466
  const out = /* @__PURE__ */ new Map();
@@ -6474,14 +6495,21 @@ function wire(source, candidate, form) {
6474
6495
  };
6475
6496
  set("action", form.submitUrl);
6476
6497
  set("method", "post");
6477
- inserts.push(`data-bcms-form="${escapeAttr(form.id)}"`);
6478
- if (form.successMessage) inserts.push(`data-bcms-form-success="${escapeAttr(form.successMessage)}"`);
6498
+ set("data-bcms-form", form.id);
6499
+ if (form.successMessage) set("data-bcms-form-success", form.successMessage);
6479
6500
  source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
6501
+ for (const inner of elementsIn(el.children)) {
6502
+ for (const attr of inner.attrs) {
6503
+ if (!DECLARED_BINDING_ATTR.test(attr.name)) continue;
6504
+ const start = source.original[attr.range.start - 1] === " " ? attr.range.start - 1 : attr.range.start;
6505
+ source.remove(start, attr.range.end);
6506
+ }
6507
+ }
6480
6508
  for (const field of form.fields) {
6481
6509
  const control = candidate.controls.get(field.key);
6482
6510
  if (candidate.foreign.has(control)) continue;
6483
6511
  const host = candidate.wrapper.get(control) ?? control;
6484
- source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
6512
+ if (!attrOf(host, "data-bcms-form-field")) source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
6485
6513
  const name = attrOf(control, "name");
6486
6514
  if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field.key)}"`);
6487
6515
  else if (!name.expression && name.value !== field.key && name.valueRange) {
@@ -6525,8 +6553,8 @@ async function wireForms(briefIn, sources, options_ = {}) {
6525
6553
  continue;
6526
6554
  }
6527
6555
  const imported = dialect === "html" ? /* @__PURE__ */ new Map() : await importedControls(source, parsed, dialect, project, componentFiles);
6528
- for (const el of elementsIn(parsed.roots)) {
6529
- if (el.tag.toLowerCase() === "form") candidates.push(candidateOf(source.path, dialect, el, imported));
6556
+ for (const { el, landmark } of formsIn(parsed.roots)) {
6557
+ candidates.push(candidateOf(source.path, dialect, el, imported, landmark));
6530
6558
  }
6531
6559
  }
6532
6560
  const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
@@ -6540,6 +6568,9 @@ async function wireForms(briefIn, sources, options_ = {}) {
6540
6568
  if (file.trim() === "") throw new Error(`--pin ${ref}: no file named. Give "<form>=<file>".`);
6541
6569
  pinned.set(form.id, file);
6542
6570
  }
6571
+ for (const form of forms) {
6572
+ if (form.pin && form.pin.trim() !== "" && !pinned.has(form.id)) pinned.set(form.id, form.pin);
6573
+ }
6543
6574
  for (const [id, file] of pinned) {
6544
6575
  options.set(id, options.get(id).filter((c) => samePath(c.file, file)));
6545
6576
  for (const other of forms) {
@@ -6559,6 +6590,15 @@ async function wireForms(briefIn, sources, options_ = {}) {
6559
6590
  for (const f of rivals) options.set(f.id, options.get(f.id).filter((x) => x !== c));
6560
6591
  beat.set(c, { winner: winners[0], rivals });
6561
6592
  }
6593
+ const placed = /* @__PURE__ */ new Map();
6594
+ for (const form of forms) {
6595
+ const list = options.get(form.id);
6596
+ if (list.length < 2 || !form.landmark || form.landmark === "body") continue;
6597
+ const kept = list.filter((c) => c.landmark === form.landmark);
6598
+ if (kept.length === 0 || kept.length === list.length) continue;
6599
+ options.set(form.id, kept);
6600
+ placed.set(form.id, { kept, dropped: list.filter((c) => !kept.includes(c)) });
6601
+ }
6562
6602
  const wanted = /* @__PURE__ */ new Map();
6563
6603
  for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
6564
6604
  const matched = [];
@@ -6569,12 +6609,13 @@ async function wireForms(briefIn, sources, options_ = {}) {
6569
6609
  for (const form of forms) {
6570
6610
  const list = options.get(form.id) ?? [];
6571
6611
  if (list.length > 1) {
6572
- pend2(
6573
- form,
6574
- "FORM_AMBIGUOUS",
6575
- void 0,
6576
- `${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}.${pinned.has(form.id) ? " The pin names that file, and it holds more than one." : ` Name the right one with --pin "${form.name}=<file>".`}`
6577
- );
6612
+ pending.push({
6613
+ id: form.id,
6614
+ name: form.name,
6615
+ reason: "FORM_AMBIGUOUS",
6616
+ message: `${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}.${pinned.has(form.id) ? " The pin names that file, and it holds more than one." : ` Name the right one with --pin "${form.name}=<file>".`}`,
6617
+ candidates: [...new Set(list.map((c) => c.file))]
6618
+ });
6578
6619
  continue;
6579
6620
  }
6580
6621
  if (list.length === 1 && (wanted.get(list[0]) ?? 0) > 1) {
@@ -6612,7 +6653,7 @@ async function wireForms(briefIn, sources, options_ = {}) {
6612
6653
  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.`);
6613
6654
  continue;
6614
6655
  }
6615
- if (attrOf(candidate.el, "data-bcms-form")) {
6656
+ if (attrOf(candidate.el, "data-bcms-form")?.value === form.id) {
6616
6657
  alreadyWired++;
6617
6658
  onSite.push({ form, file: candidate.file });
6618
6659
  continue;
@@ -6644,6 +6685,12 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
6644
6685
  `form ${form.name}: ${candidate.file} posts every field of ${tie.rivals.map((f) => f.name).join(", ")} too, and more of ${form.name}'s, so it went to ${form.name} \u2014 name the file yourself with --pin "<form>=<file>" if that is wrong.`
6645
6686
  );
6646
6687
  }
6688
+ const where = placed.get(form.id);
6689
+ if (where) {
6690
+ notes.push(
6691
+ `form ${form.name}: ${where.dropped.map((c) => c.file).join(", ")} post${where.dropped.length === 1 ? "s" : ""} every field of it too, but the built form sat in the ${form.landmark} and only ${candidate.file}'s does \u2014 name the file yourself with --pin "<form>=<file>" if that is wrong.`
6692
+ );
6693
+ }
6647
6694
  }
6648
6695
  for (const { form, candidate } of matched) {
6649
6696
  const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);