@bettercms-ai/convert 0.16.0 → 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/CHANGELOG.md +24 -0
- package/README.md +7 -0
- package/dist/cli.js +91 -30
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +79 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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, "&").replace(/</g, "<").replace(/"/g, """);
|
|
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
|
-
|
|
6478
|
-
if (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,38 +6553,51 @@ 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
|
|
6529
|
-
|
|
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;
|
|
6533
6561
|
const covers2 = (c, f) => f.fields.length > 0 && holds(c, f) === f.fields.length;
|
|
6534
|
-
const posts = (c) => new Set(c.controls.values()).size;
|
|
6535
6562
|
const samePath = (a, b) => a.replace(/\\/g, "/").replace(/^\.\//, "") === b.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6536
6563
|
const options = new Map(forms.map((f) => [f.id, candidates.filter((c) => covers2(c, f))]));
|
|
6537
6564
|
const pinned = /* @__PURE__ */ new Map();
|
|
6538
6565
|
for (const [ref, file] of Object.entries(options_.pins ?? {})) {
|
|
6539
6566
|
const form = forms.find((f) => f.id === ref || f.name === ref);
|
|
6540
6567
|
if (!form) throw new Error(`--pin ${ref}: the brief lists no form with that id or name.`);
|
|
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
|
+
}
|
|
6574
|
+
for (const [id, file] of pinned) {
|
|
6575
|
+
options.set(id, options.get(id).filter((c) => samePath(c.file, file)));
|
|
6543
6576
|
for (const other of forms) {
|
|
6544
|
-
|
|
6577
|
+
const its = pinned.get(other.id);
|
|
6578
|
+
if (other.id === id || its !== void 0 && samePath(its, file)) continue;
|
|
6579
|
+
options.set(other.id, options.get(other.id).filter((c) => !samePath(c.file, file)));
|
|
6545
6580
|
}
|
|
6546
6581
|
}
|
|
6582
|
+
const beat = /* @__PURE__ */ new Map();
|
|
6547
6583
|
for (const c of candidates) {
|
|
6548
6584
|
const claimants = forms.filter((f) => options.get(f.id).includes(c));
|
|
6549
6585
|
if (claimants.length < 2) continue;
|
|
6550
6586
|
const top = Math.max(...claimants.map((f) => holds(c, f)));
|
|
6551
6587
|
const winners = claimants.filter((f) => holds(c, f) === top);
|
|
6552
6588
|
if (winners.length !== 1) continue;
|
|
6553
|
-
|
|
6589
|
+
const rivals = claimants.filter((f) => f !== winners[0]);
|
|
6590
|
+
for (const f of rivals) options.set(f.id, options.get(f.id).filter((x) => x !== c));
|
|
6591
|
+
beat.set(c, { winner: winners[0], rivals });
|
|
6554
6592
|
}
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
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)) });
|
|
6560
6601
|
}
|
|
6561
6602
|
const wanted = /* @__PURE__ */ new Map();
|
|
6562
6603
|
for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
|
|
@@ -6568,12 +6609,13 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6568
6609
|
for (const form of forms) {
|
|
6569
6610
|
const list = options.get(form.id) ?? [];
|
|
6570
6611
|
if (list.length > 1) {
|
|
6571
|
-
|
|
6572
|
-
form,
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
`${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}. Name the right one with --pin "${form.name}=<file>".`
|
|
6576
|
-
|
|
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
|
+
});
|
|
6577
6619
|
continue;
|
|
6578
6620
|
}
|
|
6579
6621
|
if (list.length === 1 && (wanted.get(list[0]) ?? 0) > 1) {
|
|
@@ -6611,7 +6653,7 @@ async function wireForms(briefIn, sources, options_ = {}) {
|
|
|
6611
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.`);
|
|
6612
6654
|
continue;
|
|
6613
6655
|
}
|
|
6614
|
-
if (attrOf(candidate.el, "data-bcms-form")) {
|
|
6656
|
+
if (attrOf(candidate.el, "data-bcms-form")?.value === form.id) {
|
|
6615
6657
|
alreadyWired++;
|
|
6616
6658
|
onSite.push({ form, file: candidate.file });
|
|
6617
6659
|
continue;
|
|
@@ -6636,6 +6678,20 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6636
6678
|
files.push({ path, operation: "modify", content: source.toString() });
|
|
6637
6679
|
}
|
|
6638
6680
|
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.`);
|
|
6681
|
+
for (const { form, candidate } of matched) {
|
|
6682
|
+
const tie = beat.get(candidate);
|
|
6683
|
+
if (tie?.winner === form) {
|
|
6684
|
+
notes.push(
|
|
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.`
|
|
6686
|
+
);
|
|
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
|
+
}
|
|
6694
|
+
}
|
|
6639
6695
|
for (const { form, candidate } of matched) {
|
|
6640
6696
|
const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
|
|
6641
6697
|
if (keys.length > 0) {
|