diff --git a/package-lock.json b/package-lock.json index 1c01f7dc6e..77356e2d6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6624,7 +6624,6 @@ "packages/web": { "version": "0.0.0", "dependencies": { - "ansi-to-html": "^0.7.2", "codemirror": "^5.65.9", "xterm": "^5.1.0", "xterm-addon-fit": "^0.7.0" @@ -10630,7 +10629,6 @@ "web": { "version": "file:packages/web", "requires": { - "ansi-to-html": "^0.7.2", "codemirror": "^5.65.9", "xterm": "^5.1.0", "xterm-addon-fit": "^0.7.0" diff --git a/packages/trace-viewer/src/ui/consoleTab.tsx b/packages/trace-viewer/src/ui/consoleTab.tsx index 22b96d83cf..7ec3238197 100644 --- a/packages/trace-viewer/src/ui/consoleTab.tsx +++ b/packages/trace-viewer/src/ui/consoleTab.tsx @@ -19,9 +19,9 @@ import * as React from 'react'; import './consoleTab.css'; import * as modelUtil from './modelUtil'; import { ListView } from '@web/components/listView'; -import { ansi2htmlMarkup } from '@web/components/errorMessage'; import type { Boundaries } from '../geometry'; import { msToString } from '@web/uiUtils'; +import { ansi2html } from '@web/ansi2html'; import type * as trace from '@trace/trace'; type ConsoleEntry = { @@ -124,10 +124,10 @@ export const ConsoleTab: React.FunctionComponent<{ } if (nodeMessage?.text) - messageInnerHTML = ansi2htmlMarkup(nodeMessage.text.trim()) || ''; + messageInnerHTML = ansi2html(nodeMessage.text.trim()) || ''; if (nodeMessage?.base64) - messageInnerHTML = ansi2htmlMarkup(atob(nodeMessage.base64).trim()) || ''; + messageInnerHTML = ansi2html(atob(nodeMessage.base64).trim()) || ''; return
{timestampElement} diff --git a/packages/web/package.json b/packages/web/package.json index fd68fcb78d..2b21846455 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -4,7 +4,6 @@ "version": "0.0.0", "scripts": {}, "dependencies": { - "ansi-to-html": "^0.7.2", "codemirror": "^5.65.9", "xterm": "^5.1.0", "xterm-addon-fit": "^0.7.0" diff --git a/packages/web/src/ansi2html.ts b/packages/web/src/ansi2html.ts new file mode 100644 index 0000000000..210d1210bc --- /dev/null +++ b/packages/web/src/ansi2html.ts @@ -0,0 +1,107 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +export function ansi2html(text: string): string { + const regex = /(\x1b\[(\d+(;\d+)*)m)|([^\x1b]+)/g; + const tokens: string[] = []; + let match; + let style: any = {}; + while ((match = regex.exec(text)) !== null) { + const [, , codeStr, , text] = match; + if (codeStr) { + const code = +codeStr; + switch (code) { + case 0: style = {}; break; + case 1: style['font-weight'] = 'bold'; break; + case 3: style['font-style'] = 'italic'; break; + case 4: style['text-decoration'] = 'underline'; break; + case 8: style.display = 'none'; break; + case 9: style['text-decoration'] = 'line-through'; break; + case 22: style = { ...style, 'font-weight': undefined, 'font-style': undefined, 'text-decoration': undefined }; break; + case 23: style = { ...style, 'font-weight': undefined, 'font-style': undefined }; break; + case 24: style = { ...style, 'text-decoration': undefined }; break; + case 30: + case 31: + case 32: + case 33: + case 34: + case 35: + case 36: + case 37: style.color = ansiColors[code - 30]; break; + case 39: style = { ...style, color: undefined }; break; + case 40: + case 41: + case 42: + case 43: + case 44: + case 45: + case 46: + case 47: style['background-color'] = ansiColors[code - 40]; break; + case 49: style = { ...style, 'background-color': undefined }; break; + case 53: style['text-decoration'] = 'overline'; break; + case 90: + case 91: + case 92: + case 93: + case 94: + case 95: + case 96: + case 97: style.color = brightAnsiColors[code - 90]; break; + case 100: + case 101: + case 102: + case 103: + case 104: + case 105: + case 106: + case 107: style['background-color'] = brightAnsiColors[code - 100]; break; + } + } else if (text) { + tokens.push(`${escapeHTML(text)}`); + } + } + return tokens.join(''); +} + +const ansiColors: Record = { + 0: 'var(--vscode-terminal-ansiBlack)', + 1: 'var(--vscode-terminal-ansiRed)', + 2: 'var(--vscode-terminal-ansiGreen)', + 3: 'var(--vscode-terminal-ansiYellow)', + 4: 'var(--vscode-terminal-ansiBlue)', + 5: 'var(--vscode-terminal-ansiMagenta)', + 6: 'var(--vscode-terminal-ansiCyan)', + 7: 'var(--vscode-terminal-ansiWhite)', +}; + +const brightAnsiColors: Record = { + 0: 'var(--vscode-terminal-ansiBrightBlack)', + 1: 'var(--vscode-terminal-ansiBrightRed)', + 2: 'var(--vscode-terminal-ansiBrightGreen)', + 3: 'var(--vscode-terminal-ansiBrightYellow)', + 4: 'var(--vscode-terminal-ansiBrightBlue)', + 5: 'var(--vscode-terminal-ansiBrightMagenta)', + 6: 'var(--vscode-terminal-ansiBrightCyan)', + 7: 'var(--vscode-terminal-ansiBrightWhite)', +}; + +function escapeHTML(text: string): string { + return text.replace(/[&"<>]/g, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!)); +} + +function styleBody(style: any): string { + return Object.entries(style).map(([name, value]) => `${name}: ${value}`).join('; '); +} diff --git a/packages/web/src/components/DEPS.list b/packages/web/src/components/DEPS.list index 69cd00dc55..42e031fcc4 100644 --- a/packages/web/src/components/DEPS.list +++ b/packages/web/src/components/DEPS.list @@ -2,6 +2,7 @@ ../theme.ts ../third_party/vscode/codicon.css ../uiUtils.ts +../ansi2html.ts [expandable.spec.tsx] *** diff --git a/packages/web/src/components/codeMirrorWrapper.tsx b/packages/web/src/components/codeMirrorWrapper.tsx index 1f8e0c0ac0..3f64a1d944 100644 --- a/packages/web/src/components/codeMirrorWrapper.tsx +++ b/packages/web/src/components/codeMirrorWrapper.tsx @@ -17,7 +17,7 @@ import './codeMirrorWrapper.css'; import * as React from 'react'; import type { CodeMirror } from './codeMirrorModule'; -import { ansi2htmlMarkup } from './errorMessage'; +import { ansi2html } from '../ansi2html'; import { useMeasure } from '../uiUtils'; export type SourceHighlight = { @@ -152,7 +152,7 @@ export const CodeMirrorWrapper: React.FC = ({ } const errorWidgetElement = document.createElement('div'); - errorWidgetElement.innerHTML = ansi2htmlMarkup(h.message || ''); + errorWidgetElement.innerHTML = ansi2html(h.message || ''); errorWidgetElement.className = 'source-line-error-widget'; widgets.push(codemirror.addLineWidget(h.line, errorWidgetElement, { above: true, coverGutter: false })); } diff --git a/packages/web/src/components/errorMessage.tsx b/packages/web/src/components/errorMessage.tsx index b2d0452c59..c9f4500ece 100644 --- a/packages/web/src/components/errorMessage.tsx +++ b/packages/web/src/components/errorMessage.tsx @@ -14,45 +14,13 @@ * limitations under the License. */ -import ansi2html from 'ansi-to-html'; +import { ansi2html } from '@web/ansi2html'; import * as React from 'react'; import './errorMessage.css'; export const ErrorMessage: React.FC<{ error: string; }> = ({ error }) => { - const html = React.useMemo(() => ansi2htmlMarkup(error), [error]); + const html = React.useMemo(() => ansi2html(error), [error]); return
; }; - -export function ansi2htmlMarkup(text: string) { - const config: any = { - bg: 'var(--vscode-panel-background)', - fg: 'var(--vscode-foreground)', - }; - config.colors = ansiColors; - return new ansi2html(config).toHtml(escapeHTML(text)); -} - -const ansiColors = { - 0: '#000', - 1: '#C00', - 2: '#0C0', - 3: '#C50', - 4: '#00C', - 5: '#C0C', - 6: '#0CC', - 7: '#CCC', - 8: '#555', - 9: '#F55', - 10: '#5F5', - 11: '#FF5', - 12: '#55F', - 13: '#F5F', - 14: '#5FF', - 15: '#FFF' -}; - -function escapeHTML(text: string): string { - return text.replace(/[&"<>]/g, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!)); -} diff --git a/tests/playwright-test/ui-mode-test-output.spec.ts b/tests/playwright-test/ui-mode-test-output.spec.ts index 436c849a6d..d82e3b8f40 100644 --- a/tests/playwright-test/ui-mode-test-output.spec.ts +++ b/tests/playwright-test/ui-mode-test-output.spec.ts @@ -108,8 +108,8 @@ test('should show console messages for test', async ({ runUITest }, testInfo) => 'codicon codicon-file status-none', ]); - await expect(page.getByText('RED', { exact: true })).toHaveCSS('color', 'rgb(204, 0, 0)'); - await expect(page.getByText('GREEN', { exact: true })).toHaveCSS('color', 'rgb(0, 204, 0)'); + await expect.soft(page.getByText('RED', { exact: true })).toHaveCSS('color', 'rgb(205, 49, 49)'); + await expect.soft(page.getByText('GREEN', { exact: true })).toHaveCSS('color', 'rgb(0, 188, 0)'); }); test('should format console messages in page', async ({ runUITest }, testInfo) => {