@pi-archimedes/core 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/bus.test.ts +154 -0
- package/src/color.test.ts +331 -0
- package/src/text.test.ts +116 -0
package/package.json
CHANGED
package/src/bus.test.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { getBus, initBus, Events } from "./bus.js";
|
|
3
|
+
|
|
4
|
+
// ── globalThis cleanup ──────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const BUS_KEY = Symbol.for("archimedes:bus");
|
|
7
|
+
const QUEUE_KEY = Symbol.for("archimedes:busQueue");
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
// Reset globalThis bus and queue to avoid test pollution
|
|
11
|
+
delete (globalThis as Record<symbol, unknown>)[BUS_KEY];
|
|
12
|
+
delete (globalThis as Record<symbol, unknown>)[QUEUE_KEY];
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// ── emit → on delivery ─────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe("emit → on delivery", () => {
|
|
18
|
+
it("subscriber receives payload", () => {
|
|
19
|
+
const bus = getBus();
|
|
20
|
+
const received: unknown[] = [];
|
|
21
|
+
bus.on("test:event", (payload) => received.push(payload));
|
|
22
|
+
bus.emit("test:event", { hello: "world" });
|
|
23
|
+
expect(received).toEqual([{ hello: "world" }]);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ── multiple subscribers ────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
describe("multiple subscribers", () => {
|
|
30
|
+
it("all receive the event", () => {
|
|
31
|
+
const bus = getBus();
|
|
32
|
+
const a: unknown[] = [];
|
|
33
|
+
const b: unknown[] = [];
|
|
34
|
+
bus.on("multi", (p) => a.push(p));
|
|
35
|
+
bus.on("multi", (p) => b.push(p));
|
|
36
|
+
bus.emit("multi", "shared");
|
|
37
|
+
expect(a).toEqual(["shared"]);
|
|
38
|
+
expect(b).toEqual(["shared"]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ── unsubscribe ─────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
describe("unsubscribe", () => {
|
|
45
|
+
it("removed subscriber does not receive subsequent events", () => {
|
|
46
|
+
const bus = getBus();
|
|
47
|
+
const received: unknown[] = [];
|
|
48
|
+
const unsub = bus.on("unsub:test", (p) => received.push(p));
|
|
49
|
+
bus.emit("unsub:test", "first");
|
|
50
|
+
unsub();
|
|
51
|
+
bus.emit("unsub:test", "second");
|
|
52
|
+
expect(received).toEqual(["first"]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ── error isolation ─────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
describe("error isolation", () => {
|
|
59
|
+
it("one listener throwing does not prevent others from receiving", () => {
|
|
60
|
+
const bus = getBus();
|
|
61
|
+
const received: unknown[] = [];
|
|
62
|
+
bus.on("err:test", () => {
|
|
63
|
+
throw new Error("boom");
|
|
64
|
+
});
|
|
65
|
+
bus.on("err:test", (p) => received.push(p));
|
|
66
|
+
bus.emit("err:test", "payload");
|
|
67
|
+
expect(received).toEqual(["payload"]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ── late subscriber queue ───────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
describe("late subscriber queue", () => {
|
|
74
|
+
it("events emitted before on() are delivered when subscriber registers", () => {
|
|
75
|
+
// Create a fresh bus
|
|
76
|
+
const bus = getBus();
|
|
77
|
+
|
|
78
|
+
// Emit before anyone subscribes — this queues the event
|
|
79
|
+
bus.emit("queued:event", "queued-payload");
|
|
80
|
+
|
|
81
|
+
// Now subscribe — queued events should be delivered
|
|
82
|
+
const received: unknown[] = [];
|
|
83
|
+
bus.on("queued:event", (p) => received.push(p));
|
|
84
|
+
|
|
85
|
+
expect(received).toEqual(["queued-payload"]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── initBus ─────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
describe("initBus", () => {
|
|
92
|
+
it("flushes queued events to subscribers that registered before init", () => {
|
|
93
|
+
// Emit before any subscriber exists — queues the event
|
|
94
|
+
const bus = getBus();
|
|
95
|
+
bus.emit("init:test", "queued-value");
|
|
96
|
+
|
|
97
|
+
// Subscribe — on() drains queue and delivers
|
|
98
|
+
const received: unknown[] = [];
|
|
99
|
+
bus.on("init:test", (p) => received.push(p));
|
|
100
|
+
expect(received).toEqual(["queued-value"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("initBus re-emits queued events through the bus", () => {
|
|
104
|
+
// Emit with no subscriber — queues
|
|
105
|
+
const bus = getBus();
|
|
106
|
+
bus.emit("init:test2", "queued");
|
|
107
|
+
|
|
108
|
+
// initBus re-emits through the bus
|
|
109
|
+
initBus();
|
|
110
|
+
|
|
111
|
+
// Since no subscriber for test2, re-emit goes back to queue.
|
|
112
|
+
// Now subscribe — on() drains the re-queued event
|
|
113
|
+
const received: unknown[] = [];
|
|
114
|
+
bus.on("init:test2", (p) => received.push(p));
|
|
115
|
+
expect(received).toEqual(["queued"]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("initBus snapshots and clears queue before iterating", () => {
|
|
119
|
+
// Emit with no subscriber — queues
|
|
120
|
+
const bus = getBus();
|
|
121
|
+
bus.emit("snapshot:test", "a");
|
|
122
|
+
bus.emit("snapshot:test", "b");
|
|
123
|
+
|
|
124
|
+
// Subscribe — on() drains queue and delivers (but queue array persists)
|
|
125
|
+
const received: unknown[] = [];
|
|
126
|
+
bus.on("snapshot:test", (p) => received.push(p));
|
|
127
|
+
expect(received).toEqual(["a", "b"]);
|
|
128
|
+
|
|
129
|
+
// Queue array still has items (on() delivers but doesn't remove)
|
|
130
|
+
const queueBefore = (globalThis as Record<symbol, unknown>)[QUEUE_KEY] as Array<unknown>;
|
|
131
|
+
expect(queueBefore.length).toBe(2);
|
|
132
|
+
|
|
133
|
+
// initBus snapshots, clears queue, and re-emits
|
|
134
|
+
initBus();
|
|
135
|
+
// Re-emits go to existing subscriber (not re-queued)
|
|
136
|
+
expect(received).toEqual(["a", "b", "a", "b"]);
|
|
137
|
+
|
|
138
|
+
// Queue is now empty (initBus cleared it)
|
|
139
|
+
const queueAfter = (globalThis as Record<symbol, unknown>)[QUEUE_KEY] as Array<unknown>;
|
|
140
|
+
expect(queueAfter.length).toBe(0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ── Events constant ─────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
describe("Events constant", () => {
|
|
147
|
+
it("all event names are defined", () => {
|
|
148
|
+
expect(Events.COST_UPDATE).toBe("archimedes:cost_update");
|
|
149
|
+
expect(Events.TODOS_UPDATE).toBe("archimedes:todos_update");
|
|
150
|
+
expect(Events.TODOS_CLEAR).toBe("archimedes:todos_clear");
|
|
151
|
+
expect(Events.ASK_REQUEST).toBe("archimedes:ask_request");
|
|
152
|
+
expect(Events.ASK_RESPONSE).toBe("archimedes:ask_response");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
hexToRgb,
|
|
4
|
+
rgbToHex,
|
|
5
|
+
rgbToHsl,
|
|
6
|
+
hslToRgb,
|
|
7
|
+
ansi256ToRgb,
|
|
8
|
+
parseAnsiFgToRgb,
|
|
9
|
+
deriveDimColor,
|
|
10
|
+
rgbToTruecolorFg,
|
|
11
|
+
gray,
|
|
12
|
+
rgb,
|
|
13
|
+
extractRgb,
|
|
14
|
+
lerp,
|
|
15
|
+
} from "./color.js";
|
|
16
|
+
|
|
17
|
+
// ── hexToRgb ────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
describe("hexToRgb", () => {
|
|
20
|
+
it("parses valid 6-char hex", () => {
|
|
21
|
+
expect(hexToRgb("#ff00aa")).toEqual({ r: 255, g: 0, b: 170 });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("parses 3-char shorthand", () => {
|
|
25
|
+
expect(hexToRgb("#f0a")).toEqual({ r: 255, g: 0, b: 170 });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("parses without hash prefix", () => {
|
|
29
|
+
expect(hexToRgb("ff00aa")).toEqual({ r: 255, g: 0, b: 170 });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("parses with hash prefix", () => {
|
|
33
|
+
expect(hexToRgb("#ff00aa")).toEqual({ r: 255, g: 0, b: 170 });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("handles mixed case", () => {
|
|
37
|
+
expect(hexToRgb("#Ff00Aa")).toEqual({ r: 255, g: 0, b: 170 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("throws on invalid input", () => {
|
|
41
|
+
expect(() => hexToRgb("#gggggg")).toThrow("Invalid hex color");
|
|
42
|
+
expect(() => hexToRgb("#ff")).toThrow("Invalid hex color");
|
|
43
|
+
expect(() => hexToRgb("#ffffffg")).toThrow("Invalid hex color");
|
|
44
|
+
expect(() => hexToRgb("")).toThrow("Invalid hex color");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ── rgbToHex ────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
describe("rgbToHex", () => {
|
|
51
|
+
it("round-trips with hexToRgb", () => {
|
|
52
|
+
const hex = "#ff00aa";
|
|
53
|
+
expect(rgbToHex(hexToRgb(hex))).toBe(hex.toLowerCase());
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("round-trips shorthand hex", () => {
|
|
57
|
+
const hex = "#f0a";
|
|
58
|
+
// Shorthand expands to 6-char, so round-trip is 6-char form
|
|
59
|
+
expect(rgbToHex(hexToRgb(hex))).toBe("#ff00aa");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("clamps values to 0-255", () => {
|
|
63
|
+
expect(rgbToHex({ r: -10, g: 0, b: 0 })).toBe("#000000");
|
|
64
|
+
expect(rgbToHex({ r: 255, g: 300, b: 0 })).toBe("#ffff00");
|
|
65
|
+
expect(rgbToHex({ r: 255, g: 255, b: 255 })).toBe("#ffffff");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("pads single-digit hex values", () => {
|
|
69
|
+
expect(rgbToHex({ r: 0, g: 0, b: 10 })).toBe("#00000a");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ── rgbToHsl ────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
describe("rgbToHsl", () => {
|
|
76
|
+
it("grayscale r=g=b returns s=0", () => {
|
|
77
|
+
const hsl = rgbToHsl({ r: 128, g: 128, b: 128 });
|
|
78
|
+
expect(hsl.s).toBe(0);
|
|
79
|
+
expect(hsl.h).toBe(0);
|
|
80
|
+
expect(hsl.l).toBeCloseTo(128 / 255);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("pure red", () => {
|
|
84
|
+
const hsl = rgbToHsl({ r: 255, g: 0, b: 0 });
|
|
85
|
+
expect(hsl.h).toBe(0);
|
|
86
|
+
expect(hsl.s).toBe(1);
|
|
87
|
+
expect(hsl.l).toBe(0.5);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("pure green", () => {
|
|
91
|
+
const hsl = rgbToHsl({ r: 0, g: 255, b: 0 });
|
|
92
|
+
expect(hsl.h).toBe(120);
|
|
93
|
+
expect(hsl.s).toBe(1);
|
|
94
|
+
expect(hsl.l).toBe(0.5);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("pure blue", () => {
|
|
98
|
+
const hsl = rgbToHsl({ r: 0, g: 0, b: 255 });
|
|
99
|
+
expect(hsl.h).toBe(240);
|
|
100
|
+
expect(hsl.s).toBe(1);
|
|
101
|
+
expect(hsl.l).toBe(0.5);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("white returns h=0, s=0, l=1", () => {
|
|
105
|
+
const hsl = rgbToHsl({ r: 255, g: 255, b: 255 });
|
|
106
|
+
expect(hsl.h).toBe(0);
|
|
107
|
+
expect(hsl.s).toBe(0);
|
|
108
|
+
expect(hsl.l).toBe(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("black returns h=0, s=0, l=0", () => {
|
|
112
|
+
const hsl = rgbToHsl({ r: 0, g: 0, b: 0 });
|
|
113
|
+
expect(hsl.h).toBe(0);
|
|
114
|
+
expect(hsl.s).toBe(0);
|
|
115
|
+
expect(hsl.l).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── hslToRgb ────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
describe("hslToRgb", () => {
|
|
122
|
+
it("round-trips with rgbToHsl for pure red", () => {
|
|
123
|
+
const hsl = rgbToHsl({ r: 255, g: 0, b: 0 });
|
|
124
|
+
expect(hslToRgb(hsl)).toEqual({ r: 255, g: 0, b: 0 });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("round-trips with rgbToHsl for pure green", () => {
|
|
128
|
+
const hsl = rgbToHsl({ r: 0, g: 255, b: 0 });
|
|
129
|
+
expect(hslToRgb(hsl)).toEqual({ r: 0, g: 255, b: 0 });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("round-trips with rgbToHsl for pure blue", () => {
|
|
133
|
+
const hsl = rgbToHsl({ r: 0, g: 0, b: 255 });
|
|
134
|
+
expect(hslToRgb(hsl)).toEqual({ r: 0, g: 0, b: 255 });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("s=0 produces grayscale", () => {
|
|
138
|
+
const result = hslToRgb({ h: 0, s: 0, l: 0.5 });
|
|
139
|
+
expect(result.r).toBe(result.g);
|
|
140
|
+
expect(result.g).toBe(result.b);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("h wraps at 360", () => {
|
|
144
|
+
const a = hslToRgb({ h: 0, s: 1, l: 0.5 });
|
|
145
|
+
const b = hslToRgb({ h: 360, s: 1, l: 0.5 });
|
|
146
|
+
expect(a).toEqual(b);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("negative h wraps correctly", () => {
|
|
150
|
+
const a = hslToRgb({ h: 0, s: 1, l: 0.5 });
|
|
151
|
+
const b = hslToRgb({ h: -360, s: 1, l: 0.5 });
|
|
152
|
+
expect(a).toEqual(b);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ── ansi256ToRgb ────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
describe("ansi256ToRgb", () => {
|
|
159
|
+
it("code 0 returns black", () => {
|
|
160
|
+
expect(ansi256ToRgb(0)).toEqual({ r: 0, g: 0, b: 0 });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("code 9 returns red", () => {
|
|
164
|
+
expect(ansi256ToRgb(9)).toEqual({ r: 255, g: 0, b: 0 });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("code 15 returns white", () => {
|
|
168
|
+
expect(ansi256ToRgb(15)).toEqual({ r: 255, g: 255, b: 255 });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("code 16 returns cube start (0,0,0)", () => {
|
|
172
|
+
expect(ansi256ToRgb(16)).toEqual({ r: 0, g: 0, b: 0 });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("code 196 returns cube red (255,0,0)", () => {
|
|
176
|
+
expect(ansi256ToRgb(196)).toEqual({ r: 255, g: 0, b: 0 });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("code 231 returns cube end (255,255,255)", () => {
|
|
180
|
+
expect(ansi256ToRgb(231)).toEqual({ r: 255, g: 255, b: 255 });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("code 232 returns grayscale start (8,8,8)", () => {
|
|
184
|
+
expect(ansi256ToRgb(232)).toEqual({ r: 8, g: 8, b: 8 });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("code 255 returns grayscale end (238,238,238)", () => {
|
|
188
|
+
expect(ansi256ToRgb(255)).toEqual({ r: 238, g: 238, b: 238 });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("out-of-range throws", () => {
|
|
192
|
+
expect(() => ansi256ToRgb(-1)).toThrow("out of range");
|
|
193
|
+
expect(() => ansi256ToRgb(256)).toThrow("out of range");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ── parseAnsiFgToRgb ────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
describe("parseAnsiFgToRgb", () => {
|
|
200
|
+
it("parses truecolor sequence", () => {
|
|
201
|
+
const result = parseAnsiFgToRgb("\x1b[38;2;255;128;64m");
|
|
202
|
+
expect(result).toEqual({ r: 255, g: 128, b: 64 });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("parses 256 palette sequence", () => {
|
|
206
|
+
const result = parseAnsiFgToRgb("\x1b[38;5;196m");
|
|
207
|
+
expect(result).toEqual({ r: 255, g: 0, b: 0 });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("returns null for empty input", () => {
|
|
211
|
+
expect(parseAnsiFgToRgb("")).toBeNull();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("returns null for null input", () => {
|
|
215
|
+
expect(parseAnsiFgToRgb(null as unknown as string)).toBeNull();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("returns null for non-matching string", () => {
|
|
219
|
+
expect(parseAnsiFgToRgb("hello")).toBeNull();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ── deriveDimColor ──────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
describe("deriveDimColor", () => {
|
|
226
|
+
it("number input with anchorLightness", () => {
|
|
227
|
+
const result = deriveDimColor(100, 0.5);
|
|
228
|
+
expect(typeof result).toBe("string");
|
|
229
|
+
expect(result.startsWith("#")).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("string hex input with custom saturationFactor", () => {
|
|
233
|
+
const result = deriveDimColor("#ff0000", 0.3, 0.7);
|
|
234
|
+
expect(typeof result).toBe("string");
|
|
235
|
+
expect(result.startsWith("#")).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("default saturationFactor is 0.5", () => {
|
|
239
|
+
const withDefault = deriveDimColor("#ff0000", 0.5);
|
|
240
|
+
const explicit = deriveDimColor("#ff0000", 0.5, 0.5);
|
|
241
|
+
expect(withDefault).toBe(explicit);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("lightness clamping — anchorLightness limits l", () => {
|
|
245
|
+
// Pure red has l=0.5, anchorLightness=0.2 should clamp to 0.2
|
|
246
|
+
const result = deriveDimColor("#ff0000", 0.2);
|
|
247
|
+
const hsl = rgbToHsl(hexToRgb(result));
|
|
248
|
+
expect(hsl.l).toBeLessThanOrEqual(0.2);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ── rgbToTruecolorFg ────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
describe("rgbToTruecolorFg", () => {
|
|
255
|
+
it("produces correct format", () => {
|
|
256
|
+
expect(rgbToTruecolorFg({ r: 255, g: 128, b: 64 })).toBe(
|
|
257
|
+
"\x1b[38;2;255;128;64m"
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("clamps values to 0-255", () => {
|
|
262
|
+
expect(rgbToTruecolorFg({ r: -1, g: 0, b: 300 })).toBe(
|
|
263
|
+
"\x1b[38;2;0;0;255m"
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ── gray ────────────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
describe("gray", () => {
|
|
271
|
+
it("produces truecolor gray with correct level", () => {
|
|
272
|
+
expect(gray(128, "hello")).toBe("\x1b[38;2;128;128;128mhello\x1b[0m");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("clamps level to 0-255", () => {
|
|
276
|
+
expect(gray(-10, "x")).toBe("\x1b[38;2;0;0;0mx\x1b[0m");
|
|
277
|
+
expect(gray(300, "x")).toBe("\x1b[38;2;255;255;255mx\x1b[0m");
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ── rgb ─────────────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
describe("rgb", () => {
|
|
284
|
+
it("produces truecolor with correct values", () => {
|
|
285
|
+
expect(rgb(255, 128, 64, "text")).toBe(
|
|
286
|
+
"\x1b[38;2;255;128;64mtext\x1b[0m"
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("floors float values", () => {
|
|
291
|
+
expect(rgb(255.9, 128.1, 64.5, "x")).toBe(
|
|
292
|
+
"\x1b[38;2;255;128;64mx\x1b[0m"
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// ── extractRgb ──────────────────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
describe("extractRgb", () => {
|
|
300
|
+
it("extracts from themed string", () => {
|
|
301
|
+
expect(extractRgb("\x1b[38;2;200;100;50mhello")).toEqual([200, 100, 50]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("returns default for non-themed string", () => {
|
|
305
|
+
expect(extractRgb("plain text")).toEqual([100, 100, 100]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("returns default for empty string", () => {
|
|
309
|
+
expect(extractRgb("")).toEqual([100, 100, 100]);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ── lerp ────────────────────────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
describe("lerp", () => {
|
|
316
|
+
it("t=0 returns a", () => {
|
|
317
|
+
expect(lerp(10, 20, 0)).toBe(10);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("t=1 returns b", () => {
|
|
321
|
+
expect(lerp(10, 20, 1)).toBe(20);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("t=0.5 returns midpoint", () => {
|
|
325
|
+
expect(lerp(10, 20, 0.5)).toBe(15);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("works with negative values", () => {
|
|
329
|
+
expect(lerp(-10, 10, 0.5)).toBe(0);
|
|
330
|
+
});
|
|
331
|
+
});
|
package/src/text.test.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { stripAnsi, isParentBorder, formatKey } from "./text.js";
|
|
3
|
+
|
|
4
|
+
// ── stripAnsi ───────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
describe("stripAnsi", () => {
|
|
7
|
+
it("strips CSI sequences", () => {
|
|
8
|
+
expect(stripAnsi("\x1b[31mhello\x1b[0m")).toBe("hello");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("strips multiple CSI sequences", () => {
|
|
12
|
+
expect(stripAnsi("\x1b[1;32mred\x1b[0m \x1b[33myellow\x1b[0m")).toBe(
|
|
13
|
+
"red yellow"
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("strips OSC sequences", () => {
|
|
18
|
+
expect(stripAnsi("before\x1b]0;title\x07after")).toBe("beforeafter");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("strips OSC with ST (ESC \\)", () => {
|
|
22
|
+
expect(stripAnsi("before\x1b]0;title\x1b\\after")).toBe("beforeafter");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("strips DCS sequences", () => {
|
|
26
|
+
expect(stripAnsi("before\x1bPdata\x07after")).toBe("beforeafter");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("strips SOS sequences", () => {
|
|
30
|
+
expect(stripAnsi("before\x1b^data\x07after")).toBe("beforeafter");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("strips APC sequences", () => {
|
|
34
|
+
expect(stripAnsi("before\x1b_data\x07after")).toBe("beforeafter");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("strips PM sequences", () => {
|
|
38
|
+
expect(stripAnsi("before\x1b\\data\x07after")).toBe("beforeafter");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("strips character set escapes (ESC ( / ESC ))", () => {
|
|
42
|
+
// The regex strips ESC( and ESC) but not the following charset designator
|
|
43
|
+
expect(stripAnsi("before\x1b(Bafter")).toBe("beforeBafter");
|
|
44
|
+
expect(stripAnsi("before\x1b)Bafter")).toBe("beforeBafter");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("handles nested escapes", () => {
|
|
48
|
+
expect(stripAnsi("\x1b[1m\x1b[31mhello\x1b[0m\x1b[0m")).toBe("hello");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns empty string for empty input", () => {
|
|
52
|
+
expect(stripAnsi("")).toBe("");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns input unchanged for no-escape string", () => {
|
|
56
|
+
expect(stripAnsi("plain text")).toBe("plain text");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("trims leading and trailing whitespace", () => {
|
|
60
|
+
expect(stripAnsi(" \x1b[31mhello\x1b[0m ")).toBe("hello");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ── isParentBorder ──────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
describe("isParentBorder", () => {
|
|
67
|
+
it("returns true for border char", () => {
|
|
68
|
+
expect(isParentBorder("─")).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns true for border with SGR", () => {
|
|
72
|
+
expect(isParentBorder("\x1b[90m─\x1b[0m")).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns false for non-border", () => {
|
|
76
|
+
expect(isParentBorder("hello")).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns false for empty string", () => {
|
|
80
|
+
expect(isParentBorder("")).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ── formatKey ───────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
describe("formatKey", () => {
|
|
87
|
+
it("formats ctrl+a", () => {
|
|
88
|
+
expect(formatKey("ctrl+a")).toBe("Ctrl+A");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("formats alt+shift+f", () => {
|
|
92
|
+
expect(formatKey("alt+shift+f")).toBe("Alt+Shift+F");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("formats cmd as Cmd", () => {
|
|
96
|
+
expect(formatKey("cmd")).toBe("Cmd");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("formats meta as Cmd", () => {
|
|
100
|
+
expect(formatKey("meta")).toBe("Cmd");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("uppercases single char", () => {
|
|
104
|
+
expect(formatKey("a")).toBe("A");
|
|
105
|
+
expect(formatKey("z")).toBe("Z");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("capitalizes multi-word", () => {
|
|
109
|
+
expect(formatKey("escape")).toBe("Escape");
|
|
110
|
+
expect(formatKey("enter")).toBe("Enter");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns that key for undefined", () => {
|
|
114
|
+
expect(formatKey(undefined)).toBe("that key");
|
|
115
|
+
});
|
|
116
|
+
});
|