/** * Shared Shiki configuration. * * Single source of truth for syntax highlighting across the editor (live * decorations) and the publisher (static SSR). Using the same config in both * environments guarantees: * - identical supported languages * - identical themes / token colors * - identical output structure () * * Shiki is async-initialized (it loads an Oniguruma WASM engine + grammars). * A singleton highlighter is created on first use and reused forever. The * editor kicks off creation at mount; the publisher awaits it synchronously * during render (publish is a one-shot pipeline). */ import type { BundledLanguage, BundledTheme, HighlighterGeneric } from "shiki"; import { createHighlighter } from "shiki"; /** * Languages bundled into the highlighter. Chosen for research/tech articles. * Keep this list in sync with what the editor's language selector advertises. * Adding a language = one line here + redeploy; the cost is ~50KB gzipped per * grammar loaded lazily. */ export const SHIKI_LANGS: BundledLanguage[] = [ "javascript", "typescript", "tsx", "jsx", "python", "bash", "shell", "json", "yaml", "toml", "css", "html", "xml", "markdown", "sql", "rust", "go", "c", "cpp", "csharp", "java", "r", "julia", "ruby", "php", "swift", "kotlin", "scala", "haskell", "lua", "dockerfile", "diff", "latex", "ini", "makefile", "graphql", ]; /** * Dual theme: Shiki emits each token with inline `color` (light) plus a * `--shiki-dark` CSS variable (dark). A small CSS rule toggles which wins * based on the `data-theme` attribute on . See `_code-blocks.css`. * * Pick well-balanced, muted themes that read well in an academic context. */ export const SHIKI_THEMES = { light: "github-light" as BundledTheme, dark: "github-dark" as BundledTheme, }; export type ShikiHighlighter = HighlighterGeneric; let highlighterPromise: Promise | null = null; /** * Returns the shared highlighter, creating it on first call. All subsequent * callers wait on the same promise (deduped). */ export function getSharedHighlighter(): Promise { if (!highlighterPromise) { highlighterPromise = createHighlighter({ themes: [SHIKI_THEMES.light, SHIKI_THEMES.dark], langs: SHIKI_LANGS, }); } return highlighterPromise; } /** * Checks whether a language string is supported. Falls back to plain-text * rendering if not, which is what both editor and publisher should do. */ export function isSupportedLang(lang: string | null | undefined): lang is BundledLanguage { if (!lang) return false; return (SHIKI_LANGS as readonly string[]).includes(lang); } /** * Normalize a language alias to the canonical name Shiki expects. Handles * common aliases that users type (`js` → `javascript`, `py` → `python`, etc.). */ export function normalizeLang(lang: string | null | undefined): string { if (!lang) return ""; const l = lang.toLowerCase().trim(); const aliases: Record = { js: "javascript", ts: "typescript", py: "python", rb: "ruby", sh: "bash", zsh: "bash", yml: "yaml", md: "markdown", "c++": "cpp", "c#": "csharp", docker: "dockerfile", tex: "latex", }; return aliases[l] ?? l; }