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); + }); +});