@reliverse/relico 1.3.0 → 1.3.2

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.
Files changed (4) hide show
  1. package/package.json +27 -32
  2. package/src/mod.ts +492 -0
  3. package/bin/mod.d.ts +0 -75
  4. package/bin/mod.js +0 -455
package/package.json CHANGED
@@ -1,43 +1,38 @@
1
1
  {
2
- "dependencies": {},
3
- "description": "@reliverse/relico is a themeable, chainable, typed, truecolor-powered, modern terminal styling toolkit. Designed to make your CLI output colorful, accessible, and human-friendly. It gives you a flexible way to apply colors and styles — with reliability and a smart developer experience baked in. Built for humans, not just terminals.",
4
- "homepage": "https://docs.reliverse.org",
5
- "license": "MIT",
6
2
  "name": "@reliverse/relico",
7
- "type": "module",
8
- "version": "1.3.0",
9
- "keywords": [
10
- "reliverse",
11
- "cli",
12
- "command-line",
13
- "dler"
14
- ],
15
3
  "author": "reliverse",
16
- "bugs": {
17
- "email": "blefnk@gmail.com",
18
- "url": "https://github.com/reliverse/relico/issues"
19
- },
20
- "repository": {
21
- "type": "git",
22
- "url": "git+https://github.com/reliverse/relico.git"
23
- },
24
- "bin": {
25
- "dler": "bin/cli.js"
26
- },
27
- "devDependencies": {},
4
+ "version": "1.3.2",
5
+ "type": "module",
6
+ "description": "@reliverse/relico is a themeable, chainable, typed, truecolor-powered, modern terminal styling toolkit. Designed to make your CLI output colorful, accessible, and human-friendly. It gives you a flexible way to apply colors and styles — with reliability and a smart developer experience baked in. Built for humans, not just terminals.",
28
7
  "exports": {
29
- ".": "./bin/mod.js"
8
+ ".": "./src/mod.ts"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
30
12
  },
31
13
  "files": [
32
- "bin",
33
14
  "package.json",
34
15
  "README.md",
35
16
  "LICENSE",
36
- "LICENSES"
17
+ "src"
37
18
  ],
38
- "main": "./bin/mod.js",
39
- "module": "./bin/mod.js",
40
- "publishConfig": {
41
- "access": "public"
19
+ "scripts": {
20
+ "pub": "dler pub",
21
+ "dev": "bun examples/core.ts",
22
+ "latest": "bun update --latest && bun check",
23
+ "check": "tsc --noEmit && biome check --fix --unsafe",
24
+ "bench": "bun examples/benchmarks/performance.ts",
25
+ "size": "bun examples/benchmarks/bundle-size.ts",
26
+ "perf": "bun bench && bun size"
27
+ },
28
+ "devDependencies": {
29
+ "@biomejs/biome": "2.2.0",
30
+ "@reliverse/dler": "^1.7.92",
31
+ "@reliverse/rse-cfg": "^1.7.12",
32
+ "@total-typescript/ts-reset": "^0.6.1",
33
+ "@types/bun": "^1.2.20",
34
+ "@types/node": "^24.3.0",
35
+ "typescript": "^5.9.2",
36
+ "ultracite": "^5.1.5"
42
37
  }
43
- }
38
+ }
package/src/mod.ts ADDED
@@ -0,0 +1,492 @@
1
+ /* @reliverse/relico - Tiny, type-safe terminal color library with chainable API
2
+ - Levels: 0 (off), 1 (ANSI 8/bright), 2 (ANSI 256), 3 (Truecolor)
3
+ - Named palettes (std, web, grayscale), Bright & Pastel variants, bg-variants
4
+ - Chainable: re.bold.red.underline("text"), chain(re.bold, re.red)("text")
5
+ - Multiline-safe: styles applied per line with reset to prevent bleed
6
+ */
7
+
8
+ type ColorLevel = 0 | 1 | 2 | 3;
9
+
10
+ interface Rgb {
11
+ r: number;
12
+ g: number;
13
+ b: number;
14
+ }
15
+
16
+ type SgrOp =
17
+ | { kind: "style"; open: number[] } // closed by global reset at line end
18
+ | { kind: "fg-basic"; idx: number; bright: boolean }
19
+ | { kind: "bg-basic"; idx: number; bright: boolean }
20
+ | { kind: "fg-256"; code: number }
21
+ | { kind: "bg-256"; code: number }
22
+ | { kind: "fg-true"; rgb: Rgb }
23
+ | { kind: "bg-true"; rgb: Rgb };
24
+
25
+ type ApplyInput = string | number;
26
+
27
+ type FormatCallable = ((input: ApplyInput) => string) & { readonly [OP_SYMBOL]: SgrOp[] };
28
+
29
+ type BaseColorName =
30
+ | "black"
31
+ | "red"
32
+ | "green"
33
+ | "yellow"
34
+ | "blue"
35
+ | "magenta"
36
+ | "cyan"
37
+ | "white"
38
+ | "gray"
39
+ | "orange"
40
+ | "pink"
41
+ | "purple"
42
+ | "teal"
43
+ | "lime"
44
+ | "brown"
45
+ | "navy"
46
+ | "maroon"
47
+ | "olive"
48
+ | "silver";
49
+
50
+ type ColorName = BaseColorName | BrightColorName | BgColorName;
51
+
52
+ type BrightColorName =
53
+ | "blackBright"
54
+ | "redBright"
55
+ | "greenBright"
56
+ | "yellowBright"
57
+ | "blueBright"
58
+ | "magentaBright"
59
+ | "cyanBright"
60
+ | "whiteBright"
61
+ | "orangeBright"
62
+ | "pinkBright"
63
+ | "purpleBright"
64
+ | "tealBright"
65
+ | "limeBright"
66
+ | "brownBright"
67
+ | "navyBright"
68
+ | "maroonBright"
69
+ | "oliveBright"
70
+ | "silverBright";
71
+
72
+ type BgColorName = `bg${Capitalize<BaseColorName>}` | `bg${Capitalize<BrightColorName>}`;
73
+
74
+ type ReStyleKey =
75
+ | "reset"
76
+ | "bold"
77
+ | "dim"
78
+ | "italic"
79
+ | "underline"
80
+ | "inverse"
81
+ | "hidden"
82
+ | "strikethrough";
83
+
84
+ type Re = FormatCallable & {
85
+ readonly [K in ReStyleKey]: Re;
86
+ } & {
87
+ readonly [K in ColorName]: Re;
88
+ } & {
89
+ readonly [K in BgColorName]: Re;
90
+ };
91
+
92
+ const ESC = "\x1B[";
93
+ const RESET = `${ESC}0m`;
94
+ const OP_SYMBOL: unique symbol = Symbol("re.ops");
95
+
96
+ // Color level constants
97
+ const COLOR_LEVEL_OFF = 0;
98
+ const COLOR_LEVEL_BASIC = 1;
99
+ const COLOR_LEVEL_256 = 2;
100
+ const COLOR_LEVEL_TRUECOLOR = 3;
101
+
102
+ // RGB and byte constants
103
+ const MIN_BYTE = 0;
104
+ const MAX_BYTE = 255;
105
+ const WHITE_RGB = 255;
106
+
107
+ // ANSI 256 color constants
108
+ const ANSI_256_GRAYSCALE_MIN = 8;
109
+ const ANSI_256_GRAYSCALE_MAX = 248;
110
+ const ANSI_256_BASE_OFFSET = 16;
111
+ const ANSI_256_GRAYSCALE_BASE = 232;
112
+ const ANSI_256_GRAYSCALE_RANGE = 247;
113
+ const ANSI_256_GRAYSCALE_STEPS = 24;
114
+ const ANSI_256_BRIGHT_THRESHOLD = 231;
115
+ const ANSI_256_RGB_LEVELS = 5;
116
+ const ANSI_256_RGB_RED_MULTIPLIER = 36;
117
+ const ANSI_256_RGB_GREEN_MULTIPLIER = 6;
118
+
119
+ // SGR code constants
120
+ const SGR_FG_BASE = 30;
121
+ const SGR_BG_BASE = 40;
122
+ const SGR_FG_BRIGHT_BASE = 90;
123
+ const SGR_BG_BRIGHT_BASE = 100;
124
+
125
+ // Style SGR codes
126
+ const SGR_RESET = 0;
127
+ const SGR_BOLD = 1;
128
+ const SGR_DIM = 2;
129
+ const SGR_ITALIC = 3;
130
+ const SGR_UNDERLINE = 4;
131
+ const SGR_INVERSE = 7;
132
+ const SGR_HIDDEN = 8;
133
+ const SGR_STRIKETHROUGH = 9;
134
+
135
+ // Hex parsing constants
136
+ const HEX_BYTE_LENGTH = 2;
137
+ const HEX_RED_START = 0;
138
+ const HEX_GREEN_START = 2;
139
+ const HEX_BLUE_START = 4;
140
+ const HEX_BLUE_END = 6;
141
+ const HEX_RADIX = 16;
142
+
143
+ // String processing constants
144
+ const BRIGHT_SUFFIX_LENGTH = 6;
145
+ const BG_PREFIX_LENGTH = 2;
146
+ const BG_COLOR_START = 3;
147
+
148
+ // Color mixing constants
149
+ const BRIGHT_MIX_FACTOR = 0.25;
150
+
151
+ // Regex constants
152
+ const BRIGHT_SUFFIX_REGEX = /Bright$/u;
153
+
154
+ let CURRENT_LEVEL: ColorLevel = COLOR_LEVEL_TRUECOLOR;
155
+
156
+ export const setColorLevel = (level: ColorLevel): void => {
157
+ if (
158
+ level !== COLOR_LEVEL_OFF &&
159
+ level !== COLOR_LEVEL_BASIC &&
160
+ level !== COLOR_LEVEL_256 &&
161
+ level !== COLOR_LEVEL_TRUECOLOR
162
+ ) {
163
+ throw new Error("Invalid color level");
164
+ }
165
+ CURRENT_LEVEL = level;
166
+ };
167
+
168
+ const clampByte = (n: number): number => {
169
+ if (!Number.isFinite(n)) {
170
+ return MIN_BYTE;
171
+ }
172
+ if (n < MIN_BYTE) {
173
+ return MIN_BYTE;
174
+ }
175
+ if (n > MAX_BYTE) {
176
+ return MAX_BYTE;
177
+ }
178
+ return Math.round(n);
179
+ };
180
+
181
+ // Base 8-color RGB anchors (non-bright)
182
+ const BASIC8: Rgb[] = [
183
+ { r: 0, g: 0, b: 0 }, // black
184
+ { r: 205, g: 0, b: 0 }, // red
185
+ { r: 0, g: 205, b: 0 }, // green
186
+ { r: 205, g: 205, b: 0 }, // yellow
187
+ { r: 0, g: 0, b: 238 }, // blue
188
+ { r: 205, g: 0, b: 205 }, // magenta
189
+ { r: 0, g: 205, b: 205 }, // cyan
190
+ { r: 229, g: 229, b: 229 }, // white (light gray)
191
+ ];
192
+
193
+ // SGR code builders
194
+ const sgr = (codes: number[]): string => `${ESC}${codes.join(";")}m`;
195
+
196
+ // RGB → closest of BASIC8 index (0..7)
197
+ const nearestBasicIndex = (rgb: Rgb): number => {
198
+ let best = 0;
199
+ let bestDist = Number.POSITIVE_INFINITY;
200
+ for (let i = 0; i < BASIC8.length; i++) {
201
+ const c = BASIC8[i];
202
+ const dr = c.r - rgb.r;
203
+ const dg = c.g - rgb.g;
204
+ const db = c.b - rgb.b;
205
+ const d = dr * dr + dg * dg + db * db;
206
+ if (d < bestDist) {
207
+ bestDist = d;
208
+ best = i;
209
+ }
210
+ }
211
+ return best;
212
+ };
213
+
214
+ // RGB → ANSI 256 index
215
+ const rgbToAnsi256 = (rgb: Rgb): number => {
216
+ // Try grayscale if r≈g≈b
217
+ if (rgb.r === rgb.g && rgb.g === rgb.b) {
218
+ if (rgb.r < ANSI_256_GRAYSCALE_MIN) {
219
+ return ANSI_256_BASE_OFFSET;
220
+ }
221
+ if (rgb.r > ANSI_256_GRAYSCALE_MAX) {
222
+ return ANSI_256_BRIGHT_THRESHOLD;
223
+ }
224
+ const step = Math.round(
225
+ ((rgb.r - ANSI_256_GRAYSCALE_MIN) / ANSI_256_GRAYSCALE_RANGE) * ANSI_256_GRAYSCALE_STEPS,
226
+ );
227
+ return ANSI_256_GRAYSCALE_BASE + step;
228
+ }
229
+ const r = Math.round((rgb.r / MAX_BYTE) * ANSI_256_RGB_LEVELS);
230
+ const g = Math.round((rgb.g / MAX_BYTE) * ANSI_256_RGB_LEVELS);
231
+ const b = Math.round((rgb.b / MAX_BYTE) * ANSI_256_RGB_LEVELS);
232
+ return (
233
+ ANSI_256_BASE_OFFSET + ANSI_256_RGB_RED_MULTIPLIER * r + ANSI_256_RGB_GREEN_MULTIPLIER * g + b
234
+ );
235
+ };
236
+
237
+ // Color data
238
+ const NAMED_COLORS: Record<BaseColorName, string> = {
239
+ black: "#000000",
240
+ red: "#ff0000",
241
+ green: "#00ff00",
242
+ yellow: "#ffff00",
243
+ blue: "#0000ff",
244
+ magenta: "#ff00ff",
245
+ cyan: "#00ffff",
246
+ white: "#ffffff",
247
+ gray: "#808080",
248
+ orange: "#ffa500",
249
+ pink: "#ffc0cb",
250
+ purple: "#800080",
251
+ teal: "#008080",
252
+ lime: "#00ff00",
253
+ brown: "#a52a2a",
254
+ navy: "#000080",
255
+ maroon: "#800000",
256
+ olive: "#808000",
257
+ silver: "#c0c0c0",
258
+ };
259
+
260
+ const mixWithWhite = (rgb: Rgb, factor: number): Rgb => {
261
+ const t = factor;
262
+ return {
263
+ r: clampByte(rgb.r * (1 - t) + WHITE_RGB * t),
264
+ g: clampByte(rgb.g * (1 - t) + WHITE_RGB * t),
265
+ b: clampByte(rgb.b * (1 - t) + WHITE_RGB * t),
266
+ };
267
+ };
268
+
269
+ const fromNamed = (name: BaseColorName): Rgb => {
270
+ const hex = NAMED_COLORS[name];
271
+ // Simple hex to RGB conversion for named colors only
272
+ const clean = hex.startsWith("#") ? hex.slice(1) : hex;
273
+ const r = Number.parseInt(clean.slice(HEX_RED_START, HEX_BYTE_LENGTH), HEX_RADIX);
274
+ const g = Number.parseInt(clean.slice(HEX_GREEN_START, HEX_BLUE_START), HEX_RADIX);
275
+ const b = Number.parseInt(clean.slice(HEX_BLUE_START, HEX_BLUE_END), HEX_RADIX);
276
+ return { r, g, b };
277
+ };
278
+
279
+ const toBaseName = (compound: BrightColorName): BaseColorName => {
280
+ const base = compound.replace(BRIGHT_SUFFIX_REGEX, "");
281
+ const key = base.charAt(0).toLowerCase() + base.slice(1);
282
+ return key as BaseColorName;
283
+ };
284
+
285
+ const parseColorName = (name: ColorName): { rgb: Rgb; wantBright: boolean } => {
286
+ if ((name as string).endsWith("Bright")) {
287
+ const base = toBaseName(name as BrightColorName);
288
+ const rgb = fromNamed(base);
289
+ // Lighten a bit in high levels; level 1 will use bright SGR
290
+ const rgbAdj = mixWithWhite(rgb, BRIGHT_MIX_FACTOR);
291
+ return { rgb: rgbAdj, wantBright: true };
292
+ }
293
+ return { rgb: fromNamed(name as BaseColorName), wantBright: false };
294
+ };
295
+
296
+ const openForOp = (op: SgrOp): string => {
297
+ if (CURRENT_LEVEL === COLOR_LEVEL_OFF) {
298
+ return "";
299
+ }
300
+ switch (op.kind) {
301
+ case "style":
302
+ return sgr(op.open);
303
+ case "fg-basic":
304
+ return sgr([(op.bright ? SGR_FG_BRIGHT_BASE : SGR_FG_BASE) + op.idx]);
305
+ case "bg-basic":
306
+ return sgr([(op.bright ? SGR_BG_BRIGHT_BASE : SGR_BG_BASE) + op.idx]);
307
+ case "fg-256":
308
+ return `${ESC}38;5;${op.code}m`;
309
+ case "bg-256":
310
+ return `${ESC}48;5;${op.code}m`;
311
+ case "fg-true":
312
+ return `${ESC}38;2;${op.rgb.r};${op.rgb.g};${op.rgb.b}m`;
313
+ case "bg-true":
314
+ return `${ESC}48;2;${op.rgb.r};${op.rgb.g};${op.rgb.b}m`;
315
+ default:
316
+ return "";
317
+ }
318
+ };
319
+
320
+ const opsToOpen = (ops: SgrOp[]): string => {
321
+ if (CURRENT_LEVEL === COLOR_LEVEL_OFF) {
322
+ return "";
323
+ }
324
+ let out = "";
325
+ for (const op of ops) {
326
+ out += openForOp(op);
327
+ }
328
+ return out;
329
+ };
330
+
331
+ // Optimized multiline processing with fewer allocations and branches
332
+ const applyOpsToText = (ops: SgrOp[], input: ApplyInput): string => {
333
+ const text = String(input);
334
+ if (CURRENT_LEVEL === COLOR_LEVEL_OFF || ops.length === 0 || text.length === 0) {
335
+ return text;
336
+ }
337
+
338
+ const open = opsToOpen(ops);
339
+
340
+ // Fast path for single-line text (most common case)
341
+ if (!text.includes("\n")) {
342
+ return `${open}${text}${RESET}`;
343
+ }
344
+
345
+ // Optimized multiline handling with pre-calculated string lengths
346
+ const lines = text.split("\n");
347
+ const result = new Array(lines.length);
348
+
349
+ for (let i = 0; i < lines.length; i++) {
350
+ const line = lines[i];
351
+ if (line.endsWith("\r")) {
352
+ result[i] = `${open}${line.slice(0, -1)}\r${RESET}`;
353
+ } else {
354
+ result[i] = `${open}${line}${RESET}`;
355
+ }
356
+ }
357
+
358
+ return result.join("\n");
359
+ };
360
+
361
+ // Build operations for a color request according to CURRENT_LEVEL
362
+ const mkFgOpsFromRgb = (rgb: Rgb, wantBright = false): SgrOp[] => {
363
+ if (CURRENT_LEVEL === COLOR_LEVEL_BASIC) {
364
+ const idx = nearestBasicIndex(rgb);
365
+ return [{ kind: "fg-basic", idx, bright: wantBright }];
366
+ }
367
+ if (CURRENT_LEVEL === COLOR_LEVEL_256) {
368
+ return [{ kind: "fg-256", code: rgbToAnsi256(rgb) }];
369
+ }
370
+ return [{ kind: "fg-true", rgb }];
371
+ };
372
+
373
+ const mkBgOpsFromRgb = (rgb: Rgb, wantBright = false): SgrOp[] => {
374
+ if (CURRENT_LEVEL === COLOR_LEVEL_BASIC) {
375
+ const idx = nearestBasicIndex(rgb);
376
+ return [{ kind: "bg-basic", idx, bright: wantBright }];
377
+ }
378
+ if (CURRENT_LEVEL === COLOR_LEVEL_256) {
379
+ return [{ kind: "bg-256", code: rgbToAnsi256(rgb) }];
380
+ }
381
+ return [{ kind: "bg-true", rgb }];
382
+ };
383
+
384
+ // Style ops
385
+ const STYLE_TABLE: Record<ReStyleKey, SgrOp> = {
386
+ reset: { kind: "style", open: [SGR_RESET] },
387
+ bold: { kind: "style", open: [SGR_BOLD] },
388
+ dim: { kind: "style", open: [SGR_DIM] },
389
+ italic: { kind: "style", open: [SGR_ITALIC] },
390
+ underline: { kind: "style", open: [SGR_UNDERLINE] },
391
+ inverse: { kind: "style", open: [SGR_INVERSE] },
392
+ hidden: { kind: "style", open: [SGR_HIDDEN] },
393
+ strikethrough: { kind: "style", open: [SGR_STRIKETHROUGH] },
394
+ };
395
+
396
+ // Lookup maps
397
+ const STYLE_KEYS = new Set([
398
+ "reset",
399
+ "bold",
400
+ "dim",
401
+ "italic",
402
+ "underline",
403
+ "inverse",
404
+ "hidden",
405
+ "strikethrough",
406
+ ]);
407
+
408
+ // Direct color/bg key checks
409
+ const isColorKey = (key: string): boolean => {
410
+ // Base colors and extended colors
411
+ if (key in NAMED_COLORS) {
412
+ return true;
413
+ }
414
+ // Bright variants
415
+ if (key.endsWith("Bright") && isColorKey(key.slice(0, -BRIGHT_SUFFIX_LENGTH))) {
416
+ return true;
417
+ }
418
+ return false;
419
+ };
420
+
421
+ const isBgKey = (key: string): boolean => {
422
+ if (!key.startsWith("bg") || key.length <= BG_PREFIX_LENGTH) {
423
+ return false;
424
+ }
425
+ const colorPart = key.charAt(BG_PREFIX_LENGTH).toLowerCase() + key.slice(BG_COLOR_START);
426
+ return isColorKey(colorPart);
427
+ };
428
+
429
+ // Proxy with performance through pre-computed lookups
430
+ const callableProxy = (ops: SgrOp[]): Re => {
431
+ const base = ((input: ApplyInput) => applyOpsToText(ops, input)) as FormatCallable;
432
+ Object.defineProperty(base, OP_SYMBOL, {
433
+ value: ops,
434
+ enumerable: false,
435
+ configurable: false,
436
+ writable: false,
437
+ });
438
+
439
+ return new Proxy(base as unknown as Re, {
440
+ apply(_target, _thisArg, argArray) {
441
+ const [input] = argArray as [ApplyInput];
442
+ return applyOpsToText(ops, input);
443
+ },
444
+ get(_target, prop) {
445
+ const key = String(prop);
446
+
447
+ // Ops extractor for chain()
448
+ if (prop === OP_SYMBOL) {
449
+ return ops;
450
+ }
451
+
452
+ // Fast path for styles using Set lookup
453
+ if (STYLE_KEYS.has(key)) {
454
+ const op = STYLE_TABLE[key as ReStyleKey];
455
+ return callableProxy([...ops, op]);
456
+ }
457
+
458
+ // Fast path for colors
459
+ if (isBgKey(key)) {
460
+ const raw = key.slice(BG_PREFIX_LENGTH); // remove 'bg'
461
+ const colorName = raw.charAt(0).toLowerCase() + raw.slice(1);
462
+ const { rgb, wantBright } = parseColorName(colorName as ColorName);
463
+ return callableProxy([...ops, ...mkBgOpsFromRgb(rgb, wantBright)]);
464
+ }
465
+
466
+ if (isColorKey(key)) {
467
+ const { rgb, wantBright } = parseColorName(key as ColorName);
468
+ return callableProxy([...ops, ...mkFgOpsFromRgb(rgb, wantBright)]);
469
+ }
470
+
471
+ // Unknown key → return self (no-op), keeps chain resilient
472
+ return callableProxy(ops);
473
+ },
474
+ });
475
+ };
476
+
477
+ // Public root
478
+ export const re: Re = callableProxy([]);
479
+
480
+ // chain(re.bold, re.red, re.underline)("text")
481
+ export const chain = (...parts: FormatCallable[]): Re => {
482
+ const collected: SgrOp[] = [];
483
+ for (const p of parts) {
484
+ const ops = (p as FormatCallable)[OP_SYMBOL] as SgrOp[] | undefined;
485
+ if (ops && ops.length > 0) {
486
+ for (const op of ops) {
487
+ collected.push(op);
488
+ }
489
+ }
490
+ }
491
+ return callableProxy(collected);
492
+ };
package/bin/mod.d.ts DELETED
@@ -1,75 +0,0 @@
1
- type ColorLevel = 0 | 1 | 2 | 3;
2
- type Rgb = {
3
- r: number;
4
- g: number;
5
- b: number;
6
- };
7
- type SgrOp = {
8
- kind: "style";
9
- open: number[];
10
- } | {
11
- kind: "fg-basic";
12
- idx: number;
13
- bright: boolean;
14
- } | {
15
- kind: "bg-basic";
16
- idx: number;
17
- bright: boolean;
18
- } | {
19
- kind: "fg-256";
20
- code: number;
21
- } | {
22
- kind: "bg-256";
23
- code: number;
24
- } | {
25
- kind: "fg-true";
26
- rgb: Rgb;
27
- } | {
28
- kind: "bg-true";
29
- rgb: Rgb;
30
- };
31
- type ApplyInput = string | number;
32
- type FormatCallable = ((input: ApplyInput) => string) & {
33
- readonly [OP_SYMBOL]: SgrOp[];
34
- };
35
- type BaseColorName = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" | "orange" | "pink" | "purple" | "teal" | "lime" | "brown" | "navy" | "maroon" | "olive" | "silver";
36
- type ColorName = BaseColorName | GrayScaleName | BrightColorName | PastelColorName | BgColorName;
37
- type GrayScaleName = "gray10" | "gray20" | "gray30" | "gray40" | "gray50" | "gray60" | "gray70" | "gray80" | "gray90";
38
- type BrightColorName = "blackBright" | "redBright" | "greenBright" | "yellowBright" | "blueBright" | "magentaBright" | "cyanBright" | "whiteBright" | "orangeBright" | "pinkBright" | "purpleBright" | "tealBright" | "limeBright" | "brownBright" | "navyBright" | "maroonBright" | "oliveBright" | "silverBright";
39
- type PastelColorName = "blackPastel" | "redPastel" | "greenPastel" | "yellowPastel" | "bluePastel" | "magentaPastel" | "cyanPastel" | "whitePastel" | "grayPastel" | "orangePastel" | "pinkPastel" | "purplePastel" | "tealPastel" | "limePastel" | "brownPastel" | "navyPastel" | "maroonPastel" | "olivePastel" | "silverPastel";
40
- type BgColorName = `bg${Capitalize<BaseColorName>}` | `bg${Capitalize<GrayScaleName>}` | `bg${Capitalize<BrightColorName>}` | `bg${Capitalize<PastelColorName>}`;
41
- type MkRgbFn = (r: number, g: number, b: number) => Re;
42
- type MkHexFn = (hex: string) => Re;
43
- type MkHslFn = (h: number, s: number, l: number) => Re;
44
- type StyleKeys = "reset" | "bold" | "dim" | "italic" | "underline" | "inverse" | "hidden" | "strikethrough";
45
- type Re = FormatCallable & {
46
- readonly [K in StyleKeys]: Re;
47
- } & {
48
- readonly [K in ColorName]: Re;
49
- } & {
50
- readonly [K in BgColorName]: Re;
51
- } & {
52
- readonly rgb: MkRgbFn;
53
- readonly hex: MkHexFn;
54
- readonly hsl: MkHslFn;
55
- readonly bgRgb: MkRgbFn;
56
- readonly bgHex: MkHexFn;
57
- readonly bgHsl: MkHslFn;
58
- };
59
- declare const OP_SYMBOL: unique symbol;
60
- export declare const setColorLevel: (level: ColorLevel) => void;
61
- export declare const re: Re;
62
- export declare const rgb: MkRgbFn;
63
- export declare const hex: MkHexFn;
64
- export declare const hsl: MkHslFn;
65
- export declare const bgRgb: MkRgbFn;
66
- export declare const bgHex: MkHexFn;
67
- export declare const bgHsl: MkHslFn;
68
- export declare const chain: (...parts: FormatCallable[]) => Re;
69
- export declare const parseRgb: (value: string) => Rgb | null;
70
- export declare const parseHsl: (value: string) => {
71
- h: number;
72
- s: number;
73
- l: number;
74
- } | null;
75
- export {};
package/bin/mod.js DELETED
@@ -1,455 +0,0 @@
1
- const ESC = "\x1B[";
2
- const RESET = `${ESC}0m`;
3
- const OP_SYMBOL = Symbol("re.ops");
4
- let CURRENT_LEVEL = 3;
5
- export const setColorLevel = (level) => {
6
- if (level !== 0 && level !== 1 && level !== 2 && level !== 3) {
7
- throw new Error("Invalid color level");
8
- }
9
- CURRENT_LEVEL = level;
10
- };
11
- const clampByte = (n) => {
12
- if (!Number.isFinite(n)) return 0;
13
- if (n < 0) return 0;
14
- if (n > 255) return 255;
15
- return Math.round(n);
16
- };
17
- const clampPct = (n) => {
18
- if (!Number.isFinite(n)) return 0;
19
- if (n < 0) return 0;
20
- if (n > 100) return 100;
21
- return n;
22
- };
23
- const hexRe = /^#?([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$/;
24
- const hexToRgb = (hex2) => {
25
- const len = hex2.length;
26
- const start = hex2[0] === "#" ? 1 : 0;
27
- const raw = hex2.slice(start);
28
- const rawLen = raw.length;
29
- if (rawLen !== 3 && rawLen !== 6) return null;
30
- for (let i = 0; i < rawLen; i++) {
31
- const c = raw.charCodeAt(i);
32
- if (!(c >= 48 && c <= 57 || c >= 65 && c <= 70 || c >= 97 && c <= 102)) {
33
- return null;
34
- }
35
- }
36
- if (rawLen === 3) {
37
- const r = parseInt(raw[0] + raw[0], 16);
38
- const g = parseInt(raw[1] + raw[1], 16);
39
- const b = parseInt(raw[2] + raw[2], 16);
40
- return { r, g, b };
41
- }
42
- return {
43
- r: parseInt(raw.slice(0, 2), 16),
44
- g: parseInt(raw.slice(2, 4), 16),
45
- b: parseInt(raw.slice(4, 6), 16)
46
- };
47
- };
48
- const hslToRgb = (h, s, l) => {
49
- const H = (h % 360 + 360) % 360;
50
- const S = Math.min(100, Math.max(0, s)) / 100;
51
- const L = Math.min(100, Math.max(0, l)) / 100;
52
- if (S === 0) {
53
- const v = Math.round(L * 255);
54
- return { r: v, g: v, b: v };
55
- }
56
- const c = (1 - Math.abs(2 * L - 1)) * S;
57
- const x = c * (1 - Math.abs(H / 60 % 2 - 1));
58
- const m = L - c / 2;
59
- const sector = Math.floor(H / 60) % 6;
60
- const rgbValues = [
61
- [c, x, 0],
62
- [x, c, 0],
63
- [0, c, x],
64
- [0, x, c],
65
- [x, 0, c],
66
- [c, 0, x]
67
- ][sector];
68
- return {
69
- r: Math.round((rgbValues[0] + m) * 255),
70
- g: Math.round((rgbValues[1] + m) * 255),
71
- b: Math.round((rgbValues[2] + m) * 255)
72
- };
73
- };
74
- const BASIC8 = [
75
- { r: 0, g: 0, b: 0 },
76
- // black
77
- { r: 205, g: 0, b: 0 },
78
- // red
79
- { r: 0, g: 205, b: 0 },
80
- // green
81
- { r: 205, g: 205, b: 0 },
82
- // yellow
83
- { r: 0, g: 0, b: 238 },
84
- // blue
85
- { r: 205, g: 0, b: 205 },
86
- // magenta
87
- { r: 0, g: 205, b: 205 },
88
- // cyan
89
- { r: 229, g: 229, b: 229 }
90
- // white (light gray)
91
- ];
92
- const sgr = (codes) => `${ESC}${codes.join(";")}m`;
93
- const nearestBasicIndex = (rgb2) => {
94
- let best = 0;
95
- let bestDist = Number.POSITIVE_INFINITY;
96
- for (let i = 0; i < BASIC8.length; i++) {
97
- const c = BASIC8[i];
98
- const dr = c.r - rgb2.r;
99
- const dg = c.g - rgb2.g;
100
- const db = c.b - rgb2.b;
101
- const d = dr * dr + dg * dg + db * db;
102
- if (d < bestDist) {
103
- bestDist = d;
104
- best = i;
105
- }
106
- }
107
- return best;
108
- };
109
- const rgbToAnsi256 = (rgb2) => {
110
- if (rgb2.r === rgb2.g && rgb2.g === rgb2.b) {
111
- if (rgb2.r < 8) return 16;
112
- if (rgb2.r > 248) return 231;
113
- const step = Math.round((rgb2.r - 8) / 247 * 24);
114
- return 232 + step;
115
- }
116
- const r = Math.round(rgb2.r / 255 * 5);
117
- const g = Math.round(rgb2.g / 255 * 5);
118
- const b = Math.round(rgb2.b / 255 * 5);
119
- return 16 + 36 * r + 6 * g + b;
120
- };
121
- const CORE_COLORS = {
122
- black: "#000000",
123
- red: "#ff0000",
124
- green: "#00ff00",
125
- yellow: "#ffff00",
126
- blue: "#0000ff",
127
- magenta: "#ff00ff",
128
- cyan: "#00ffff",
129
- white: "#ffffff",
130
- gray: "#808080"
131
- };
132
- const EXTENDED_COLORS = {
133
- orange: "#ffa500",
134
- pink: "#ffc0cb",
135
- purple: "#800080",
136
- teal: "#008080",
137
- lime: "#00ff00",
138
- brown: "#a52a2a",
139
- navy: "#000080",
140
- maroon: "#800000",
141
- olive: "#808000",
142
- silver: "#c0c0c0"
143
- };
144
- let NAMED_HEX;
145
- const getNamedColors = () => {
146
- if (NAMED_HEX) return NAMED_HEX;
147
- NAMED_HEX = { ...CORE_COLORS, ...EXTENDED_COLORS };
148
- return NAMED_HEX;
149
- };
150
- let GRAYS;
151
- const getGrayColors = () => {
152
- if (GRAYS) return GRAYS;
153
- GRAYS = {};
154
- for (let i = 1; i <= 9; i++) {
155
- const value = Math.round(26 * i - 10);
156
- const hex2 = value.toString(16).padStart(2, "0");
157
- const key = `gray${i}0`;
158
- GRAYS[key] = `#${hex2}${hex2}${hex2}`;
159
- }
160
- return GRAYS;
161
- };
162
- const mixWithWhite = (rgb2, factor) => {
163
- const t = factor;
164
- return {
165
- r: clampByte(rgb2.r * (1 - t) + 255 * t),
166
- g: clampByte(rgb2.g * (1 - t) + 255 * t),
167
- b: clampByte(rgb2.b * (1 - t) + 255 * t)
168
- };
169
- };
170
- const fromNamed = (name) => {
171
- const hex2 = getNamedColors()[name];
172
- const rgb2 = hexToRgb(hex2);
173
- return rgb2 ?? { r: 255, g: 255, b: 255 };
174
- };
175
- const fromGrayName = (name) => {
176
- const hex2 = getGrayColors()[name];
177
- const rgb2 = hexToRgb(hex2);
178
- return rgb2 ?? { r: 128, g: 128, b: 128 };
179
- };
180
- const toBaseName = (compound) => {
181
- const base = compound.replace(/(Bright|Pastel)$/u, "");
182
- const key = base.charAt(0).toLowerCase() + base.slice(1);
183
- return key;
184
- };
185
- const parseColorName = (name) => {
186
- if (name.endsWith("Bright")) {
187
- const base = toBaseName(name);
188
- const rgb2 = fromNamed(base);
189
- const rgbAdj = mixWithWhite(rgb2, 0.25);
190
- return { rgb: rgbAdj, wantBright: true };
191
- }
192
- if (name.endsWith("Pastel")) {
193
- const base = toBaseName(name);
194
- const rgb2 = fromNamed(base);
195
- const rgbAdj = mixWithWhite(rgb2, 0.6);
196
- return { rgb: rgbAdj, wantBright: false };
197
- }
198
- if (name.startsWith("gray")) {
199
- return { rgb: fromGrayName(name), wantBright: false };
200
- }
201
- return { rgb: fromNamed(name), wantBright: false };
202
- };
203
- const openForOp = (op) => {
204
- if (CURRENT_LEVEL === 0) return "";
205
- switch (op.kind) {
206
- case "style":
207
- return sgr(op.open);
208
- case "fg-basic":
209
- return sgr([(op.bright ? 90 : 30) + op.idx]);
210
- case "bg-basic":
211
- return sgr([(op.bright ? 100 : 40) + op.idx]);
212
- case "fg-256":
213
- return `${ESC}38;5;${op.code}m`;
214
- case "bg-256":
215
- return `${ESC}48;5;${op.code}m`;
216
- case "fg-true":
217
- return `${ESC}38;2;${op.rgb.r};${op.rgb.g};${op.rgb.b}m`;
218
- case "bg-true":
219
- return `${ESC}48;2;${op.rgb.r};${op.rgb.g};${op.rgb.b}m`;
220
- }
221
- };
222
- const opsToOpen = (ops) => {
223
- if (CURRENT_LEVEL === 0) return "";
224
- let out = "";
225
- for (const op of ops) out += openForOp(op);
226
- return out;
227
- };
228
- const applyOpsToText = (ops, input) => {
229
- const text = String(input);
230
- if (CURRENT_LEVEL === 0 || ops.length === 0 || text.length === 0) return text;
231
- const open = opsToOpen(ops);
232
- if (!text.includes("\n")) {
233
- return `${open}${text}${RESET}`;
234
- }
235
- const lines = text.split("\n");
236
- const result = new Array(lines.length);
237
- for (let i = 0; i < lines.length; i++) {
238
- const line = lines[i];
239
- if (line.endsWith("\r")) {
240
- result[i] = `${open}${line.slice(0, -1)}\r${RESET}`;
241
- } else {
242
- result[i] = `${open}${line}${RESET}`;
243
- }
244
- }
245
- return result.join("\n");
246
- };
247
- const mkFgOpsFromRgb = (rgb2, wantBright = false) => {
248
- if (CURRENT_LEVEL === 1) {
249
- const idx = nearestBasicIndex(rgb2);
250
- return [{ kind: "fg-basic", idx, bright: wantBright }];
251
- }
252
- if (CURRENT_LEVEL === 2) {
253
- return [{ kind: "fg-256", code: rgbToAnsi256(rgb2) }];
254
- }
255
- return [{ kind: "fg-true", rgb: rgb2 }];
256
- };
257
- const mkBgOpsFromRgb = (rgb2, wantBright = false) => {
258
- if (CURRENT_LEVEL === 1) {
259
- const idx = nearestBasicIndex(rgb2);
260
- return [{ kind: "bg-basic", idx, bright: wantBright }];
261
- }
262
- if (CURRENT_LEVEL === 2) {
263
- return [{ kind: "bg-256", code: rgbToAnsi256(rgb2) }];
264
- }
265
- return [{ kind: "bg-true", rgb: rgb2 }];
266
- };
267
- const STYLE_TABLE = {
268
- reset: { kind: "style", open: [0] },
269
- bold: { kind: "style", open: [1] },
270
- dim: { kind: "style", open: [2] },
271
- italic: { kind: "style", open: [3] },
272
- underline: { kind: "style", open: [4] },
273
- inverse: { kind: "style", open: [7] },
274
- hidden: { kind: "style", open: [8] },
275
- strikethrough: { kind: "style", open: [9] }
276
- };
277
- const STYLE_KEYS = /* @__PURE__ */ new Set([
278
- "reset",
279
- "bold",
280
- "dim",
281
- "italic",
282
- "underline",
283
- "inverse",
284
- "hidden",
285
- "strikethrough"
286
- ]);
287
- const DYNAMIC_KEYS = /* @__PURE__ */ new Set(["rgb", "hex", "hsl", "bgRgb", "bgHex", "bgHsl"]);
288
- let COLOR_KEY_CACHE;
289
- let BG_KEY_CACHE;
290
- const getColorKeys = () => {
291
- if (COLOR_KEY_CACHE) return COLOR_KEY_CACHE;
292
- const baseColors = [
293
- "black",
294
- "red",
295
- "green",
296
- "yellow",
297
- "blue",
298
- "magenta",
299
- "cyan",
300
- "white",
301
- "gray"
302
- ];
303
- const extendedColors = [
304
- "orange",
305
- "pink",
306
- "purple",
307
- "teal",
308
- "lime",
309
- "brown",
310
- "navy",
311
- "maroon",
312
- "olive",
313
- "silver"
314
- ];
315
- const grayNames = Array.from({ length: 9 }, (_, i) => `gray${(i + 1) * 10}`);
316
- COLOR_KEY_CACHE = /* @__PURE__ */ new Set([
317
- ...baseColors,
318
- ...extendedColors,
319
- ...grayNames,
320
- ...baseColors.map((c) => `${c}Bright`),
321
- ...extendedColors.map((c) => `${c}Bright`),
322
- ...baseColors.map((c) => `${c}Pastel`),
323
- ...extendedColors.map((c) => `${c}Pastel`),
324
- "grayPastel"
325
- ]);
326
- return COLOR_KEY_CACHE;
327
- };
328
- const getBgKeys = () => {
329
- if (BG_KEY_CACHE) return BG_KEY_CACHE;
330
- BG_KEY_CACHE = /* @__PURE__ */ new Set();
331
- for (const colorKey of getColorKeys()) {
332
- const capitalizedKey = colorKey.charAt(0).toUpperCase() + colorKey.slice(1);
333
- BG_KEY_CACHE.add(`bg${capitalizedKey}`);
334
- }
335
- return BG_KEY_CACHE;
336
- };
337
- const callableFromOps = (ops) => {
338
- const fn = ((input) => applyOpsToText(ops, input));
339
- Object.defineProperty(fn, OP_SYMBOL, {
340
- value: ops,
341
- enumerable: false,
342
- configurable: false,
343
- writable: false
344
- });
345
- return fn;
346
- };
347
- const appendOps = (base, extra) => {
348
- const out = [];
349
- for (const op of base) out.push(op);
350
- for (const op of extra) out.push(op);
351
- return out;
352
- };
353
- const mkRgbMethod = (ops, isBg) => (r, g, b) => {
354
- const rgb2 = { r: clampByte(r), g: clampByte(g), b: clampByte(b) };
355
- const add = isBg ? mkBgOpsFromRgb(rgb2) : mkFgOpsFromRgb(rgb2);
356
- return callableProxy(appendOps(ops, add));
357
- };
358
- const mkHexMethod = (ops, isBg) => (hex2) => {
359
- const rgb2 = hexToRgb(hex2);
360
- if (!rgb2) return callableProxy(ops);
361
- const add = isBg ? mkBgOpsFromRgb(rgb2) : mkFgOpsFromRgb(rgb2);
362
- return callableProxy(appendOps(ops, add));
363
- };
364
- const mkHslMethod = (ops, isBg) => (h, s, l) => {
365
- const rgb2 = hslToRgb(h, s, l);
366
- const add = isBg ? mkBgOpsFromRgb(rgb2) : mkFgOpsFromRgb(rgb2);
367
- return callableProxy(appendOps(ops, add));
368
- };
369
- const callableProxy = (ops) => {
370
- const base = callableFromOps(ops);
371
- return new Proxy(base, {
372
- apply(_target, _thisArg, argArray) {
373
- const [input] = argArray;
374
- return applyOpsToText(ops, input);
375
- },
376
- get(_target, prop) {
377
- const key = String(prop);
378
- if (prop === OP_SYMBOL) return ops;
379
- if (STYLE_KEYS.has(key)) {
380
- const op = STYLE_TABLE[key];
381
- return callableProxy(appendOps(ops, [op]));
382
- }
383
- if (DYNAMIC_KEYS.has(key)) {
384
- const isBg = key.startsWith("bg");
385
- switch (key) {
386
- case "rgb":
387
- return mkRgbMethod(ops, false);
388
- case "hex":
389
- return mkHexMethod(ops, false);
390
- case "hsl":
391
- return mkHslMethod(ops, false);
392
- case "bgRgb":
393
- return mkRgbMethod(ops, true);
394
- case "bgHex":
395
- return mkHexMethod(ops, true);
396
- case "bgHsl":
397
- return mkHslMethod(ops, true);
398
- }
399
- }
400
- const bgKeys = getBgKeys();
401
- const colorKeys = getColorKeys();
402
- if (bgKeys.has(key)) {
403
- const raw = key.slice(2);
404
- const colorName = raw.charAt(0).toLowerCase() + raw.slice(1);
405
- const { rgb: rgb2, wantBright } = parseColorName(colorName);
406
- return callableProxy(appendOps(ops, mkBgOpsFromRgb(rgb2, wantBright)));
407
- }
408
- if (colorKeys.has(key)) {
409
- const { rgb: rgb2, wantBright } = parseColorName(key);
410
- return callableProxy(appendOps(ops, mkFgOpsFromRgb(rgb2, wantBright)));
411
- }
412
- return callableProxy(ops);
413
- }
414
- });
415
- };
416
- export const re = callableProxy([]);
417
- export const rgb = (r, g, b) => re.rgb(r, g, b);
418
- export const hex = (h) => re.hex(h);
419
- export const hsl = (h, s, l) => re.hsl(h, s, l);
420
- export const bgRgb = (r, g, b) => re.bgRgb(r, g, b);
421
- export const bgHex = (h) => re.bgHex(h);
422
- export const bgHsl = (h, s, l) => re.bgHsl(h, s, l);
423
- export const chain = (...parts) => {
424
- const collected = [];
425
- for (const p of parts) {
426
- const ops = p[OP_SYMBOL];
427
- if (ops && ops.length > 0) {
428
- for (const op of ops) collected.push(op);
429
- }
430
- }
431
- return callableProxy(collected);
432
- };
433
- export const parseRgb = (value) => {
434
- const v = value.trim();
435
- if (!v.toLowerCase().startsWith("rgb(") || !v.endsWith(")")) return null;
436
- const inner = v.slice(4, -1);
437
- const parts = inner.split(",").map((s) => s.trim());
438
- if (parts.length !== 3) return null;
439
- const nums = parts.map((p) => Number.parseInt(p, 10));
440
- if (!Number.isFinite(nums[0]) || !Number.isFinite(nums[1]) || !Number.isFinite(nums[2]))
441
- return null;
442
- return { r: clampByte(nums[0]), g: clampByte(nums[1]), b: clampByte(nums[2]) };
443
- };
444
- export const parseHsl = (value) => {
445
- const v = value.trim().toLowerCase();
446
- if (!v.startsWith("hsl(") || !v.endsWith(")")) return null;
447
- const inner = v.slice(4, -1);
448
- const parts = inner.split(",").map((s2) => s2.trim().replace(/%$/u, ""));
449
- if (parts.length !== 3) return null;
450
- const h = Number.parseFloat(parts[0]);
451
- const s = Number.parseFloat(parts[1]);
452
- const l = Number.parseFloat(parts[2]);
453
- if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) return null;
454
- return { h, s, l };
455
- };