Skip to content
94 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { markSpans, paletteRgb, parseAnsi, spanLook, stripAnsi } from "./ansi.ts";
5
6const E = "\u001b";
7
8test("plain text is one unstyled span", () => {
9 assert.deepEqual(parseAnsi("cargo build"), [{ text: "cargo build", style: {} }]);
10 assert.deepEqual(parseAnsi(""), []);
11});
12
13test("SGR codes style the text after them until a reset", () => {
14 assert.deepEqual(parseAnsi(`${E}[1m${E}[32mCompiling${E}[0m g1t v0.2.0`), [
15 { text: "Compiling", style: { bold: true, fg: { palette: 2 } } },
16 { text: " g1t v0.2.0", style: {} },
17 ]);
18 // Several in one sequence, an empty one resetting, and the switches off.
19 assert.deepEqual(parseAnsi(`${E}[2;3;4;9mx${E}[22;23my${E}[24;29mz${E}[mw`), [
20 { text: "x", style: { dim: true, italic: true, underline: true, strike: true } },
21 { text: "y", style: { underline: true, strike: true } },
22 // The same look, one span.
23 { text: "zw", style: {} },
24 ]);
25 // Bright colours, backgrounds, and the defaults back.
26 assert.deepEqual(parseAnsi(`${E}[91;104mhot${E}[39mbg${E}[49mnone`), [
27 { text: "hot", style: { fg: { palette: 9 }, bg: { palette: 12 } } },
28 { text: "bg", style: { bg: { palette: 12 } } },
29 { text: "none", style: {} },
30 ]);
31 assert.deepEqual(parseAnsi(`${E}[7minverse${E}[27m`), [{ text: "inverse", style: { inverse: true } }]);
32});
33
34test("256 colours and 24-bit colours, in both of their forms", () => {
35 assert.deepEqual(parseAnsi(`${E}[38;5;208morange${E}[48;5;3mon`), [
36 { text: "orange", style: { fg: { palette: 208 } } },
37 { text: "on", style: { fg: { palette: 208 }, bg: { palette: 3 } } },
38 ]);
39 assert.deepEqual(parseAnsi(`${E}[38;2;255;100;0mrgb${E}[0m`), [{ text: "rgb", style: { fg: { rgb: [255, 100, 0] } } }]);
40 assert.deepEqual(parseAnsi(`${E}[38:2::10:20:30mitu${E}[48:5:17mbg`), [
41 { text: "itu", style: { fg: { rgb: [10, 20, 30] } } },
42 { text: "bg", style: { fg: { rgb: [10, 20, 30] }, bg: { palette: 17 } } },
43 ]);
44 // A colour out of range is left alone, and what follows still reads.
45 assert.deepEqual(parseAnsi(`${E}[38;5;999;1mx`), [{ text: "x", style: { bold: true } }]);
46 assert.deepEqual(paletteRgb(16), [0, 0, 0]);
47 assert.deepEqual(paletteRgb(196), [255, 0, 0]);
48 assert.deepEqual(paletteRgb(232), [8, 8, 8]);
49 assert.deepEqual(paletteRgb(255), [238, 238, 238]);
50});
51
52test("every other escape is dropped, and a link keeps its text", () => {
53 // Moving the cursor and clearing the line, as progress bars do.
54 assert.equal(stripAnsi(`${E}[2K${E}[1G Downloading${E}[?25l done${E}[?25h`), " Downloading done");
55 // An OSC 8 hyperlink, closed with BEL and with ESC \.
56 assert.equal(stripAnsi(`see ${E}]8;;https://g1t.sh${E}\\the docs${E}]8;;${E}\\ now`), "see the docs now");
57 assert.equal(stripAnsi(`${E}]0;title\u0007after`), "after");
58 // A character set, a two-character escape, a bell and a backspace.
59 assert.equal(stripAnsi(`${E}(Bplain${E}7 kept\u0007\u0008`), "plain kept");
60 // A tab stays; a sequence cut off at the end of the line goes.
61 assert.equal(stripAnsi(`a\tb${E}[3`), "a\tb");
62 assert.equal(stripAnsi(`end${E}`), "end");
63 assert.equal(stripAnsi("no escapes"), "no escapes");
64});
65
66test("the 16 colours are theme classes; the rest are inline colours", () => {
67 assert.deepEqual(spanLook({ bold: true, fg: { palette: 1 }, bg: { palette: 15 } }), { className: "ansi-fg-1 ansi-bg-15 font-bold", style: undefined });
68 assert.deepEqual(spanLook({ fg: { palette: 196 }, bg: { rgb: [1, 2, 3] } }), {
69 className: "",
70 style: { color: "rgb(255, 0, 0)", backgroundColor: "rgb(1, 2, 3)" },
71 });
72 assert.deepEqual(spanLook({ inverse: true, fg: { palette: 4 } }), { className: "ansi-fg-inverse ansi-bg-4", style: undefined });
73 assert.equal(spanLook({ dim: true, italic: true, underline: true }).className, "opacity-70 italic underline");
74 assert.equal(spanLook({}).className, "");
75});
76
77test("a search marks its matches across spans, colours kept", () => {
78 const spans = parseAnsi(`${E}[32mtest${E}[0m result: ok. Test`);
79 assert.deepEqual(markSpans(spans, "T R"), [
80 { text: "tes", style: { fg: { palette: 2 } }, match: false },
81 { text: "t", style: { fg: { palette: 2 } }, match: true },
82 { text: " r", style: {}, match: true },
83 { text: "esult: ok. Test", style: {}, match: false },
84 ]);
85 assert.deepEqual(
86 markSpans(spans, "test").map((span) => [span.text, span.match]),
87 [
88 ["test", true],
89 [" result: ok. ", false],
90 ["Test", true],
91 ],
92 );
93 assert.deepEqual(markSpans(spans, " "), spans.map((span) => ({ ...span, match: false })));
94});