@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/CHANGELOG.md +11 -0
- package/dist/cli.js +62 -15
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +61 -14
- 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,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
|
|
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;
|
|
@@ -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
|
-
|
|
6573
|
-
form,
|
|
6574
|
-
|
|
6575
|
-
|
|
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);
|