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