feat(Markdown): support disabling single dollar math (#9131)

* feat(Markdown): support disabling single dollar math

* fix: lint error
This commit is contained in:
one 2025-08-13 16:14:59 +08:00 committed by GitHub
parent ceef19e55b
commit 4cda5f1787
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 114 additions and 44 deletions

View File

@ -2715,6 +2715,17 @@
"title": "Launch", "title": "Launch",
"totray": "Minimize to Tray on Launch" "totray": "Minimize to Tray on Launch"
}, },
"math": {
"engine": {
"label": "Math engine",
"none": "None"
},
"single_dollar": {
"label": "Enable $...$",
"tip": "Render math equations quoted by single dollar signs $...$. Default is enabled."
},
"title": "Math Settings"
},
"mcp": { "mcp": {
"actions": "Actions", "actions": "Actions",
"active": "Active", "active": "Active",
@ -2945,10 +2956,6 @@
"title": "Input Settings" "title": "Input Settings"
}, },
"markdown_rendering_input_message": "Markdown render input message", "markdown_rendering_input_message": "Markdown render input message",
"math_engine": {
"label": "Math engine",
"none": "None"
},
"metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec", "metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
"model": { "model": {
"title": "Model Settings" "title": "Model Settings"

View File

@ -2715,6 +2715,17 @@
"title": "起動", "title": "起動",
"totray": "起動時にトレイに最小化" "totray": "起動時にトレイに最小化"
}, },
"math": {
"engine": {
"label": "数式エンジン",
"none": "なし"
},
"single_dollar": {
"label": "$...$ を有効にする",
"tip": "単一のドル記号 $...$ で囲まれた数式をレンダリングします。デフォルトで有効です。"
},
"title": "数式設定"
},
"mcp": { "mcp": {
"actions": "操作", "actions": "操作",
"active": "有効", "active": "有効",
@ -2945,10 +2956,6 @@
"title": "入力設定" "title": "入力設定"
}, },
"markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング", "markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング",
"math_engine": {
"label": "数式エンジン",
"none": "なし"
},
"metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec", "metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",
"model": { "model": {
"title": "モデル設定" "title": "モデル設定"

View File

@ -2715,6 +2715,17 @@
"title": "Запуск", "title": "Запуск",
"totray": "Свернуть в трей при запуске" "totray": "Свернуть в трей при запуске"
}, },
"math": {
"engine": {
"label": "Математический движок",
"none": "Нет"
},
"single_dollar": {
"label": "Включить $...$",
"tip": "Отображать математические формулы, заключенные в одиночные символы доллара $...$. По умолчанию включено."
},
"title": "Настройки математических формул"
},
"mcp": { "mcp": {
"actions": "Действия", "actions": "Действия",
"active": "Активен", "active": "Активен",
@ -2945,10 +2956,6 @@
"title": "Настройки ввода" "title": "Настройки ввода"
}, },
"markdown_rendering_input_message": "Отображение ввода в формате Markdown", "markdown_rendering_input_message": "Отображение ввода в формате Markdown",
"math_engine": {
"label": "Математический движок",
"none": "Нет"
},
"metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec", "metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",
"model": { "model": {
"title": "Настройки модели" "title": "Настройки модели"

View File

@ -2715,6 +2715,17 @@
"title": "启动", "title": "启动",
"totray": "启动时最小化到托盘" "totray": "启动时最小化到托盘"
}, },
"math": {
"engine": {
"label": "数学公式引擎",
"none": "无"
},
"single_dollar": {
"label": "启用 $...$",
"tip": "渲染单个美元符号 $...$ 包裹的数学公式,默认启用。"
},
"title": "数学公式设置"
},
"mcp": { "mcp": {
"actions": "操作", "actions": "操作",
"active": "启用", "active": "启用",
@ -2945,10 +2956,6 @@
"title": "输入设置" "title": "输入设置"
}, },
"markdown_rendering_input_message": "Markdown 渲染输入消息", "markdown_rendering_input_message": "Markdown 渲染输入消息",
"math_engine": {
"label": "数学公式引擎",
"none": "无"
},
"metrics": "首字时延 {{time_first_token_millsec}} ms | 每秒 {{token_speed}} tokens", "metrics": "首字时延 {{time_first_token_millsec}} ms | 每秒 {{token_speed}} tokens",
"model": { "model": {
"title": "模型设置" "title": "模型设置"

View File

@ -2715,6 +2715,17 @@
"title": "啟動", "title": "啟動",
"totray": "啟動時最小化到系统匣" "totray": "啟動時最小化到系统匣"
}, },
"math": {
"engine": {
"label": "數學公式引擎",
"none": "無"
},
"single_dollar": {
"label": "啟用 $...$",
"tip": "渲染單個美元符號 $...$ 包裹的數學公式,默認啟用。"
},
"title": "數學公式設定"
},
"mcp": { "mcp": {
"actions": "操作", "actions": "操作",
"active": "啟用", "active": "啟用",
@ -2945,10 +2956,6 @@
"title": "輸入設定" "title": "輸入設定"
}, },
"markdown_rendering_input_message": "Markdown 渲染輸入訊息", "markdown_rendering_input_message": "Markdown 渲染輸入訊息",
"math_engine": {
"label": "數學公式引擎",
"none": "無"
},
"metrics": "首字延遲 {{time_first_token_millsec}} ms | 每秒 {{token_speed}} tokens", "metrics": "首字延遲 {{time_first_token_millsec}} ms | 每秒 {{token_speed}} tokens",
"model": { "model": {
"title": "模型設定" "title": "模型設定"

View File

@ -46,7 +46,7 @@ interface Props {
const Markdown: FC<Props> = ({ block, postProcess }) => { const Markdown: FC<Props> = ({ block, postProcess }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { mathEngine } = useSettings() const { mathEngine, mathEnableSingleDollar } = useSettings()
const isTrulyDone = 'status' in block && block.status === 'success' const isTrulyDone = 'status' in block && block.status === 'success'
const [displayedContent, setDisplayedContent] = useState(postProcess ? postProcess(block.content) : block.content) const [displayedContent, setDisplayedContent] = useState(postProcess ? postProcess(block.content) : block.content)
@ -98,10 +98,10 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
remarkDisableConstructs(['codeIndented']) remarkDisableConstructs(['codeIndented'])
] ]
if (mathEngine !== 'none') { if (mathEngine !== 'none') {
plugins.push(remarkMath) plugins.push([remarkMath, { singleDollarTextMath: mathEnableSingleDollar }])
} }
return plugins return plugins
}, [mathEngine]) }, [mathEngine, mathEnableSingleDollar])
const messageContent = useMemo(() => { const messageContent = useMemo(() => {
if ('status' in block && block.status === 'paused' && isEmpty(block.content)) { if ('status' in block && block.status === 'paused' && isEmpty(block.content)) {

View File

@ -144,7 +144,7 @@ describe('Markdown', () => {
vi.clearAllMocks() vi.clearAllMocks()
// Default settings // Default settings
mockUseSettings.mockReturnValue({ mathEngine: 'KaTeX' }) mockUseSettings.mockReturnValue({ mathEngine: 'KaTeX', mathEnableSingleDollar: true })
mockUseTranslation.mockReturnValue({ mockUseTranslation.mockReturnValue({
t: (key: string) => (key === 'message.chat.completion.paused' ? 'Paused' : key) t: (key: string) => (key === 'message.chat.completion.paused' ? 'Paused' : key)
}) })
@ -270,7 +270,7 @@ describe('Markdown', () => {
describe('math engine configuration', () => { describe('math engine configuration', () => {
it('should configure KaTeX when mathEngine is KaTeX', () => { it('should configure KaTeX when mathEngine is KaTeX', () => {
mockUseSettings.mockReturnValue({ mathEngine: 'KaTeX' }) mockUseSettings.mockReturnValue({ mathEngine: 'KaTeX', mathEnableSingleDollar: true })
render(<Markdown block={createMainTextBlock()} />) render(<Markdown block={createMainTextBlock()} />)
@ -279,7 +279,7 @@ describe('Markdown', () => {
}) })
it('should configure MathJax when mathEngine is MathJax', () => { it('should configure MathJax when mathEngine is MathJax', () => {
mockUseSettings.mockReturnValue({ mathEngine: 'MathJax' }) mockUseSettings.mockReturnValue({ mathEngine: 'MathJax', mathEnableSingleDollar: true })
render(<Markdown block={createMainTextBlock()} />) render(<Markdown block={createMainTextBlock()} />)
@ -288,7 +288,7 @@ describe('Markdown', () => {
}) })
it('should not load math plugins when mathEngine is none', () => { it('should not load math plugins when mathEngine is none', () => {
mockUseSettings.mockReturnValue({ mathEngine: 'none' }) mockUseSettings.mockReturnValue({ mathEngine: 'none', mathEnableSingleDollar: true })
render(<Markdown block={createMainTextBlock()} />) render(<Markdown block={createMainTextBlock()} />)
@ -384,12 +384,12 @@ describe('Markdown', () => {
}) })
it('should re-render when math engine changes', () => { it('should re-render when math engine changes', () => {
mockUseSettings.mockReturnValue({ mathEngine: 'KaTeX' }) mockUseSettings.mockReturnValue({ mathEngine: 'KaTeX', mathEnableSingleDollar: true })
const { rerender } = render(<Markdown block={createMainTextBlock()} />) const { rerender } = render(<Markdown block={createMainTextBlock()} />)
expect(screen.getByTestId('markdown-content')).toBeInTheDocument() expect(screen.getByTestId('markdown-content')).toBeInTheDocument()
mockUseSettings.mockReturnValue({ mathEngine: 'MathJax' }) mockUseSettings.mockReturnValue({ mathEngine: 'MathJax', mathEnableSingleDollar: true })
rerender(<Markdown block={createMainTextBlock()} />) rerender(<Markdown block={createMainTextBlock()} />)
// Should still render correctly with new math engine // Should still render correctly with new math engine

View File

@ -1,4 +1,4 @@
import type { Root, Node, Element, Text } from 'hast' import type { Element, Node, Root, Text } from 'hast'
import { visit } from 'unist-util-visit' import { visit } from 'unist-util-visit'
/** /**

View File

@ -29,6 +29,7 @@ import {
setEnableBackspaceDeleteModel, setEnableBackspaceDeleteModel,
setEnableQuickPanelTriggers, setEnableQuickPanelTriggers,
setFontSize, setFontSize,
setMathEnableSingleDollar,
setMathEngine, setMathEngine,
setMessageFont, setMessageFont,
setMessageNavigation, setMessageNavigation,
@ -97,6 +98,7 @@ const SettingsTab: FC<Props> = (props) => {
codeImageTools, codeImageTools,
codeExecution, codeExecution,
mathEngine, mathEngine,
mathEnableSingleDollar,
autoTranslateWithSpace, autoTranslateWithSpace,
pasteLongTextThreshold, pasteLongTextThreshold,
multiModelMessageStyle, multiModelMessageStyle,
@ -382,19 +384,6 @@ const SettingsTab: FC<Props> = (props) => {
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.math_engine.label')}</SettingRowTitleSmall>
<Selector
value={mathEngine}
onChange={(value) => dispatch(setMathEngine(value as MathEngine))}
options={[
{ value: 'KaTeX', label: 'KaTeX' },
{ value: 'MathJax', label: 'MathJax' },
{ value: 'none', label: t('settings.messages.math_engine.none') }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall>
</SettingRow> </SettingRow>
@ -418,6 +407,37 @@ const SettingsTab: FC<Props> = (props) => {
<SettingDivider /> <SettingDivider />
</SettingGroup> </SettingGroup>
</CollapsibleSettingGroup> </CollapsibleSettingGroup>
<CollapsibleSettingGroup title={t('settings.math.title')} defaultExpanded={true}>
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>{t('settings.math.engine.label')}</SettingRowTitleSmall>
<Selector
value={mathEngine}
onChange={(value) => dispatch(setMathEngine(value as MathEngine))}
options={[
{ value: 'KaTeX', label: 'KaTeX' },
{ value: 'MathJax', label: 'MathJax' },
{ value: 'none', label: t('settings.math.engine.none') }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('settings.math.single_dollar.label')}{' '}
<Tooltip title={t('settings.math.single_dollar.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<Switch
size="small"
checked={mathEnableSingleDollar}
onChange={(checked) => dispatch(setMathEnableSingleDollar(checked))}
/>
</SettingRow>
<SettingDivider />
</SettingGroup>
</CollapsibleSettingGroup>
<CollapsibleSettingGroup title={t('chat.settings.code.title')} defaultExpanded={true}> <CollapsibleSettingGroup title={t('chat.settings.code.title')} defaultExpanded={true}>
<SettingGroup> <SettingGroup>
<SettingRow> <SettingRow>

View File

@ -62,7 +62,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 130, version: 131,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'], blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
migrate migrate
}, },

View File

@ -2094,6 +2094,15 @@ const migrateConfig = {
logger.error('migrate 130 error', error as Error) logger.error('migrate 130 error', error as Error)
return state return state
} }
},
'131': (state: RootState) => {
try {
state.settings.mathEnableSingleDollar = true
return state
} catch (error) {
logger.error('migrate 131 error', error as Error)
return state
}
} }
} }

View File

@ -105,6 +105,7 @@ export interface SettingsState {
codeWrappable: boolean codeWrappable: boolean
codeImageTools: boolean codeImageTools: boolean
mathEngine: MathEngine mathEngine: MathEngine
mathEnableSingleDollar: boolean
messageStyle: 'plain' | 'bubble' messageStyle: 'plain' | 'bubble'
foldDisplayMode: 'expanded' | 'compact' foldDisplayMode: 'expanded' | 'compact'
gridColumns: number gridColumns: number
@ -287,6 +288,7 @@ export const initialState: SettingsState = {
codeWrappable: false, codeWrappable: false,
codeImageTools: false, codeImageTools: false,
mathEngine: 'KaTeX', mathEngine: 'KaTeX',
mathEnableSingleDollar: true,
messageStyle: 'plain', messageStyle: 'plain',
foldDisplayMode: 'expanded', foldDisplayMode: 'expanded',
gridColumns: 2, gridColumns: 2,
@ -616,6 +618,9 @@ const settingsSlice = createSlice({
setMathEngine: (state, action: PayloadAction<MathEngine>) => { setMathEngine: (state, action: PayloadAction<MathEngine>) => {
state.mathEngine = action.payload state.mathEngine = action.payload
}, },
setMathEnableSingleDollar: (state, action: PayloadAction<boolean>) => {
state.mathEnableSingleDollar = action.payload
},
setFoldDisplayMode: (state, action: PayloadAction<'expanded' | 'compact'>) => { setFoldDisplayMode: (state, action: PayloadAction<'expanded' | 'compact'>) => {
state.foldDisplayMode = action.payload state.foldDisplayMode = action.payload
}, },
@ -898,6 +903,7 @@ export const {
setCodeWrappable, setCodeWrappable,
setCodeImageTools, setCodeImageTools,
setMathEngine, setMathEngine,
setMathEnableSingleDollar,
setFoldDisplayMode, setFoldDisplayMode,
setGridColumns, setGridColumns,
setGridPopoverTrigger, setGridPopoverTrigger,