import type { Theme } from "@earendil-works/pi-coding-agent"; type CodeToAnsi = typeof import("@shikijs/cli")["codeToANSI"]; export type ShikiLanguage = Parameters[1]; export type ShikiTheme = Parameters[2]; const MAX_HIGHLIGHT_CHARS = 32_000; const CACHE_LIMIT = 48; const cache = new Map(); let codeToAnsiLoader: Promise | undefined; const EXTENSION_LANGUAGE: Record = { ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx", mjs: "javascript", cjs: "javascript", py: "python", rb: "ruby", rs: "rust", go: "go", java: "java", c: "c", cpp: "cpp", cc: "cpp", cxx: "cpp", h: "c", hpp: "cpp", cs: "csharp", swift: "swift", kt: "kotlin", html: "html", css: "css", scss: "scss", less: "less", json: "json", jsonc: "jsonc", yaml: "yaml", yml: "yaml", toml: "toml", md: "markdown", mdx: "mdx", sql: "sql", sh: "bash", bash: "bash", zsh: "bash", lua: "lua", php: "php", dart: "dart", xml: "xml", graphql: "graphql", svelte: "svelte", vue: "vue", zig: "zig", nim: "nim", ex: "elixir", exs: "elixir", erb: "erb", hbs: "handlebars", }; interface Rgb { r: number; g: number; b: number; } function xterm256ToRgb(index: number): Rgb | undefined { if (!Number.isInteger(index) || index < 0 || index > 255) return undefined; if (index < 16) { const basic: Array<[number, number, number]> = [ [0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0], [0, 0, 128], [128, 0, 128], [0, 128, 128], [192, 192, 192], [128, 128, 128], [255, 0, 0], [0, 255, 0], [255, 255, 0], [0, 0, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255], ]; const color = basic[index]; return color ? { r: color[0], g: color[1], b: color[2] } : undefined; } if (index < 232) { const levels = [0, 95, 135, 175, 215, 255]; const value = index - 16; return { r: levels[Math.floor(value / 36) % 6]!, g: levels[Math.floor(value / 6) % 6]!, b: levels[value % 6]!, }; } const level = 8 + (index - 232) * 10; return { r: level, g: level, b: level }; } function parseAnsiRgb(ansi: string | undefined): Rgb | undefined { if (!ansi) return undefined; const trueColor = ansi.match(/\x1b\[(?:38|48);2;(\d+);(\d+);(\d+)m/); if (trueColor) return { r: Number(trueColor[1]), g: Number(trueColor[2]), b: Number(trueColor[3]) }; const indexed = ansi.match(/\x1b\[(?:38|48);5;(\d+)m/); return indexed ? xterm256ToRgb(Number(indexed[1])) : undefined; } function luminance(rgb: Rgb): number { return 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b; } function safeBg(theme: Theme, key: "customMessageBg" | "toolSuccessBg" | "selectedBg"): string | undefined { try { return theme.getBgAnsi(key); } catch { return undefined; } } function safeFg(theme: Theme, key: "text" | "muted"): string | undefined { try { return theme.getFgAnsi(key); } catch { return undefined; } } export function isLightSyntaxTheme(theme: Theme): boolean { if (theme.name?.toLowerCase().includes("light")) return true; for (const key of ["customMessageBg", "toolSuccessBg", "selectedBg"] as const) { const color = parseAnsiRgb(safeBg(theme, key)); if (color) return luminance(color) > 165; } const text = parseAnsiRgb(safeFg(theme, "text")); return text ? luminance(text) < 95 : false; } export function languageFromPath(filePath: string): ShikiLanguage | undefined { const base = filePath.split("/").at(-1)?.toLowerCase() ?? ""; if (base === "dockerfile") return "dockerfile"; if (base === "makefile" || base === "gnumakefile") return "make"; if (base === ".env" || base === ".envrc") return "bash"; const extension = base.includes(".") ? base.slice(base.lastIndexOf(".") + 1) : ""; return EXTENSION_LANGUAGE[extension]; } function syntaxTheme(theme: Theme): ShikiTheme { const configured = process.env.DIFF_THEME; if (configured) return configured as ShikiTheme; return isLightSyntaxTheme(theme) ? "github-light" : "github-dark"; } async function codeToAnsi(code: string, language: ShikiLanguage, theme: ShikiTheme): Promise { if (!codeToAnsiLoader) { codeToAnsiLoader = import("@shikijs/cli").then( (module) => module.codeToANSI, (error) => { codeToAnsiLoader = undefined; throw error; }, ); } return (await codeToAnsiLoader)(code, language, theme); } /** Keep dark-theme tokens out of black and light-theme tokens out of white. */ function normalizeContrast(ansi: string, theme: Theme, light: boolean): string { const safeMuted = safeFg(theme, "muted"); if (!safeMuted) return ansi; const threshold = light ? 140 : 72; return ansi.replace(/\x1b\[38;2;(\d+);(\d+);(\d+)m/g, (sequence, rText: string, gText: string, bText: string) => { const value = luminance({ r: Number(rText), g: Number(gText), b: Number(bText) }); if (light) return value >= threshold ? safeMuted : sequence; return value < threshold ? safeMuted : sequence; }); } function touchCache(key: string, value: string[]): string[] { cache.delete(key); cache.set(key, value); while (cache.size > CACHE_LIMIT) { const oldest = cache.keys().next().value; if (oldest === undefined) break; cache.delete(oldest); } return value; } export async function highlightBlock(code: string, filePath: string, theme: Theme): Promise { if (!code) return [""]; const language = languageFromPath(filePath); if (!language || code.length > MAX_HIGHLIGHT_CHARS) return code.split("\n"); const selectedTheme = syntaxTheme(theme); const key = `${selectedTheme}\0${language}\0${safeFg(theme, "muted") ?? ""}\0${code}`; const cached = cache.get(key); if (cached) return touchCache(key, cached); try { const ansi = normalizeContrast(await codeToAnsi(code, language, selectedTheme), theme, isLightSyntaxTheme(theme)); const lines = (ansi.endsWith("\n") ? ansi.slice(0, -1) : ansi).split("\n"); return touchCache(key, lines); } catch { return code.split("\n"); } } export function clearSyntaxCache(): void { cache.clear(); }