diff --git a/e2e/cases/server/overlay-type-errors/index.test.ts b/e2e/cases/server/overlay-type-errors/index.test.ts
index 18fed604b8..f0bffde326 100644
--- a/e2e/cases/server/overlay-type-errors/index.test.ts
+++ b/e2e/cases/server/overlay-type-errors/index.test.ts
@@ -18,7 +18,7 @@ test('should display type errors on overlay correctly', async ({ page }) => {
// The first span is "TS2322: "
const firstSpan = errorOverlay.locator('span').first();
expect(await firstSpan.textContent()).toEqual('TS2322: ');
- expect(await firstSpan.getAttribute('style')).toEqual('color:#888');
+ expect(await firstSpan.getAttribute('style')).toEqual('color:#888;');
// The first link is "/src/index.ts:3:1"
const firstLink = errorOverlay.locator('.file-link').first();
diff --git a/packages/core/src/server/ansiHTML.ts b/packages/core/src/server/ansiHTML.ts
index a0c6da369c..d6db8c2181 100644
--- a/packages/core/src/server/ansiHTML.ts
+++ b/packages/core/src/server/ansiHTML.ts
@@ -1,89 +1,36 @@
-/**
- * This module is modified based on `ansi-html-community`
- * https://github.com/mahdyar/ansi-html-community
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * https://github.com/mahdyar/ansi-html-community/blob/master/LICENSE
- */
-
-// https://github.com/chalk/ansi-regex
-function ansiRegex() {
- // Valid string terminator sequences are BEL, ESC\, and 0x9c
- const ST = '(?:\\u0007|\\u001B\\u005C|\\u009C)';
- const pattern = [
- `[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?${ST})`,
- '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))',
- ].join('|');
-
- return new RegExp(pattern, 'g');
-}
-
-const colors: Record = {
- black: '#000',
- // hsl(0deg 95% 70%)
- red: '#fb6a6a',
- // hsl(135deg 90% 70%)
- green: '#6ef790',
- // hsl(65deg 90% 75%)
- yellow: '#eff986',
- // hsl(185deg 90% 70%)
- cyan: '#6eecf7',
- // hsl(210deg 90% 70%)
- blue: '#6eb2f7',
- // hsl(325deg 90% 70%)
- magenta: '#f76ebe',
- lightgrey: '#f0f0f0',
- darkgrey: '#888',
-};
-
-const styles: Record = {
- 30: 'black',
- 31: 'red',
- 32: 'green',
- 33: 'yellow',
- 34: 'blue',
- 35: 'magenta',
- 36: 'cyan',
- 37: 'lightgrey',
-};
-
-const openTags: Record = {
- '1': 'font-weight:bold', // bold
- '2': 'opacity:0.5', // dim
- '3': '', // italic
- '4': '', // underscore
- '8': 'display:none', // hidden
- '9': '', // delete
+const openCodes: Record = {
+ 1: 'font-weight:bold', // bold
+ 2: 'opacity:0.5', // dim
+ 3: 'font-style:italic', // italic
+ 4: 'text-decoration:underline', // underscore
+ 8: 'display:none', // hidden
+ 9: 'text-decoration:line-through', // delete
+ 30: 'color:#000', // darkgrey
+ 31: 'color:#fb6a6a', // red, hsl(0deg 95% 70%)
+ 32: 'color:#6ef790', // green, hsl(65deg 90% 75%)
+ 33: 'color:#eff986', // yellow, hsl(185deg 90% 70%)
+ 34: 'color:#6eb2f7', // blue, hsl(325deg 90% 70%)
+ 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
};
-const closeTags: Record = {
- '23': '', // reset italic
- '24': '', // reset underscore
- '29': '', // reset delete
-};
-
-for (const n of [0, 21, 22, 27, 28, 39, 49]) {
- closeTags[n.toString()] = '';
-}
+const closeCode = [0, 21, 22, 23, 24, 27, 28, 29, 39, 49];
/**
- * Converts text with ANSI color codes to HTML markup.
+ * Converts text with ANSI color codes to HTML markup
*/
export function ansiHTML(text: string): string {
- // Returns the text if the string has no ANSI escape code.
- if (!ansiRegex().test(text)) {
- return text;
- }
-
- // Cache opened sequence.
+ // Cache opened sequence
const ansiCodes: string[] = [];
- // Replace with markup.
+ // Replace with markup
let ret = text.replace(
// biome-ignore lint/suspicious/noControlCharactersInRegex: allowed
- /\x1B\[(\d+)m/g,
+ /\x1B\[([0-9;]+)m/g,
(_match: string, seq: string): string => {
- const ot = openTags[seq];
- if (ot) {
+ const openStyle = openCodes[seq];
+ if (openStyle) {
// If current sequence has been opened, close it.
if (ansiCodes.indexOf(seq) !== -1) {
ansiCodes.pop();
@@ -91,38 +38,24 @@ export function ansiHTML(text: string): string {
}
// Open tag.
ansiCodes.push(seq);
- return ot[0] === '<' ? ot : ``;
+ return ``;
}
- const ct = closeTags[seq];
- if (ct) {
+ if (closeCode.includes(Number(seq))) {
// Pop sequence
ansiCodes.pop();
- return ct;
+ return '';
}
return '';
},
);
- // Make sure tags are closed.
- const l = ansiCodes.length;
- if (l > 0) {
- ret += Array(l + 1).join('');
+ // Make sure tags are closed
+ if (ansiCodes.length > 0) {
+ ret += Array(ansiCodes.length + 1).join('');
}
return ret;
}
-function setTags(): void {
- openTags['90'] = `color:${colors.darkgrey}`;
-
- for (const code in styles) {
- const color = styles[code];
- const oriColor = colors[color] || colors.black;
- openTags[code] = `color:${oriColor}`;
- }
-}
-
-setTags();
-
export default ansiHTML;
diff --git a/packages/core/tests/ansi.test.ts b/packages/core/tests/ansi.test.ts
new file mode 100644
index 0000000000..940314ee15
--- /dev/null
+++ b/packages/core/tests/ansi.test.ts
@@ -0,0 +1,70 @@
+import { ansiHTML } from '../src/server/ansiHTML';
+
+describe('ansiHTML', () => {
+ it('should convert ANSI color codes to HTML', () => {
+ const redInput = '\x1B[31mHello, World!\x1B[0m';
+ const redExpected = 'Hello, World!';
+ expect(ansiHTML(redInput)).toEqual(redExpected);
+
+ const blueInput = '\x1B[34mHello, World!\x1B[0m';
+ const blueExpected = 'Hello, World!';
+ expect(ansiHTML(blueInput)).toEqual(blueExpected);
+
+ const greenInput = '\x1B[32mHello, World!\x1B[0m';
+ const greenExpected = 'Hello, World!';
+ expect(ansiHTML(greenInput)).toEqual(greenExpected);
+
+ const yellowInput = '\x1B[33mHello, World!\x1B[0m';
+ const yellowExpected = 'Hello, World!';
+ expect(ansiHTML(yellowInput)).toEqual(yellowExpected);
+
+ const cyanInput = '\x1B[36mHello, World!\x1B[0m';
+ const cyanExpected = 'Hello, World!';
+ expect(ansiHTML(cyanInput)).toEqual(cyanExpected);
+
+ const magentaInput = '\x1B[35mHello, World!\x1B[0m';
+ const magentaExpected = 'Hello, World!';
+ expect(ansiHTML(magentaInput)).toEqual(magentaExpected);
+
+ const lightgreyInput = '\x1B[37mHello, World!\x1B[0m';
+ const lightgreyExpected =
+ 'Hello, World!';
+ expect(ansiHTML(lightgreyInput)).toEqual(lightgreyExpected);
+
+ const darkgreyInput = '\x1B[90mHello, World!\x1B[0m';
+ const darkgreyExpected = 'Hello, World!';
+ expect(ansiHTML(darkgreyInput)).toEqual(darkgreyExpected);
+ });
+
+ it('should convert ANSI bold codes to HTML', () => {
+ const input = '\x1B[1mHello, World!\x1B[0m';
+ const expected = 'Hello, World!';
+ expect(ansiHTML(input)).toEqual(expected);
+ });
+
+ it('should convert ANSI dim codes to HTML', () => {
+ const input = '\x1B[2mHello, World!\x1B[0m';
+ const expected = 'Hello, World!';
+ expect(ansiHTML(input)).toEqual(expected);
+ });
+
+ it('should convert ANSI italic codes to HTML', () => {
+ const input = '\x1B[3mHello, World!\x1B[0m';
+ const expected = 'Hello, World!';
+ expect(ansiHTML(input)).toEqual(expected);
+ });
+
+ it('should convert ANSI underline codes to HTML', () => {
+ const input = '\x1B[4mHello, World!\x1B[0m';
+ const expected =
+ 'Hello, World!';
+ expect(ansiHTML(input)).toEqual(expected);
+ });
+
+ it('should convert ANSI delete codes to HTML', () => {
+ const input = '\x1B[9mHello, World!\x1B[0m';
+ const expected =
+ 'Hello, World!';
+ expect(ansiHTML(input)).toEqual(expected);
+ });
+});