UNPKG

@taml/encoder

Version:

Convert ANSI escape sequences to TAML (Terminal ANSI Markup Language) tags

319 lines (279 loc) 7.9 kB
import { convert256ToBackgroundColor, convert256ToStandardColor, convertBasicAnsiToBackgroundColor, convertBasicAnsiToStandardColor, convertRGBToBackgroundColor, convertRGBToStandardColor, } from "./color-converter.js"; import type { AnsiState, AnsiToken, ParsedAnsiSequence } from "./types.js"; // biome-ignore lint/suspicious/noControlCharactersInRegex: <explanation> const ANSI_ESCAPE_REGEX = /(\u001b\[[0-9;]*m)/g; /** * Regular expression specifically for SGR (Select Graphic Rendition) sequences * These control text formatting, colors, etc. */ // biome-ignore lint/suspicious/noControlCharactersInRegex: <explanation> const SGR_REGEX = /^(\u001b)\[([0-9;]*)m$/; /** * Create an initial ANSI state with default values */ export function createInitialAnsiState(): AnsiState { return { foreground: null, background: null, bold: false, dim: false, italic: false, underline: false, strikethrough: false, }; } /** * Parse ANSI text into tokens containing text and escape sequences */ export function tokenizeAnsiText(text: string): AnsiToken[] { const tokens: AnsiToken[] = []; let lastIndex = 0; const matches = Array.from(text.matchAll(ANSI_ESCAPE_REGEX)); for (const match of matches) { const matchIndex = match.index; if (matchIndex === undefined) continue; const matchText = match[0]; // Add text before this match as a text token if (matchIndex > lastIndex) { const textContent = text.slice(lastIndex, matchIndex); if (textContent) { tokens.push({ type: "text", content: textContent, }); } } // Parse the ANSI sequence const sequence = parseAnsiSequence(matchText); if (sequence) { tokens.push({ type: "sequence", content: matchText, sequence, }); } lastIndex = matchIndex + matchText.length; } // Add remaining text if (lastIndex < text.length) { const textContent = text.slice(lastIndex); if (textContent) { tokens.push({ type: "text", content: textContent, }); } } return tokens; } /** * Parse a single ANSI escape sequence */ function parseAnsiSequence(sequence: string): ParsedAnsiSequence | null { const sgrMatch = sequence.match(SGR_REGEX); if (!sgrMatch) { // Not an SGR sequence, ignore return null; } const params = sgrMatch[2]; if (!params) { // Reset sequence (ESC[m or ESC[0m) return { type: "reset", value: 0, raw: sequence, }; } const codes = params.split(";").map((code) => Number.parseInt(code, 10) || 0); // Handle multiple codes in one sequence for (let i = 0; i < codes.length; i++) { const code = codes[i]; if (code === undefined) continue; // Reset if (code === 0) { return { type: "reset", value: 0, raw: sequence, }; } // Styles if (code === 1) { return { type: "style", value: "bold", raw: sequence }; } if (code === 2) { return { type: "style", value: "dim", raw: sequence }; } if (code === 3) { return { type: "style", value: "italic", raw: sequence }; } if (code === 4) { return { type: "style", value: "underline", raw: sequence }; } if (code === 9) { return { type: "style", value: "strikethrough", raw: sequence }; } // Basic foreground colors (30-37, 90-97) if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) { const color = convertBasicAnsiToStandardColor(code); return { type: "color", value: color, raw: sequence, }; } // Foreground color reset (39) if (code === 39) { return { type: "foreground-reset", value: null, raw: sequence, }; } // Basic background colors (40-47, 100-107) if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) { const color = convertBasicAnsiToBackgroundColor(code); return { type: "background", value: color, raw: sequence, }; } // Background color reset (49) if (code === 49) { return { type: "background-reset", value: null, raw: sequence, }; } // Extended foreground color (38;5;n or 38;2;r;g;b) if (code === 38 && i + 1 < codes.length) { const mode = codes[i + 1]; if (mode === undefined) continue; if (mode === 5 && i + 2 < codes.length) { // 256-color mode: 38;5;n const colorIndex = codes[i + 2]; if (colorIndex === undefined) continue; const color = convert256ToStandardColor(colorIndex); return { type: "color", value: color, raw: sequence, }; } if (mode === 2 && i + 4 < codes.length) { // RGB mode: 38;2;r;g;b const r = codes[i + 2]; const g = codes[i + 3]; const b = codes[i + 4]; if (r === undefined || g === undefined || b === undefined) continue; const color = convertRGBToStandardColor(r, g, b); return { type: "color", value: color, raw: sequence, }; } } // Extended background color (48;5;n or 48;2;r;g;b) if (code === 48 && i + 1 < codes.length) { const mode = codes[i + 1]; if (mode === undefined) continue; if (mode === 5 && i + 2 < codes.length) { // 256-color mode: 48;5;n const colorIndex = codes[i + 2]; if (colorIndex === undefined) continue; const color = convert256ToBackgroundColor(colorIndex); return { type: "background", value: color, raw: sequence, }; } if (mode === 2 && i + 4 < codes.length) { // RGB mode: 48;2;r;g;b const r = codes[i + 2]; const g = codes[i + 3]; const b = codes[i + 4]; if (r === undefined || g === undefined || b === undefined) continue; const color = convertRGBToBackgroundColor(r, g, b); return { type: "background", value: color, raw: sequence, }; } } } // Unknown sequence, ignore return null; } /** * Apply a parsed ANSI sequence to the current state */ export function applyAnsiSequence( state: AnsiState, sequence: ParsedAnsiSequence, ): AnsiState { const newState = { ...state }; switch (sequence.type) { case "reset": return createInitialAnsiState(); case "foreground-reset": newState.foreground = null; break; case "background-reset": newState.background = null; break; case "color": newState.foreground = sequence.value as string; break; case "background": newState.background = sequence.value as string; break; case "style": { const styleValue = sequence.value as string; switch (styleValue) { case "bold": newState.bold = true; break; case "dim": newState.dim = true; break; case "italic": newState.italic = true; break; case "underline": newState.underline = true; break; case "strikethrough": newState.strikethrough = true; break; } break; } } return newState; } /** * Get the active style tags from the current ANSI state */ export function getActiveStyleTags(state: AnsiState): string[] { const tags: string[] = []; if (state.bold) tags.push("bold"); if (state.dim) tags.push("dim"); if (state.italic) tags.push("italic"); if (state.underline) tags.push("underline"); if (state.strikethrough) tags.push("strikethrough"); if (state.background) tags.push(state.background); if (state.foreground) tags.push(state.foreground); return tags; }