+
+
{label}
+
{themeDisplayName(themeId)}
+
+
+ {html ? (
+
自带 inline style (bg/fg/color),直接渲染
+ dangerouslySetInnerHTML={{ __html: html }}
+ />
+ ) : error ? (
+ {error}
+ ) : (
+
+ {PREVIEW_CODE}
+
+ )}
+
+
+ )
+}
+
+// ============================================
+// Main section
+// ============================================
+
+export function CodeBlockThemeSettings() {
+ const { t } = useTranslation(['settings', 'common'])
+ const {
+ codeBlockThemeLight,
+ codeBlockThemeDark,
+ setCodeBlockThemeLight,
+ setCodeBlockThemeDark,
+ resolvedTheme,
+ } = useTheme()
+
+ return (
+
+ {t('appearance.codeBlockThemesDesc')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+// 导出 unused type 仅用于未来扩展(被引用以避免 tree-shake 误删)
+export type { CodeBlockThemeInfo }
diff --git a/src/hooks/useSyntaxHighlight.test.ts b/src/hooks/useSyntaxHighlight.test.ts
index 3bd42d5f..062752be 100644
--- a/src/hooks/useSyntaxHighlight.test.ts
+++ b/src/hooks/useSyntaxHighlight.test.ts
@@ -12,6 +12,16 @@ describe('getShikiTheme', () => {
expect(getShikiTheme(true).key).toBe('github-dark-default')
expect(getShikiTheme(false).key).toBe('github-light-default')
})
+
+ it('respects user-configured light/dark themes', () => {
+ expect(getShikiTheme(false, 'one-light', 'one-dark-pro').theme).toBe('one-light')
+ expect(getShikiTheme(true, 'one-light', 'one-dark-pro').theme).toBe('one-dark-pro')
+ })
+
+ it('falls back to GitHub Default when given an unknown theme id', () => {
+ expect(getShikiTheme(false, 'not-a-real-theme', 'also-fake').theme).toBe('github-light-default')
+ expect(getShikiTheme(true, 'not-a-real-theme', 'also-fake').theme).toBe('github-dark-default')
+ })
})
describe('Shiki language metadata', () => {
diff --git a/src/hooks/useSyntaxHighlight.ts b/src/hooks/useSyntaxHighlight.ts
index e08a97c4..89f31201 100644
--- a/src/hooks/useSyntaxHighlight.ts
+++ b/src/hooks/useSyntaxHighlight.ts
@@ -1,14 +1,31 @@
-import { useState, useEffect, useMemo, useRef, useId } from 'react'
+import { useState, useEffect, useMemo, useRef, useId, useSyncExternalStore } from 'react'
import type { ShikiThemeInput } from '../lib/shikiTheme'
import { getShikiTheme, useIsDarkMode } from '../lib/shikiTheme'
import { disposeShikiWorkerKey, highlightHtmlInWorker, highlightTokensInWorker } from '../lib/shikiWorkerClient'
import type { HighlightTokens } from '../lib/highlightTypes'
import { normalizeLanguage } from '../utils/languageUtils'
import { THEME_SWITCH_DISABLE_MS } from '../constants'
+import { themeStore } from '../store/themeStore'
export type { HighlightTokens } from '../lib/highlightTypes'
export type { ShikiThemeInput } from '../lib/shikiTheme'
+// ============================================
+// 代码块主题订阅(仅在 codeBlockThemeLight/Dark 变化时触发 re-render)
+// ============================================
+
+function codeBlockThemeKey(): string {
+ const s = themeStore.getState()
+ return s.codeBlockThemeLight + '|' + s.codeBlockThemeDark
+}
+
+function useCodeBlockThemes(): { light: string; dark: string } {
+ // 订阅派生字符串,避免其它 appearance 字段变化时让所有代码块重渲染
+ useSyncExternalStore(themeStore.subscribe, codeBlockThemeKey)
+ const s = themeStore.getState()
+ return { light: s.codeBlockThemeLight, dark: s.codeBlockThemeDark }
+}
+
type IdleWindowApi = {
requestIdleCallback?: (callback: () => void, options?: { timeout?: number }) => number
cancelIdleCallback?: (id: number) => void
@@ -210,11 +227,12 @@ export function useStreamingSyntaxHighlight(
const { lang = 'text', theme, enabled = true } = options
const normalizedLang = normalizeLanguage(lang)
const isDark = useIsDarkMode()
+ const codeBlockThemes = useCodeBlockThemes()
const instanceId = useId()
const resolvedTheme = useMemo(() => {
if (theme) return { theme, key: theme }
- return getShikiTheme(isDark)
- }, [theme, isDark])
+ return getShikiTheme(isDark, codeBlockThemes.light, codeBlockThemes.dark)
+ }, [theme, isDark, codeBlockThemes.light, codeBlockThemes.dark])
const [outputState, setOutputState] = useState<{ code: string; tokens: HighlightTokens } | null>(null)
const [isLoading, setIsLoading] = useState(false)
@@ -295,13 +313,14 @@ export function useSyntaxHighlight(code: string, options: HighlightOptions & { m
const normalizedLang = normalizeLanguage(lang)
const isDark = useIsDarkMode()
+ const codeBlockThemes = useCodeBlockThemes()
const resolvedTheme = useMemo(() => {
if (theme) {
return { theme, key: theme }
}
- return getShikiTheme(isDark)
- }, [theme, isDark])
+ return getShikiTheme(isDark, codeBlockThemes.light, codeBlockThemes.dark)
+ }, [theme, isDark, codeBlockThemes.light, codeBlockThemes.dark])
const cacheKey = useMemo(
() => getCacheKey(code, normalizedLang, resolvedTheme.key),
@@ -406,12 +425,13 @@ export function useSyntaxHighlightRef(
const normalizedLang = normalizeLanguage(lang)
const isDark = useIsDarkMode()
+ const codeBlockThemes = useCodeBlockThemes()
const resolvedTheme = useMemo(() => {
if (theme) {
return { theme, key: theme }
}
- return getShikiTheme(isDark)
- }, [theme, isDark])
+ return getShikiTheme(isDark, codeBlockThemes.light, codeBlockThemes.dark)
+ }, [theme, isDark, codeBlockThemes.light, codeBlockThemes.dark])
const tokensRef = useRef
(null)
const [version, setVersion] = useState(0)
diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts
index cc30fbbd..97301aa2 100644
--- a/src/hooks/useTheme.ts
+++ b/src/hooks/useTheme.ts
@@ -238,6 +238,14 @@ export function useTheme() {
themeStore.setProcessCollapseEnabled(enabled)
}, [])
+ const setCodeBlockThemeLight = useCallback((id: string) => {
+ themeStore.setCodeBlockThemeLight(id)
+ }, [])
+
+ const setCodeBlockThemeDark = useCallback((id: string) => {
+ themeStore.setCodeBlockThemeDark(id)
+ }, [])
+
return {
// 日夜模式(向后兼容)
mode: state.colorMode,
@@ -352,5 +360,11 @@ export function useTheme() {
// 过程折叠
processCollapseEnabled: state.processCollapseEnabled,
setProcessCollapseEnabled,
+
+ // 代码块主题(Shiki)
+ codeBlockThemeLight: state.codeBlockThemeLight,
+ codeBlockThemeDark: state.codeBlockThemeDark,
+ setCodeBlockThemeLight,
+ setCodeBlockThemeDark,
}
}
diff --git a/src/lib/codeBlockThemes.ts b/src/lib/codeBlockThemes.ts
new file mode 100644
index 00000000..39fb7012
--- /dev/null
+++ b/src/lib/codeBlockThemes.ts
@@ -0,0 +1,38 @@
+/**
+ * Code block (Shiki) theme catalog + helpers.
+ *
+ * `bundledThemesInfo` 来自 `shiki/themes`,包含全部 65 个内置 Shiki 主题的
+ * `{ id, displayName, type }` 元数据。Worker 端使用同源的 lazy `import` 字段
+ * 按需加载,主线程只读元数据用于下拉菜单。
+ */
+
+import { bundledThemesInfo } from 'shiki/themes'
+import type { BundledTheme } from 'shiki/themes'
+
+export type ShikiThemeType = 'light' | 'dark'
+
+export interface CodeBlockThemeInfo {
+ id: BundledTheme
+ displayName: string
+ type: ShikiThemeType
+}
+
+/** 全部 Shiki 内置主题元数据,按 displayName 字母序排序 */
+export const AVAILABLE_CODE_BLOCK_THEMES: readonly CodeBlockThemeInfo[] = bundledThemesInfo
+ .map(t => ({ id: t.id as BundledTheme, displayName: t.displayName, type: t.type as ShikiThemeType }))
+ .sort((a, b) => a.displayName.localeCompare(b.displayName))
+
+export const DEFAULT_CODE_BLOCK_THEME_LIGHT = 'github-light-default' as const
+export const DEFAULT_CODE_BLOCK_THEME_DARK = 'github-dark-default' as const
+
+const knownIds = new Set(AVAILABLE_CODE_BLOCK_THEMES.map(t => t.id))
+
+/** 校验 Shiki theme id 是否存在;不存在则回退到对应默认值 */
+export function normalizeCodeBlockTheme(id: string, fallback: BundledTheme): BundledTheme {
+ return knownIds.has(id) ? (id as BundledTheme) : fallback
+}
+
+/** 按 type 过滤(light/dark) */
+export function filterThemesByType(type: ShikiThemeType): readonly CodeBlockThemeInfo[] {
+ return AVAILABLE_CODE_BLOCK_THEMES.filter(t => t.type === type)
+}
diff --git a/src/lib/shikiTheme.ts b/src/lib/shikiTheme.ts
index aaa05fa9..f4183d0b 100644
--- a/src/lib/shikiTheme.ts
+++ b/src/lib/shikiTheme.ts
@@ -1,10 +1,25 @@
import { useState, useEffect } from 'react'
import type { BundledTheme } from 'shiki/themes'
+import {
+ DEFAULT_CODE_BLOCK_THEME_DARK,
+ DEFAULT_CODE_BLOCK_THEME_LIGHT,
+ normalizeCodeBlockTheme,
+} from './codeBlockThemes'
export type ShikiThemeInput = BundledTheme
-export function getShikiTheme(isDark: boolean): { theme: ShikiThemeInput; key: string } {
- const theme = isDark ? 'github-dark-default' : 'github-light-default'
+/**
+ * 根据 isDark + 用户在设置里选择的代码块主题解析出实际使用的 Shiki 主题。
+ * 入参为空字符串/无效值时回退到 GitHub Default。
+ */
+export function getShikiTheme(
+ isDark: boolean,
+ codeBlockThemeLight: string = DEFAULT_CODE_BLOCK_THEME_LIGHT,
+ codeBlockThemeDark: string = DEFAULT_CODE_BLOCK_THEME_DARK,
+): { theme: ShikiThemeInput; key: string } {
+ const fallback = isDark ? DEFAULT_CODE_BLOCK_THEME_DARK : DEFAULT_CODE_BLOCK_THEME_LIGHT
+ const requested = isDark ? codeBlockThemeDark : codeBlockThemeLight
+ const theme = normalizeCodeBlockTheme(requested, fallback)
return { theme, key: theme }
}
diff --git a/src/lib/shikiWorkerClient.ts b/src/lib/shikiWorkerClient.ts
index c3f734f6..f050628c 100644
--- a/src/lib/shikiWorkerClient.ts
+++ b/src/lib/shikiWorkerClient.ts
@@ -1,6 +1,11 @@
import type { BundledTheme } from 'shiki/themes'
import type { WorkerRequest, WorkerResponse, WorkerToken } from '../workers/shikiWorker'
import type { HighlightTokens } from './highlightTypes'
+import {
+ DEFAULT_CODE_BLOCK_THEME_DARK,
+ DEFAULT_CODE_BLOCK_THEME_LIGHT,
+ normalizeCodeBlockTheme,
+} from './codeBlockThemes'
type PendingRequest = {
resolve: (response: WorkerResponse) => void
@@ -73,7 +78,18 @@ export function ensureShikiWorkerReady(): Promise {
workerReadyPromiseResolve = resolve
workerReadyPromiseReject = reject
})
- getWorker().postMessage({ type: 'init', themes: ['github-dark-default', 'github-light-default'] } satisfies WorkerRequest)
+ // 预加载用户当前选择的主题;其他主题在第一次 highlight 时 lazy load。
+ // 用 localStorage 直接读避免循环依赖(themeStore 也会反向引用此模块树)。
+ const light = normalizeCodeBlockTheme(
+ typeof localStorage !== 'undefined' && localStorage.getItem('code-block-theme-light') || DEFAULT_CODE_BLOCK_THEME_LIGHT,
+ DEFAULT_CODE_BLOCK_THEME_LIGHT,
+ )
+ const dark = normalizeCodeBlockTheme(
+ typeof localStorage !== 'undefined' && localStorage.getItem('code-block-theme-dark') || DEFAULT_CODE_BLOCK_THEME_DARK,
+ DEFAULT_CODE_BLOCK_THEME_DARK,
+ )
+ const themes = Array.from(new Set([light, dark]))
+ getWorker().postMessage({ type: 'init', themes } satisfies WorkerRequest)
return workerReady
}
diff --git a/src/locales/en/settings.json b/src/locales/en/settings.json
index 83e09c9b..4a8910c6 100644
--- a/src/locales/en/settings.json
+++ b/src/locales/en/settings.json
@@ -202,6 +202,14 @@
"uiFontScaleDesc": "Adjust the font size for menus, labels, and other UI elements",
"codeFontScale": "Code Font Size",
"codeFontScaleDesc": "Adjust the font size for code blocks, diffs, and terminal",
+ "codeBlockThemes": "Code Block Theme",
+ "codeBlockThemesDesc": "Pick the syntax highlighting theme used in fenced code blocks. Light and dark are configured independently and switch with your color mode.",
+ "codeBlockThemeLight": "Light Mode Code Block",
+ "codeBlockThemeLightDesc": "Syntax theme used when the color mode resolves to light",
+ "codeBlockThemeDark": "Dark Mode Code Block",
+ "codeBlockThemeDarkDesc": "Syntax theme used when the color mode resolves to dark",
+ "codeBlockPreviewLight": "Light preview",
+ "codeBlockPreviewDark": "Dark preview",
"fontScaleReset": "Reset to default",
"codeWordWrap": "Code Word Wrap",
"codeWordWrapDesc": "Wrap code blocks and diffs to the available width to reduce horizontal scrolling",
diff --git a/src/locales/zh-CN/settings.json b/src/locales/zh-CN/settings.json
index 2153983e..0ce073b6 100644
--- a/src/locales/zh-CN/settings.json
+++ b/src/locales/zh-CN/settings.json
@@ -202,6 +202,14 @@
"uiFontScaleDesc": "调整菜单、标签等 UI 元素的字体大小",
"codeFontScale": "代码字号",
"codeFontScaleDesc": "调整代码块、差异对比和终端的字体大小",
+ "codeBlockThemes": "代码块主题",
+ "codeBlockThemesDesc": "选择 fenced 代码块使用的语法高亮主题。亮色和暗色可分别设置,并随颜色模式自动切换。",
+ "codeBlockThemeLight": "亮色模式代码块",
+ "codeBlockThemeLightDesc": "颜色模式为亮色时使用的语法主题",
+ "codeBlockThemeDark": "暗色模式代码块",
+ "codeBlockThemeDarkDesc": "颜色模式为暗色时使用的语法主题",
+ "codeBlockPreviewLight": "亮色预览",
+ "codeBlockPreviewDark": "暗色预览",
"fontScaleReset": "恢复默认",
"codeWordWrap": "代码自动换行",
"codeWordWrapDesc": "让代码块和 diff 在可用宽度内自动换行,减少横向滚动",
diff --git a/src/store/themeStore.ts b/src/store/themeStore.ts
index 18494377..b34c7921 100644
--- a/src/store/themeStore.ts
+++ b/src/store/themeStore.ts
@@ -128,6 +128,9 @@ const DEFAULT_COMPACT_INLINE_PERMISSION = false
const DEFAULT_GLASS_EFFECT = true
const DEFAULT_QUEUE_FOLLOWUP_MESSAGES = false
const DEFAULT_MANUAL_TERMINAL_TITLES = false
+/** Shiki 代码块主题:默认 GitHub,保留现有行为 */
+const DEFAULT_CODE_BLOCK_THEME_LIGHT = 'github-light-default'
+const DEFAULT_CODE_BLOCK_THEME_DARK = 'github-dark-default'
const DEFAULT_EXTERNAL_FILE_DROP_MODE: ExternalFileDropMode = 'upload-first'
const DEFAULT_OUTLINE_CURRENT_HIGHLIGHT = true
/** 连续助手消息时,仅在回合末尾显示分叉/复制按钮 */
@@ -194,6 +197,10 @@ export interface ThemeState {
desktopCollapsedInputDock: boolean
/** 过程折叠:用户发送后显示 Working 计时,结束后收成折叠块,最终回答留在外面 */
processCollapseEnabled: boolean
+ /** 代码块语法高亮主题(亮色模式),Shiki theme id */
+ codeBlockThemeLight: string
+ /** 代码块语法高亮主题(暗色模式),Shiki theme id */
+ codeBlockThemeDark: string
}
export type ThemeBackup = ThemeState
@@ -230,6 +237,8 @@ const STORAGE_KEY_OUTLINE_CURRENT_HIGHLIGHT = 'outline-current-highlight'
const STORAGE_KEY_ACTIONS_ON_LATEST_ASSISTANT_ONLY = 'actions-on-latest-assistant-only'
const STORAGE_KEY_DESKTOP_COLLAPSED_INPUT_DOCK = 'desktop-collapsed-input-dock'
const STORAGE_KEY_PROCESS_COLLAPSE_ENABLED = 'process-collapse-enabled'
+const STORAGE_KEY_CODE_BLOCK_THEME_LIGHT = 'code-block-theme-light'
+const STORAGE_KEY_CODE_BLOCK_THEME_DARK = 'code-block-theme-dark'
// ============================================
// DOM Style Element IDs
@@ -377,6 +386,11 @@ class ThemeStore {
? DEFAULT_PROCESS_COLLAPSE_ENABLED
: savedProcessCollapseEnabled === 'true'
+ const savedCodeBlockThemeLight = localStorage.getItem(STORAGE_KEY_CODE_BLOCK_THEME_LIGHT)
+ const codeBlockThemeLight = savedCodeBlockThemeLight || DEFAULT_CODE_BLOCK_THEME_LIGHT
+ const savedCodeBlockThemeDark = localStorage.getItem(STORAGE_KEY_CODE_BLOCK_THEME_DARK)
+ const codeBlockThemeDark = savedCodeBlockThemeDark || DEFAULT_CODE_BLOCK_THEME_DARK
+
this.state = {
presetId: normalizedPreset,
colorMode: savedMode,
@@ -406,6 +420,8 @@ class ThemeStore {
actionsOnLatestAssistantOnly,
desktopCollapsedInputDock,
processCollapseEnabled,
+ codeBlockThemeLight,
+ codeBlockThemeDark,
}
}
@@ -503,6 +519,14 @@ class ThemeStore {
return this.state.processCollapseEnabled
}
+ get codeBlockThemeLight() {
+ return this.state.codeBlockThemeLight
+ }
+
+ get codeBlockThemeDark() {
+ return this.state.codeBlockThemeDark
+ }
+
/** 获取当前主题预设(内置主题返回对象,自定义返回 undefined) */
getPreset(): ThemePreset | undefined {
return getThemePreset(this.state.presetId)
@@ -804,6 +828,20 @@ class ThemeStore {
this.emit()
}
+ setCodeBlockThemeLight(id: string) {
+ if (this.state.codeBlockThemeLight === id) return
+ this.state = { ...this.state, codeBlockThemeLight: id }
+ localStorage.setItem(STORAGE_KEY_CODE_BLOCK_THEME_LIGHT, id)
+ this.emit()
+ }
+
+ setCodeBlockThemeDark(id: string) {
+ if (this.state.codeBlockThemeDark === id) return
+ this.state = { ...this.state, codeBlockThemeDark: id }
+ localStorage.setItem(STORAGE_KEY_CODE_BLOCK_THEME_DARK, id)
+ this.emit()
+ }
+
// ---- Theme Application ----
/** 初始化:应用当前主题到 DOM */
@@ -1063,6 +1101,14 @@ function normalizeThemeBackup(raw: unknown): ThemeBackup {
typeof parsed?.processCollapseEnabled === 'boolean'
? parsed.processCollapseEnabled
: DEFAULT_PROCESS_COLLAPSE_ENABLED,
+ codeBlockThemeLight:
+ typeof parsed?.codeBlockThemeLight === 'string' && parsed.codeBlockThemeLight
+ ? parsed.codeBlockThemeLight
+ : DEFAULT_CODE_BLOCK_THEME_LIGHT,
+ codeBlockThemeDark:
+ typeof parsed?.codeBlockThemeDark === 'string' && parsed.codeBlockThemeDark
+ ? parsed.codeBlockThemeDark
+ : DEFAULT_CODE_BLOCK_THEME_DARK,
}
}
@@ -1112,4 +1158,6 @@ export function importThemeBackup(raw: unknown): void {
)
localStorage.setItem(STORAGE_KEY_DESKTOP_COLLAPSED_INPUT_DOCK, String(backup.desktopCollapsedInputDock))
localStorage.setItem(STORAGE_KEY_PROCESS_COLLAPSE_ENABLED, String(backup.processCollapseEnabled))
+ localStorage.setItem(STORAGE_KEY_CODE_BLOCK_THEME_LIGHT, backup.codeBlockThemeLight)
+ localStorage.setItem(STORAGE_KEY_CODE_BLOCK_THEME_DARK, backup.codeBlockThemeDark)
}
diff --git a/src/workers/shikiWorker.ts b/src/workers/shikiWorker.ts
index 751957e9..67236ad4 100644
--- a/src/workers/shikiWorker.ts
+++ b/src/workers/shikiWorker.ts
@@ -9,7 +9,7 @@ import {
import { createOnigurumaEngine } from 'shiki/engine/oniguruma'
import onigWasmUrl from 'shiki/onig.wasm?url'
import { bundledLanguagesAlias, bundledLanguagesBase } from 'shiki/langs'
-import type { BundledTheme } from 'shiki/themes'
+import { bundledThemesInfo, type BundledTheme } from 'shiki/themes'
export type WorkerToken = [content: string, color: string]
@@ -82,6 +82,38 @@ async function ensureLang(instance: HighlighterCore, lang: string): Promise")` 静态字面量),让 Vite 为每个主题
+ * 生成独立 chunk,按需 lazy-load。预加载过的主题记录在 loadedThemes 里。
+ */
+const themeImporters = new Map Promise<{ default: unknown }>>(
+ bundledThemesInfo.map(t => [t.id, t.import as () => Promise<{ default: unknown }>]),
+)
+const loadedThemes = new Set()
+const pendingThemeLoads = new Map>()
+
+async function ensureTheme(instance: HighlighterCore, theme: string): Promise {
+ if (loadedThemes.has(theme)) return
+ const existing = pendingThemeLoads.get(theme)
+ if (existing) return existing
+
+ const importer = themeImporters.get(theme)
+ if (!importer) throw new Error(`Unknown Shiki theme: ${theme}`)
+
+ const promise = (async () => {
+ const mod = await importer()
+ await instance.loadTheme(mod.default as Parameters[0])
+ loadedThemes.add(theme)
+ })()
+ pendingThemeLoads.set(theme, promise)
+ try {
+ await promise
+ } finally {
+ pendingThemeLoads.delete(theme)
+ }
+}
+
function toWorkerToken(value: ThemedToken): WorkerToken {
return [value.content, value.color ?? '']
}
@@ -104,6 +136,9 @@ async function highlight(request: Extract)
const instance = await highlighter
if (!instance) throw new Error('Shiki worker not initialized')
+ // 主题按需加载(init 时只预加载了用户当前选择;切换主题后第一次 highlight 触发 lazy load)
+ await ensureTheme(instance, request.theme)
+
const requestedLanguage = request.language.toLowerCase()
const language = plainLanguages.has(requestedLanguage) || findLangLoader(requestedLanguage) ? requestedLanguage : 'text'
const isPlainText = plainLanguages.has(language)
@@ -216,13 +251,26 @@ const themeLoaders: Record Promise> = {
self.onmessage = (event: MessageEvent) => {
const msg = event.data
if (msg.type === 'init') {
+ // 用 bundledThemesInfo 解析 init 传入的主题 id(来自用户当前选择),用静态字面量
+ // import 让 Vite 把它们打成独立 chunk。未知 id 回退到 github-dark-default。
+ const resolvedThemeSpecs = msg.themes.map(t => {
+ const info = bundledThemesInfo.find(b => b.id === t)
+ return info ? info.import : themeLoaders['github-dark-default']!
+ })
highlighter ??= createHighlighterCore({
engine: createOnigurumaEngine(loadOnigWasm),
- themes: msg.themes.map(t => themeLoaders[t]?.() ?? themeLoaders['github-dark-default']!()) as Parameters[0]['themes'],
+ themes: resolvedThemeSpecs as Parameters[0]['themes'],
langs: [],
})
void highlighter
- .then(() => post({ type: 'ready' }))
+ .then(async instance => {
+ // 标记 init 预加载的主题为已加载,避免重复 ensureTheme
+ msg.themes.forEach(t => {
+ if (bundledThemesInfo.some(b => b.id === t)) loadedThemes.add(t)
+ })
+ await instance
+ post({ type: 'ready' })
+ })
.catch(error => post({ type: 'init-error', message: error instanceof Error ? error.message : String(error) }))
return
}