import { assertEquals } from "@std/assert"; import { TERMINAL_PALETTE } from "@berry/util/palette.ts"; import { ansiParser, type AnsiRun, ansiRuns } from "./ansi.ts"; Deno.test("ANSI output preserves literal text, whitespace, and newlines", () => { const text = ' \n\tend\n'; assertEquals(ansiRuns(text), [{ text, style: {} }]); assertEquals(ansiRuns(""), []); }); Deno.test("ANSI colours and emphasis persist across lines until reset", () => { assertEquals(ansiRuns("before\x1b[1;31mred\nline\x1b[22m normal\x1b[39m after"), [ { text: "before", style: {} }, { text: "red\nline", style: { color: TERMINAL_PALETTE[1], "font-weight": "bold" } }, { text: " normal", style: { color: TERMINAL_PALETTE[1] } }, { text: " after", style: {} }, ]); assertEquals(ansiRuns("\x1b[2;94;103mdim\x1b[mplain"), [ { text: "dim", style: { color: TERMINAL_PALETTE[12], "background-color": TERMINAL_PALETTE[11], opacity: "0.5", }, }, { text: "plain", style: {} }, ]); }); Deno.test("ANSI extended colours support the palette, cube, grayscale, and RGB", () => { assertEquals( ansiRuns("\x1b[38;5;2mpalette\x1b[38;5;196mcube\x1b[48;5;244mgray\x1b[38;2;12;34;56mrgb"), [ { text: "palette", style: { color: TERMINAL_PALETTE[2] } }, { text: "cube", style: { color: "rgb(255, 0, 0)" } }, { text: "gray", style: { color: "rgb(255, 0, 0)", "background-color": "rgb(128, 128, 128)" }, }, { text: "rgb", style: { color: "rgb(12, 34, 56)", "background-color": "rgb(128, 128, 128)" }, }, ], ); }); Deno.test("ANSI resets foreground and background independently and clears decorations", () => { assertEquals(ansiRuns("\x1b[3;4;9;32;41mstyled\x1b[39mbackground\x1b[49;23;24;29mplain"), [ { text: "styled", style: { color: TERMINAL_PALETTE[2], "background-color": TERMINAL_PALETTE[1], "font-style": "italic", "text-decoration": "underline line-through", }, }, { text: "background", style: { "background-color": TERMINAL_PALETTE[1], "font-style": "italic", "text-decoration": "underline line-through", }, }, { text: "plain", style: {} }, ]); }); Deno.test("ANSI reverse video uses defaults and can be turned off", () => { assertEquals(ansiRuns("\x1b[7mreverse\x1b[27mplain"), [ { text: "reverse", style: { color: "var(--panel)", "background-color": "var(--text)" } }, { text: "plain", style: {} }, ]); }); Deno.test("unsupported terminal controls and OSC links keep their visible text", () => { const text = "\x1b[?25l\x1b[2K\x1b[H\x1b]0;title\x07\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x07\x1b[999m end"; assertEquals(ansiRuns(text).map((run) => run.text).join(""), "link end"); }); Deno.test("incomplete streaming escapes stay hidden and render once complete", () => { const partial = "start\x1b[38;2;12;"; assertEquals(ansiRuns(partial), [{ text: "start", style: {} }]); assertEquals(ansiRuns(partial + "34;56mend"), [ { text: "start", style: {} }, { text: "end", style: { color: "rgb(12, 34, 56)" } }, ]); assertEquals(ansiRuns("text\x1b]unfinished title"), [{ text: "text", style: {} }]); }); Deno.test("malformed extended colours do not consume their components as SGR styles", () => { assertEquals(ansiRuns("\x1b[38;2;999;1;2mtext\x1b[48;5;999mend"), [ { text: "text", style: {} }, { text: "end", style: {} }, ]); }); Deno.test("ANSI streaming preserves styles and hides escapes at every chunk boundary", () => { const sources = [ "before\x1b[1;31mred\nline\x1b[22m normal\x1b[39m after", "\x1b[38;2;12;34;56mrgb\x1b[48;5;244mgray\x1b[0mplain", "\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x07 end", "\x1b[?25l\x1b[2K\x1b[H\x1b]0;title\x07text\x1b(B end", "text\x1b[38;2;1;", ]; const characters = (runs: AnsiRun[]) => runs.flatMap(({ text, style }) => [...text].map((text) => ({ text, style }))); for (const source of sources) { const expected = characters(ansiRuns(source)); for (let split = 0; split <= source.length; split++) { const parser = ansiParser(); assertEquals( characters([ ...parser.write(source.slice(0, split)), ...parser.write(source.slice(split)), ]), expected, `split at ${split}`, ); } const parser = ansiParser(); assertEquals(characters([...source].flatMap((char) => parser.write(char))), expected); } }); Deno.test("ANSI streaming discards long control payloads without leaking them or losing later styles", () => { const parser = ansiParser(); assertEquals(parser.write("\x1b]0;"), []); for (let i = 0; i < 100; i++) assertEquals(parser.write("x".repeat(10000)), []); assertEquals(parser.write("\x1b"), []); assertEquals(parser.write("\\\x1b[32mgreen"), [ { text: "green", style: { color: TERMINAL_PALETTE[2] } }, ]); assertEquals(parser.write("\x1b[" + "1;".repeat(10000)), []); assertEquals(parser.write("mstill green\x1b[mplain"), [ { text: "still green", style: { color: TERMINAL_PALETTE[2] } }, { text: "plain", style: {} }, ]); });