import type { Action } from "svelte/action"; const BLOCK = /
([\s\S]*?)<\/code><\/pre>/g;
const MAX_SSR_BLOCK_CHARS = 50_000;
const MAX_CSR_BLOCK_CHARS = 200_000;
const ENTITIES: Record = {
amp: "&",
lt: "<",
gt: ">",
quot: '"',
"#39": "'",
"#x27": "'",
"#x2F": "/"
};
const decodeEntities = (value: string): string =>
value.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z#0-9]+);/g, (match, body: string) => {
if (body.startsWith("#x") || body.startsWith("#X"))
try {
return String.fromCodePoint(parseInt(body.slice(2), 16));
} catch {
return match;
}
if (body.startsWith("#")) {
const point = parseInt(body.slice(1), 10);
return Number.isNaN(point) ? match : String.fromCodePoint(point);
}
return ENTITIES[body] ?? match;
});
const INNER = /]*>([\s\S]*)<\/code>\s*<\/pre>/;
type Highlighter = Awaited>;
type Loaded = { highlighter: Highlighter; theme: string };
// importing this lazily keeps shiki out of markdown pages with no code
const loadHighlighter = async (langs: string[]): Promise => {
try {
const { SYNTAX_THEME_NAME, getSharedHighlighter } = await import("$lib/syntax");
const highlighter = await getSharedHighlighter({ themes: [SYNTAX_THEME_NAME], langs });
return { highlighter, theme: SYNTAX_THEME_NAME };
} catch {
return null;
}
};
const highlightInner = (loaded: Loaded, code: string, lang: string): string | null => {
let full: string;
try {
full = loaded.highlighter.codeToHtml(code, {
lang,
theme: loaded.theme,
defaultColor: false,
cssVariablePrefix: "--diffs-token-"
});
} catch {
return null;
}
return INNER.exec(full)?.[1] ?? null;
};
export const highlightCodeblocks = async (html: string): Promise => {
const found = [...html.matchAll(BLOCK)];
if (found.length === 0) return html;
const blocks = found.flatMap((match) => {
const [raw, lang, escaped] = match as unknown as [string, string, string];
const code = decodeEntities(escaped);
return code.length > 0 && code.length <= MAX_SSR_BLOCK_CHARS ? [{ raw, lang, code }] : [];
});
if (blocks.length === 0) return html;
const loaded = await loadHighlighter([...new Set(blocks.map((block) => block.lang))]);
if (!loaded) return html;
const swapped = new Map();
for (const block of blocks) {
if (swapped.has(block.raw)) continue;
const inner = highlightInner(loaded, block.code, block.lang);
if (inner !== null)
swapped.set(
block.raw,
`${inner}
`
);
}
if (swapped.size === 0) return html;
return html.replace(BLOCK, (raw) => swapped.get(raw) ?? raw);
};
const LANG = /(?:^|\s)language-([^\s]+)/;
const idle = (): Promise =>
new Promise((resolve) => {
const ric = (
globalThis as {
requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => void;
}
).requestIdleCallback;
if (typeof ric === "function") ric(() => resolve(), { timeout: 1500 });
else setTimeout(resolve, 0);
});
export const enhanceCodeblocks: Action = (node) => {
let cancelled = false;
const run = async (): Promise => {
const plain = (el: Element): { code: HTMLElement; text: string } | null => {
const code = el.firstElementChild;
if (!(code instanceof HTMLElement) || code.querySelector("span") !== null) return null;
const text = code.textContent ?? "";
return text.length > 0 && text.length <= MAX_CSR_BLOCK_CHARS ? { code, text } : null;
};
const targets = [...node.querySelectorAll("pre")].flatMap((pre) => {
const found = plain(pre);
if (!found) return [];
const lang = LANG.exec(found.code.className)?.[1];
return lang ? [{ ...found, lang }] : [];
});
if (targets.length === 0) return;
await idle();
if (cancelled || !node.isConnected) return;
const loaded = await loadHighlighter([...new Set(targets.map((target) => target.lang))]);
if (!loaded || cancelled) return;
for (const target of targets) {
if (cancelled || !target.code.isConnected) continue;
// a re-render may have highlighted it since
if (target.code.querySelector("span") !== null) continue;
const inner = highlightInner(loaded, target.text, target.lang);
if (inner !== null) target.code.innerHTML = inner;
}
};
void run();
return {
update: () => void run(),
destroy: () => {
cancelled = true;
}
};
};