refactor(SyntaxHighlighter): modularize highlighter logic and improve theme/language loading

* Moved highlighter initialization and loading functions to a new utility file for better organization.
* Simplified theme and language loading in the SyntaxHighlighterProvider using the new utility functions.
* Removed redundant code and improved readability in MessageTools by introducing a new CollapsedContent component for rendering tool responses.
* Updated MCPDescription to use an async function for Shiki instance initialization.
This commit is contained in:
kangfenmao 2025-05-18 22:52:03 +08:00 committed by 亢奋猫
parent e91a449b82
commit 8a54ebc183
5 changed files with 77 additions and 55 deletions

View File

@ -3,22 +3,10 @@ import { useMermaid } from '@renderer/hooks/useMermaid'
import { useSettings } from '@renderer/hooks/useSettings'
import { CodeCacheService } from '@renderer/services/CodeCacheService'
import { type CodeStyleVarious, ThemeMode } from '@renderer/types'
import { getHighlighter, loadLanguageIfNeeded, loadThemeIfNeeded } from '@renderer/utils/highlighter'
import type React from 'react'
import { createContext, type PropsWithChildren, use, useCallback, useMemo } from 'react'
import { bundledLanguages, bundledThemes, createHighlighter, type Highlighter } from 'shiki'
let highlighterPromise: Promise<Highlighter> | null = null
async function getHighlighter() {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
langs: ['javascript', 'typescript', 'python', 'java', 'markdown'],
themes: ['one-light', 'material-theme-darker']
})
}
return await highlighterPromise
}
import { bundledThemes } from 'shiki'
interface SyntaxHighlighterContextType {
codeToHtml: (code: string, language: string, enableCache: boolean) => Promise<string>
@ -60,19 +48,8 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
try {
const highlighter = await getHighlighter()
if (!highlighter.getLoadedThemes().includes(highlighterTheme)) {
const themeImportFn = bundledThemes[highlighterTheme]
if (themeImportFn) {
await highlighter.loadTheme(await themeImportFn())
}
}
if (!highlighter.getLoadedLanguages().includes(mappedLanguage)) {
const languageImportFn = bundledLanguages[mappedLanguage]
if (languageImportFn) {
await highlighter.loadLanguage(await languageImportFn())
}
}
await loadThemeIfNeeded(highlighter, highlighterTheme)
await loadLanguageIfNeeded(highlighter, mappedLanguage)
// 生成高亮HTML
const html = highlighter.codeToHtml(code, {

View File

@ -39,7 +39,6 @@ const MessageTools: FC<Props> = ({ blocks }) => {
return 'Invalid Result'
}
}, [toolResponse])
const { renderedMarkdown: styledResult } = useShikiWithMarkdownIt(`\`\`\`json\n${resultString}\n\`\`\``)
if (!toolResponse) {
return null
@ -59,8 +58,6 @@ const MessageTools: FC<Props> = ({ blocks }) => {
// Format tool responses for collapse items
const getCollapseItems = () => {
const items: { key: string; label: React.ReactNode; children: React.ReactNode }[] = []
// Add tool responses
// for (const toolResponse of toolResponses) {
const { id, tool, status, response } = toolResponse
const isInvoking = status === 'invoking'
const isDone = status === 'done'
@ -123,11 +120,10 @@ const MessageTools: FC<Props> = ({ blocks }) => {
),
children: isDone && result && (
<ToolResponseContainer style={{ fontFamily, fontSize: '12px' }}>
<div className="markdown" dangerouslySetInnerHTML={{ __html: styledResult }} />
<CollapsedContent isExpanded={activeKeys.includes(id)} resultString={resultString} />
</ToolResponseContainer>
)
})
// }
return items
}
@ -140,7 +136,6 @@ const MessageTools: FC<Props> = ({ blocks }) => {
switch (parsedResult.content[0]?.type) {
case 'text':
return <PreviewBlock>{parsedResult.content[0].text}</PreviewBlock>
// TODO: support other types
default:
return <PreviewBlock>{content}</PreviewBlock>
}
@ -174,7 +169,6 @@ const MessageTools: FC<Props> = ({ blocks }) => {
styles={{ body: { maxHeight: '80vh', overflow: 'auto' } }}>
{expandedResponse && (
<ExpandedResponseContainer style={{ fontFamily, fontSize }}>
{/* mode swtich tabs */}
<Tabs
tabBarExtraContent={
<ActionButton
@ -200,7 +194,16 @@ const MessageTools: FC<Props> = ({ blocks }) => {
{
key: 'raw',
label: t('message.tools.raw'),
children: <div className="markdown" dangerouslySetInnerHTML={{ __html: styledResult }} />
children: (
<CollapsedContent
isExpanded={true}
resultString={
typeof expandedResponse.content === 'string'
? expandedResponse.content
: JSON.stringify(expandedResponse.content, null, 2)
}
/>
)
}
]}
/>
@ -211,6 +214,19 @@ const MessageTools: FC<Props> = ({ blocks }) => {
)
}
// New component to handle collapsed content
const CollapsedContent: FC<{ isExpanded: boolean; resultString: string }> = ({ isExpanded, resultString }) => {
const { renderedMarkdown: styledResult } = useShikiWithMarkdownIt(
isExpanded ? `\`\`\`json\n${resultString}\n\`\`\`` : ''
)
if (!isExpanded) {
return null
}
return <div className="markdown" dangerouslySetInnerHTML={{ __html: styledResult }} />
}
const CollapseContainer = styled(Collapse)`
margin-top: 10px;
margin-bottom: 12px;

View File

@ -1,4 +1,5 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { runAsyncFunction } from '@renderer/utils'
import { getShikiInstance } from '@renderer/utils/shiki'
import { Card } from 'antd'
import MarkdownIt from 'markdown-it'
@ -30,9 +31,11 @@ const MCPDescription = ({ searchKey }: McpDescriptionProps) => {
}, [md, searchKey])
useEffect(() => {
const sk = getShikiInstance(theme)
md.current.use(sk)
getMcpInfo()
runAsyncFunction(async () => {
const sk = await getShikiInstance(theme)
md.current.use(sk)
getMcpInfo()
})
}, [getMcpInfo, theme])
return (

View File

@ -0,0 +1,32 @@
import { bundledLanguages, bundledThemes, createHighlighter, type Highlighter } from 'shiki'
let highlighterPromise: Promise<Highlighter> | null = null
export async function getHighlighter() {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
langs: ['javascript', 'typescript', 'python', 'java', 'markdown', 'json'],
themes: ['one-light', 'material-theme-darker']
})
}
return await highlighterPromise
}
export async function loadLanguageIfNeeded(highlighter: Highlighter, language: string) {
if (!highlighter.getLoadedLanguages().includes(language)) {
const languageImportFn = bundledLanguages[language]
if (languageImportFn) {
await highlighter.loadLanguage(await languageImportFn())
}
}
}
export async function loadThemeIfNeeded(highlighter: Highlighter, theme: string) {
if (!highlighter.getLoadedThemes().includes(theme)) {
const themeImportFn = bundledThemes[theme]
if (themeImportFn) {
await highlighter.loadTheme(await themeImportFn())
}
}
}

View File

@ -1,9 +1,11 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { ThemeMode } from '@renderer/types'
import { MarkdownItShikiOptions, setupMarkdownIt } from '@shikijs/markdown-it'
import { setupMarkdownIt } from '@shikijs/markdown-it'
import MarkdownIt from 'markdown-it'
import { useEffect, useRef, useState } from 'react'
import { BuiltinLanguage, BuiltinTheme, bundledLanguages, createHighlighter } from 'shiki'
import { runAsyncFunction } from '.'
import { getHighlighter } from './highlighter'
const defaultOptions = {
themes: {
@ -13,19 +15,9 @@ const defaultOptions = {
defaultColor: 'light'
}
const initHighlighter = async (options: MarkdownItShikiOptions) => {
const themeNames = ('themes' in options ? Object.values(options.themes) : [options.theme]).filter(
Boolean
) as BuiltinTheme[]
return await createHighlighter({
themes: themeNames,
langs: options.langs || (Object.keys(bundledLanguages) as BuiltinLanguage[])
})
}
export async function getShikiInstance(theme: ThemeMode) {
const highlighter = await getHighlighter()
const highlighter = await initHighlighter(defaultOptions)
export function getShikiInstance(theme: ThemeMode) {
const options = {
...defaultOptions,
defaultColor: theme
@ -46,9 +38,11 @@ export function useShikiWithMarkdownIt(content: string) {
)
const { theme } = useTheme()
useEffect(() => {
const sk = getShikiInstance(theme)
md.current.use(sk)
setRenderedMarkdown(md.current.render(content))
runAsyncFunction(async () => {
const sk = await getShikiInstance(theme)
md.current.use(sk)
setRenderedMarkdown(md.current.render(content))
})
}, [content, theme])
return {
renderedMarkdown