import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; 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 }); } } main().catch((error) => { console.error(error); process.exit(1); });