Tokenize closed code blocks in a dedicated Shiki Web Worker instead of calling the shared highlighter synchronously on the UI thread. This removes the one-shot main-thread highlight stall when a code fence closes on a large block. Streaming behavior is unchanged: the open (streaming) fence still renders as plain text and is highlighted once on close. On any worker failure the block keeps its escaped plain code — highlighting never falls back onto the main thread. - Add markdownShikiThemeDefinition (dependency-free CSS-variable theme) so the worker can use the theme without pulling in @pierre/diffs / React. - Add markdown-worker-protocol, markdown-shiki.worker, and the main-thread markdown-worker client. - Route highlightCodeBlocks through the worker; keep the size/VSCode line guard and mermaid skip on the main thread. - Add shiki as a direct dependency (was transitive via @pierre/diffs).
56 lines
2.0 KiB
TypeScript
56 lines
2.0 KiB
TypeScript
/// <reference lib="webworker" />
|
|
|
|
import { bundledLanguages, createHighlighter, type BundledLanguage } from 'shiki';
|
|
import { MARKDOWN_SHIKI_THEME, MARKDOWN_SHIKI_THEME_DEFINITION } from './markdownShikiThemeDefinition';
|
|
import type { MarkdownWorkerRequest, MarkdownWorkerResponse } from './markdown-worker-protocol';
|
|
|
|
// Single shared highlighter for the worker. Languages load lazily on demand.
|
|
let highlighter: ReturnType<typeof createHighlighter> | undefined;
|
|
|
|
// Serialize work so language loading / tokenization never overlaps.
|
|
let queue = Promise.resolve();
|
|
|
|
const ensureHighlighter = (): ReturnType<typeof createHighlighter> => {
|
|
highlighter ??= createHighlighter({
|
|
// Cast: the theme is a CSS-variable TextMate theme; Shiki accepts the shape.
|
|
themes: [MARKDOWN_SHIKI_THEME_DEFINITION as unknown as Parameters<typeof createHighlighter>[0]['themes'][number]],
|
|
langs: [],
|
|
});
|
|
return highlighter;
|
|
};
|
|
|
|
self.onmessage = (event: MessageEvent<MarkdownWorkerRequest>) => {
|
|
const request = event.data;
|
|
if (request.type === 'init') {
|
|
ensureHighlighter();
|
|
return;
|
|
}
|
|
queue = queue.then(() => highlight(request)).catch(() => {});
|
|
};
|
|
|
|
async function highlight(request: Extract<MarkdownWorkerRequest, { type: 'highlight' }>): Promise<void> {
|
|
try {
|
|
const instance = await ensureHighlighter();
|
|
let lang = request.lang in bundledLanguages ? request.lang : 'text';
|
|
if (lang !== 'text' && !instance.getLoadedLanguages().includes(lang)) {
|
|
try {
|
|
await instance.loadLanguage(bundledLanguages[lang as BundledLanguage]);
|
|
} catch {
|
|
lang = 'text';
|
|
}
|
|
}
|
|
const html = instance.codeToHtml(request.code, {
|
|
lang,
|
|
theme: MARKDOWN_SHIKI_THEME,
|
|
tabindex: false,
|
|
});
|
|
post({ type: 'highlight', id: request.id, html });
|
|
} catch (error) {
|
|
post({ type: 'error', id: request.id, message: error instanceof Error ? error.message : String(error) });
|
|
}
|
|
}
|
|
|
|
function post(response: MarkdownWorkerResponse): void {
|
|
self.postMessage(response);
|
|
}
|