@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/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
- /** The same text with tags removed — what a reader sees, not what the file says. */
146
- declare const stripTags: (value: string) => string;
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" | "PROP_TARGET_NOT_FOUND" | "PROP_DRILLED_DEEP" | "REPEATER_FIXED_LENGTH"
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
- for (const child of node.children) {
2639
- const childWalk = { ancestors: [...walk.ancestors, node.tag], siblingIndex: index };
2640
- if (child.type === "element") index++;
2641
- for (const hit of visit(child, childWalk)) matched.add(hit);
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
- for (const child of node.children) {
2743
- const childWalk = { ancestors: [...walk.ancestors, node.tag], siblingIndex: index };
2744
- if (child.type === "element") index++;
2745
- if (partials(child, literals, childWalk, out)) deeper = true;
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, " ").replace(/<[^>]*>/g, "");
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((f) => f.content.includes(original) || f.text !== null && f.text.includes(original)).map((f) => f.path);
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 === "richtext" && !(site.mixed && site.textRange)) {
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
- fallback: target.kind === "richtext" && site.inner && !(site.mixed && site.textRange) ? content.slice(site.inner.start, site.inner.end).trim() : target.fallback,
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,