/** * ANSI escape code to HTML converter. * * Converts terminal ANSI color/style codes to HTML with inline styles. * Supports: * - Standard foreground colors (30-37) and bright variants (90-97) * - Standard background colors (40-47) and bright variants (100-107) * - 256-color palette (38;5;N and 48;5;N) * - RGB true color (38;2;R;G;B and 48;2;R;G;B) * - Text styles: bold (1), dim (2), italic (3), underline (4) * - Reset (0) */ // Standard ANSI color palette (0-15) const ANSI_COLORS = [ "#000000", // 0: black "#800000", // 1: red "#008000", // 2: green "#808000", // 3: yellow "#000080", // 4: blue "#800080", // 5: magenta "#008080", // 6: cyan "#c0c0c0", // 7: white "#808080", // 8: bright black "#ff0000", // 9: bright red "#00ff00", // 10: bright green "#ffff00", // 11: bright yellow "#0000ff", // 12: bright blue "#ff00ff", // 13: bright magenta "#00ffff", // 14: bright cyan "#ffffff", // 15: bright white ]; /** * Convert 256-color index to hex. */ function color256ToHex(index) { // Standard colors (0-15) if (index < 16) { return ANSI_COLORS[index]; } // Color cube (16-231): 6x6x6 = 216 colors if (index < 232) { const cubeIndex = index - 16; const r = Math.floor(cubeIndex / 36); const g = Math.floor((cubeIndex % 36) / 6); const b = cubeIndex % 6; const toComponent = (n) => (n === 0 ? 0 : 55 + n * 40); const toHex = (n) => toComponent(n).toString(16).padStart(2, "0"); return `#${toHex(r)}${toHex(g)}${toHex(b)}`; } // Grayscale (232-255): 24 shades const gray = 8 + (index - 232) * 10; const grayHex = gray.toString(16).padStart(2, "0"); return `#${grayHex}${grayHex}${grayHex}`; } /** * Escape HTML special characters. */ function escapeHtml(text) { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function createEmptyStyle() { return { fg: null, bg: null, bold: false, dim: false, italic: false, underline: false, }; } function styleToInlineCSS(style) { const parts = []; if (style.fg) parts.push(`color:${style.fg}`); if (style.bg) parts.push(`background-color:${style.bg}`); if (style.bold) parts.push("font-weight:bold"); if (style.dim) parts.push("opacity:0.6"); if (style.italic) parts.push("font-style:italic"); if (style.underline) parts.push("text-decoration:underline"); return parts.join(";"); } function hasStyle(style) { return ( style.fg !== null || style.bg !== null || style.bold || style.dim || style.italic || style.underline ); } /** * Parse ANSI SGR (Select Graphic Rendition) codes and update style. */ function applySgrCode(params, style) { let i = 0; while (i < params.length) { const code = params[i]; if (code === 0) { // Reset all style.fg = null; style.bg = null; style.bold = false; style.dim = false; style.italic = false; style.underline = false; } else if (code === 1) { style.bold = true; } else if (code === 2) { style.dim = true; } else if (code === 3) { style.italic = true; } else if (code === 4) { style.underline = true; } else if (code === 22) { // Reset bold/dim style.bold = false; style.dim = false; } else if (code === 23) { style.italic = false; } else if (code === 24) { style.underline = false; } else if (code >= 30 && code <= 37) { // Standard foreground colors style.fg = ANSI_COLORS[code - 30]; } else if (code === 38) { // Extended foreground color if (params[i + 1] === 5 && params.length > i + 2) { // 256-color: 38;5;N style.fg = color256ToHex(params[i + 2]); i += 2; } else if (params[i + 1] === 2 && params.length > i + 4) { // RGB: 38;2;R;G;B const r = params[i + 2]; const g = params[i + 3]; const b = params[i + 4]; style.fg = `rgb(${r},${g},${b})`; i += 4; } } else if (code === 39) { // Default foreground style.fg = null; } else if (code >= 40 && code <= 47) { // Standard background colors style.bg = ANSI_COLORS[code - 40]; } else if (code === 48) { // Extended background color if (params[i + 1] === 5 && params.length > i + 2) { // 256-color: 48;5;N style.bg = color256ToHex(params[i + 2]); i += 2; } else if (params[i + 1] === 2 && params.length > i + 4) { // RGB: 48;2;R;G;B const r = params[i + 2]; const g = params[i + 3]; const b = params[i + 4]; style.bg = `rgb(${r},${g},${b})`; i += 4; } } else if (code === 49) { // Default background style.bg = null; } else if (code >= 90 && code <= 97) { // Bright foreground colors style.fg = ANSI_COLORS[code - 90 + 8]; } else if (code >= 100 && code <= 107) { // Bright background colors style.bg = ANSI_COLORS[code - 100 + 8]; } // Ignore unrecognized codes i++; } } // Match ANSI escape sequences: ESC[ followed by params and ending with 'm' const ANSI_REGEX = /\x1b\[([\d;]*)m/g; /** * Convert ANSI-escaped text to HTML with inline styles. */ export function ansiToHtml(text) { const style = createEmptyStyle(); let result = ""; let lastIndex = 0; let inSpan = false; // Reset regex state ANSI_REGEX.lastIndex = 0; let match = ANSI_REGEX.exec(text); while (match !== null) { // Add text before this escape sequence const beforeText = text.slice(lastIndex, match.index); if (beforeText) { result += escapeHtml(beforeText); } // Parse SGR parameters const paramStr = match[1]; const params = paramStr ? paramStr.split(";").map((p) => parseInt(p, 10) || 0) : [0]; // Close existing span if we have one if (inSpan) { result += ""; inSpan = false; } // Apply the codes applySgrCode(params, style); // Open new span if we have any styling if (hasStyle(style)) { result += ``; inSpan = true; } lastIndex = match.index + match[0].length; match = ANSI_REGEX.exec(text); } // Add remaining text const remainingText = text.slice(lastIndex); if (remainingText) { result += escapeHtml(remainingText); } // Close any open span if (inSpan) { result += ""; } return result; } /** * Convert array of ANSI-escaped lines to HTML. * Each line is wrapped in a div element. */ export function ansiLinesToHtml(lines) { return lines .map( (line) => `
${ansiToHtml(line) || " "}
`, ) .join("\n"); } //# sourceMappingURL=ansi-to-html.js.map