Files
AstrBot/dashboard/scripts/build-t2i-shiki-runtime.mjs
若月千鸮 625eab223f feat: enable shiki highlighting for t2i templates and add a template (#7501)
* fix: enable shiki highlighting for t2i templates

* fix: t2i templates cr

* feat: add new t2i template astrbot_vitepress.html
2026-04-14 16:46:32 +08:00

233 lines
5.8 KiB
JavaScript

import { createRequire } from "node:module";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { build } from "vite";
const require = createRequire(import.meta.url);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dashboardRoot = path.resolve(__dirname, "..");
const runtimeOutputFile = path.resolve(
dashboardRoot,
"..",
"astrbot",
"core",
"utils",
"t2i",
"template",
"shiki_runtime.iife.js",
);
const shikiRequire = createRequire(require.resolve("shiki/package.json"));
const languageSpecs = [
["bash", "bash"],
["css", "css"],
["html", "html"],
["javascript", "javascript"],
["json", "json"],
["jsx", "jsx"],
["markdown", "markdown"],
["powershell", "powershell"],
["python", "python"],
["sql", "sql"],
["tsx", "tsx"],
["typescript", "typescript"],
["xml", "xml"],
["yaml", "yaml"],
];
const themeSpecs = [
["github-light", "github-light"],
["github-dark", "github-dark"],
];
// Shiki exposes plain text as a built-in special language, so we keep it
// in the supported language list without importing a package for it.
const builtInLanguageSpecs = ["text"];
const languageAliases = {
bat: "powershell",
cjs: "javascript",
console: "bash",
cts: "typescript",
dockerfile: "bash",
env: "bash",
htm: "html",
js: "javascript",
md: "markdown",
mjs: "javascript",
mts: "typescript",
plain: "text",
plaintext: "text",
ps1: "powershell",
pwsh: "powershell",
py: "python",
shell: "bash",
shellscript: "bash",
sh: "bash",
svg: "xml",
text: "text",
ts: "typescript",
txt: "text",
vue: "html",
xhtml: "html",
xml: "xml",
yml: "yaml",
zsh: "bash",
};
function resolveShikiModule(specifier) {
return pathToFileURL(shikiRequire.resolve(specifier)).href;
}
function buildVirtualSource() {
const shikiImport = JSON.stringify(
pathToFileURL(require.resolve("shiki")).href,
);
const languageImports = languageSpecs
.map(
([, packageName], index) =>
`import lang${index} from ${JSON.stringify(resolveShikiModule(`@shikijs/langs/${packageName}`))};`,
)
.join("\n");
const themeImports = themeSpecs
.map(
([, packageName], index) =>
`import theme${index} from ${JSON.stringify(resolveShikiModule(`@shikijs/themes/${packageName}`))};`,
)
.join("\n");
const supportedLanguages = [
...builtInLanguageSpecs,
...languageSpecs.map(([runtimeName]) => runtimeName),
];
return `import { createHighlighterCoreSync, createJavaScriptRegexEngine } from ${shikiImport};
${languageImports}
${themeImports}
const highlighter = createHighlighterCoreSync({
engine: createJavaScriptRegexEngine(),
langs: [${languageSpecs.map((_, index) => `...lang${index}`).join(", ")}],
themes: [${themeSpecs.map((_, index) => `theme${index}`).join(", ")}],
});
const supportedLanguages = new Set(${JSON.stringify(supportedLanguages)});
const languageAliases = ${JSON.stringify(languageAliases)};
const supportedThemes = new Set(${JSON.stringify(themeSpecs.map(([theme]) => theme))});
function normalizeLanguage(language) {
const normalized = String(language || "").trim().toLowerCase();
if (!normalized) {
return "text";
}
if (normalized in languageAliases) {
return languageAliases[normalized];
}
return supportedLanguages.has(normalized) ? normalized : "text";
}
function normalizeTheme(theme) {
const normalized = String(theme || "").trim();
return supportedThemes.has(normalized) ? normalized : "github-light";
}
function extractLanguage(codeElement) {
const className = codeElement.className || "";
const match = className.match(/language-([\\w+#.-]+)/i);
return match ? match[1] : "";
}
function renderCodeToHtml(code, language, theme) {
const normalizedTheme = normalizeTheme(theme);
try {
return highlighter.codeToHtml(String(code || ""), {
lang: normalizeLanguage(language),
theme: normalizedTheme,
});
} catch (error) {
console.warn("Failed to render T2I code block with Shiki.", error);
return highlighter.codeToHtml(String(code || ""), {
lang: "text",
theme: normalizedTheme,
});
}
}
function highlightAllCodeBlocks(root, theme) {
if (!root) {
return;
}
root.querySelectorAll("pre > code").forEach((codeElement) => {
const preElement = codeElement.parentElement;
if (!preElement || preElement.classList.contains("shiki")) {
return;
}
preElement.outerHTML = renderCodeToHtml(
codeElement.textContent || "",
extractLanguage(codeElement),
theme,
);
});
}
window.AstrBotT2IShiki = Object.freeze({
highlightAllCodeBlocks,
normalizeLanguage,
renderCodeToHtml,
});
`;
}
async function main() {
const tempDir = mkdtempSync(path.join(tmpdir(), "astrbot-t2i-shiki-runtime-"));
const entryPath = path.join(tempDir, "entry.mjs");
writeFileSync(entryPath, buildVirtualSource(), "utf-8");
try {
await build({
configFile: false,
logLevel: "info",
publicDir: false,
build: {
chunkSizeWarningLimit: 1500,
cssCodeSplit: false,
emptyOutDir: false,
lib: {
entry: entryPath,
fileName: () => path.basename(runtimeOutputFile),
formats: ["iife"],
name: "AstrBotT2IShikiRuntime",
},
minify: "esbuild",
outDir: path.dirname(runtimeOutputFile),
rollupOptions: {
output: {
inlineDynamicImports: true,
},
},
sourcemap: false,
target: "es2018",
},
});
} finally {
rmSync(tempDir, { force: true, recursive: true });
}
console.log(`Built ${runtimeOutputFile}`);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});