"use client"; /** * Code highlighting for editor code blocks (Prism, manual mode). * * The CodeBlock language dropdown exposes exactly these languages * (block-renderer.tsx); `LANGUAGE_GRAMMARS` is the single mapping from that * dropdown value to the Prism grammar to load. Unknown values fall back to * `plaintext` (rendered verbatim, never broken). * * `Prism.manual = true` is set at module scope: we always call * `Prism.highlight` ourselves, and the UMD default would otherwise also * auto-highlight every `code[class*=language-]` on DOMContentLoaded — a * second, uncontrolled pass over the same DOM. * * Pure enough to unit-test: `highlightCode` takes (text, language) and * returns an HTML string (or null for plaintext/empty), so the test asserts * deterministic token markup without a DOM. */ import Prism from "prismjs"; import "prismjs/components/prism-typescript"; import "prismjs/components/prism-python"; import "prismjs/components/prism-bash"; import "prismjs/components/prism-json"; import "prismjs/components/prism-sql"; import "prismjs/components/prism-go"; import "prismjs/components/prism-rust"; // `css` and `markup` ship in the prismjs default bundle (prism.js). /** The exact languages the CodeBlock dropdown offers (keep in sync). */ export const CODE_LANGUAGES = [ "plaintext", "javascript", "typescript", "python", "bash", "json", "sql", "go", "rust", "css", "html", ] as const; export type CodeLanguage = (typeof CODE_LANGUAGES)[number]; const LANGUAGE_GRAMMARS: Record = { plaintext: "plain", javascript: "javascript", typescript: "typescript", python: "python", bash: "bash", json: "json", sql: "sql", go: "go", rust: "rust", css: "css", html: "markup", }; // Never let Prism re-highlight the DOM on its own. Prism.manual = true; /** * Highlight `text` as `language`. Returns `null` when nothing should be * wrapped (plaintext, unknown language, or empty input) — callers render the * raw text as-is then. The HTML uses `Prism.token` classes * (`token comment|keyword|string|number|function|punctuation|…`) that * globals.css colors for the theme. */ export function highlightCode(text: string, language: string): string | null { if (!text) return null; const key = (CODE_LANGUAGES as readonly string[]).includes(language) ? LANGUAGE_GRAMMARS[language as CodeLanguage] : "plain"; // `plain` is a no-op grammar (Prism ships one); treat it as "no // highlighting" so callers get the unambiguous null → raw-text path. if (key === "plain" || !Prism.languages[key]) return null; return Prism.highlight(text, Prism.languages[key], key); } /** Escape a string for safe insertion as text into an HTML document. */ export function escapeHtml(text: string): string { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } /** * Render `text` for a code block: Prism token HTML for a known language, * HTML-escaped raw text otherwise (plaintext / unknown language). Always * returns a string — the component can put the result straight into * `dangerouslySetInnerHTML` without a null branch. */ export function codeToHtml(text: string, language: string): string { const html = highlightCode(text, language); return html ?? escapeHtml(text); }