@bettercms-ai/convert 0.9.0 → 0.10.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 +18 -0
- package/README.md +23 -0
- package/dist/cli.js +164 -15
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +66 -4
- package/dist/index.js +116 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,18 @@ interface BriefPath {
|
|
|
37
37
|
kind: string;
|
|
38
38
|
/** The declared field type, which `kind` deliberately loses (richtext vs document). */
|
|
39
39
|
type: string;
|
|
40
|
+
/**
|
|
41
|
+
* The richtext value IS the element's whole inline subtree — the field's `config.inline`.
|
|
42
|
+
*
|
|
43
|
+
* A heading with a styled run (`<h1>Notes from a <span class="serif">web studio</span></h1>`)
|
|
44
|
+
* derives as ONE richtext field whose value is that innerHTML, not as the text node beside the
|
|
45
|
+
* span. Without this the codemod cannot tell it from the icon+text shape, whose value really is
|
|
46
|
+
* one text node — and it wraps the text node in a `<span>`, leaving the styled run rendered
|
|
47
|
+
* twice. Optional on purpose: a brief written before the distinction existed carries none, and
|
|
48
|
+
* the old wrap-the-text-node reading is still the right answer for it.
|
|
49
|
+
* @see derive-schema.ts textType, which mints `config: { inline: true }` for exactly this shape.
|
|
50
|
+
*/
|
|
51
|
+
inline?: true;
|
|
40
52
|
label: string;
|
|
41
53
|
/** The copy the REPO renders today. Null when the import captured no default. */
|
|
42
54
|
original: string | null;
|
|
@@ -142,8 +154,13 @@ interface UnlocatedPath {
|
|
|
142
154
|
* correctly formatted file, which is every file.
|
|
143
155
|
*/
|
|
144
156
|
declare const flat: (value: string) => string;
|
|
145
|
-
/**
|
|
146
|
-
|
|
157
|
+
/**
|
|
158
|
+
* The same text with tags removed — what a reader sees, not what the file says.
|
|
159
|
+
*
|
|
160
|
+
* `br` is what a line break becomes. A SPACE is what a reader sees; the empty string is what
|
|
161
|
+
* `textContent` sees, and the matcher works on the latter. @see locate, which searches both.
|
|
162
|
+
*/
|
|
163
|
+
declare const stripTags: (value: string, br?: string) => string;
|
|
147
164
|
interface BriefLike {
|
|
148
165
|
pages: {
|
|
149
166
|
route: string;
|
|
@@ -192,7 +209,9 @@ declare function locate(brief: BriefLike, sources: SourceFile[], options?: Locat
|
|
|
192
209
|
* sentence appears in three files" and "this field is bound" are different questions — and a path
|
|
193
210
|
* is only `rewritten` when EVERY located occurrence of it was.
|
|
194
211
|
*/
|
|
195
|
-
type PendingReason = "NO_ORIGINAL" | "NOT_IN_SOURCE" | "DIALECT_UNSUPPORTED" | "IN_EXPRESSION" | "IN_SCRIPT_OR_COMMENT" | "IN_DATA_FILE" | "AMBIGUOUS_LITERAL" | "KIND_MISMATCH" | "SUBSTRING_ONLY"
|
|
212
|
+
type PendingReason = "NO_ORIGINAL" | "NOT_IN_SOURCE" | "DIALECT_UNSUPPORTED" | "IN_EXPRESSION" | "IN_SCRIPT_OR_COMMENT" | "IN_DATA_FILE" | "AMBIGUOUS_LITERAL" | "KIND_MISMATCH" | "SUBSTRING_ONLY"
|
|
213
|
+
/** An INLINE richtext value whose element holds a component, a `<slot>` or an expression. */
|
|
214
|
+
| "SUBTREE_NOT_HTML" | "PROP_TARGET_NOT_FOUND" | "PROP_DRILLED_DEEP" | "REPEATER_FIXED_LENGTH"
|
|
196
215
|
/** One array declaration, two loops over it: the row this leaf belongs to has two homes. */
|
|
197
216
|
| "REPEATER_AMBIGUOUS"
|
|
198
217
|
/** An image whose built url maps to no repository asset, or to more than one. */
|
|
@@ -515,6 +534,12 @@ interface Site {
|
|
|
515
534
|
literal: string;
|
|
516
535
|
/** The literal is only PART of what this element renders; rewriting it would eat the rest. */
|
|
517
536
|
partial: boolean;
|
|
537
|
+
/**
|
|
538
|
+
* The subtree holds markup only the FRAMEWORK renders — a component, a `<slot>`, an expression.
|
|
539
|
+
*
|
|
540
|
+
* Absent on every site whose children are plain html, which is the common case. @see ownsSubtree
|
|
541
|
+
*/
|
|
542
|
+
nonHtml?: true;
|
|
518
543
|
/** Tag chain from the document root down to the element's parent, outermost first. */
|
|
519
544
|
ancestors: string[];
|
|
520
545
|
/** Index among the element's ELEMENT siblings. */
|
|
@@ -629,6 +654,8 @@ interface Rewrite {
|
|
|
629
654
|
path: string;
|
|
630
655
|
/** The brief's kind: "text" | "richtext" | "image". */
|
|
631
656
|
kind: string;
|
|
657
|
+
/** The richtext value is the element's whole inline subtree. @see BriefPath.inline */
|
|
658
|
+
inline?: true;
|
|
632
659
|
/** The copy the repo renders today, kept as the in-code fallback. */
|
|
633
660
|
fallback: string;
|
|
634
661
|
/** The identifier this page's snapshot is imported under. */
|
|
@@ -1132,6 +1159,41 @@ declare const SOURCE_EXTENSIONS: string[];
|
|
|
1132
1159
|
/** Is this repository-relative path a file worth opening? */
|
|
1133
1160
|
declare function isSourceCandidate(path: string): boolean;
|
|
1134
1161
|
|
|
1162
|
+
/**
|
|
1163
|
+
* Text that is NOT copy, and is therefore outside both halves of the ratio.
|
|
1164
|
+
*
|
|
1165
|
+
* 🔴 IN THE DENOMINATOR TOO. Counting a `✓`, a `01`, the platform badge or a skip link as
|
|
1166
|
+
* unaddressable text would put a floor under every route's percentage that no amount of binding
|
|
1167
|
+
* could lift — and the target is a number someone has to be able to reach. They are not copy an
|
|
1168
|
+
* author edits, so they are not copy this measures.
|
|
1169
|
+
*
|
|
1170
|
+
* - a decorative glyph: two characters or fewer, a single non-letter symbol, or an ordinal
|
|
1171
|
+
* (`01`, `2.`, `3 /`) that numbers a list rather than saying anything;
|
|
1172
|
+
* - the platform badge every free site renders;
|
|
1173
|
+
* - a skip link, which exists for keyboards and is never edited.
|
|
1174
|
+
*/
|
|
1175
|
+
declare const isDecorativeGlyph: (text: string) => boolean;
|
|
1176
|
+
interface XrayBucket {
|
|
1177
|
+
/** The element that renders it. */
|
|
1178
|
+
tag: string;
|
|
1179
|
+
/** Its landmark chain, outermost first — `main>ul`. */
|
|
1180
|
+
context: string;
|
|
1181
|
+
chars: number;
|
|
1182
|
+
nodes: number;
|
|
1183
|
+
/** Up to two examples, ≤ 50 characters each. */
|
|
1184
|
+
samples: string[];
|
|
1185
|
+
}
|
|
1186
|
+
interface Xray {
|
|
1187
|
+
/** Characters of visible copy, decorative glyphs excluded. */
|
|
1188
|
+
visible: number;
|
|
1189
|
+
/** Of those, the characters no binding owns. */
|
|
1190
|
+
unaddressable: number;
|
|
1191
|
+
/** The biggest buckets, largest first — at most 12. */
|
|
1192
|
+
buckets: XrayBucket[];
|
|
1193
|
+
}
|
|
1194
|
+
/** What a reader sees on this page, and how much of it no field owns. */
|
|
1195
|
+
declare function measureUnaddressable(html: string): Xray;
|
|
1196
|
+
|
|
1135
1197
|
/**
|
|
1136
1198
|
* The project's own `compilerOptions.paths`, when a tsconfig is among the files.
|
|
1137
1199
|
*
|
|
@@ -1644,4 +1706,4 @@ declare function convertSources(briefIn: Brief, sources: SourceFile[], options?:
|
|
|
1644
1706
|
receipt: ConversionReceipt;
|
|
1645
1707
|
}>;
|
|
1646
1708
|
|
|
1647
|
-
export { ASTRO_LANE_PENDING, type AstNode, type AttrBinding, type Brief, type BriefForm, type BriefPage, type BriefPath, COMPONENT_DIR, CONTENT_DIR, type CanvasResult, type CanvasSource, type Carried, type ComponentizeOptions, type ComponentizePlan, type ComponentizePlanComponent, type ComponentizePlanField, type ComponentizePlanPage, type ComponentizePlanSection, type ComponentizeReceipt, type ConversionReceipt, ConvertError, type ConvertErrorCode, type ConvertOptions, DIALECT_RULES, type DataMember, type DataResult, type DeclaredPath, type Dialect, type DynamicBinding, FORM_SCRIPT, type FileDeclaration, type FindSitesResult, FormInvariantError, type FormPendingReason, type FormsReceipt, HELPER_PATH, HELPER_VERSION, type ImageResult, type ImageTarget, type ImportedFrom, type LlmFallback, type LocatedPath, type Node, PARSE_FILE, type ParsedFile, type ParserError, type PathLocator, type PendingForm, type PendingPath, type PendingReason, type PendingSection, type PlanFile, REGISTRY_MARKER, type ReceiptFile, ReceiptInvariantError, type ReceiptNote, type Rewrite, SECTIONS_LIB, SECTION_MARKER, SKIP_DIRS, SKIP_FILES, SOURCE_EXTENSIONS, SSR_DRAFT_RECIPE, STUB_CONTENT, SectionInvariantError, type SectionPendingReason, type SectionsReceipt, type Site, type SiteWhere, type SourceFile, type Splice, type TargetIdentity, type UnlocatedPath, type ValidationCode, ValidationError, aliasesFrom, assertForms, assertReceipt, assertSections, assetCandidates, assetImport, attrsFor, bindDataLiterals, bindImages, briefDigest, canvasBridge, carryFor, componentizeSources, convertSources, convertedHere, coverageOf, declaringElements, dialectOf, exportedFunction, extendedConfigs, findSites, findSitesTolerant, flat, helperSource, importsIn, isDynamicRoute, isKnownHelper, isSourceCandidate, locate, overlapping, pageFilesFor, parseFile, propsAttribute, readBrief, readComponentizePlan, readDeclarations, readExpr, readPlan, relativeImport, resolveSpecifier, rewriteFile, routeOfFile, stripTags, unreadableConfigs, walkAst, wireForms };
|
|
1709
|
+
export { ASTRO_LANE_PENDING, type AstNode, type AttrBinding, type Brief, type BriefForm, type BriefPage, type BriefPath, COMPONENT_DIR, CONTENT_DIR, type CanvasResult, type CanvasSource, type Carried, type ComponentizeOptions, type ComponentizePlan, type ComponentizePlanComponent, type ComponentizePlanField, type ComponentizePlanPage, type ComponentizePlanSection, type ComponentizeReceipt, type ConversionReceipt, ConvertError, type ConvertErrorCode, type ConvertOptions, DIALECT_RULES, type DataMember, type DataResult, type DeclaredPath, type Dialect, type DynamicBinding, FORM_SCRIPT, type FileDeclaration, type FindSitesResult, FormInvariantError, type FormPendingReason, type FormsReceipt, HELPER_PATH, HELPER_VERSION, type ImageResult, type ImageTarget, type ImportedFrom, type LlmFallback, type LocatedPath, type Node, PARSE_FILE, type ParsedFile, type ParserError, type PathLocator, type PendingForm, type PendingPath, type PendingReason, type PendingSection, type PlanFile, REGISTRY_MARKER, type ReceiptFile, ReceiptInvariantError, type ReceiptNote, type Rewrite, SECTIONS_LIB, SECTION_MARKER, SKIP_DIRS, SKIP_FILES, SOURCE_EXTENSIONS, SSR_DRAFT_RECIPE, STUB_CONTENT, SectionInvariantError, type SectionPendingReason, type SectionsReceipt, type Site, type SiteWhere, type SourceFile, type Splice, type TargetIdentity, type UnlocatedPath, type ValidationCode, ValidationError, type Xray, type XrayBucket, aliasesFrom, assertForms, assertReceipt, assertSections, assetCandidates, assetImport, attrsFor, bindDataLiterals, bindImages, briefDigest, canvasBridge, carryFor, componentizeSources, convertSources, convertedHere, coverageOf, declaringElements, dialectOf, exportedFunction, extendedConfigs, findSites, findSitesTolerant, flat, helperSource, importsIn, isDecorativeGlyph, isDynamicRoute, isKnownHelper, isSourceCandidate, locate, measureUnaddressable, overlapping, pageFilesFor, parseFile, propsAttribute, readBrief, readComponentizePlan, readDeclarations, readExpr, readPlan, relativeImport, resolveSpecifier, rewriteFile, routeOfFile, stripTags, unreadableConfigs, walkAst, wireForms };
|
package/dist/index.js
CHANGED
|
@@ -2560,6 +2560,7 @@ 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) => kind === "richtext" && (inline === true ? !site.nonHtml : !(site.mixed && site.textRange !== null));
|
|
2563
2564
|
var norm = (value) => decodeEntities(value.normalize("NFC")).replace(/\s+/g, " ").trim();
|
|
2564
2565
|
var BCMS_ATTRS = [
|
|
2565
2566
|
"data-bcms-field",
|
|
@@ -2592,6 +2593,7 @@ function openTagEnd(content, start) {
|
|
|
2592
2593
|
}
|
|
2593
2594
|
return content.length;
|
|
2594
2595
|
}
|
|
2596
|
+
var OPAQUE_TAGS = /* @__PURE__ */ new Set(["pre"]);
|
|
2595
2597
|
var isBlank = (value) => value.trim() === "";
|
|
2596
2598
|
function renderedText(node) {
|
|
2597
2599
|
if (node.type === "text") return node.value;
|
|
@@ -2599,6 +2601,11 @@ function renderedText(node) {
|
|
|
2599
2601
|
return "";
|
|
2600
2602
|
}
|
|
2601
2603
|
var attrsOf = (node) => Object.fromEntries(node.attrs.map((a) => [a.name, a.value]));
|
|
2604
|
+
function nonHtmlSubtree(node) {
|
|
2605
|
+
return node.children.some(
|
|
2606
|
+
(c) => c.type === "expr" || c.type === "element" && (!isIntrinsic(c.tag) || c.tag === "slot" || nonHtmlSubtree(c))
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2602
2609
|
function soleTextChild(node) {
|
|
2603
2610
|
const texts = node.children.filter((c) => c.type === "text" && !isBlank(c.value));
|
|
2604
2611
|
return texts.length === 1 ? texts[0] : null;
|
|
@@ -2635,10 +2642,12 @@ function collectSites(roots, literals) {
|
|
|
2635
2642
|
if (node.type !== "element") return matched;
|
|
2636
2643
|
const attrs = attrsOf(node);
|
|
2637
2644
|
let index = 0;
|
|
2638
|
-
|
|
2639
|
-
const
|
|
2640
|
-
|
|
2641
|
-
|
|
2645
|
+
if (!OPAQUE_TAGS.has(node.tag)) {
|
|
2646
|
+
for (const child of node.children) {
|
|
2647
|
+
const childWalk = { ancestors: [...walk.ancestors, node.tag], siblingIndex: index };
|
|
2648
|
+
if (child.type === "element") index++;
|
|
2649
|
+
for (const hit of visit(child, childWalk)) matched.add(hit);
|
|
2650
|
+
}
|
|
2642
2651
|
}
|
|
2643
2652
|
if (node.raw) {
|
|
2644
2653
|
const text2 = norm(renderedTextRaw(node));
|
|
@@ -2664,6 +2673,7 @@ function collectSites(roots, literals) {
|
|
|
2664
2673
|
textRange: soleTextChild(node)?.range ?? null,
|
|
2665
2674
|
literal,
|
|
2666
2675
|
partial: false,
|
|
2676
|
+
...nonHtmlSubtree(node) ? { nonHtml: true } : {},
|
|
2667
2677
|
ancestors: walk.ancestors,
|
|
2668
2678
|
siblingIndex: walk.siblingIndex
|
|
2669
2679
|
});
|
|
@@ -2689,6 +2699,7 @@ function collectSites(roots, literals) {
|
|
|
2689
2699
|
textRange: own.range,
|
|
2690
2700
|
literal,
|
|
2691
2701
|
partial: false,
|
|
2702
|
+
...nonHtmlSubtree(node) ? { nonHtml: true } : {},
|
|
2692
2703
|
ancestors: walk.ancestors,
|
|
2693
2704
|
siblingIndex: walk.siblingIndex
|
|
2694
2705
|
});
|
|
@@ -2739,10 +2750,12 @@ function partials(node, literals, walk, out) {
|
|
|
2739
2750
|
if (node.type !== "element" || node.raw) return false;
|
|
2740
2751
|
let deeper = false;
|
|
2741
2752
|
let index = 0;
|
|
2742
|
-
|
|
2743
|
-
const
|
|
2744
|
-
|
|
2745
|
-
|
|
2753
|
+
if (!OPAQUE_TAGS.has(node.tag)) {
|
|
2754
|
+
for (const child of node.children) {
|
|
2755
|
+
const childWalk = { ancestors: [...walk.ancestors, node.tag], siblingIndex: index };
|
|
2756
|
+
if (child.type === "element") index++;
|
|
2757
|
+
if (partials(child, literals, childWalk, out)) deeper = true;
|
|
2758
|
+
}
|
|
2746
2759
|
}
|
|
2747
2760
|
if (deeper) return true;
|
|
2748
2761
|
const text = norm(renderedText(node));
|
|
@@ -4382,14 +4395,23 @@ function isKnownHelper(content, dialect) {
|
|
|
4382
4395
|
|
|
4383
4396
|
// src/locate.ts
|
|
4384
4397
|
var flat = (value) => value.replace(/\s+/g, " ").trim();
|
|
4385
|
-
var stripTags = (value) => value.replace(/<br\s*\/?>/gi,
|
|
4398
|
+
var stripTags = (value, br = " ") => value.replace(/<br\s*\/?>/gi, br).replace(/<[^>]*>/g, "");
|
|
4386
4399
|
function locate(brief, sources, options) {
|
|
4387
4400
|
const flattened = sources.map((s) => {
|
|
4388
4401
|
const readable = unwrapJsxStrings(decodeEntities(s.content));
|
|
4389
4402
|
return {
|
|
4390
4403
|
path: s.path,
|
|
4391
4404
|
content: flat(readable),
|
|
4392
|
-
text: options?.stripTags ? flat(stripTags(readable)) : null
|
|
4405
|
+
text: options?.stripTags ? flat(stripTags(readable)) : null,
|
|
4406
|
+
/**
|
|
4407
|
+
* 🔴 A `<br>` IS A SPACE TO A READER AND NOTHING TO `textContent`, and the derive lane reads
|
|
4408
|
+
* the DOM. `<h1>Launch your site<br /><span>fast.</span></h1>` is one INLINE richtext field
|
|
4409
|
+
* whose value is `Launch your sitefast.` — no space, because no text node holds one — while
|
|
4410
|
+
* this prefilter's only reading inserted one, so the file was never opened and every split
|
|
4411
|
+
* heading on every imported site came back NOT_IN_SOURCE. Both readings are searched: this
|
|
4412
|
+
* decides which FILES to open, and `collectSites` still has to match an element exactly.
|
|
4413
|
+
*/
|
|
4414
|
+
tight: options?.stripTags ? flat(stripTags(readable, "")) : null
|
|
4393
4415
|
};
|
|
4394
4416
|
});
|
|
4395
4417
|
const located = [];
|
|
@@ -4401,7 +4423,9 @@ function locate(brief, sources, options) {
|
|
|
4401
4423
|
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "no-original" });
|
|
4402
4424
|
continue;
|
|
4403
4425
|
}
|
|
4404
|
-
const files = flattened.filter(
|
|
4426
|
+
const files = flattened.filter(
|
|
4427
|
+
(f) => f.content.includes(original) || f.text !== null && f.text.includes(original) || f.tight !== null && f.tight.includes(original)
|
|
4428
|
+
).map((f) => f.path);
|
|
4405
4429
|
if (files.length === 0) {
|
|
4406
4430
|
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "not-in-source" });
|
|
4407
4431
|
continue;
|
|
@@ -5100,7 +5124,7 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
|
|
|
5100
5124
|
addDeclaration(site.attrInsertAt, declaration(rewrite));
|
|
5101
5125
|
continue;
|
|
5102
5126
|
}
|
|
5103
|
-
if (kind
|
|
5127
|
+
if (ownsSubtree(kind, site, rewrite.inline)) {
|
|
5104
5128
|
if (!isIntrinsic(site.tag)) continue;
|
|
5105
5129
|
if (rule.richtext && site.inner) {
|
|
5106
5130
|
source.remove(site.inner.start, site.inner.end);
|
|
@@ -6386,6 +6410,66 @@ function isSourceCandidate(path) {
|
|
|
6386
6410
|
return lower.split("/").slice(0, -1).some((segment) => TEMPLATE_DIRS.has(segment));
|
|
6387
6411
|
}
|
|
6388
6412
|
|
|
6413
|
+
// src/xray.ts
|
|
6414
|
+
var SKIP_TAGS = /* @__PURE__ */ new Set(["svg", "iframe", "head", "title"]);
|
|
6415
|
+
var CONTEXT_TAGS = /* @__PURE__ */ new Set([
|
|
6416
|
+
"nav",
|
|
6417
|
+
"header",
|
|
6418
|
+
"footer",
|
|
6419
|
+
"main",
|
|
6420
|
+
"aside",
|
|
6421
|
+
"form",
|
|
6422
|
+
"button",
|
|
6423
|
+
"a",
|
|
6424
|
+
"ul",
|
|
6425
|
+
"ol",
|
|
6426
|
+
"table",
|
|
6427
|
+
"dialog",
|
|
6428
|
+
"details",
|
|
6429
|
+
"figure",
|
|
6430
|
+
"blockquote"
|
|
6431
|
+
]);
|
|
6432
|
+
var BOUND_ATTRS = ["data-bcms-field", "data-bcms-layout-field", "data-bcms-path", "data-bcms-block"];
|
|
6433
|
+
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);
|
|
6434
|
+
var MAX_BUCKETS = 12;
|
|
6435
|
+
var flat2 = (value) => value.replace(/\s+/g, " ").trim();
|
|
6436
|
+
function measureUnaddressable(html) {
|
|
6437
|
+
const parsed = parse2(html);
|
|
6438
|
+
let visible = 0;
|
|
6439
|
+
let unaddressable = 0;
|
|
6440
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
6441
|
+
const visit = (node, tag, context, bound) => {
|
|
6442
|
+
if (node.type === "text") {
|
|
6443
|
+
const text = flat2(node.value);
|
|
6444
|
+
if (!text || isDecorativeGlyph(text)) return;
|
|
6445
|
+
visible += text.length;
|
|
6446
|
+
if (bound) return;
|
|
6447
|
+
unaddressable += text.length;
|
|
6448
|
+
const key = `${tag} @ ${context}`;
|
|
6449
|
+
const held = buckets.get(key) ?? { tag, context, chars: 0, nodes: 0, samples: [] };
|
|
6450
|
+
held.chars += text.length;
|
|
6451
|
+
held.nodes += 1;
|
|
6452
|
+
if (held.samples.length < 2) held.samples.push(text.slice(0, 50));
|
|
6453
|
+
buckets.set(key, held);
|
|
6454
|
+
return;
|
|
6455
|
+
}
|
|
6456
|
+
if (node.type !== "element" || node.raw || SKIP_TAGS.has(node.tag)) return;
|
|
6457
|
+
const attrs = new Map(node.attrs.map((a) => [a.name, a.value]));
|
|
6458
|
+
if (attrs.has("hidden") || attrs.get("aria-hidden") === "true" || /display\s*:\s*none/.test(attrs.get("style") ?? "")) {
|
|
6459
|
+
return;
|
|
6460
|
+
}
|
|
6461
|
+
const owned = bound || BOUND_ATTRS.some((name) => attrs.has(name));
|
|
6462
|
+
const chain = CONTEXT_TAGS.has(node.tag) ? context === "body" ? node.tag : `${context}>${node.tag}` : context;
|
|
6463
|
+
for (const child of node.children) visit(child, node.tag, chain, owned);
|
|
6464
|
+
};
|
|
6465
|
+
for (const root of parsed.roots) visit(root, "body", "body", false);
|
|
6466
|
+
return {
|
|
6467
|
+
visible,
|
|
6468
|
+
unaddressable,
|
|
6469
|
+
buckets: [...buckets.values()].sort((a, b) => b.chars - a.chars).slice(0, MAX_BUCKETS)
|
|
6470
|
+
};
|
|
6471
|
+
}
|
|
6472
|
+
|
|
6389
6473
|
// src/declared.ts
|
|
6390
6474
|
async function readDeclarations(file, content) {
|
|
6391
6475
|
const dialect = dialectOf(file, content);
|
|
@@ -8274,7 +8358,8 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8274
8358
|
kind: p.kind,
|
|
8275
8359
|
fallback: p.original ?? "",
|
|
8276
8360
|
literal: norm(p.original ?? ""),
|
|
8277
|
-
locator: p.locator
|
|
8361
|
+
locator: p.locator,
|
|
8362
|
+
...p.inline ? { inline: true } : {}
|
|
8278
8363
|
});
|
|
8279
8364
|
}
|
|
8280
8365
|
}
|
|
@@ -8499,6 +8584,16 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8499
8584
|
declaredRow(target, witness !== null, witness ?? byHand);
|
|
8500
8585
|
} else rewritten++;
|
|
8501
8586
|
}
|
|
8587
|
+
const templated = dynamicBindings.filter((d) => d.prop === null);
|
|
8588
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
8589
|
+
const row = pending[i];
|
|
8590
|
+
const target = targets.get(keyOf(row.route, row.scope, row.path));
|
|
8591
|
+
if (row.reason !== "NO_ORIGINAL" || !target || target.fallback !== "") continue;
|
|
8592
|
+
const shape = shapeOfPath(target.path);
|
|
8593
|
+
if (!templated.some((d) => d.path === shape && attributable(target, d.file))) continue;
|
|
8594
|
+
pending.splice(i, 1);
|
|
8595
|
+
rewritten++;
|
|
8596
|
+
}
|
|
8502
8597
|
for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
|
|
8503
8598
|
const source = new MagicString6(contents.get(file));
|
|
8504
8599
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -8811,6 +8906,10 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8811
8906
|
}
|
|
8812
8907
|
let refusal = null;
|
|
8813
8908
|
for (const site of fresh) {
|
|
8909
|
+
if (target.inline && site.where === "text" && !ownsSubtree(target.kind, site, true)) {
|
|
8910
|
+
refusal = "SUBTREE_NOT_HTML";
|
|
8911
|
+
continue;
|
|
8912
|
+
}
|
|
8814
8913
|
let bindings;
|
|
8815
8914
|
let bindingsMerge;
|
|
8816
8915
|
if (site.where === "prop" && site.attr) {
|
|
@@ -8845,7 +8944,8 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8845
8944
|
// 🔴 AND ONLY WHEN THE ELEMENT'S SUBTREE IS THE VALUE. A value that is one TEXT NODE of
|
|
8846
8945
|
// a mixed element gets a wrapper of its own, so taking the element's inner range as its
|
|
8847
8946
|
// fallback swallowed the `<em>` beside it — the wrapper then rendered the sibling twice.
|
|
8848
|
-
|
|
8947
|
+
...target.inline ? { inline: true } : {},
|
|
8948
|
+
fallback: site.inner && ownsSubtree(target.kind, site, target.inline) ? content.slice(site.inner.start, site.inner.end).trim() : target.fallback,
|
|
8849
8949
|
snapshot: snapshotName(target.slug),
|
|
8850
8950
|
slug: target.slug,
|
|
8851
8951
|
scope: target.scope,
|
|
@@ -9249,10 +9349,12 @@ export {
|
|
|
9249
9349
|
flat,
|
|
9250
9350
|
helperSource,
|
|
9251
9351
|
importsIn,
|
|
9352
|
+
isDecorativeGlyph,
|
|
9252
9353
|
isDynamicRoute,
|
|
9253
9354
|
isKnownHelper,
|
|
9254
9355
|
isSourceCandidate,
|
|
9255
9356
|
locate,
|
|
9357
|
+
measureUnaddressable,
|
|
9256
9358
|
overlapping,
|
|
9257
9359
|
pageFilesFor,
|
|
9258
9360
|
parseFile4 as parseFile,
|