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,
|
||||
} from "@alcalzone/ansi-tokenize";
|
||||
import { applyChalk } from "./text-style.ts";
|
||||
import { sanitizeAnsi } from "./sanitize-ansi.ts";
|
||||
import Yoga from "yoga-layout";
|
||||
import type {
|
||||
TuiNode,
|
||||
@@ -288,7 +289,7 @@ function renderTextWithInlineStyles(node: TuiText | TuiVirtualText, acc: TextPro
|
||||
}
|
||||
// Skip comments inserted by Vue for null/undefined renders
|
||||
}
|
||||
return out;
|
||||
return sanitizeAnsi(out);
|
||||
}
|
||||
|
||||
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