diff --git a/packages/core/src/server/ansiHTML.ts b/packages/core/src/server/ansiHTML.ts
index d6db8c2181..7cebcbca03 100644
--- a/packages/core/src/server/ansiHTML.ts
+++ b/packages/core/src/server/ansiHTML.ts
@@ -1,8 +1,8 @@
-const openCodes: Record = {
+const styles: Record = {
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
@@ -13,9 +13,14 @@ const openCodes: Record = {
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];
/**
@@ -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 '';
+ (_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 ``;
}
- if (closeCode.includes(Number(seq))) {
- // Pop sequence
+ if (style) {
+ ansiCodes.push(sequences);
+ return ``;
+ }
+
+ if (closeCode.includes(Number(sequences)) && ansiCodes.length > 0) {
ansiCodes.pop();
return '';
}
diff --git a/packages/core/tests/ansi.test.ts b/packages/core/tests/ansi.test.ts
index 940314ee15..1498fa6e81 100644
--- a/packages/core/tests/ansi.test.ts
+++ b/packages/core/tests/ansi.test.ts
@@ -57,7 +57,7 @@ describe('ansiHTML', () => {
it('should convert ANSI underline codes to HTML', () => {
const input = '\x1B[4mHello, World!\x1B[0m';
const expected =
- 'Hello, World!';
+ 'Hello, World!';
expect(ansiHTML(input)).toEqual(expected);
});
@@ -67,4 +67,49 @@ describe('ansiHTML', () => {
'Hello, World!';
expect(ansiHTML(input)).toEqual(expected);
});
+
+ it('should convert multiple styles ', () => {
+ const input = '\x1B[31;1;4mHello, World!\x1B[0m';
+ const expected =
+ 'Hello, World!';
+ 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 =
+ '[/path/to/src/index.js: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 =
+ 'Hello, World!';
+ expect(ansiHTML(bgBlueInput)).toEqual(bgBlueExpected);
+ });
+
+ it('should handle nested styles', () => {
+ const input = '\x1B[31mRed \x1B[1mBold Red \x1B[34mBold Blue\x1B[0m';
+ const expected =
+ 'Red Bold Red Bold Blue';
+ expect(ansiHTML(input)).toEqual(expected);
+ });
+
+ it('should handle bright colors', () => {
+ const input = '\x1B[91mBright Red\x1B[0m';
+ const expected = 'Bright Red';
+ expect(ansiHTML(input)).toEqual(expected);
+ });
+
+ it('should handle reset within text', () => {
+ const input = '\x1B[31mRed\x1B[0m Normal \x1B[34mBlue\x1B[0m';
+ const expected =
+ 'Red Normal Blue';
+ expect(ansiHTML(input)).toEqual(expected);
+ });
});