feat(frontend): Shiki 白名单语言高亮并支持 aardio

显式加载常用语言;修复 pretty-code 去 shiki class 导致不上色,以及 aardio 循环别名。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 06:09:25 +08:00
parent 3f88b38610
commit 8d5e00a112
7 changed files with 290 additions and 23 deletions

View File

@@ -0,0 +1,116 @@
import { createHighlighterCore, type HighlighterCore } from "shiki/core";
import { createOnigurumaEngine } from "shiki/engine/oniguruma";
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<string, string> = {
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<string>([...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<HighlighterCore> | null = null;
/** 站点单例 highlighter:仅白名单语言 + aardio */
export function getSiteHighlighter(): Promise<HighlighterCore> {
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<string> {
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 的 getHighlighter:
* 忽略其默认 langs:['plaintext'],始终返回站点白名单单例。
*/
export async function getPrettyCodeHighlighter(): Promise<HighlighterCore> {
return getSiteHighlighter();
}