mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 22:39:36 +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 { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { CodeCacheService } from '@renderer/services/CodeCacheService'
|
import { CodeCacheService } from '@renderer/services/CodeCacheService'
|
||||||
import { type CodeStyleVarious, ThemeMode } from '@renderer/types'
|
import { type CodeStyleVarious, ThemeMode } from '@renderer/types'
|
||||||
|
import { getHighlighter, loadLanguageIfNeeded, loadThemeIfNeeded } from '@renderer/utils/highlighter'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import { createContext, type PropsWithChildren, use, useCallback, useMemo } from 'react'
|
import { createContext, type PropsWithChildren, use, useCallback, useMemo } from 'react'
|
||||||
import { bundledLanguages, bundledThemes, createHighlighter, type Highlighter } from 'shiki'
|
import { bundledThemes } 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
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SyntaxHighlighterContextType {
|
interface SyntaxHighlighterContextType {
|
||||||
codeToHtml: (code: string, language: string, enableCache: boolean) => Promise<string>
|
codeToHtml: (code: string, language: string, enableCache: boolean) => Promise<string>
|
||||||
@ -60,19 +48,8 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
|
|||||||
try {
|
try {
|
||||||
const highlighter = await getHighlighter()
|
const highlighter = await getHighlighter()
|
||||||
|
|
||||||
if (!highlighter.getLoadedThemes().includes(highlighterTheme)) {
|
await loadThemeIfNeeded(highlighter, highlighterTheme)
|
||||||
const themeImportFn = bundledThemes[highlighterTheme]
|
await loadLanguageIfNeeded(highlighter, mappedLanguage)
|
||||||
if (themeImportFn) {
|
|
||||||
await highlighter.loadTheme(await themeImportFn())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!highlighter.getLoadedLanguages().includes(mappedLanguage)) {
|
|
||||||
const languageImportFn = bundledLanguages[mappedLanguage]
|
|
||||||
if (languageImportFn) {
|
|
||||||
await highlighter.loadLanguage(await languageImportFn())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成高亮HTML
|
// 生成高亮HTML
|
||||||
const html = highlighter.codeToHtml(code, {
|
const html = highlighter.codeToHtml(code, {
|
||||||
|
|||||||
@ -39,7 +39,6 @@ const MessageTools: FC<Props> = ({ blocks }) => {
|
|||||||
return 'Invalid Result'
|
return 'Invalid Result'
|
||||||
}
|
}
|
||||||
}, [toolResponse])
|
}, [toolResponse])
|
||||||
const { renderedMarkdown: styledResult } = useShikiWithMarkdownIt(`\`\`\`json\n${resultString}\n\`\`\``)
|
|
||||||
|
|
||||||
if (!toolResponse) {
|
if (!toolResponse) {
|
||||||
return null
|
return null
|
||||||
@ -59,8 +58,6 @@ const MessageTools: FC<Props> = ({ blocks }) => {
|
|||||||
// Format tool responses for collapse items
|
// Format tool responses for collapse items
|
||||||
const getCollapseItems = () => {
|
const getCollapseItems = () => {
|
||||||
const items: { key: string; label: React.ReactNode; children: React.ReactNode }[] = []
|
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 { id, tool, status, response } = toolResponse
|
||||||
const isInvoking = status === 'invoking'
|
const isInvoking = status === 'invoking'
|
||||||
const isDone = status === 'done'
|
const isDone = status === 'done'
|
||||||
@ -123,11 +120,10 @@ const MessageTools: FC<Props> = ({ blocks }) => {
|
|||||||
),
|
),
|
||||||
children: isDone && result && (
|
children: isDone && result && (
|
||||||
<ToolResponseContainer style={{ fontFamily, fontSize: '12px' }}>
|
<ToolResponseContainer style={{ fontFamily, fontSize: '12px' }}>
|
||||||
<div className="markdown" dangerouslySetInnerHTML={{ __html: styledResult }} />
|
<CollapsedContent isExpanded={activeKeys.includes(id)} resultString={resultString} />
|
||||||
</ToolResponseContainer>
|
</ToolResponseContainer>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
// }
|
|
||||||
|
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
@ -140,7 +136,6 @@ const MessageTools: FC<Props> = ({ blocks }) => {
|
|||||||
switch (parsedResult.content[0]?.type) {
|
switch (parsedResult.content[0]?.type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
return <PreviewBlock>{parsedResult.content[0].text}</PreviewBlock>
|
return <PreviewBlock>{parsedResult.content[0].text}</PreviewBlock>
|
||||||
// TODO: support other types
|
|
||||||
default:
|
default:
|
||||||
return <PreviewBlock>{content}</PreviewBlock>
|
return <PreviewBlock>{content}</PreviewBlock>
|
||||||
}
|
}
|
||||||
@ -174,7 +169,6 @@ const MessageTools: FC<Props> = ({ blocks }) => {
|
|||||||
styles={{ body: { maxHeight: '80vh', overflow: 'auto' } }}>
|
styles={{ body: { maxHeight: '80vh', overflow: 'auto' } }}>
|
||||||
{expandedResponse && (
|
{expandedResponse && (
|
||||||
<ExpandedResponseContainer style={{ fontFamily, fontSize }}>
|
<ExpandedResponseContainer style={{ fontFamily, fontSize }}>
|
||||||
{/* mode swtich tabs */}
|
|
||||||
<Tabs
|
<Tabs
|
||||||
tabBarExtraContent={
|
tabBarExtraContent={
|
||||||
<ActionButton
|
<ActionButton
|
||||||
@ -200,7 +194,16 @@ const MessageTools: FC<Props> = ({ blocks }) => {
|
|||||||
{
|
{
|
||||||
key: 'raw',
|
key: 'raw',
|
||||||
label: t('message.tools.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)`
|
const CollapseContainer = styled(Collapse)`
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { runAsyncFunction } from '@renderer/utils'
|
||||||
import { getShikiInstance } from '@renderer/utils/shiki'
|
import { getShikiInstance } from '@renderer/utils/shiki'
|
||||||
import { Card } from 'antd'
|
import { Card } from 'antd'
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
@ -30,9 +31,11 @@ const MCPDescription = ({ searchKey }: McpDescriptionProps) => {
|
|||||||
}, [md, searchKey])
|
}, [md, searchKey])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sk = getShikiInstance(theme)
|
runAsyncFunction(async () => {
|
||||||
md.current.use(sk)
|
const sk = await getShikiInstance(theme)
|
||||||
getMcpInfo()
|
md.current.use(sk)
|
||||||
|
getMcpInfo()
|
||||||
|
})
|
||||||
}, [getMcpInfo, theme])
|
}, [getMcpInfo, theme])
|
||||||
|
|
||||||
return (
|
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 { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { ThemeMode } from '@renderer/types'
|
import { ThemeMode } from '@renderer/types'
|
||||||
import { MarkdownItShikiOptions, setupMarkdownIt } from '@shikijs/markdown-it'
|
import { setupMarkdownIt } from '@shikijs/markdown-it'
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { BuiltinLanguage, BuiltinTheme, bundledLanguages, createHighlighter } from 'shiki'
|
|
||||||
|
import { runAsyncFunction } from '.'
|
||||||
|
import { getHighlighter } from './highlighter'
|
||||||
|
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
themes: {
|
themes: {
|
||||||
@ -13,19 +15,9 @@ const defaultOptions = {
|
|||||||
defaultColor: 'light'
|
defaultColor: 'light'
|
||||||
}
|
}
|
||||||
|
|
||||||
const initHighlighter = async (options: MarkdownItShikiOptions) => {
|
export async function getShikiInstance(theme: ThemeMode) {
|
||||||
const themeNames = ('themes' in options ? Object.values(options.themes) : [options.theme]).filter(
|
const highlighter = await getHighlighter()
|
||||||
Boolean
|
|
||||||
) as BuiltinTheme[]
|
|
||||||
return await createHighlighter({
|
|
||||||
themes: themeNames,
|
|
||||||
langs: options.langs || (Object.keys(bundledLanguages) as BuiltinLanguage[])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const highlighter = await initHighlighter(defaultOptions)
|
|
||||||
|
|
||||||
export function getShikiInstance(theme: ThemeMode) {
|
|
||||||
const options = {
|
const options = {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
defaultColor: theme
|
defaultColor: theme
|
||||||
@ -46,9 +38,11 @@ export function useShikiWithMarkdownIt(content: string) {
|
|||||||
)
|
)
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sk = getShikiInstance(theme)
|
runAsyncFunction(async () => {
|
||||||
md.current.use(sk)
|
const sk = await getShikiInstance(theme)
|
||||||
setRenderedMarkdown(md.current.render(content))
|
md.current.use(sk)
|
||||||
|
setRenderedMarkdown(md.current.render(content))
|
||||||
|
})
|
||||||
}, [content, theme])
|
}, [content, theme])
|
||||||
return {
|
return {
|
||||||
renderedMarkdown
|
renderedMarkdown
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user