import { createHighlighterCore, type HighlighterCore } from "shiki/core"; import { createOnigurumaEngine } from "shiki/engine/oniguruma"; import type { LanguageRegistration } from "shiki"; import { aardioGrammar } from "./aardioGrammar"; /** 站点 Markdown 代码高亮:显式白名单(非全量) */ export const SHIKI_LANG_IDS = [ "go", "python", "r", "markdown", "javascript", "typescript", "tsx", "jsx", "json", "yaml", "toml", "bash", "sql", "html", "css", "scss", "diff", "plaintext", ] as const; export type SiteShikiLang = (typeof SHIKI_LANG_IDS)[number] | "aardio"; export const SHIKI_LANG_ALIAS: Record = { py: "python", md: "markdown", js: "javascript", ts: "typescript", sh: "bash", shell: "bash", zsh: "bash", yml: "yaml", text: "plaintext", txt: "plaintext", }; export const SHIKI_THEMES = { light: "github-light", dark: "github-dark-dimmed", } as const; const ALLOWED = new Set([...SHIKI_LANG_IDS, "aardio"]); /** 未知语言降级为 plaintext;别名归一到白名单 id */ export function resolveShikiLang(raw?: string | null): SiteShikiLang { const id = (raw || "").trim().toLowerCase(); if (!id) return "plaintext"; const mapped = SHIKI_LANG_ALIAS[id] || id; if (ALLOWED.has(mapped)) return mapped as SiteShikiLang; return "plaintext"; } let highlighterPromise: Promise | null = null; /** 站点单例 highlighter:仅白名单语言 + aardio */ export function getSiteHighlighter(): Promise { if (!highlighterPromise) { highlighterPromise = createHighlighterCore({ themes: [ import("@shikijs/themes/github-light"), import("@shikijs/themes/github-dark-dimmed"), ], langs: [ import("@shikijs/langs/go"), import("@shikijs/langs/python"), import("@shikijs/langs/r"), import("@shikijs/langs/markdown"), import("@shikijs/langs/javascript"), import("@shikijs/langs/typescript"), import("@shikijs/langs/tsx"), import("@shikijs/langs/jsx"), import("@shikijs/langs/json"), import("@shikijs/langs/yaml"), import("@shikijs/langs/toml"), import("@shikijs/langs/bash"), import("@shikijs/langs/sql"), import("@shikijs/langs/html"), import("@shikijs/langs/css"), import("@shikijs/langs/scss"), import("@shikijs/langs/diff"), // plaintext / text / txt 为 Shiki 特殊语言,无需 grammar aardioGrammar, ], langAlias: SHIKI_LANG_ALIAS, engine: createOnigurumaEngine(import("shiki/wasm")), }); } return highlighterPromise; } /** 客户端/工具:按白名单高亮为双主题 HTML */ export async function siteCodeToHtml(code: string, langRaw?: string | null): Promise { const highlighter = await getSiteHighlighter(); const lang = resolveShikiLang(langRaw); return highlighter.codeToHtml(code, { lang, themes: { light: SHIKI_THEMES.light, dark: SHIKI_THEMES.dark, }, defaultColor: false, }); } /** * rehype-pretty-code 会对每个 fence 调用 loadLanguage(langId: string)。 * HighlighterCore 不能把裸 string 当 grammar(会 TypeError 并可能搞坏注册表), * 这里包装:已装载的语言直接跳过。 */ export async function getPrettyCodeHighlighter(): Promise { const core = await getSiteHighlighter(); const loadLanguage = async ( ...langs: (string | LanguageRegistration | Promise)[] ) => { const pending: (LanguageRegistration | Promise)[] = []; const loaded = new Set(core.getLoadedLanguages()); for (const lang of langs.flat()) { if (typeof lang === "string") { const id = resolveShikiLang(lang); if (loaded.has(lang) || loaded.has(id) || id === "plaintext") continue; continue; } pending.push(lang); } if (pending.length > 0) { await core.loadLanguage(...pending); } }; return new Proxy(core, { get(target, prop, receiver) { if (prop === "loadLanguage") return loadLanguage; const value = Reflect.get(target, prop, receiver); return typeof value === "function" ? value.bind(target) : value; }, }) as HighlighterCore; }