feat: port ANSI sanitizer and integrate into paint pipeline
Port Ink's sanitize-ansi.ts to strip non-SGR/non-OSC ANSI escape sequences (cursor movement, screen clearing) from text nodes. Integrated as the final step of renderTextWithInlineStyles() so all squashed text is sanitized before layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
|||||||
tokenize,
|
tokenize,
|
||||||
} from "@alcalzone/ansi-tokenize";
|
} from "@alcalzone/ansi-tokenize";
|
||||||
import { applyChalk } from "./text-style.ts";
|
import { applyChalk } from "./text-style.ts";
|
||||||
|
import { sanitizeAnsi } from "./sanitize-ansi.ts";
|
||||||
import Yoga from "yoga-layout";
|
import Yoga from "yoga-layout";
|
||||||
import type {
|
import type {
|
||||||
TuiNode,
|
TuiNode,
|
||||||
@@ -288,7 +289,7 @@ function renderTextWithInlineStyles(node: TuiText | TuiVirtualText, acc: TextPro
|
|||||||
}
|
}
|
||||||
// Skip comments inserted by Vue for null/undefined renders
|
// Skip comments inserted by Vue for null/undefined renders
|
||||||
}
|
}
|
||||||
return out;
|
return sanitizeAnsi(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
type BoxStyle = (typeof cliBoxes)[keyof cliBoxes.Boxes];
|
type BoxStyle = (typeof cliBoxes)[keyof cliBoxes.Boxes];
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { describe, test, expect } from "vite-plus/test";
|
||||||
|
import { sanitizeAnsi } from "./sanitize-ansi.ts";
|
||||||
|
|
||||||
|
describe("sanitize-ansi", () => {
|
||||||
|
test("preserves SGR (color) sequences", () => {
|
||||||
|
expect(sanitizeAnsi("\x1b[31mred\x1b[0m")).toBe("\x1b[31mred\x1b[0m");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("strips cursor movement", () => {
|
||||||
|
expect(sanitizeAnsi("\x1b[2Ahello")).toBe("hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("strips screen clearing", () => {
|
||||||
|
expect(sanitizeAnsi("\x1b[2Jhello")).toBe("hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves OSC (hyperlinks)", () => {
|
||||||
|
const link = "\x1b]8;;https://example.com\x07click\x1b]8;;\x07";
|
||||||
|
expect(sanitizeAnsi(link)).toBe(link);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("passes through plain text unchanged", () => {
|
||||||
|
expect(sanitizeAnsi("hello world")).toBe("hello world");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { tokenizeAnsi, hasAnsiControlCharacters } from "./ansi-tokenizer.ts";
|
||||||
|
|
||||||
|
const sgrParametersRegex = /^[\d:;]*$/;
|
||||||
|
|
||||||
|
// Strip ANSI escape sequences that would conflict with vue-tui's layout.
|
||||||
|
// Preserved: SGR sequences (colors, bold, etc. - end with 'm') and
|
||||||
|
// OSC sequences (hyperlinks, etc. - ESC ] or C1 OSC).
|
||||||
|
// Stripped: cursor movement, screen clearing, and other control sequences.
|
||||||
|
export function sanitizeAnsi(text: string): string {
|
||||||
|
if (!hasAnsiControlCharacters(text)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = "";
|
||||||
|
|
||||||
|
for (const token of tokenizeAnsi(text)) {
|
||||||
|
if (token.type === "text" || token.type === "osc") {
|
||||||
|
output += token.value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
token.type === "csi" &&
|
||||||
|
token.finalCharacter === "m" &&
|
||||||
|
token.intermediateString === "" &&
|
||||||
|
sgrParametersRegex.test(token.parameterString)
|
||||||
|
) {
|
||||||
|
output += token.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user