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); }