"use client"; import { useEffect, useState, useMemo } from "react"; import { createHighlighter, type Highlighter } from "shiki"; import { DiffCard } from "../diff-viewer"; /** Map file extensions to shiki language identifiers. */ const EXT_TO_LANG: Record = { ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx", mjs: "javascript", cjs: "javascript", py: "python", rb: "ruby", go: "go", rs: "rust", java: "java", kt: "kotlin", swift: "swift", c: "c", cpp: "cpp", h: "c", hpp: "cpp", cs: "csharp", css: "css", scss: "scss", less: "less", html: "html", htm: "html", xml: "xml", svg: "xml", json: "json", jsonc: "jsonc", yaml: "yaml", yml: "yaml", toml: "toml", md: "markdown", mdx: "mdx", sh: "bash", bash: "bash", zsh: "bash", fish: "fish", ps1: "powershell", sql: "sql", graphql: "graphql", gql: "graphql", dockerfile: "dockerfile", docker: "dockerfile", makefile: "makefile", cmake: "cmake", r: "r", lua: "lua", php: "php", vue: "vue", svelte: "svelte", diff: "diff", patch: "diff", ini: "ini", env: "ini", tf: "terraform", proto: "proto", zig: "zig", elixir: "elixir", ex: "elixir", erl: "erlang", hs: "haskell", scala: "scala", clj: "clojure", dart: "dart", }; /** All language IDs we might need to load. */ const ALL_LANGS = [...new Set(Object.values(EXT_TO_LANG))]; function extFromFilename(filename: string): string { const lower = filename.toLowerCase(); // Handle special filenames if (lower === "dockerfile" || lower.startsWith("dockerfile.")) {return "dockerfile";} if (lower === "makefile" || lower === "gnumakefile") {return "makefile";} if (lower === "cmakelists.txt") {return "cmake";} return lower.split(".").pop() ?? ""; } export function langFromFilename(filename: string): string { const ext = extFromFilename(filename); return EXT_TO_LANG[ext] ?? "text"; } export function isCodeFile(filename: string): boolean { const ext = extFromFilename(filename); return ext in EXT_TO_LANG; } type CodeViewerProps = { content: string; filename: string; }; // Singleton highlighter so we only create it once let highlighterPromise: Promise | null = null; function getHighlighter(): Promise { if (!highlighterPromise) { highlighterPromise = createHighlighter({ themes: ["github-dark", "github-light"], langs: ALL_LANGS, }); } return highlighterPromise; } export function CodeViewer({ content, filename }: CodeViewerProps) { const lang = langFromFilename(filename); const ext = extFromFilename(filename); // For .diff/.patch files, use the DiffCard instead if (ext === "diff" || ext === "patch") { return (
); } return ; } function HighlightedCode({ content, filename, lang, }: { content: string; filename: string; lang: string; }) { const [html, setHtml] = useState(null); const lineCount = useMemo(() => content.split("\n").length, [content]); useEffect(() => { let cancelled = false; void getHighlighter().then((highlighter) => { if (cancelled) {return;} const result = highlighter.codeToHtml(content, { lang: lang === "text" ? "text" : lang, themes: { dark: "github-dark", light: "github-light", }, // We'll handle line numbers ourselves }); setHtml(result); }); return () => { cancelled = true; }; }, [content, lang]); return (
{/* File header */}
{filename} {lang.toUpperCase()} {lineCount} lines
{/* Code content */}
{html ? (
) : ( // Fallback: plain text with line numbers while loading
            
              {content.split("\n").map((line, idx) => (
                
{idx + 1} {line || " "}
))}
)}
); }