import { TERMINAL_PALETTE as palette } from "@berry/util/palette.ts"; function indexedColor(index: number): string | undefined { if (!Number.isInteger(index) || index < 0 || index > 255) return undefined; if (index < 16) return palette[index]; if (index >= 232) { const gray = 8 + (index - 232) * 10; return `rgb(${gray}, ${gray}, ${gray})`; } const value = index - 16; const levels = [0, 95, 135, 175, 215, 255]; return `rgb(${levels[Math.floor(value / 36)]}, ${levels[Math.floor(value / 6) % 6]}, ${ levels[value % 6] })`; } export type AnsiRun = { text: string; style: Record }; /** An incremental SGR decoder that hides terminal controls, even across chunk boundaries. */ export function ansiParser() { let foreground: string | undefined; let background: string | undefined; let bold = false; let dim = false; let italic = false; let underline = false; let strike = false; let inverse = false; let mode: "text" | "escape" | "csi" | "osc" | "oscEscape" = "text"; let parametersText = ""; let invalid = false; return { write(source: string): AnsiRun[] { const runs: AnsiRun[] = []; let offset = 0; // discard non-SGR controls, including OSC links and a streaming chunk's unfinished escape while (offset < source.length) { if (mode === "text") { const escape = source.indexOf("\x1b", offset); const text = source.slice(offset, escape === -1 ? source.length : escape); if (text) { const style: Record = {}; const fg = inverse ? background ?? "var(--panel)" : foreground; const bg = inverse ? foreground ?? "var(--text)" : background; if (fg) style.color = fg; if (bg) style["background-color"] = bg; if (bold) style["font-weight"] = "bold"; if (dim) style.opacity = "0.5"; if (italic) style["font-style"] = "italic"; const decoration = [underline ? "underline" : "", strike ? "line-through" : ""].filter( Boolean, ); if (decoration.length) style["text-decoration"] = decoration.join(" "); runs.push({ text, style }); } if (escape === -1) break; offset = escape + 1; mode = "escape"; continue; } const char = source[offset++]; if (mode === "escape") { if (char === "[") { mode = "csi"; parametersText = ""; invalid = false; } else if (char === "]") mode = "osc"; else if (char !== "\x1b" && !(char >= " " && char <= "/")) mode = "text"; } else if (mode === "osc") { if (char === "\x07") mode = "text"; else if (char === "\x1b") mode = "oscEscape"; } else if (mode === "oscEscape") { if (char === "\\" || char === "\x07") mode = "text"; else if (char !== "\x1b") mode = "osc"; } else if (char >= "@" && char <= "~") { mode = "text"; if (char !== "m" || invalid || !/^[\d;]*$/.test(parametersText)) continue; const parameters = parametersText.split(";").map(Number); for (let i = 0; i < parameters.length; i++) { const parameter = parameters[i]; if (parameter === 0) { foreground = background = undefined; bold = dim = false; italic = underline = false; strike = inverse = false; } else if (parameter === 1) bold = true; else if (parameter === 2) dim = true; else if (parameter === 3) italic = true; else if (parameter === 4) underline = true; else if (parameter === 7) inverse = true; else if (parameter === 9) strike = true; else if (parameter === 22) bold = dim = false; else if (parameter === 23) italic = false; else if (parameter === 24) underline = false; else if (parameter === 27) inverse = false; else if (parameter === 29) strike = false; else if (parameter === 39) foreground = undefined; else if (parameter === 49) background = undefined; else if (parameter >= 30 && parameter <= 37) foreground = palette[parameter - 30]; else if (parameter >= 90 && parameter <= 97) foreground = palette[parameter - 82]; else if (parameter >= 40 && parameter <= 47) background = palette[parameter - 40]; else if (parameter >= 100 && parameter <= 107) background = palette[parameter - 92]; else if (parameter === 38 || parameter === 48) { const mode = parameters[++i]; let color: string | undefined; if (mode === 5) color = indexedColor(parameters[++i]); else if (mode === 2) { const rgb = parameters.slice(i + 1, i + 4); i += 3; if ( rgb.length === 3 && rgb.every((n) => Number.isInteger(n) && n >= 0 && n <= 255) ) { color = `rgb(${rgb.join(", ")})`; } } else break; if (color !== undefined) { if (parameter === 38) foreground = color; else background = color; } } } parametersText = ""; } else if (char >= " " && char <= "?") { // bound malformed CSI sequences; OSC payloads are never buffered if (parametersText.length < 1024 && char >= "0" && !invalid) parametersText += char; else invalid = true; } else { mode = char === "\x1b" ? "escape" : "text"; } } return runs; }, }; } /** Text and SGR styles, without interpreting terminal commands or output as markup. */ export function ansiRuns(source: string): AnsiRun[] { return ansiParser().write(source); }