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.
This commit is contained in:
Chris Wanstrath 2026-03-18 10:59:07 -07:00
parent 33d21777d3
commit 33d91747af

View File

@ -17,14 +17,10 @@ const COLORS: Record<number, string> = {
97: '#fff', // bright white
}
const ESC = /\x1b\[([0-9;]*)m/g
const escape = (s: string) =>
s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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 += '</span>'; open = false }
} else if (COLORS[code]) {
if (open) result += '</span>'
result += `<span style="color:${COLORS[code]}">`
open = true
styles.push(`color:${COLORS[code]}`)
} else if (code === 1) {
if (open) result += '</span>'
result += '<span style="font-weight:bold">'
open = true
styles.push('font-weight:bold')
} else if (code === 2) {
if (open) result += '</span>'
result += '<span style="opacity:0.7">'
open = true
styles.push('opacity:0.7')
}
}
if (styles.length) {
if (open) result += '</span>'
result += `<span style="${styles.join(';')}">`
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')