fix: handle null/undefined Text children without crashing yoga
Guard flattenLeaves and renderTextWithInlineStyles to skip comment nodes (produced by Vue for null/undefined children) and return empty strings for childless text nodes. Also guard bindTextMeasure to return zero dimensions for empty text. Expand TuiInlineNode to include TuiComment since Vue inserts comments into text containers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ test("CJK wide characters render without corruption", async () => {
|
||||
|
||||
// --- Ink text tests ---
|
||||
|
||||
test.skip("<Text> with undefined children — crashes yoga measure", async () => {
|
||||
test("<Text> with undefined children", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => <Text />),
|
||||
{ columns: 100 },
|
||||
@@ -32,7 +32,7 @@ test.skip("<Text> with undefined children — crashes yoga measure", async () =>
|
||||
expect(lastFrame()).toBe("");
|
||||
});
|
||||
|
||||
test.skip("<Text> with null children — crashes yoga measure", async () => {
|
||||
test("<Text> with null children", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => <Text>{null}</Text>),
|
||||
{ columns: 100 },
|
||||
@@ -169,7 +169,7 @@ test("remeasure text when text is changed", async () => {
|
||||
expect(lastFrame()).toBe("abcx");
|
||||
});
|
||||
|
||||
test.skip("remeasure text when text nodes are changed — null child crashes yoga", async () => {
|
||||
test("remeasure text when text nodes are changed", async () => {
|
||||
const add = shallowRef(false);
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
|
||||
@@ -79,7 +79,7 @@ export interface TuiTransform extends NodeBase {
|
||||
transform: (line: string, lineIndex: number) => string;
|
||||
}
|
||||
|
||||
export type TuiInlineNode = TuiVirtualText | TuiTextLeaf;
|
||||
export type TuiInlineNode = TuiVirtualText | TuiTextLeaf | TuiComment;
|
||||
export type TuiContainer = TuiRoot | TuiBox | TuiStatic | TuiTransform | TuiText | TuiVirtualText;
|
||||
export type TuiNode = TuiContainer | TuiTextLeaf | TuiComment;
|
||||
|
||||
|
||||
@@ -4,13 +4,15 @@ import wrapAnsi from "wrap-ansi";
|
||||
import type { TextProps, TuiText, TuiVirtualText } from "./nodes.ts";
|
||||
|
||||
export function flattenLeaves(node: TuiText | TuiVirtualText): string {
|
||||
if (!node.children || node.children.length === 0) return "";
|
||||
let out = "";
|
||||
for (const child of node.children) {
|
||||
if (child.type === "text-leaf") {
|
||||
out += child.value;
|
||||
} else {
|
||||
} else if (child.type === "virtual-text") {
|
||||
out += flattenLeaves(child);
|
||||
}
|
||||
// Skip comments inserted by Vue for null/undefined renders
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -264,6 +264,11 @@ export function bindTextMeasure(text: TuiText): void {
|
||||
text.yoga.setMeasureFunc((availableWidth) => {
|
||||
const raw = flattenLeaves(text);
|
||||
text.measuredCache = raw;
|
||||
|
||||
// Empty text (no children or all-null children) — return zero dimensions
|
||||
// so yoga doesn't crash trying to measure an empty string.
|
||||
if (raw === "") return { width: 0, height: 0 };
|
||||
|
||||
const natural = measureText(raw, Infinity, text.props.wrap ?? "wrap");
|
||||
|
||||
// Text fits into container, no need to wrap.
|
||||
|
||||
@@ -191,15 +191,17 @@ function placeLine(grid: string[][], x: number, y: number, line: string): void {
|
||||
}
|
||||
|
||||
function renderTextWithInlineStyles(node: TuiText | TuiVirtualText, acc: TextProps = {}): string {
|
||||
if (!node.children || node.children.length === 0) return "";
|
||||
const defined = Object.fromEntries(Object.entries(node.props).filter(([, v]) => v !== undefined));
|
||||
const merged: TextProps = { ...acc, ...defined };
|
||||
let out = "";
|
||||
for (const child of node.children) {
|
||||
if (child.type === "text-leaf") {
|
||||
out += applyChalk(child.value, merged);
|
||||
} else {
|
||||
} else if (child.type === "virtual-text") {
|
||||
out += renderTextWithInlineStyles(child, merged);
|
||||
}
|
||||
// Skip comments inserted by Vue for null/undefined renders
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user