From 33d91747af628093be9d8e96bd412f8ab9751c15 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 18 Mar 2026 10:59:07 -0700 Subject: [PATCH] Combine ANSI style codes into a single span element Multiple SGR parameters in one escape sequence (e.g. bold + color) were each opening a new span, losing earlier styles. Collect styles per sequence and emit one span with all of them. --- src/client/ansi.ts | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/client/ansi.ts b/src/client/ansi.ts index 16cd941..bae5c38 100644 --- a/src/client/ansi.ts +++ b/src/client/ansi.ts @@ -17,14 +17,10 @@ const COLORS: Record = { 97: '#fff', // bright white } -const ESC = /\x1b\[([0-9;]*)m/g - -const escape = (s: string) => - s.replace(/&/g, '&').replace(//g, '>') - export function ansiToHtml(text: string): string { if (!text.includes('\x1b')) return escape(text) + const ESC = /\x1b\[([0-9;]*)m/g let result = '' let last = 0 let open = false @@ -35,24 +31,26 @@ export function ansiToHtml(text: string): string { last = match.index + match[0].length const codes = match[1] ? match[1].split(';').map(Number) : [0] + const styles: string[] = [] for (const code of codes) { if (code === 0 || code === 39) { + styles.length = 0 if (open) { result += ''; open = false } } else if (COLORS[code]) { - if (open) result += '' - result += `` - open = true + styles.push(`color:${COLORS[code]}`) } else if (code === 1) { - if (open) result += '' - result += '' - open = true + styles.push('font-weight:bold') } else if (code === 2) { - if (open) result += '' - result += '' - open = true + styles.push('opacity:0.7') } } + + if (styles.length) { + if (open) result += '' + result += `` + open = true + } } result += escape(text.slice(last)) @@ -60,3 +58,6 @@ export function ansiToHtml(text: string): string { return result } + +const escape = (s: string) => + s.replace(/&/g, '&').replace(//g, '>')