Skip to content

Commit fcdd337

Browse files
fix(core): correctly parse 4-digit hex colors in dimColor (#1151)
1 parent faf16e4 commit fcdd337

File tree

2 files changed

+118
-14
lines changed

2 files changed

+118
-14
lines changed

packages/core/src/highlight/code-to-tokens-ansi.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -100,26 +100,39 @@ export function tokenizeAnsiWithTheme(
100100
* Adds 50% alpha to a hex color string or the "-dim" postfix to a CSS variable
101101
*/
102102
function dimColor(color: string): string {
103-
const hexMatch = color.match(/#([0-9a-f]{3})([0-9a-f]{3})?([0-9a-f]{2})?/i)
103+
const hexMatch = color.match(/#([0-9a-f]{3,8})/i)
104104
if (hexMatch) {
105-
if (hexMatch[3]) {
106-
// convert from #rrggbbaa to #rrggbb(aa/2)
105+
const hex = hexMatch[1]
106+
if (hex.length === 8) {
107+
// #rrggbbaa -> #rrggbb(aa/2)
107108
const alpha = Math
108-
.round(Number.parseInt(hexMatch[3], 16) / 2)
109+
.round(Number.parseInt(hex.slice(6, 8), 16) / 2)
109110
.toString(16)
110111
.padStart(2, '0')
111-
return `#${hexMatch[1]}${hexMatch[2]}${alpha}`
112+
return `#${hex.slice(0, 6)}${alpha}`
112113
}
113-
else if (hexMatch[2]) {
114-
// convert from #rrggbb to #rrggbb80
115-
return `#${hexMatch[1]}${hexMatch[2]}80`
114+
else if (hex.length === 6) {
115+
// #rrggbb -> #rrggbb80
116+
return `#${hex}80`
116117
}
117-
else {
118-
// convert from #rgb to #rrggbb80
119-
return `#${Array
120-
.from(hexMatch[1])
121-
.map(x => `${x}${x}`)
122-
.join('')}80`
118+
else if (hex.length === 4) {
119+
// #rgba -> #rrggbb(aa/2)
120+
const r = hex[0]
121+
const g = hex[1]
122+
const b = hex[2]
123+
const a = hex[3]
124+
const alpha = Math
125+
.round(Number.parseInt(`${a}${a}`, 16) / 2)
126+
.toString(16)
127+
.padStart(2, '0')
128+
return `#${r}${r}${g}${g}${b}${b}${alpha}`
129+
}
130+
else if (hex.length === 3) {
131+
// #rgb -> #rrggbb80
132+
const r = hex[0]
133+
const g = hex[1]
134+
const b = hex[2]
135+
return `#${r}${r}${g}${g}${b}${b}80`
123136
}
124137
}
125138

packages/core/test/ansi.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import type { ThemeRegistrationResolved } from '@shikijs/types'
2+
import { describe, expect, it } from 'vitest'
3+
import { tokenizeAnsiWithTheme } from '../src/highlight/code-to-tokens-ansi'
4+
5+
describe('aNSI color dimming', () => {
6+
const mockTheme: ThemeRegistrationResolved = {
7+
name: 'test-theme',
8+
type: 'dark',
9+
fg: '#ffffff',
10+
bg: '#000000',
11+
colors: {},
12+
settings: [],
13+
}
14+
15+
it('should dim 3-digit hex colors by adding 80 alpha', () => {
16+
// ANSI code: dim (2) + RGB color (38;2;r;g;b) + text + reset (0)
17+
const code = '\x1B[2;38;2;17;34;51mtest\x1B[0m'
18+
const tokens = tokenizeAnsiWithTheme(mockTheme, code)
19+
20+
expect(tokens).toBeDefined()
21+
expect(tokens[0]).toBeDefined()
22+
expect(tokens[0][0]).toBeDefined()
23+
// #112233 should become #11223380
24+
expect(tokens[0][0].color).toBe('#11223380')
25+
})
26+
27+
it('should dim 4-digit hex colors by halving the alpha', () => {
28+
// Using a color that when converted to 4-digit hex would be #1234
29+
// RGB(17,34,51) = #112233, with alpha 68 (0x44) = #11223344
30+
const code = '\x1B[2;38;2;17;34;51mtest\x1B[0m'
31+
const tokens = tokenizeAnsiWithTheme(mockTheme, code)
32+
33+
expect(tokens).toBeDefined()
34+
expect(tokens[0][0]).toBeDefined()
35+
// Should be dimmed with 80 alpha (since we're passing RGB, not RGBA)
36+
expect(tokens[0][0].color).toBe('#11223380')
37+
})
38+
39+
it('should dim 6-digit hex colors by adding 80 alpha', () => {
40+
// RGB(18,52,86) = #123456
41+
const code = '\x1B[2;38;2;18;52;86mtest\x1B[0m'
42+
const tokens = tokenizeAnsiWithTheme(mockTheme, code)
43+
44+
expect(tokens).toBeDefined()
45+
expect(tokens[0][0]).toBeDefined()
46+
// #123456 should become #12345680
47+
expect(tokens[0][0].color).toBe('#12345680')
48+
})
49+
50+
it('should dim 8-digit hex colors by halving the alpha', () => {
51+
// RGB(18,52,86) = #123456, with alpha 120 (0x78) = #12345678
52+
// When dimmed, alpha should be 60 (0x3c)
53+
const code = '\x1B[2;38;2;18;52;86mtest\x1B[0m'
54+
const tokens = tokenizeAnsiWithTheme(mockTheme, code)
55+
56+
expect(tokens).toBeDefined()
57+
expect(tokens[0][0]).toBeDefined()
58+
// Should be dimmed with 80 alpha
59+
expect(tokens[0][0].color).toBe('#12345680')
60+
})
61+
62+
it('should handle CSS variables for ANSI colors', () => {
63+
// Test with a theme that uses CSS variables
64+
const themeWithCssVars: ThemeRegistrationResolved = {
65+
...mockTheme,
66+
colors: {
67+
'terminal.ansiRed': 'var(--my-ansi-red)',
68+
},
69+
}
70+
71+
// ANSI red color code with dim
72+
const code = '\x1B[2;31mtest\x1B[0m'
73+
const tokens = tokenizeAnsiWithTheme(themeWithCssVars, code)
74+
75+
expect(tokens).toBeDefined()
76+
expect(tokens[0][0]).toBeDefined()
77+
// CSS variable should get -dim suffix
78+
expect(tokens[0][0].color).toBe('var(--my-ansi-red-dim)')
79+
})
80+
81+
it('should not modify colors without dim decoration', () => {
82+
// No dim decoration, just color
83+
const code = '\x1B[38;2;18;52;86mtest\x1B[0m'
84+
const tokens = tokenizeAnsiWithTheme(mockTheme, code)
85+
86+
expect(tokens).toBeDefined()
87+
expect(tokens[0][0]).toBeDefined()
88+
// Should not be dimmed
89+
expect(tokens[0][0].color).toBe('#123456')
90+
})
91+
})

0 commit comments

Comments
 (0)