1"use client"; 2 3/** 4 * Code highlighting for editor code blocks (Prism, manual mode). 5 * 6 * The CodeBlock language dropdown exposes exactly these languages 7 * (block-renderer.tsx); `LANGUAGE_GRAMMARS` is the single mapping from that 8 * dropdown value to the Prism grammar to load. Unknown values fall back to 9 * `plaintext` (rendered verbatim, never broken). 10 * 11 * `Prism.manual = true` is set at module scope: we always call 12 * `Prism.highlight` ourselves, and the UMD default would otherwise also 13 * auto-highlight every `code[class*=language-]` on DOMContentLoaded — a 14 * second, uncontrolled pass over the same DOM. 15 * 16 * Pure enough to unit-test: `highlightCode` takes (text, language) and 17 * returns an HTML string (or null for plaintext/empty), so the test asserts 18 * deterministic token markup without a DOM. 19 */ 20import Prism from "prismjs"; 21import "prismjs/components/prism-typescript"; 22import "prismjs/components/prism-python"; 23import "prismjs/components/prism-bash"; 24import "prismjs/components/prism-json"; 25import "prismjs/components/prism-sql"; 26import "prismjs/components/prism-go"; 27import "prismjs/components/prism-rust"; 28// `css` and `markup` ship in the prismjs default bundle (prism.js). 29 30/** The exact languages the CodeBlock dropdown offers (keep in sync). */ 31export const CODE_LANGUAGES = [ 32 "plaintext", 33 "javascript", 34 "typescript", 35 "python", 36 "bash", 37 "json", 38 "sql", 39 "go", 40 "rust", 41 "css", 42 "html", 43] as const; 44 45export type CodeLanguage = (typeof CODE_LANGUAGES)[number]; 46 47const LANGUAGE_GRAMMARS: Record<CodeLanguage, string> = { 48 plaintext: "plain", 49 javascript: "javascript", 50 typescript: "typescript", 51 python: "python", 52 bash: "bash", 53 json: "json", 54 sql: "sql", 55 go: "go", 56 rust: "rust", 57 css: "css", 58 html: "markup", 59}; 60 61// Never let Prism re-highlight the DOM on its own. 62Prism.manual = true; 63 64/** 65 * Highlight `text` as `language`. Returns `null` when nothing should be 66 * wrapped (plaintext, unknown language, or empty input) — callers render the 67 * raw text as-is then. The HTML uses `Prism.token` classes 68 * (`token comment|keyword|string|number|function|punctuation|…`) that 69 * globals.css colors for the theme. 70 */ 71export function highlightCode(text: string, language: string): string | null { 72 if (!text) return null; 73 const key = (CODE_LANGUAGES as readonly string[]).includes(language) 74 ? LANGUAGE_GRAMMARS[language as CodeLanguage] 75 : "plain"; 76 // `plain` is a no-op grammar (Prism ships one); treat it as "no 77 // highlighting" so callers get the unambiguous null → raw-text path. 78 if (key === "plain" || !Prism.languages[key]) return null; 79 return Prism.highlight(text, Prism.languages[key], key); 80} 81 82/** Escape a string for safe insertion as text into an HTML document. */ 83export function escapeHtml(text: string): string { 84 return text 85 .replace(/&/g, "&") 86 .replace(/</g, "<") 87 .replace(/>/g, ">") 88 .replace(/"/g, """) 89 .replace(/'/g, "'"); 90} 91 92/** 93 * Render `text` for a code block: Prism token HTML for a known language, 94 * HTML-escaped raw text otherwise (plaintext / unknown language). Always 95 * returns a string — the component can put the result straight into 96 * `dangerouslySetInnerHTML` without a null branch. 97 */ 98export function codeToHtml(text: string, language: string): string { 99 const html = highlightCode(text, language); 100 return html ?? escapeHtml(text); 101} 102