/**
* ANSI escape code → HTML/CSS utilities.
* Re-exports the full AnsiRenderer parser and adds text-stripping helpers.
*/
// ─── Strip ANSI ───────────────────────────────────────────────────────────────
/** Regex matching ANSI/VT100 escape sequences. */
const ANSI_RE = /\x1b\[[0-9;]*[mGKHF]|\x1b\][^\x07]*\x07|\x1b[()][AB012]/g;
/** Remove all ANSI escape sequences from a string. */
export function stripAnsi(text: string): string {
return text.replace(ANSI_RE, "");
}
/** Returns true if the string contains any ANSI escape sequences. */
export function hasAnsi(text: string): boolean {
ANSI_RE.lastIndex = 0;
return ANSI_RE.test(text);
}
// ─── ANSI → inline HTML spans ─────────────────────────────────────────────────
// 16-color ANSI foreground palette (matches common terminal defaults / xterm256)
const FG: Record = {
30: "#3d3d3d", 31: "#cc0000", 32: "#4e9a06", 33: "#c4a000",
34: "#3465a4", 35: "#75507b", 36: "#06989a", 37: "#d3d7cf",
90: "#555753", 91: "#ef2929", 92: "#8ae234", 93: "#fce94f",
94: "#729fcf", 95: "#ad7fa8", 96: "#34e2e2", 97: "#eeeeec",
};
const BG: Record = {
40: "#3d3d3d", 41: "#cc0000", 42: "#4e9a06", 43: "#c4a000",
44: "#3465a4", 45: "#75507b", 46: "#06989a", 47: "#d3d7cf",
100: "#555753", 101: "#ef2929", 102: "#8ae234", 103: "#fce94f",
104: "#729fcf", 105: "#ad7fa8", 106: "#34e2e2", 107: "#eeeeec",
};
function get256Color(n: number): string {
if (n < 16) return FG[n + 30] ?? FG[n + 82] ?? "#ffffff";
if (n < 232) {
const i = n - 16;
const b = i % 6, g = Math.floor(i / 6) % 6, r = Math.floor(i / 36);
const h = (v: number) => (v === 0 ? 0 : 55 + v * 40).toString(16).padStart(2, "0");
return `#${h(r)}${h(g)}${h(b)}`;
}
const gray = (n - 232) * 10 + 8;
const hex = gray.toString(16).padStart(2, "0");
return `#${hex}${hex}${hex}`;
}
interface AnsiStyle {
color?: string;
background?: string;
bold?: boolean;
dim?: boolean;
italic?: boolean;
underline?: boolean;
strikethrough?: boolean;
}
export interface AnsiSegment {
text: string;
style: AnsiStyle;
}
/**
* Parse an ANSI-encoded string into styled text segments.
* This is the core parser used by AnsiRenderer and can be used directly
* when you need to process segments programmatically.
*/
export function parseAnsiSegments(input: string): AnsiSegment[] {
const segments: AnsiSegment[] = [];
let current: AnsiStyle = {};
let pos = 0;
let textStart = 0;
const push = (end: number) => {
const text = input.slice(textStart, end);
if (text) segments.push({ text, style: { ...current } });
};
while (pos < input.length) {
const esc = input.indexOf("\x1b[", pos);
if (esc === -1) break;
push(esc);
let seqEnd = esc + 2;
while (seqEnd < input.length && !/[A-Za-z]/.test(input[seqEnd])) seqEnd++;
const term = input[seqEnd];
const params = input.slice(esc + 2, seqEnd).split(";").map(Number);
if (term === "m") {
let i = 0;
while (i < params.length) {
const p = params[i];
if (p === 0 || isNaN(p)) { current = {}; }
else if (p === 1) { current.bold = true; }
else if (p === 2) { current.dim = true; }
else if (p === 3) { current.italic = true; }
else if (p === 4) { current.underline = true; }
else if (p === 9) { current.strikethrough = true; }
else if (p === 22) { current.bold = false; current.dim = false; }
else if (p === 23) { current.italic = false; }
else if (p === 24) { current.underline = false; }
else if (p === 29) { current.strikethrough = false; }
else if (p >= 30 && p <= 37) { current.color = FG[p]; }
else if (p === 38) {
if (params[i+1] === 5 && params[i+2] !== undefined) {
current.color = get256Color(params[i+2]); i += 2;
} else if (params[i+1] === 2 && params[i+4] !== undefined) {
current.color = `rgb(${params[i+2]},${params[i+3]},${params[i+4]})`; i += 4;
}
}
else if (p === 39) { delete current.color; }
else if (p >= 40 && p <= 47) { current.background = BG[p]; }
else if (p === 48) {
if (params[i+1] === 5 && params[i+2] !== undefined) {
current.background = get256Color(params[i+2]); i += 2;
} else if (params[i+1] === 2 && params[i+4] !== undefined) {
current.background = `rgb(${params[i+2]},${params[i+3]},${params[i+4]})`; i += 4;
}
}
else if (p === 49) { delete current.background; }
else if (p >= 90 && p <= 97) { current.color = FG[p]; }
else if (p >= 100 && p <= 107) { current.background = BG[p]; }
i++;
}
}
pos = seqEnd + 1;
textStart = pos;
}
push(input.length);
return segments;
}
/**
* Convert an ANSI string to a plain HTML string with inline styles.
* Each styled run is wrapped in a ``.
* Suitable for dangerouslySetInnerHTML when the source is trusted server output.
*/
export function ansiToHtml(text: string): string {
const segments = parseAnsiSegments(text);
return segments
.map(({ text: t, style }) => {
const parts: string[] = [];
if (style.color) parts.push(`color:${style.color}`);
if (style.background) parts.push(`background:${style.background}`);
if (style.bold) parts.push("font-weight:bold");
if (style.dim) parts.push("opacity:0.7");
if (style.italic) parts.push("font-style:italic");
const deco = [style.underline && "underline", style.strikethrough && "line-through"]
.filter(Boolean).join(" ");
if (deco) parts.push(`text-decoration:${deco}`);
const escaped = t
.replace(/&/g, "&")
.replace(//g, ">");
return parts.length ? `${escaped}` : escaped;
})
.join("");
}