feat(TranslatePage): replace ReactMarkdown with MarkdownIt. (#7545)

* feat(TranslatePage): replace ReactMarkdown with MarkdownIt.

* fix: line wrapping in plain text and shiki code block

---------

Co-authored-by: one <wangan.cs@gmail.com>
This commit is contained in:
Xin Rui 2025-07-01 01:42:25 +08:00 committed by GitHub
parent ad0b10c517
commit acbe8c7605
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 56 additions and 10 deletions

View File

@ -4,6 +4,7 @@ import CopyIcon from '@renderer/components/Icons/CopyIcon'
import { HStack } from '@renderer/components/Layout'
import { isEmbeddingModel } from '@renderer/config/models'
import { translateLanguageOptions } from '@renderer/config/translate'
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import db from '@renderer/databases'
import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { useProviders } from '@renderer/hooks/useProvider'
@ -26,7 +27,6 @@ import { find, isEmpty, sortBy } from 'lodash'
import { HelpCircle, Settings2, TriangleAlert } from 'lucide-react'
import { FC, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
import styled from 'styled-components'
let _text = ''
@ -216,9 +216,11 @@ const TranslateSettings: FC<{
const TranslatePage: FC = () => {
const { t } = useTranslation()
const { shikiMarkdownIt } = useCodeStyle()
const [targetLanguage, setTargetLanguage] = useState(_targetLanguage)
const [text, setText] = useState(_text)
const [result, setResult] = useState(_result)
const [renderedMarkdown, setRenderedMarkdown] = useState<string>('')
const { translateModel, setTranslateModel } = useDefaultModel()
const [loading, setLoading] = useState(false)
const [copied, setCopied] = useState(false)
@ -379,6 +381,24 @@ const TranslatePage: FC = () => {
isEmpty(text) && setResult('')
}, [text])
// Render markdown content when result or enableMarkdown changes
useEffect(() => {
if (enableMarkdown && result) {
let isMounted = true
shikiMarkdownIt(result).then((rendered) => {
if (isMounted) {
setRenderedMarkdown(rendered)
}
})
return () => {
isMounted = false
}
} else {
setRenderedMarkdown('')
return undefined
}
}, [result, enableMarkdown, shikiMarkdownIt])
useEffect(() => {
runAsyncFunction(async () => {
const targetLang = await db.settings.get({ id: 'translate:target:language' })
@ -607,13 +627,13 @@ const TranslatePage: FC = () => {
/>
</OperationBar>
<OutputText ref={outputTextRef} onScroll={handleOutputScroll} className="selectable">
<OutputText ref={outputTextRef} onScroll={handleOutputScroll} className={'selectable'}>
{!result ? (
t('translate.output.placeholder')
) : enableMarkdown ? (
<ReactMarkdown>{result}</ReactMarkdown>
<div className="markdown" dangerouslySetInnerHTML={{ __html: renderedMarkdown }} />
) : (
result
<div className="plain">{result}</div>
)}
</OutputText>
</OutputContainer>
@ -703,7 +723,19 @@ const OutputText = styled.div`
flex: 1;
padding: 5px 16px;
overflow-y: auto;
white-space: pre-wrap;
.plain {
white-space: pre-wrap;
overflow-wrap: break-word;
}
.markdown {
/* for shiki code block overflow */
.line * {
white-space: pre-wrap;
overflow-wrap: break-word;
}
}
`
const TranslateButton = styled(Button)``

View File

@ -149,13 +149,27 @@ export async function getMarkdownIt(theme: string, markdown: string) {
const md = await mdInitializer.get()
const { fromHighlighter } = await import('@shikijs/markdown-it/core')
let actualTheme = theme
try {
actualTheme = await loadThemeIfNeeded(highlighter, theme)
} catch (error) {
console.debug(`Failed to load theme '${theme}', using 'one-light' as fallback:`, error)
actualTheme = 'one-light'
}
const themes: Record<string, string> = {
'one-light': 'one-light',
'material-theme-darker': 'material-theme-darker'
}
if (actualTheme !== 'one-light' && actualTheme !== 'material-theme-darker') {
themes[actualTheme] = actualTheme
}
md.use(
fromHighlighter(highlighter, {
themes: {
'one-light': 'one-light',
'material-theme-darker': 'material-theme-darker'
},
defaultColor: theme,
themes,
defaultColor: actualTheme,
defaultLanguage: 'json',
fallbackLanguage: 'json'
})