Skip to content

Commit

Permalink
fix: ansi HTML edge cases (#4279)
Browse files Browse the repository at this point in the history
  • Loading branch information
chenjiahan authored Dec 27, 2024
1 parent 638c519 commit 7ae6c2a
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 16 deletions.
34 changes: 19 additions & 15 deletions packages/core/src/server/ansiHTML.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const openCodes: Record<string, string> = {
const styles: Record<string, string> = {
1: 'font-weight:bold', // bold
2: 'opacity:0.5', // dim
3: 'font-style:italic', // italic
4: 'text-decoration:underline', // underscore
4: 'text-decoration:underline;text-underline-offset:3px', // underscore
8: 'display:none', // hidden
9: 'text-decoration:line-through', // delete
30: 'color:#000', // darkgrey
Expand All @@ -13,9 +13,14 @@ const openCodes: Record<string, string> = {
35: 'color:#f76ebe', // magenta, hsl(300deg 90% 70%)
36: 'color:#6eecf7', // cyan, hsl(210deg 90% 70%)
37: 'color:#f0f0f0', // lightgrey, hsl(0deg 0% 94%)
90: 'color:#888', // darkgrey
90: 'color:#888', // bright black
};

// use the same color for bright colors
for (let i = 91; i <= 97; i++) {
styles[i] = styles[i - 60];
}

const closeCode = [0, 21, 22, 23, 24, 27, 28, 29, 39, 49];

/**
Expand All @@ -28,21 +33,20 @@ export function ansiHTML(text: string): string {
let ret = text.replace(
// biome-ignore lint/suspicious/noControlCharactersInRegex: allowed
/\x1B\[([0-9;]+)m/g,
(_match: string, seq: string): string => {
const openStyle = openCodes[seq];
if (openStyle) {
// If current sequence has been opened, close it.
if (ansiCodes.indexOf(seq) !== -1) {
ansiCodes.pop();
return '</span>';
(_match: string, sequences: string): string => {
let style = '';
for (const seq of sequences.split(';')) {
if (styles[seq]) {
style += `${styles[seq]};`;
}
// Open tag.
ansiCodes.push(seq);
return `<span style="${openStyle};">`;
}

if (closeCode.includes(Number(seq))) {
// Pop sequence
if (style) {
ansiCodes.push(sequences);
return `<span style="${style}">`;
}

if (closeCode.includes(Number(sequences)) && ansiCodes.length > 0) {
ansiCodes.pop();
return '</span>';
}
Expand Down
47 changes: 46 additions & 1 deletion packages/core/tests/ansi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('ansiHTML', () => {
it('should convert ANSI underline codes to HTML', () => {
const input = '\x1B[4mHello, World!\x1B[0m';
const expected =
'<span style="text-decoration:underline;">Hello, World!</span>';
'<span style="text-decoration:underline;text-underline-offset:3px;">Hello, World!</span>';
expect(ansiHTML(input)).toEqual(expected);
});

Expand All @@ -67,4 +67,49 @@ describe('ansiHTML', () => {
'<span style="text-decoration:line-through;">Hello, World!</span>';
expect(ansiHTML(input)).toEqual(expected);
});

it('should convert multiple styles ', () => {
const input = '\x1B[31;1;4mHello, World!\x1B[0m';
const expected =
'<span style="color:#fb6a6a;font-weight:bold;text-decoration:underline;text-underline-offset:3px;">Hello, World!</span>';
expect(ansiHTML(input)).toEqual(expected);
});

it('should convert file path with ANSI color codes to HTML', () => {
const input = '[\u001b[36;1;4m/path/to/src/index.js\u001b[0m:4:1]';
const expected =
'[<span style="color:#6eecf7;font-weight:bold;text-decoration:underline;text-underline-offset:3px;">/path/to/src/index.js</span>:4:1]';
expect(ansiHTML(input)).toEqual(expected);
});

it('should ignore background colors', () => {
const bgRedInput = '\x1B[41mHello, World!\x1B[0m';
const bgRedExpected = 'Hello, World!';
expect(ansiHTML(bgRedInput)).toEqual(bgRedExpected);

const bgBlueInput = '\x1B[44;1mHello, World!\x1B[0m';
const bgBlueExpected =
'<span style="font-weight:bold;">Hello, World!</span>';
expect(ansiHTML(bgBlueInput)).toEqual(bgBlueExpected);
});

it('should handle nested styles', () => {
const input = '\x1B[31mRed \x1B[1mBold Red \x1B[34mBold Blue\x1B[0m';
const expected =
'<span style="color:#fb6a6a;">Red <span style="font-weight:bold;">Bold Red <span style="color:#6eb2f7;">Bold Blue</span></span></span>';
expect(ansiHTML(input)).toEqual(expected);
});

it('should handle bright colors', () => {
const input = '\x1B[91mBright Red\x1B[0m';
const expected = '<span style="color:#fb6a6a;">Bright Red</span>';
expect(ansiHTML(input)).toEqual(expected);
});

it('should handle reset within text', () => {
const input = '\x1B[31mRed\x1B[0m Normal \x1B[34mBlue\x1B[0m';
const expected =
'<span style="color:#fb6a6a;">Red</span> Normal <span style="color:#6eb2f7;">Blue</span>';
expect(ansiHTML(input)).toEqual(expected);
});
});

0 comments on commit 7ae6c2a

Please sign in to comment.