mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
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:
parent
e91a449b82
commit
8a54ebc183
@ -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, {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 (
|
||||
|
||||
32
src/renderer/src/utils/highlighter.ts
Normal file
32
src/renderer/src/utils/highlighter.ts
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user