@bettercms-ai/convert 0.9.0 → 0.11.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 +34 -0
- package/README.md +23 -0
- package/dist/cli.js +312 -25
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +81 -5
- package/dist/index.js +264 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2562,6 +2562,7 @@ var IMAGE_SOURCES = {
|
|
|
2562
2562
|
video: ["poster"]
|
|
2563
2563
|
};
|
|
2564
2564
|
var isImageSource = (site) => site.where === "attr" && (IMAGE_SOURCES[site.tag.toLowerCase()] ?? []).includes(site.attr ?? "");
|
|
2565
|
+
var ownsSubtree = (kind, site, inline) => kind === "richtext" && (inline === true ? !site.nonHtml : !(site.mixed && site.textRange !== null));
|
|
2565
2566
|
var norm = (value) => decodeEntities(value.normalize("NFC")).replace(/\s+/g, " ").trim();
|
|
2566
2567
|
var BCMS_ATTRS = [
|
|
2567
2568
|
"data-bcms-field",
|
|
@@ -2594,6 +2595,7 @@ function openTagEnd(content, start) {
|
|
|
2594
2595
|
}
|
|
2595
2596
|
return content.length;
|
|
2596
2597
|
}
|
|
2598
|
+
var OPAQUE_TAGS = /* @__PURE__ */ new Set(["pre"]);
|
|
2597
2599
|
var isBlank = (value) => value.trim() === "";
|
|
2598
2600
|
function renderedText(node) {
|
|
2599
2601
|
if (node.type === "text") return node.value;
|
|
@@ -2601,6 +2603,11 @@ function renderedText(node) {
|
|
|
2601
2603
|
return "";
|
|
2602
2604
|
}
|
|
2603
2605
|
var attrsOf = (node) => Object.fromEntries(node.attrs.map((a) => [a.name, a.value]));
|
|
2606
|
+
function nonHtmlSubtree(node) {
|
|
2607
|
+
return node.children.some(
|
|
2608
|
+
(c) => c.type === "expr" || c.type === "element" && (!isIntrinsic(c.tag) || c.tag === "slot" || nonHtmlSubtree(c))
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
2604
2611
|
function soleTextChild(node) {
|
|
2605
2612
|
const texts = node.children.filter((c) => c.type === "text" && !isBlank(c.value));
|
|
2606
2613
|
return texts.length === 1 ? texts[0] : null;
|
|
@@ -2637,10 +2644,12 @@ function collectSites(roots, literals) {
|
|
|
2637
2644
|
if (node.type !== "element") return matched;
|
|
2638
2645
|
const attrs = attrsOf(node);
|
|
2639
2646
|
let index = 0;
|
|
2640
|
-
|
|
2641
|
-
const
|
|
2642
|
-
|
|
2643
|
-
|
|
2647
|
+
if (!OPAQUE_TAGS.has(node.tag)) {
|
|
2648
|
+
for (const child of node.children) {
|
|
2649
|
+
const childWalk = { ancestors: [...walk.ancestors, node.tag], siblingIndex: index };
|
|
2650
|
+
if (child.type === "element") index++;
|
|
2651
|
+
for (const hit of visit(child, childWalk)) matched.add(hit);
|
|
2652
|
+
}
|
|
2644
2653
|
}
|
|
2645
2654
|
if (node.raw) {
|
|
2646
2655
|
const text2 = norm(renderedTextRaw(node));
|
|
@@ -2666,6 +2675,7 @@ function collectSites(roots, literals) {
|
|
|
2666
2675
|
textRange: soleTextChild(node)?.range ?? null,
|
|
2667
2676
|
literal,
|
|
2668
2677
|
partial: false,
|
|
2678
|
+
...nonHtmlSubtree(node) ? { nonHtml: true } : {},
|
|
2669
2679
|
ancestors: walk.ancestors,
|
|
2670
2680
|
siblingIndex: walk.siblingIndex
|
|
2671
2681
|
});
|
|
@@ -2691,6 +2701,7 @@ function collectSites(roots, literals) {
|
|
|
2691
2701
|
textRange: own.range,
|
|
2692
2702
|
literal,
|
|
2693
2703
|
partial: false,
|
|
2704
|
+
...nonHtmlSubtree(node) ? { nonHtml: true } : {},
|
|
2694
2705
|
ancestors: walk.ancestors,
|
|
2695
2706
|
siblingIndex: walk.siblingIndex
|
|
2696
2707
|
});
|
|
@@ -2741,10 +2752,12 @@ function partials(node, literals, walk, out) {
|
|
|
2741
2752
|
if (node.type !== "element" || node.raw) return false;
|
|
2742
2753
|
let deeper = false;
|
|
2743
2754
|
let index = 0;
|
|
2744
|
-
|
|
2745
|
-
const
|
|
2746
|
-
|
|
2747
|
-
|
|
2755
|
+
if (!OPAQUE_TAGS.has(node.tag)) {
|
|
2756
|
+
for (const child of node.children) {
|
|
2757
|
+
const childWalk = { ancestors: [...walk.ancestors, node.tag], siblingIndex: index };
|
|
2758
|
+
if (child.type === "element") index++;
|
|
2759
|
+
if (partials(child, literals, childWalk, out)) deeper = true;
|
|
2760
|
+
}
|
|
2748
2761
|
}
|
|
2749
2762
|
if (deeper) return true;
|
|
2750
2763
|
const text = norm(renderedText(node));
|
|
@@ -4129,7 +4142,7 @@ function bindImages(ctx, targets) {
|
|
|
4129
4142
|
|
|
4130
4143
|
// src/helper.ts
|
|
4131
4144
|
import { createHash as createHash2 } from "crypto";
|
|
4132
|
-
var HELPER_VERSION =
|
|
4145
|
+
var HELPER_VERSION = 7;
|
|
4133
4146
|
var HEADER = "// @bettercms-ai/convert helper v";
|
|
4134
4147
|
var HELPER_PATH = Object.fromEntries(
|
|
4135
4148
|
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
@@ -4322,9 +4335,65 @@ export function bcmsSection(page: unknown, bind?: string, overrides?: unknown):
|
|
|
4322
4335
|
* how the codemod ran: one component renders a Phase A placement on one page and a Phase B
|
|
4323
4336
|
* instance on the next, and a repository does not have to be re-cut when its copy moves.
|
|
4324
4337
|
*/
|
|
4338
|
+
/** How a flat field family maps page paths to an instance's override keys. @see bcmsSectionPaths */
|
|
4339
|
+
export type BcmsPathMap = {
|
|
4340
|
+
scalars?: Record<string, string>;
|
|
4341
|
+
tables?: Record<string, { key: string; columns: Record<string, string> }>;
|
|
4342
|
+
};
|
|
4343
|
+
|
|
4344
|
+
/**
|
|
4345
|
+
* The snapshot for a section whose page fields are a FLAT family (\`wrap-h2-latest\`, not \`wrap.h2_latest\`).
|
|
4346
|
+
*
|
|
4347
|
+
* \`bcmsSection\` returns a group object read through a dotted path, which a flat family does not have: its
|
|
4348
|
+
* copy lives in top-level keys of the page snapshot. So this answers in the page's own spelling \u2014 the
|
|
4349
|
+
* markup reads \`bcms(snapshot, "wrap-h2-latest", \u2026)\` verbatim \u2014 with every mapped path set explicitly:
|
|
4350
|
+
*
|
|
4351
|
+
* - the instance's override for that key when it holds one (a table's rows re-spelled with the page's
|
|
4352
|
+
* column names, \`{ rows }\` unwrapped as in \`bcmsSection\`);
|
|
4353
|
+
* - else, for a placement bound to a group, the page value at the path re-based onto that group;
|
|
4354
|
+
* - else NOTHING \u2014 a placement that owns its copy never falls back to the page's stale copy, exactly as
|
|
4355
|
+
* \`bcmsSection\` synthesizes no group for it.
|
|
4356
|
+
*
|
|
4357
|
+
* Every other page key passes through, so reads outside the family (a shared link) keep working.
|
|
4358
|
+
*/
|
|
4359
|
+
export function bcmsSectionPaths(
|
|
4360
|
+
page: unknown,
|
|
4361
|
+
bind: string | undefined,
|
|
4362
|
+
overrides: unknown,
|
|
4363
|
+
prefix: string,
|
|
4364
|
+
map: BcmsPathMap,
|
|
4365
|
+
): Record<string, unknown> {
|
|
4366
|
+
const out: Record<string, unknown> = isRecord(page) ? { ...page } : {};
|
|
4367
|
+
const own = isRecord(overrides) ? overrides : {};
|
|
4368
|
+
const separator = prefix.slice(-1);
|
|
4369
|
+
const fromPage = (path: string): unknown =>
|
|
4370
|
+
bind ? readPath(page, bind + separator + path.slice(prefix.length)) : undefined;
|
|
4371
|
+
for (const [path, key] of Object.entries(map.scalars ?? {})) {
|
|
4372
|
+
const value = own[key];
|
|
4373
|
+
out[path] = value !== undefined && value !== null ? value : fromPage(path);
|
|
4374
|
+
}
|
|
4375
|
+
for (const [path, table] of Object.entries(map.tables ?? {})) {
|
|
4376
|
+
const held = own[table.key];
|
|
4377
|
+
const rows = isRecord(held) && Array.isArray(held.rows) ? held.rows : held;
|
|
4378
|
+
if (!Array.isArray(rows)) {
|
|
4379
|
+
out[path] = fromPage(path);
|
|
4380
|
+
continue;
|
|
4381
|
+
}
|
|
4382
|
+
const pageColumn = new Map(Object.entries(table.columns).map(([column, key]) => [key, column]));
|
|
4383
|
+
out[path] = rows.map((row) => {
|
|
4384
|
+
if (!isRecord(row)) return row;
|
|
4385
|
+
const spelled: Record<string, unknown> = {};
|
|
4386
|
+
for (const [key, value] of Object.entries(row)) spelled[pageColumn.get(key) ?? key] = value;
|
|
4387
|
+
return spelled;
|
|
4388
|
+
});
|
|
4389
|
+
}
|
|
4390
|
+
return out;
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4325
4393
|
export function bcmsFields(
|
|
4326
4394
|
placement: { blockId?: string; bind?: string; overrides?: unknown },
|
|
4327
4395
|
pathPrefix: string,
|
|
4396
|
+
map?: BcmsPathMap,
|
|
4328
4397
|
): (path: string) => string {
|
|
4329
4398
|
const id = placement.blockId;
|
|
4330
4399
|
const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
|
|
@@ -4337,6 +4406,24 @@ export function bcmsFields(
|
|
|
4337
4406
|
* so prepending one would invent an address rather than correct it.
|
|
4338
4407
|
*/
|
|
4339
4408
|
const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
|
|
4409
|
+
if (map) {
|
|
4410
|
+
/**
|
|
4411
|
+
* A FLAT family: the address is not "prefix swapped". The instance lane names the override key the
|
|
4412
|
+
* plan allocated (\`h2_latest\`, \`group_webgl[0].a_webgl\`); the bind lane keeps the page's own
|
|
4413
|
+
* separator (\`landing-h2-latest\`, never \`landing.h2-latest\`).
|
|
4414
|
+
*/
|
|
4415
|
+
const separator = pathPrefix.slice(-1);
|
|
4416
|
+
return (path: string): string => {
|
|
4417
|
+
if (!path.startsWith(pathPrefix) || !(own || placement.bind)) return path;
|
|
4418
|
+
if (!own) return placement.bind + separator + path.slice(pathPrefix.length);
|
|
4419
|
+
const scalar = map.scalars?.[path];
|
|
4420
|
+
if (scalar !== undefined) return id + "__overrides." + scalar;
|
|
4421
|
+
const row = /^([^[]+)\\[(\\d+)\\]\\.(.+)$/.exec(path);
|
|
4422
|
+
const table = row ? map.tables?.[row[1]] : undefined;
|
|
4423
|
+
const column = row && table ? table.columns[row[3]] : undefined;
|
|
4424
|
+
return table && column !== undefined ? id + "__overrides." + table.key + "[" + row![2] + "]." + column : path;
|
|
4425
|
+
};
|
|
4426
|
+
}
|
|
4340
4427
|
return (path: string): string => {
|
|
4341
4428
|
// A path outside the group this section was cut from is not one this placement re-addresses:
|
|
4342
4429
|
// it names a key the instance does not hold and a group this placement is not bound to.
|
|
@@ -4353,21 +4440,24 @@ var HISTORICAL = {
|
|
|
4353
4440
|
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
4354
4441
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
4355
4442
|
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4356
|
-
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
|
|
4443
|
+
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
|
|
4444
|
+
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
|
|
4357
4445
|
},
|
|
4358
4446
|
"src/lib/bcms-content.ts": {
|
|
4359
4447
|
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
4360
4448
|
2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
|
|
4361
4449
|
3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
|
|
4362
4450
|
4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
|
|
4363
|
-
5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba"
|
|
4451
|
+
5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba",
|
|
4452
|
+
6: "63f1fa29e47e70c5193b81ee24addb238ba100247124d55bc17713102d587df5"
|
|
4364
4453
|
},
|
|
4365
4454
|
"composables/bcms-content.ts": {
|
|
4366
4455
|
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
4367
4456
|
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
4368
4457
|
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
4369
4458
|
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4370
|
-
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
|
|
4459
|
+
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da",
|
|
4460
|
+
6: "c907e7245578d947c6541b1ab2c5c6659ea7789a1519e895b4a4aed122c6f649"
|
|
4371
4461
|
}
|
|
4372
4462
|
};
|
|
4373
4463
|
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
@@ -4384,14 +4474,23 @@ function isKnownHelper(content, dialect) {
|
|
|
4384
4474
|
|
|
4385
4475
|
// src/locate.ts
|
|
4386
4476
|
var flat = (value) => value.replace(/\s+/g, " ").trim();
|
|
4387
|
-
var stripTags = (value) => value.replace(/<br\s*\/?>/gi,
|
|
4477
|
+
var stripTags = (value, br = " ") => value.replace(/<br\s*\/?>/gi, br).replace(/<[^>]*>/g, "");
|
|
4388
4478
|
function locate(brief, sources, options) {
|
|
4389
4479
|
const flattened = sources.map((s) => {
|
|
4390
4480
|
const readable = unwrapJsxStrings(decodeEntities(s.content));
|
|
4391
4481
|
return {
|
|
4392
4482
|
path: s.path,
|
|
4393
4483
|
content: flat(readable),
|
|
4394
|
-
text: options?.stripTags ? flat(stripTags(readable)) : null
|
|
4484
|
+
text: options?.stripTags ? flat(stripTags(readable)) : null,
|
|
4485
|
+
/**
|
|
4486
|
+
* 🔴 A `<br>` IS A SPACE TO A READER AND NOTHING TO `textContent`, and the derive lane reads
|
|
4487
|
+
* the DOM. `<h1>Launch your site<br /><span>fast.</span></h1>` is one INLINE richtext field
|
|
4488
|
+
* whose value is `Launch your sitefast.` — no space, because no text node holds one — while
|
|
4489
|
+
* this prefilter's only reading inserted one, so the file was never opened and every split
|
|
4490
|
+
* heading on every imported site came back NOT_IN_SOURCE. Both readings are searched: this
|
|
4491
|
+
* decides which FILES to open, and `collectSites` still has to match an element exactly.
|
|
4492
|
+
*/
|
|
4493
|
+
tight: options?.stripTags ? flat(stripTags(readable, "")) : null
|
|
4395
4494
|
};
|
|
4396
4495
|
});
|
|
4397
4496
|
const located = [];
|
|
@@ -4403,7 +4502,9 @@ function locate(brief, sources, options) {
|
|
|
4403
4502
|
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "no-original" });
|
|
4404
4503
|
continue;
|
|
4405
4504
|
}
|
|
4406
|
-
const files = flattened.filter(
|
|
4505
|
+
const files = flattened.filter(
|
|
4506
|
+
(f) => f.content.includes(original) || f.text !== null && f.text.includes(original) || f.tight !== null && f.tight.includes(original)
|
|
4507
|
+
).map((f) => f.path);
|
|
4407
4508
|
if (files.length === 0) {
|
|
4408
4509
|
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "not-in-source" });
|
|
4409
4510
|
continue;
|
|
@@ -5102,7 +5203,7 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
|
|
|
5102
5203
|
addDeclaration(site.attrInsertAt, declaration(rewrite));
|
|
5103
5204
|
continue;
|
|
5104
5205
|
}
|
|
5105
|
-
if (kind
|
|
5206
|
+
if (ownsSubtree(kind, site, rewrite.inline)) {
|
|
5106
5207
|
if (!isIntrinsic(site.tag)) continue;
|
|
5107
5208
|
if (rule.richtext && site.inner) {
|
|
5108
5209
|
source.remove(site.inner.start, site.inner.end);
|
|
@@ -6376,6 +6477,66 @@ function isSourceCandidate(path) {
|
|
|
6376
6477
|
return lower.split("/").slice(0, -1).some((segment) => TEMPLATE_DIRS.has(segment));
|
|
6377
6478
|
}
|
|
6378
6479
|
|
|
6480
|
+
// src/xray.ts
|
|
6481
|
+
var SKIP_TAGS = /* @__PURE__ */ new Set(["svg", "iframe", "head", "title"]);
|
|
6482
|
+
var CONTEXT_TAGS = /* @__PURE__ */ new Set([
|
|
6483
|
+
"nav",
|
|
6484
|
+
"header",
|
|
6485
|
+
"footer",
|
|
6486
|
+
"main",
|
|
6487
|
+
"aside",
|
|
6488
|
+
"form",
|
|
6489
|
+
"button",
|
|
6490
|
+
"a",
|
|
6491
|
+
"ul",
|
|
6492
|
+
"ol",
|
|
6493
|
+
"table",
|
|
6494
|
+
"dialog",
|
|
6495
|
+
"details",
|
|
6496
|
+
"figure",
|
|
6497
|
+
"blockquote"
|
|
6498
|
+
]);
|
|
6499
|
+
var BOUND_ATTRS = ["data-bcms-field", "data-bcms-layout-field", "data-bcms-path", "data-bcms-block"];
|
|
6500
|
+
var isDecorativeGlyph = (text) => text.length <= 2 || /^\d{1,2}\s*[/.)\-–]?$/.test(text) || !new RegExp("\\p{L}", "u").test(text) || /^made with\b/i.test(text) || /^skip to\b/i.test(text);
|
|
6501
|
+
var MAX_BUCKETS = 12;
|
|
6502
|
+
var flat2 = (value) => value.replace(/\s+/g, " ").trim();
|
|
6503
|
+
function measureUnaddressable(html) {
|
|
6504
|
+
const parsed = parse2(html);
|
|
6505
|
+
let visible = 0;
|
|
6506
|
+
let unaddressable = 0;
|
|
6507
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
6508
|
+
const visit = (node, tag, context, bound) => {
|
|
6509
|
+
if (node.type === "text") {
|
|
6510
|
+
const text = flat2(node.value);
|
|
6511
|
+
if (!text || isDecorativeGlyph(text)) return;
|
|
6512
|
+
visible += text.length;
|
|
6513
|
+
if (bound) return;
|
|
6514
|
+
unaddressable += text.length;
|
|
6515
|
+
const key = `${tag} @ ${context}`;
|
|
6516
|
+
const held = buckets.get(key) ?? { tag, context, chars: 0, nodes: 0, samples: [] };
|
|
6517
|
+
held.chars += text.length;
|
|
6518
|
+
held.nodes += 1;
|
|
6519
|
+
if (held.samples.length < 2) held.samples.push(text.slice(0, 50));
|
|
6520
|
+
buckets.set(key, held);
|
|
6521
|
+
return;
|
|
6522
|
+
}
|
|
6523
|
+
if (node.type !== "element" || node.raw || SKIP_TAGS.has(node.tag)) return;
|
|
6524
|
+
const attrs = new Map(node.attrs.map((a) => [a.name, a.value]));
|
|
6525
|
+
if (attrs.has("hidden") || attrs.get("aria-hidden") === "true" || /display\s*:\s*none/.test(attrs.get("style") ?? "")) {
|
|
6526
|
+
return;
|
|
6527
|
+
}
|
|
6528
|
+
const owned = bound || BOUND_ATTRS.some((name) => attrs.has(name));
|
|
6529
|
+
const chain = CONTEXT_TAGS.has(node.tag) ? context === "body" ? node.tag : `${context}>${node.tag}` : context;
|
|
6530
|
+
for (const child of node.children) visit(child, node.tag, chain, owned);
|
|
6531
|
+
};
|
|
6532
|
+
for (const root of parsed.roots) visit(root, "body", "body", false);
|
|
6533
|
+
return {
|
|
6534
|
+
visible,
|
|
6535
|
+
unaddressable,
|
|
6536
|
+
buckets: [...buckets.values()].sort((a, b) => b.chars - a.chars).slice(0, MAX_BUCKETS)
|
|
6537
|
+
};
|
|
6538
|
+
}
|
|
6539
|
+
|
|
6379
6540
|
// src/carry.ts
|
|
6380
6541
|
var GLOBALS = /* @__PURE__ */ new Set([
|
|
6381
6542
|
"Astro",
|
|
@@ -6785,6 +6946,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
6785
6946
|
return held;
|
|
6786
6947
|
};
|
|
6787
6948
|
const fresh = /* @__PURE__ */ new Set();
|
|
6949
|
+
const freshFiles = /* @__PURE__ */ new Map();
|
|
6788
6950
|
const bindOf = (section) => instanceOwned(section) ? void 0 : section.groupKey;
|
|
6789
6951
|
const placementOf = (slug, section) => {
|
|
6790
6952
|
const bind = bindOf(section);
|
|
@@ -7112,6 +7274,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
7112
7274
|
}
|
|
7113
7275
|
extractable.set(resolved.slug, { name: resolved.name, path, kind: "default", imported: null });
|
|
7114
7276
|
fresh.add(resolved.slug);
|
|
7277
|
+
freshFiles.set(resolved.slug, path);
|
|
7115
7278
|
addFile(outFiles, contents, {
|
|
7116
7279
|
path,
|
|
7117
7280
|
operation: existing === void 0 ? "add" : "modify",
|
|
@@ -7197,6 +7360,7 @@ async function componentizeSources(briefIn, planIn, sources, options) {
|
|
|
7197
7360
|
sections,
|
|
7198
7361
|
// What THIS run wrote a file for. A component an earlier run left is reused, not re-reported.
|
|
7199
7362
|
components: [...fresh].sort(),
|
|
7363
|
+
componentFiles: [...freshFiles].sort(([a], [b]) => a.localeCompare(b)).map(([slug, path]) => ({ slug, path, export: "default" })),
|
|
7200
7364
|
planPending,
|
|
7201
7365
|
notes
|
|
7202
7366
|
}
|
|
@@ -7376,7 +7540,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
|
|
|
7376
7540
|
for (const section of sections) {
|
|
7377
7541
|
const paths = fieldPaths(section);
|
|
7378
7542
|
const literals = new Set(
|
|
7379
|
-
briefPaths.filter((p) => (p.scope ?? "page") === "page" &&
|
|
7543
|
+
briefPaths.filter((p) => (p.scope ?? "page") === "page" && inSection(section, p.path) && p.original).map((p) => flat(stripTags(p.original))).filter(Boolean)
|
|
7380
7544
|
);
|
|
7381
7545
|
const hits = /* @__PURE__ */ new Map();
|
|
7382
7546
|
for (const source of sources) {
|
|
@@ -7387,7 +7551,7 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
|
|
|
7387
7551
|
const found = /* @__PURE__ */ new Map();
|
|
7388
7552
|
for (const declaration2 of declarationsIn(parsed.roots, source.content)) {
|
|
7389
7553
|
if (declaration2.scope !== "page") continue;
|
|
7390
|
-
if (
|
|
7554
|
+
if (!inSection(section, declaration2.path)) continue;
|
|
7391
7555
|
const placed2 = elements.get(declaration2.at);
|
|
7392
7556
|
if (placed2) found.set(declaration2.at, placed2);
|
|
7393
7557
|
}
|
|
@@ -7412,6 +7576,12 @@ async function locateSections(sections, briefPaths, sources, treeOf) {
|
|
|
7412
7576
|
return { placed, misses };
|
|
7413
7577
|
}
|
|
7414
7578
|
var groupOf = (path) => path.split(/[.[]/)[0] ?? "";
|
|
7579
|
+
var inSection = (section, path) => {
|
|
7580
|
+
if (groupOf(path) === section.groupKey) return true;
|
|
7581
|
+
const rowless = (value) => value.replace(/\[\d+\]/g, "[]");
|
|
7582
|
+
const target = rowless(path);
|
|
7583
|
+
return fieldPaths(section).some((fieldPath) => rowless(fieldPath) === target);
|
|
7584
|
+
};
|
|
7415
7585
|
var fieldPaths = (section) => (section.fields ?? []).map((field) => field.path ?? `${section.groupKey}.${field.key ?? ""}`);
|
|
7416
7586
|
function elementIndex(roots) {
|
|
7417
7587
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -7514,6 +7684,9 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
|
|
|
7514
7684
|
...addresses.opaque
|
|
7515
7685
|
];
|
|
7516
7686
|
if (unconvertible.length > 0) return { content: "", free: [], duplicates: [], unconvertible };
|
|
7687
|
+
const flat3 = pathPrefix !== "" && !pathPrefix.endsWith(".");
|
|
7688
|
+
const pathMap = flat3 ? flatPathMap(resolved.section, pathPrefix) : null;
|
|
7689
|
+
if (flat3 && !pathMap) return { content: "", free: [], duplicates: [], unconvertible: [`${pathPrefix}* (a repeater leaf without an override address)`] };
|
|
7517
7690
|
const cut = content.slice(resolved.root.start, resolved.root.end);
|
|
7518
7691
|
const reserved = /* @__PURE__ */ new Set([...topLevelBindings(dialect, parsed, content), ...referencedNames2(cut)]);
|
|
7519
7692
|
const introduced = allocateNames(reserved, [
|
|
@@ -7532,7 +7705,7 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
|
|
|
7532
7705
|
const helpers = [
|
|
7533
7706
|
.../* @__PURE__ */ new Set([
|
|
7534
7707
|
...helperNames.map((h) => alias(h.name, h.local)),
|
|
7535
|
-
"bcmsSection",
|
|
7708
|
+
flat3 ? "bcmsSectionPaths" : "bcmsSection",
|
|
7536
7709
|
...bound > 0 ? [alias("bcmsFields", fieldsName)] : []
|
|
7537
7710
|
])
|
|
7538
7711
|
].sort();
|
|
@@ -7562,9 +7735,10 @@ function componentFile(dialect, path, fromFile, content, parsed, resolved, pathP
|
|
|
7562
7735
|
const key = /^[A-Za-z_$][\w$]*$/.test(groupKey) ? groupKey : JSON.stringify(groupKey);
|
|
7563
7736
|
const carriedDeclarations = carry.declarations.length > 0 ? `${carry.declarations.join("\n")}
|
|
7564
7737
|
` : "";
|
|
7565
|
-
const copy = snapshot ? `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) }
|
|
7566
|
-
const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)});` : "";
|
|
7738
|
+
const copy = !snapshot ? "" : flat3 ? `const ${snapshot} = bcmsSectionPaths(page, bind, overrides, ${JSON.stringify(pathPrefix)}, ${JSON.stringify(pathMap)});` : `const ${snapshot} = { ${key}: bcmsSection(page, bind, overrides) };`;
|
|
7739
|
+
const field = bound > 0 ? `const ${fieldName} = ${fieldsName}({ blockId, bind, overrides }, ${JSON.stringify(pathPrefix)}${flat3 ? `, ${JSON.stringify(pathMap)}` : ""});` : "";
|
|
7567
7740
|
const declared = [copy, field].filter(Boolean).join("\n");
|
|
7741
|
+
const styles = dialect === "astro" ? carriedStyles(content) : "";
|
|
7568
7742
|
if (dialect === "astro") {
|
|
7569
7743
|
return { free: [], duplicates: [], unconvertible: [], content: `---
|
|
7570
7744
|
${SECTION_MARKER}
|
|
@@ -7584,7 +7758,9 @@ const { blockId, bind, overrides, page } = Astro.props;
|
|
|
7584
7758
|
${carriedDeclarations}${declared}
|
|
7585
7759
|
---
|
|
7586
7760
|
${markup}
|
|
7587
|
-
`
|
|
7761
|
+
${styles ? `
|
|
7762
|
+
${styles}
|
|
7763
|
+
` : ""}` };
|
|
7588
7764
|
}
|
|
7589
7765
|
return { free: [], duplicates: [], unconvertible: [], content: `${SECTION_MARKER}
|
|
7590
7766
|
${[...carried, `import { ${helpers.join(", ")} } from "${helperSpecifier}";`, `import type { BcmsSectionProps } from "${relativeImport(path, SECTIONS_LIB).replace(/\.tsx?$/, "")}";`].join("\n")}
|
|
@@ -7858,6 +8034,9 @@ import { bcmsFields } from "${helper}";`);
|
|
|
7858
8034
|
return { files, refused };
|
|
7859
8035
|
}
|
|
7860
8036
|
function propPrefix(section) {
|
|
8037
|
+
return exactPrefix(section) ?? flatPrefix(section);
|
|
8038
|
+
}
|
|
8039
|
+
function exactPrefix(section) {
|
|
7861
8040
|
const prefixes = /* @__PURE__ */ new Set();
|
|
7862
8041
|
for (const field of section.fields ?? []) {
|
|
7863
8042
|
const { path, key } = field;
|
|
@@ -7868,6 +8047,49 @@ function propPrefix(section) {
|
|
|
7868
8047
|
if (prefixes.size > 1) return null;
|
|
7869
8048
|
return prefixes.size === 1 ? [...prefixes][0] : `${section.groupKey}.`;
|
|
7870
8049
|
}
|
|
8050
|
+
var foldKey = (remainder) => remainder.replace(/\[(\d+)\]/g, "_$1").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "value";
|
|
8051
|
+
function flatPrefix(section) {
|
|
8052
|
+
const prefixes = /* @__PURE__ */ new Set();
|
|
8053
|
+
let informative = false;
|
|
8054
|
+
for (const field of section.fields ?? []) {
|
|
8055
|
+
const { path, key } = field;
|
|
8056
|
+
if (typeof path !== "string" || typeof key !== "string") continue;
|
|
8057
|
+
informative = true;
|
|
8058
|
+
const prefix = ["-", "_"].map((sep2) => `${section.groupKey}${sep2}`).find((candidate) => path.startsWith(candidate) && (typeof field.overrideKey === "string" || foldKey(path.slice(candidate.length)) === key));
|
|
8059
|
+
if (!prefix) return null;
|
|
8060
|
+
prefixes.add(prefix);
|
|
8061
|
+
}
|
|
8062
|
+
return informative && prefixes.size === 1 ? [...prefixes][0] : null;
|
|
8063
|
+
}
|
|
8064
|
+
function flatPathMap(section, prefix) {
|
|
8065
|
+
const scalars = {};
|
|
8066
|
+
const tables = {};
|
|
8067
|
+
for (const field of section.fields ?? []) {
|
|
8068
|
+
const { path, key } = field;
|
|
8069
|
+
if (typeof path !== "string" || typeof key !== "string" || !path.startsWith(prefix)) continue;
|
|
8070
|
+
const row = /^([^[]+)\[\d+\]\.(.+)$/.exec(path);
|
|
8071
|
+
if (!row) {
|
|
8072
|
+
scalars[path] = field.overrideKey ?? key;
|
|
8073
|
+
continue;
|
|
8074
|
+
}
|
|
8075
|
+
const address = typeof field.overrideKey === "string" ? /^([^[]+)\[\d+\]\.(.+)$/.exec(field.overrideKey) : null;
|
|
8076
|
+
if (!address) return null;
|
|
8077
|
+
const table = tables[row[1]] ??= { key: address[1], columns: {} };
|
|
8078
|
+
if (table.key !== address[1]) return null;
|
|
8079
|
+
table.columns[row[2]] = address[2];
|
|
8080
|
+
}
|
|
8081
|
+
return { scalars, tables };
|
|
8082
|
+
}
|
|
8083
|
+
function carriedStyles(content) {
|
|
8084
|
+
const blocks = [];
|
|
8085
|
+
for (const match of content.matchAll(/<style\b([^>]*)>([\s\S]*?)<\/style>/g)) {
|
|
8086
|
+
const attributes = match[1] ?? "";
|
|
8087
|
+
if (/\bis:global\b|\bdefine:vars\b/.test(attributes)) continue;
|
|
8088
|
+
if (/\blang\s*=/.test(attributes) || /@(import|use|forward)\b/.test(match[2] ?? "")) continue;
|
|
8089
|
+
blocks.push(match[0]);
|
|
8090
|
+
}
|
|
8091
|
+
return blocks.join("\n");
|
|
8092
|
+
}
|
|
7871
8093
|
function referencedNames2(markup) {
|
|
7872
8094
|
const out = /* @__PURE__ */ new Set();
|
|
7873
8095
|
for (const [name] of markup.matchAll(/[A-Za-z_$][\w$]*/g)) out.add(name);
|
|
@@ -8256,7 +8478,8 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8256
8478
|
kind: p.kind,
|
|
8257
8479
|
fallback: p.original ?? "",
|
|
8258
8480
|
literal: norm(p.original ?? ""),
|
|
8259
|
-
locator: p.locator
|
|
8481
|
+
locator: p.locator,
|
|
8482
|
+
...p.inline ? { inline: true } : {}
|
|
8260
8483
|
});
|
|
8261
8484
|
}
|
|
8262
8485
|
}
|
|
@@ -8481,6 +8704,16 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8481
8704
|
declaredRow(target, witness !== null, witness ?? byHand);
|
|
8482
8705
|
} else rewritten++;
|
|
8483
8706
|
}
|
|
8707
|
+
const templated = dynamicBindings.filter((d) => d.prop === null);
|
|
8708
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
8709
|
+
const row = pending[i];
|
|
8710
|
+
const target = targets.get(keyOf(row.route, row.scope, row.path));
|
|
8711
|
+
if (row.reason !== "NO_ORIGINAL" || !target || target.fallback !== "") continue;
|
|
8712
|
+
const shape = shapeOfPath(target.path);
|
|
8713
|
+
if (!templated.some((d) => d.path === shape && attributable(target, d.file))) continue;
|
|
8714
|
+
pending.splice(i, 1);
|
|
8715
|
+
rewritten++;
|
|
8716
|
+
}
|
|
8484
8717
|
for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
|
|
8485
8718
|
const source = new MagicString6(contents.get(file));
|
|
8486
8719
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -8793,6 +9026,10 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8793
9026
|
}
|
|
8794
9027
|
let refusal = null;
|
|
8795
9028
|
for (const site of fresh) {
|
|
9029
|
+
if (target.inline && site.where === "text" && !ownsSubtree(target.kind, site, true)) {
|
|
9030
|
+
refusal = "SUBTREE_NOT_HTML";
|
|
9031
|
+
continue;
|
|
9032
|
+
}
|
|
8796
9033
|
let bindings;
|
|
8797
9034
|
let bindingsMerge;
|
|
8798
9035
|
if (site.where === "prop" && site.attr) {
|
|
@@ -8827,7 +9064,8 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8827
9064
|
// 🔴 AND ONLY WHEN THE ELEMENT'S SUBTREE IS THE VALUE. A value that is one TEXT NODE of
|
|
8828
9065
|
// a mixed element gets a wrapper of its own, so taking the element's inner range as its
|
|
8829
9066
|
// fallback swallowed the `<em>` beside it — the wrapper then rendered the sibling twice.
|
|
8830
|
-
|
|
9067
|
+
...target.inline ? { inline: true } : {},
|
|
9068
|
+
fallback: site.inner && ownsSubtree(target.kind, site, target.inline) ? content.slice(site.inner.start, site.inner.end).trim() : target.fallback,
|
|
8831
9069
|
snapshot: snapshotName(target.slug),
|
|
8832
9070
|
slug: target.slug,
|
|
8833
9071
|
scope: target.scope,
|
|
@@ -9188,7 +9426,7 @@ function emitStubs2(dialects, slugs, contents, out) {
|
|
|
9188
9426
|
// package.json
|
|
9189
9427
|
var package_default = {
|
|
9190
9428
|
name: "@bettercms-ai/convert",
|
|
9191
|
-
version: "0.
|
|
9429
|
+
version: "0.11.0",
|
|
9192
9430
|
type: "module",
|
|
9193
9431
|
description: "Deterministic codemod that turns an imported site's text-matched bindings into declared ones \u2014 parser offsets in, one magic-string pass out, plus a receipt of everything it could not do.",
|
|
9194
9432
|
bin: {
|
|
@@ -9249,6 +9487,7 @@ function parseArgs(argv) {
|
|
|
9249
9487
|
brief: void 0,
|
|
9250
9488
|
componentize: false,
|
|
9251
9489
|
forms: false,
|
|
9490
|
+
xray: void 0,
|
|
9252
9491
|
plan: void 0,
|
|
9253
9492
|
copy: "bind",
|
|
9254
9493
|
root: ".",
|
|
@@ -9264,6 +9503,7 @@ function parseArgs(argv) {
|
|
|
9264
9503
|
if (arg === "--brief") args.brief = argv[++i];
|
|
9265
9504
|
else if (arg === "--componentize") args.componentize = true;
|
|
9266
9505
|
else if (arg === "--forms") args.forms = true;
|
|
9506
|
+
else if (arg === "--xray") args.xray = argv[++i];
|
|
9267
9507
|
else if (arg === "--plan") args.plan = argv[++i];
|
|
9268
9508
|
else if (arg === "--copy") {
|
|
9269
9509
|
const mode = argv[++i];
|
|
@@ -9291,6 +9531,7 @@ var HELP = `bettercms-convert ${VERSION}
|
|
|
9291
9531
|
bettercms-convert --brief <json> --root . [options]
|
|
9292
9532
|
bettercms-convert --componentize --brief <json> --plan <json> --root . [options]
|
|
9293
9533
|
bettercms-convert --forms --brief <json> --root . [options]
|
|
9534
|
+
bettercms-convert --xray <dist-dir>
|
|
9294
9535
|
|
|
9295
9536
|
--brief <file> The conversion brief, as get_conversion_brief returns it. Required.
|
|
9296
9537
|
--componentize Lift each planned SECTION into a component the CMS can reorder.
|
|
@@ -9298,6 +9539,11 @@ var HELP = `bettercms-convert ${VERSION}
|
|
|
9298
9539
|
endpoint, the id, a marker per field and the submit script. A form
|
|
9299
9540
|
the brief lists as a DRAFT is wired and receipted: publish it in the
|
|
9300
9541
|
Forms tab, or it rejects every submission with a 403.
|
|
9542
|
+
--xray <dist-dir> Measure a LOCAL build instead of converting: how much of each page's
|
|
9543
|
+
visible copy no binding owns, and the biggest buckets of it. Run it
|
|
9544
|
+
after a build to see what the next conversion still has to reach,
|
|
9545
|
+
without deploying. Takes no --brief. Exit 2 with --strict when any
|
|
9546
|
+
page is above 2 %.
|
|
9301
9547
|
--plan <file> The componentize plan, as get_componentize_plan returns it.
|
|
9302
9548
|
Required with --componentize.
|
|
9303
9549
|
--copy bind|instance Which copy model the plan is expected to describe. Default: bind
|
|
@@ -9418,12 +9664,53 @@ ${extracted} sections extracted (${registered} registered, ${inlineOnly} inline-
|
|
|
9418
9664
|
);
|
|
9419
9665
|
}
|
|
9420
9666
|
}
|
|
9667
|
+
async function xray(dir, strict) {
|
|
9668
|
+
const pages = [];
|
|
9669
|
+
const walk = async (at) => {
|
|
9670
|
+
for (const entry of (await readdir(at, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
9671
|
+
const full = join2(at, entry.name);
|
|
9672
|
+
if (entry.isDirectory()) await walk(full);
|
|
9673
|
+
else if (/\.html?$/i.test(entry.name)) pages.push(full);
|
|
9674
|
+
}
|
|
9675
|
+
};
|
|
9676
|
+
try {
|
|
9677
|
+
await walk(dir);
|
|
9678
|
+
} catch (error) {
|
|
9679
|
+
throw new CliError(`${dir} could not be read (${error.message}). Build the site first.`);
|
|
9680
|
+
}
|
|
9681
|
+
if (pages.length === 0) throw new CliError(`No .html files under ${dir}. Is that the build output?`);
|
|
9682
|
+
let over = 0;
|
|
9683
|
+
for (const page of pages) {
|
|
9684
|
+
const { visible, unaddressable, buckets } = measureUnaddressable(await readFile(page, "utf-8"));
|
|
9685
|
+
const share = visible === 0 ? 0 : 100 * unaddressable / visible;
|
|
9686
|
+
if (share > 2) over++;
|
|
9687
|
+
process.stdout.write(
|
|
9688
|
+
`${relative(dir, page) || page} visible=${visible} unaddressable=${unaddressable} (${share.toFixed(1)}%)
|
|
9689
|
+
`
|
|
9690
|
+
);
|
|
9691
|
+
for (const bucket of buckets) {
|
|
9692
|
+
const samples = bucket.samples.map((s) => JSON.stringify(s)).join(" | ");
|
|
9693
|
+
process.stdout.write(
|
|
9694
|
+
` ${String(bucket.chars).padStart(6)}ch ${String(bucket.nodes).padStart(4)}n ${bucket.tag} @ ${bucket.context} e.g. ${samples}
|
|
9695
|
+
`
|
|
9696
|
+
);
|
|
9697
|
+
}
|
|
9698
|
+
}
|
|
9699
|
+
process.stdout.write(`
|
|
9700
|
+
${pages.length - over} of ${pages.length} pages at or under 2% unaddressable
|
|
9701
|
+
`);
|
|
9702
|
+
return strict && over > 0 ? 2 : 0;
|
|
9703
|
+
}
|
|
9421
9704
|
async function main() {
|
|
9422
9705
|
const args = parseArgs(process.argv.slice(2));
|
|
9423
9706
|
if (args.help) {
|
|
9424
9707
|
process.stdout.write(HELP);
|
|
9425
9708
|
return 0;
|
|
9426
9709
|
}
|
|
9710
|
+
if (args.xray) {
|
|
9711
|
+
if (args.brief) throw new CliError("--xray measures a build; it reads no brief. See --help.");
|
|
9712
|
+
return await xray(resolve(args.xray), args.strict);
|
|
9713
|
+
}
|
|
9427
9714
|
if (!args.brief) throw new CliError("--brief <file> is required. See --help.");
|
|
9428
9715
|
if (args.plan && !args.componentize) {
|
|
9429
9716
|
throw new CliError("--plan is only read with --componentize. See --help.");
|