Files
jiang13-bbs/frontend/components/markdown/shikiHighlighter.ts
freefire 8d5e00a112 feat(frontend): Shiki 白名单语言高亮并支持 aardio
显式加载常用语言;修复 pretty-code 去 shiki class 导致不上色,以及 aardio 循环别名。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 06:09:25 +08:00

117 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}