import { createHighlighterCoreSync, type HighlighterCore } from "shiki/core"; import { createJavaScriptRegexEngine } from "shiki/engine/javascript"; // --------------------------------------------------------------------------- // Eagerly loaded languages (sync — available immediately) // --------------------------------------------------------------------------- import langTypescript from "@shikijs/langs/typescript"; import langJavascript from "@shikijs/langs/javascript"; import langTsx from "@shikijs/langs/tsx"; import langJsx from "@shikijs/langs/jsx"; import langJson from "@shikijs/langs/json"; import langShellscript from "@shikijs/langs/shellscript"; import githubDark from "@shikijs/themes/github-dark"; import githubLight from "@shikijs/themes/github-light"; // --------------------------------------------------------------------------- // Lazily loaded languages — imported on first use // --------------------------------------------------------------------------- const SUPPORTED_LANGS = [ "json", "xml", "yaml", "shellscript", "typescript", "javascript", "python", "html", "css", "markdown", "sql", "graphql", "go", "rust", "java", "ruby", "php", "swift", "kotlin", "c", "cpp", "csharp", "tsx", "jsx", "toml", "dockerfile", "diff", "http", "jsonc", "log", "proto", ] as const; type SupportedLang = (typeof SUPPORTED_LANGS)[number]; const LANG_ALIASES: Record = { sh: "shellscript", shell: "shellscript", bash: "shellscript", zsh: "shellscript", ts: "typescript", js: "javascript", py: "python", rb: "ruby", rs: "rust", "c++": "cpp", "c#": "csharp", cs: "csharp", kt: "kotlin", md: "markdown", gql: "graphql", yml: "yaml", }; const LAZY_LANG_LOADERS: Partial Promise>> = { xml: () => import("@shikijs/langs/xml"), yaml: () => import("@shikijs/langs/yaml"), python: () => import("@shikijs/langs/python"), html: () => import("@shikijs/langs/html"), css: () => import("@shikijs/langs/css"), markdown: () => import("@shikijs/langs/markdown"), sql: () => import("@shikijs/langs/sql"), graphql: () => import("@shikijs/langs/graphql"), go: () => import("@shikijs/langs/go"), rust: () => import("@shikijs/langs/rust"), java: () => import("@shikijs/langs/java"), ruby: () => import("@shikijs/langs/ruby"), php: () => import("@shikijs/langs/php"), swift: () => import("@shikijs/langs/swift"), kotlin: () => import("@shikijs/langs/kotlin"), c: () => import("@shikijs/langs/c"), cpp: () => import("@shikijs/langs/cpp"), csharp: () => import("@shikijs/langs/csharp"), toml: () => import("@shikijs/langs/toml"), dockerfile: () => import("@shikijs/langs/dockerfile"), diff: () => import("@shikijs/langs/diff"), http: () => import("@shikijs/langs/http"), jsonc: () => import("@shikijs/langs/jsonc"), log: () => import("@shikijs/langs/log"), proto: () => import("@shikijs/langs/proto"), }; const supportedSet = new Set([...SUPPORTED_LANGS, ...Object.keys(LANG_ALIASES)]); export const SUPPORTED_THEMES = ["github-dark", "github-light"] as const; export type SupportedTheme = (typeof SUPPORTED_THEMES)[number]; export const DEFAULT_LIGHT_THEME: SupportedTheme = "github-light"; export const DEFAULT_DARK_THEME: SupportedTheme = "github-dark"; export type ShikiThemeProp = SupportedTheme | { light: SupportedTheme; dark: SupportedTheme }; /** * Resolve a `ShikiThemeProp` to the `{ light, dark }` pair handed to shiki's * dual-theme mode. A single theme means "this theme in BOTH modes". * * Dual-theme + `light-dark()` colors is what keeps highlighting correct from * the FIRST frame: the rendered markup carries both palettes and the * browser's own color-scheme picks one — no JS dark-mode probe, so an SSR'd * page can't paint light-theme tokens and then snap once `useIsDark` syncs. */ export function resolveShikiThemes(theme?: ShikiThemeProp): { light: SupportedTheme; dark: SupportedTheme; } { if (typeof theme === "string") return { light: theme, dark: theme }; return { light: theme?.light ?? DEFAULT_LIGHT_THEME, dark: theme?.dark ?? DEFAULT_DARK_THEME, }; } /** The shiki options that render dual-theme `light-dark(...)` colors. */ export const dualThemeOptions = (theme?: ShikiThemeProp) => ({ themes: resolveShikiThemes(theme), defaultColor: "light-dark()" as const, }); export function resolveLang(lang: string): SupportedLang | null { const l = lang.trim().toLowerCase(); if (supportedSet.has(l)) { if (l in LANG_ALIASES) return LANG_ALIASES[l]!; return l as SupportedLang; } return null; } export function isSupportedLang(lang: string): boolean { return supportedSet.has(lang.trim().toLowerCase()); } // --------------------------------------------------------------------------- // Shared singleton highlighter — synchronous, created eagerly with core langs // --------------------------------------------------------------------------- const highlighter: HighlighterCore = createHighlighterCoreSync({ themes: [githubDark, githubLight], langs: [langTypescript, langJavascript, langTsx, langJsx, langJson, langShellscript], engine: createJavaScriptRegexEngine({ forgiving: true }), }); export function getHighlighter(): HighlighterCore { return highlighter; } /** * Ensure a language is loaded into the highlighter. Returns true if the * language is ready for synchronous use, false if it needs to be loaded * asynchronously (in which case `onLoaded` will be called when ready). */ const loadingLangs = new Set(); export function ensureLang(lang: SupportedLang, onLoaded?: () => void): boolean { const loaded = highlighter.getLoadedLanguages(); if (loaded.includes(lang)) return true; const loader = LAZY_LANG_LOADERS[lang]; if (!loader) return true; // Not a lazy lang, must be already loaded if (!loadingLangs.has(lang)) { loadingLangs.add(lang); void loader().then((mod: any) => { const registration = mod.default ?? mod; highlighter.loadLanguageSync(registration); loadingLangs.delete(lang); onLoaded?.(); }); } else if (onLoaded) { const l = loader; void l().then((mod: any) => { const registration = mod.default ?? mod; if (!highlighter.getLoadedLanguages().includes(lang)) { highlighter.loadLanguageSync(registration); } onLoaded(); }); } return false; } // --------------------------------------------------------------------------- // Streamdown code highlighter plugin // --------------------------------------------------------------------------- import type { CodeHighlighterPlugin, ThemeInput } from "streamdown"; type HighlightResult = NonNullable>; const tokensCache = new Map(); export function createCodeHighlighterPlugin(): CodeHighlighterPlugin { return { name: "shiki" as const, type: "code-highlighter" as const, getSupportedLanguages: () => [...SUPPORTED_LANGS], getThemes: () => [DEFAULT_LIGHT_THEME as ThemeInput, DEFAULT_DARK_THEME as ThemeInput], supportsLanguage: (language: string) => isSupportedLang(language), highlight(options, callback) { const resolved = resolveLang(options.language); const lang = resolved ?? "json"; // Dual-theme tokens (light-dark() colors): correct in both color // schemes, so the cache never holds the wrong palette and a mid-stream // scheme flip needs no re-render. const key = `${lang}:${options.code.length}:${options.code.slice(0, 128)}`; const cached = tokensCache.get(key); if (cached) return cached; const isReady = ensureLang(lang, () => { // Language just loaded — highlight and notify via callback const result = highlighter.codeToTokens(options.code, { lang, ...dualThemeOptions() }); tokensCache.set(key, result); callback?.(result); }); if (!isReady) return null; const result = highlighter.codeToTokens(options.code, { lang, ...dualThemeOptions() }); tokensCache.set(key, result); return result; }, }; }