// HighlightedCode — Shiki-powered syntax highlighting as a reusable
// primitive. Apps drop `` anywhere and
// get dual-theme (light + dark) output that flips with the
// `.dark` class on .
//
// Lazy + shared singleton: the first usage triggers an async Shiki
// load (~200ms WASM bootstrap), subsequent renders share the same
// highlighter instance. Until Shiki is ready, the component renders
// plain escaped code so the layout doesn't jump.
//
// Browser-safe via dynamic import; runs on the Node side too (SSR /
// `voltro build` waits for it via `whenReady`).
import {
useEffect, useMemo, useState, type ReactNode,
} from 'react'
import type { Highlighter } from 'shiki'
import { cn } from '../cn'
declare global {
var __voltroShikiHighlighter: Highlighter | null | undefined
var __voltroShikiHighlighterPromise: Promise | null | undefined
}
// Languages we ship support for. Add to this list as new doc + landing
// snippets need them; loading is one-shot at highlighter init.
export const SHIKI_LANGS = [
'text', 'ts', 'tsx', 'typescript', 'js', 'jsx', 'json', 'bash', 'sh',
'shell', 'sql', 'yaml', 'toml', 'md', 'mdx', 'html', 'css', 'diff',
] as const
export type ShikiLang = typeof SHIKI_LANGS[number]
// Global singleton — shared across HighlightedCode calls, HMR reloads,
// and docs-content rendering so we only pay the WASM bootstrap once.
const ensure = async (): Promise => {
if (globalThis.__voltroShikiHighlighter) return globalThis.__voltroShikiHighlighter
if (globalThis.__voltroShikiHighlighterPromise) {
return globalThis.__voltroShikiHighlighterPromise
}
globalThis.__voltroShikiHighlighterPromise = (async () => {
const { createHighlighter } = await import('shiki')
const highlighter = await createHighlighter({
themes: ['github-dark-dimmed', 'github-light'],
langs: SHIKI_LANGS as unknown as string[],
})
globalThis.__voltroShikiHighlighter = highlighter
return highlighter
})()
return globalThis.__voltroShikiHighlighterPromise
}
/** Promise that resolves once Shiki has loaded — for SSR/build callers
* that want to await before rendering. */
export const whenHighlighterReady = (): Promise =>
ensure().then(() => { /* discard */ })
const escapeHtml = (s: string): string =>
s.replace(/&/g, '&').replace(//g, '>')
interface HighlightedCodeProps {
readonly code: string
readonly lang?: ShikiLang
/** Outer wrapper class. */
readonly className?: string
}
export const HighlightedCode = ({
code, lang = 'ts', className,
}: HighlightedCodeProps): ReactNode => {
const [html, setHtml] = useState(null)
// Render the highlighted HTML when Shiki is ready. Re-runs only when
// code/lang changes — the highlighter itself is stable.
useEffect(() => {
let cancelled = false
ensure().then((hl) => {
if (cancelled || !hl) return
try {
const out = hl.codeToHtml(code, {
lang,
themes: { light: 'github-light', dark: 'github-dark-dimmed' },
defaultColor: false,
})
setHtml(out)
} catch {
// Lang missing → fall through to plain escaped code.
}
})
return () => { cancelled = true }
}, [code, lang])
// Cached escaped fallback — what renders before Shiki ships its
// first highlighted batch.
const fallback = useMemo(() => escapeHtml(code), [code])
if (html) {
return (
)
}
return (
)
}