function escapeForSvelte(html: string): string { return html.replace(/\{/g, '{').replace(/\}/g, '}'); } const COPY_ICON_SVG = ''; /** Wrap already-highlighted HTML in the standard code-block shell — copy button and Svelte-brace escape — around the `
` structure the engine produced. */
export function wrapCodeBlock(highlightedHtml: string): string {
return `${escapeForSvelte(highlightedHtml)}`;
}
/** Memoized snippets per highlighter before insertion-ordered eviction kicks in. */
const DEFAULT_HIGHLIGHT_CACHE_SIZE = 1000;
export interface CreateHighlighterOptions {
/** Max memoized snippets. `0` disables memoization. Default: 1000. */
cacheSize?: number;
}
/**
* Build a `highlightCode(code, lang)` function from any highlighting engine. You supply `highlight`, turning source into
* themed HTML (e.g. Shiki's `codeToHtml`), and the result composes it with the code-block wrapper, copy button, and
* Svelte-brace escape. Results memoize per `(code, lang)`, so a page re-highlighting the same snippets pays once.
*
* ```ts
* import { createHighlighter as createShiki } from 'shiki';
* import { createHighlighter } from 'mochi-framework/highlight';
*
* const shiki = await createShiki({ themes: ['vitesse-dark'], langs: ['typescript'] });
* export const highlightCode = createHighlighter((code, lang) =>
* shiki.codeToHtml(code, { lang, theme: 'vitesse-dark' }),
* );
* ```
*/
export function createHighlighter(
highlight: (code: string, lang: string) => string | Promise,
options: CreateHighlighterOptions = {},
): (code: string, lang?: string | null) => string | Promise {
const max = options.cacheSize ?? DEFAULT_HIGHLIGHT_CACHE_SIZE;
// Highlighting is pure in (code, lang) but a TextMate grammar pass costs milliseconds per snippet, enough that a page
// re-highlighting its own code blocks each SSR render spends longer in the highlighter than in Svelte. The in-flight
// promise is stored so concurrent callers share one pass, and insertion-ordered eviction bounds an app highlighting
// user content.
const cache = new Map>();
return (code, lang) => {
const language = lang ?? 'plaintext';
if (max <= 0) {
return finish(highlight(code, language));
}
const key = `${language}\0${code}`;
const hit = cache.get(key);
if (hit !== undefined) {
return hit;
}
const value = finish(highlight(code, language));
if (cache.size >= max) {
cache.delete(cache.keys().next().value!);
}
cache.set(key, value);
// A failed pass must not be cached — the next call should retry rather than
// replay a rejected promise forever. Guard on identity so a later retry that
// has already re-populated this key isn't evicted by the original's rejection.
if (typeof value !== 'string') {
void value.catch(() => {
if (cache.get(key) === value) {
cache.delete(key);
}
});
}
return value;
};
}
function finish(result: string | Promise): string | Promise {
return typeof result === 'string' ? wrapCodeBlock(result) : result.then(wrapCodeBlock);
}