diff --git a/src/ui/React/ANSIITypography.tsx b/src/ui/React/ANSIITypography.tsx index 18a931467..43e77d6a9 100644 --- a/src/ui/React/ANSIITypography.tsx +++ b/src/ui/React/ANSIITypography.tsx @@ -114,61 +114,80 @@ function ansiCodeStyle(code: string | null): Record { // and for background colors we use the dark color set. Of course, all colors are available // via the longer ESC[n8;5;c] sequence (n={3,4}, c=color). Ideally, these 8-bit maps could // be managed in the user preferences/theme. - const COLOR_MAP_BRIGHT: Record = { - 0: "#404040", - 1: "#ff0000", - 2: "#00ff00", - 3: "#ffff00", - 4: "#0000ff", - 5: "#ff00ff", - 6: "#00ffff", - 7: "#ffffff", - }; - const COLOR_MAP_DARK: Record = { - 8: "#000000", - 9: "#800000", - 10: "#008000", - 11: "#808000", - 12: "#000080", - 13: "#800080", - 14: "#008080", - 15: "#c0c0c0", + // Later note: The above justification is a bit suspect, and I doubt that the compatibility break + // vs standard ANSI codes is worth it. But, it's the system that's been baked in to BB for years + // now, so too late to change. + const COLOR_MAP_BRIGHT: string[] = [ + "#404040", + "#ff0000", + "#00ff00", + "#ffff00", + "#0000ff", + "#ff00ff", + "#00ffff", + "#ffffff", + ]; + const COLOR_MAP_DARK: string[] = [ + "#000000", + "#800000", + "#008000", + "#808000", + "#000080", + "#800080", + "#008080", + "#c0c0c0", + ]; + + // Returns [parts_consumed, style_string]. + // [-1, _] signals an error in parsing. + const ansi2rgb = (codeParts: number[], startIdx: number): [number, string] => { + if (codeParts[startIdx] === 5) { + if (codeParts.length <= startIdx + 1) { + // Don't have enough data, but we have to consume what we've seen so far + return [codeParts.length - startIdx, "inherit"]; + } + const code = codeParts[startIdx + 1]; + /* eslint-disable yoda */ + if (0 <= code && code < 8) { + // x8 RGB + return [2, COLOR_MAP_DARK[code]]; + } + if (8 <= code && code < 16) { + // x8 RGB - "High Intensity" + return [2, COLOR_MAP_BRIGHT[code - 8]]; + } + if (16 <= code && code < 232) { + // x216 RGB + const base = code - 16; + const ir = Math.floor(base / 36); + const ig = Math.floor((base % 36) / 6); + const ib = Math.floor((base % 6) / 1); + const r = ir <= 0 ? 0 : 55 + ir * 40; + const g = ig <= 0 ? 0 : 55 + ig * 40; + const b = ib <= 0 ? 0 : 55 + ib * 40; + return [2, `rgb(${r}, ${g}, ${b})`]; + } + if (232 <= code && code < 256) { + // x32 greyscale + const base = code - 232; + const grey = base * 10 + 8; + return [2, `rgb(${grey}, ${grey}, ${grey})`]; + } + // Value out of range, but the escape sequence is still well-formed + return [2, "inherit"]; + } else if (codeParts[startIdx] === 2) { + if (codeParts.length <= startIdx + 3) { + // Don't have enough data, but we have to consume what we've seen so far + return [codeParts.length - startIdx, "inherit"]; + } + return [4, `rgb(${codeParts[startIdx + 1]}, ${codeParts[startIdx + 2]}, ${codeParts[startIdx + 3]})`]; + } + return [-1, ""]; }; - const ansi2rgb = (code: number): string => { - /* eslint-disable yoda */ - if (0 <= code && code < 8) { - // x8 RGB - return COLOR_MAP_BRIGHT[code]; - } - if (8 <= code && code < 16) { - // x8 RGB - "High Intensity" (but here, actually the dark set) - return COLOR_MAP_DARK[code]; - } - if (16 <= code && code < 232) { - // x216 RGB - const base = code - 16; - const ir = Math.floor(base / 36); - const ig = Math.floor((base % 36) / 6); - const ib = Math.floor((base % 6) / 1); - const r = ir <= 0 ? 0 : 55 + ir * 40; - const g = ig <= 0 ? 0 : 55 + ig * 40; - const b = ib <= 0 ? 0 : 55 + ib * 40; - return `rgb(${r}, ${g}, ${b})`; - } - if (232 <= code && code < 256) { - // x32 greyscale - const base = code - 232; - const grey = base * 10 + 8; - return `rgb(${grey}, ${grey}, ${grey})`; - } - // shouldn't get here (under normal circumstances), but just in case - return "initial"; - }; - - type styleKey = "fontWeight" | "textDecoration" | "color" | "backgroundColor" | "padding"; const style: { fontWeight?: string; + fontStyle?: string; textDecoration?: string; color?: string; backgroundColor?: string; @@ -179,50 +198,37 @@ function ansiCodeStyle(code: string | null): Record { return style; } - const codeParts = code - .split(";") - .map((p) => parseInt(p)) - .filter( - (p, i, arr) => - // If the sequence is 38;5 (x256 foreground color) or 48;5 (x256 background color), - // filter out the 5 so the next codePart after {38,48} is the color code. - p != 5 || i == 0 || (arr[i - 1] != 38 && arr[i - 1] != 48), - ); + const codeParts = code.split(";").map((p) => (p === "" ? 0 : parseInt(p))); - let nextStyleKey: styleKey | null = null; - codeParts.forEach((codePart) => { - /* eslint-disable yoda */ - if (nextStyleKey !== null) { - style[nextStyleKey] = ansi2rgb(codePart); - nextStyleKey = null; - } + for (let i = 0; i < codeParts.length; ++i) { + const codePart = codeParts[i]; // Decorations - else if (codePart == 1) { + if (codePart === 1) { style.fontWeight = "bold"; - } else if (codePart == 4) { + } else if (codePart === 3) { + style.fontStyle = "italic"; + } else if (codePart === 4) { style.textDecoration = "underline"; } + /* eslint-disable yoda */ // Foreground Color (x8) else if (30 <= codePart && codePart < 38) { - if (COLOR_MAP_BRIGHT[codePart % 10]) { - style.color = COLOR_MAP_BRIGHT[codePart % 10]; - } + style.color = COLOR_MAP_BRIGHT[codePart - 30]; } // Background Color (x8) else if (40 <= codePart && codePart < 48) { - if (COLOR_MAP_DARK[codePart % 10]) { - style.backgroundColor = COLOR_MAP_DARK[codePart % 10]; - } + style.backgroundColor = COLOR_MAP_DARK[codePart - 40]; } // Foreground Color (x256) - else if (codePart == 38) { - nextStyleKey = "color"; + else if (codePart === 38 || codePart === 48) { + const [extra, colorString] = ansi2rgb(codeParts, i + 1); + // If it was an invalid code, we consume no extra parts + if (extra > 0) { + i += extra; + style[codePart === 38 ? "color" : "backgroundColor"] = colorString; + } } - // Background Color (x256) - else if (codePart == 48) { - nextStyleKey = "backgroundColor"; - } - }); + } // If a background color is set, add slight padding to increase the background fill area. // This was previously display:inline-block, but that has display errors when line breaks are used. if (style.backgroundColor) {