highlight.ts

1.6 KB · raw

1import {2	type BundledLanguage,3	bundledLanguages,4	createCssVariablesTheme,5	createHighlighter,6	type Highlighter,7} from "shiki";8import { type Html, html } from "./html.ts";910// Shiki's CSS-variables theme names each token's role; the role becomes a class, so the colours11// live in kepr.css and the CSP needs no inline styles.12const theme = createCssVariablesTheme({ name: "kepr", variablePrefix: "--tk-" });13const ROLE = /^var\(--tk-token-([a-z-]+)\)$/;14const ITALIC = 1;1516let highlighter: Promise<Highlighter> | undefined;1718function languageOf(path: string): BundledLanguage | null {19	const name = path.split("/").pop()?.toLowerCase() ?? "";20	const ext = name.includes(".") ? (name.split(".").pop() as string) : name;21	return ext in bundledLanguages ? (ext as BundledLanguage) : null;22}2324/** Each line of `code` as highlighted markup; plain escaped lines for a language Shiki lacks. */25export async function highlightLines(code: string, path: string): Promise<Html[]> {26	const lines = code.replace(/\n$/, "").split("\n");27	const lang = languageOf(path);28	if (!lang) return lines.map((line) => html`${line}`);29	highlighter ??= createHighlighter({ themes: [theme], langs: [] });30	const shiki = await highlighter;31	await shiki.loadLanguage(lang);32	return shiki.codeToTokensBase(code.replace(/\n$/, ""), { lang, theme }).map(33		(tokens) =>34			html`${tokens.map((token) => {35				const role = ROLE.exec(token.color ?? "")?.[1];36				const classes = [role && `tk-${role}`, (token.fontStyle ?? 0) & ITALIC && "tk-i"].filter(37					Boolean,38				);39				return classes.length40					? html`<span class="${classes.join(" ")}">${token.content}</span>`41					: token.content;42			})}`,43	);44}