import { type BundledLanguage, bundledLanguages, createCssVariablesTheme, createHighlighter, type Highlighter, } from "shiki"; import { type Html, html } from "./html.ts"; // Shiki's CSS-variables theme names each token's role; the role becomes a class, so the colours // live in kepr.css and the CSP needs no inline styles. const theme = createCssVariablesTheme({ name: "kepr", variablePrefix: "--tk-" }); const ROLE = /^var\(--tk-token-([a-z-]+)\)$/; const ITALIC = 1; let highlighter: Promise | undefined; function languageOf(path: string): BundledLanguage | null { const name = path.split("/").pop()?.toLowerCase() ?? ""; const ext = name.includes(".") ? (name.split(".").pop() as string) : name; return ext in bundledLanguages ? (ext as BundledLanguage) : null; } /** Each line of `code` as highlighted markup; plain escaped lines for a language Shiki lacks. */ export async function highlightLines(code: string, path: string): Promise { const lines = code.replace(/\n$/, "").split("\n"); const lang = languageOf(path); if (!lang) return lines.map((line) => html`${line}`); highlighter ??= createHighlighter({ themes: [theme], langs: [] }); const shiki = await highlighter; await shiki.loadLanguage(lang); return shiki.codeToTokensBase(code.replace(/\n$/, ""), { lang, theme }).map( (tokens) => html`${tokens.map((token) => { const role = ROLE.exec(token.color ?? "")?.[1]; const classes = [role && `tk-${role}`, (token.fontStyle ?? 0) & ITALIC && "tk-i"].filter( Boolean, ); return classes.length ? html`${token.content}` : token.content; })}`, ); }