/**
* Shared debounced syntax-highlighted code block.
*
* Performance characteristics vs the old per-site implementations:
* - While code is still growing (streaming), Shiki re-highlight is debounced
* to ~150 ms (trailing). Between debounce fires the previous highlighted
* HTML is kept — no blank flash.
* - A content hash gate means identical re-renders never re-invoke Shiki.
* - A final highlight is triggered immediately when streaming ends (caller
* passes streaming=false).
* - Non-streaming first paint waits for Shiki (invisible placeholder reserves
* space) so the UI never snaps from plain text to highlighted HTML.
*
* Usage:
*
*/
import React, { useEffect, useRef, useState } from "react";
const LANG_ALIASES: Record = {
js: "javascript",
ts: "typescript",
sh: "bash",
shell: "bash",
zsh: "bash",
py: "python",
yml: "yaml",
md: "markdown",
bq: "sql",
bigquery: "sql",
};
export interface HighlightedCodeBlockProps {
code: string;
lang: string;
/** Class applied to the wrapper div when Shiki HTML is rendered. */
containerClass: string;
/** Pass true while the parent message is still streaming. When false the
* block fires an immediate (non-debounced) highlight pass. */
streaming?: boolean;
/** Loader for the site-specific Shiki highlighter instance. Each call site
* has its own highlighter loader (different language sets, different CSS
* class, etc.) — pass it in so this component stays decoupled. */
loadHighlighter: () => Promise<{
codeToHtml: (
code: string,
options: {
lang: string;
themes: { light: string; dark: string };
defaultColor?: false | "light" | "dark";
},
) => string | Promise;
getLoadedLanguages: () => string[];
}>;
}
const DEBOUNCE_MS = 150;
function hashString(s: string): number {
let h = 0;
for (let i = 0; i < s.length; i++) {
h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
}
return h;
}
export function HighlightedCodeBlock({
code,
lang,
containerClass,
streaming = false,
loadHighlighter,
}: HighlightedCodeBlockProps): React.ReactElement {
const [html, setHtml] = useState(null);
const [failed, setFailed] = useState(false);
// Track the content hash for which html was last rendered so identical
// re-renders never re-invoke Shiki.
const renderedHashRef = useRef(null);
const debounceTimerRef = useRef | null>(null);
const cancelledRef = useRef(false);
useEffect(() => {
cancelledRef.current = false;
return () => {
cancelledRef.current = true;
if (debounceTimerRef.current !== null) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
};
}, []);
useEffect(() => {
const contentHash = hashString(lang + "\0" + code);
// Skip if we already rendered this exact content
if (renderedHashRef.current === contentHash) return;
const doHighlight = () => {
loadHighlighter()
.then((highlighter) => {
const requested = (lang || "text").toLowerCase();
const resolved = LANG_ALIASES[requested] ?? requested;
const loaded = highlighter.getLoadedLanguages();
const finalLang = loaded.includes(resolved) ? resolved : "text";
return highlighter.codeToHtml(code, {
lang: finalLang,
themes: {
light: "github-light-default",
dark: "github-dark-default",
},
defaultColor: false,
});
})
.then((out) => {
if (!cancelledRef.current) {
renderedHashRef.current = contentHash;
setFailed(false);
setHtml(out as string);
}
})
.catch(() => {
if (!cancelledRef.current) {
renderedHashRef.current = contentHash;
setFailed(true);
setHtml(null);
}
});
};
// Clear any existing pending debounce
if (debounceTimerRef.current !== null) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
if (streaming) {
// Debounce while the code block is still growing
debounceTimerRef.current = setTimeout(() => {
debounceTimerRef.current = null;
doHighlight();
}, DEBOUNCE_MS);
} else {
// Stream complete (or never was streaming): highlight immediately
doHighlight();
}
}, [code, lang, streaming, loadHighlighter]);
if (html) {
return (
);
}
// Streaming: show plain growing text (previous html already kept above when
// available). Non-streaming first paint: reserve space invisibly so we never
// flash unhighlighted → highlighted.
const showPlain = streaming || failed;
return (
);
}