@bettercms-ai/convert 0.13.0 → 0.15.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 +15 -0
- package/dist/cli.js +90 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +20 -4
- package/dist/index.js +89 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
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 =
|
|
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. */
|
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) =>
|
|
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) {
|
|
@@ -6389,9 +6401,11 @@ function* elementsIn(nodes) {
|
|
|
6389
6401
|
var childElements = (node) => node.children.flatMap(
|
|
6390
6402
|
(child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
|
|
6391
6403
|
);
|
|
6392
|
-
|
|
6404
|
+
var isComponentTag = (dialect, tag) => dialect !== "html" && /^[A-Z]/.test(tag);
|
|
6405
|
+
function candidateOf(file, dialect, form, imported) {
|
|
6393
6406
|
const controls = /* @__PURE__ */ new Map();
|
|
6394
6407
|
const wrapper = /* @__PURE__ */ new Map();
|
|
6408
|
+
const foreign = /* @__PURE__ */ new Set();
|
|
6395
6409
|
const all = [];
|
|
6396
6410
|
const walk = (parent) => {
|
|
6397
6411
|
const own = [];
|
|
@@ -6399,6 +6413,17 @@ function candidateOf(file, dialect, form) {
|
|
|
6399
6413
|
if (CONTROLS.has(child.tag.toLowerCase())) {
|
|
6400
6414
|
own.push(child);
|
|
6401
6415
|
all.push(child);
|
|
6416
|
+
} else if (isComponentTag(dialect, child.tag)) {
|
|
6417
|
+
const inFile = attrOf(child, "name") ? [] : imported.get(child.tag) ?? [];
|
|
6418
|
+
if (inFile.length) {
|
|
6419
|
+
for (const control of inFile) {
|
|
6420
|
+
all.push(control);
|
|
6421
|
+
foreign.add(control);
|
|
6422
|
+
}
|
|
6423
|
+
} else if (attrOf(child, "name") ?? attrOf(child, "id")) {
|
|
6424
|
+
own.push(child);
|
|
6425
|
+
all.push(child);
|
|
6426
|
+
}
|
|
6402
6427
|
}
|
|
6403
6428
|
walk(child);
|
|
6404
6429
|
}
|
|
@@ -6410,10 +6435,32 @@ function candidateOf(file, dialect, form) {
|
|
|
6410
6435
|
if (name && !controls.has(name)) controls.set(name, control);
|
|
6411
6436
|
}
|
|
6412
6437
|
for (const control of all) {
|
|
6438
|
+
if (foreign.has(control)) continue;
|
|
6413
6439
|
const id = attrOf(control, "id")?.value;
|
|
6414
6440
|
if (id && !controls.has(id)) controls.set(id, control);
|
|
6415
6441
|
}
|
|
6416
|
-
return { file, dialect, el: form, controls, wrapper };
|
|
6442
|
+
return { file, dialect, el: form, controls, wrapper, foreign };
|
|
6443
|
+
}
|
|
6444
|
+
async function importedControls(source, parsed, dialect, project, cache) {
|
|
6445
|
+
const out = /* @__PURE__ */ new Map();
|
|
6446
|
+
for (const [local, { specifier }] of importsIn(parsed, dialect)) {
|
|
6447
|
+
const path = resolveSpecifier(source.path, specifier, project.files, project.aliases);
|
|
6448
|
+
const child = path === null || path === source.path ? void 0 : project.byPath.get(path);
|
|
6449
|
+
if (!child) continue;
|
|
6450
|
+
let controls = cache.get(child.path);
|
|
6451
|
+
if (!controls) {
|
|
6452
|
+
controls = (async () => {
|
|
6453
|
+
const own = dialectOf(child.path, child.content);
|
|
6454
|
+
if (!own || !WIRABLE.includes(own)) return [];
|
|
6455
|
+
const file = await parseFile4(own, child.content);
|
|
6456
|
+
return file.error ? [] : [...elementsIn(file.roots)].filter((el) => CONTROLS.has(el.tag.toLowerCase()));
|
|
6457
|
+
})();
|
|
6458
|
+
cache.set(child.path, controls);
|
|
6459
|
+
}
|
|
6460
|
+
const found = await controls;
|
|
6461
|
+
if (found.length) out.set(local, found);
|
|
6462
|
+
}
|
|
6463
|
+
return out;
|
|
6417
6464
|
}
|
|
6418
6465
|
var hasMessage = (form) => [...elementsIn([form])].some((el) => (attrOf(el, "class") ?? attrOf(el, "classname"))?.value.includes("bcms-form-msg") ?? false);
|
|
6419
6466
|
function wire(source, candidate, form) {
|
|
@@ -6432,6 +6479,7 @@ function wire(source, candidate, form) {
|
|
|
6432
6479
|
source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6433
6480
|
for (const field of form.fields) {
|
|
6434
6481
|
const control = candidate.controls.get(field.key);
|
|
6482
|
+
if (candidate.foreign.has(control)) continue;
|
|
6435
6483
|
const host = candidate.wrapper.get(control) ?? control;
|
|
6436
6484
|
source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
|
|
6437
6485
|
const name = attrOf(control, "name");
|
|
@@ -6450,6 +6498,12 @@ async function wireForms(briefIn, sources) {
|
|
|
6450
6498
|
const forms = brief.forms ?? [];
|
|
6451
6499
|
const candidates = [];
|
|
6452
6500
|
const unreadable = [];
|
|
6501
|
+
const componentFiles = /* @__PURE__ */ new Map();
|
|
6502
|
+
const project = {
|
|
6503
|
+
byPath: new Map(sources.map((s) => [s.path, s])),
|
|
6504
|
+
files: new Set(sources.map((s) => s.path)),
|
|
6505
|
+
aliases: aliasesFrom(sources)
|
|
6506
|
+
};
|
|
6453
6507
|
for (const source of sources) {
|
|
6454
6508
|
if (!/<form[\s/>]/i.test(source.content)) continue;
|
|
6455
6509
|
const dialect = dialectOf(source.path, source.content);
|
|
@@ -6470,8 +6524,9 @@ async function wireForms(briefIn, sources) {
|
|
|
6470
6524
|
});
|
|
6471
6525
|
continue;
|
|
6472
6526
|
}
|
|
6527
|
+
const imported = dialect === "html" ? /* @__PURE__ */ new Map() : await importedControls(source, parsed, dialect, project, componentFiles);
|
|
6473
6528
|
for (const el of elementsIn(parsed.roots)) {
|
|
6474
|
-
if (el.tag.toLowerCase() === "form") candidates.push(candidateOf(source.path, dialect, el));
|
|
6529
|
+
if (el.tag.toLowerCase() === "form") candidates.push(candidateOf(source.path, dialect, el, imported));
|
|
6475
6530
|
}
|
|
6476
6531
|
}
|
|
6477
6532
|
const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
|
|
@@ -6544,6 +6599,14 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
|
|
|
6544
6599
|
files.push({ path, operation: "modify", content: source.toString() });
|
|
6545
6600
|
}
|
|
6546
6601
|
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.`);
|
|
6602
|
+
for (const { form, candidate } of matched) {
|
|
6603
|
+
const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
|
|
6604
|
+
if (keys.length > 0) {
|
|
6605
|
+
notes.push(
|
|
6606
|
+
`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"}.`
|
|
6607
|
+
);
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6547
6610
|
return {
|
|
6548
6611
|
files,
|
|
6549
6612
|
receipt: {
|
|
@@ -8789,10 +8852,28 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8789
8852
|
}
|
|
8790
8853
|
pend2(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
|
|
8791
8854
|
}
|
|
8855
|
+
const documents = /* @__PURE__ */ new Set();
|
|
8856
|
+
for (const [key, hit] of locatedByKey) {
|
|
8857
|
+
const target = targets.get(key);
|
|
8858
|
+
if (!target || target.kind !== "document") continue;
|
|
8859
|
+
documents.add(key);
|
|
8860
|
+
const byHand = declaredByHand(target, hit.files);
|
|
8861
|
+
if (byHand !== null) {
|
|
8862
|
+
declaredOnly++;
|
|
8863
|
+
declaredRow(target, false, byHand);
|
|
8864
|
+
continue;
|
|
8865
|
+
}
|
|
8866
|
+
pend2(
|
|
8867
|
+
target,
|
|
8868
|
+
"DOCUMENT_DECLARE_ONLY",
|
|
8869
|
+
hit.files[0],
|
|
8870
|
+
"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."
|
|
8871
|
+
);
|
|
8872
|
+
}
|
|
8792
8873
|
const byFile = /* @__PURE__ */ new Map();
|
|
8793
8874
|
for (const [key, hit] of locatedByKey) {
|
|
8794
8875
|
const target = targets.get(key);
|
|
8795
|
-
if (!target) continue;
|
|
8876
|
+
if (!target || documents.has(key)) continue;
|
|
8796
8877
|
for (const file of hit.files) byFile.set(file, [...byFile.get(file) ?? [], target]);
|
|
8797
8878
|
}
|
|
8798
8879
|
const drill = {
|
|
@@ -8841,6 +8922,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8841
8922
|
let rewritten = 0;
|
|
8842
8923
|
let alreadyDeclared = declaredOnly;
|
|
8843
8924
|
for (const [key, hit] of locatedByKey) {
|
|
8925
|
+
if (documents.has(key)) continue;
|
|
8844
8926
|
const target = targets.get(key);
|
|
8845
8927
|
const outcomes = hit.files.map((file) => results.get(file)?.get(target.key) ?? "NOT_IN_SOURCE");
|
|
8846
8928
|
const worst = outcomes.find((o) => o !== "rewritten" && o !== "declared");
|