From e8e9a2d86f2cc50c8b537e9a0b4da581fae04921 Mon Sep 17 00:00:00 2001 From: one Date: Fri, 27 Jun 2025 13:46:09 +0800 Subject: [PATCH] fix(Markdown, LaTeX): do not touch escaped brackets (#7582) - Keep `\\[` as is - Use a custom match algorithm rather than balanced match --- package.json | 2 - .../src/utils/__tests__/markdown.test.ts | 16 +++++ src/renderer/src/utils/markdown.ts | 58 +++++++++++++++++-- yarn.lock | 16 ----- 4 files changed, 70 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 6127aa6f27..d550355d04 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,6 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@tryfabric/martian": "^1.2.4", - "@types/balanced-match": "^3", "@types/diff": "^7", "@types/fs-extra": "^11", "@types/lodash": "^4.17.5", @@ -139,7 +138,6 @@ "archiver": "^7.0.1", "async-mutex": "^0.5.0", "axios": "^1.7.3", - "balanced-match": "^3.0.1", "browser-image-compression": "^2.0.2", "color": "^5.0.0", "dayjs": "^1.11.11", diff --git a/src/renderer/src/utils/__tests__/markdown.test.ts b/src/renderer/src/utils/__tests__/markdown.test.ts index f16509ced4..a078f2c418 100644 --- a/src/renderer/src/utils/__tests__/markdown.test.ts +++ b/src/renderer/src/utils/__tests__/markdown.test.ts @@ -490,6 +490,22 @@ $$ it('should convert inline math \\(...\\) to $...$', () => { expect(processLatexBrackets('The formula is \\(a+b=c\\)')).toBe('The formula is $a+b=c$') }) + + it('should handle complex mathematical text with escaped brackets', () => { + const input = `设 \\(A\\) 为 \\(n\\times n\\) 的实可逆矩阵, +\\[ +B=\\begin{pmatrix} O & A \\\\[2pt] A' & O \\end{pmatrix}\\;(2n\\times 2n,\\;B=B'), +\\] +求 \\(B\\) 的正惯性指数 \\(p(B)\\) 和负惯性指数 \\(q(B)\\)。` + + const expected = `设 $A$ 为 $n\\times n$ 的实可逆矩阵, +$$ +B=\\begin{pmatrix} O & A \\\\[2pt] A' & O \\end{pmatrix}\\;(2n\\times 2n,\\;B=B'), +$$ +求 $B$ 的正惯性指数 $p(B)$ 和负惯性指数 $q(B)$。` + + expect(processLatexBrackets(input)).toBe(expected) + }) }) describe('code block protection', () => { diff --git a/src/renderer/src/utils/markdown.ts b/src/renderer/src/utils/markdown.ts index 4fc499300d..8505ed36c7 100644 --- a/src/renderer/src/utils/markdown.ts +++ b/src/renderer/src/utils/markdown.ts @@ -1,5 +1,4 @@ import { languages } from '@shared/config/languages' -import { default as balanced } from 'balanced-match' import remarkParse from 'remark-parse' import remarkStringify from 'remark-stringify' import removeMarkdown from 'remove-markdown' @@ -31,7 +30,7 @@ export const findCitationInChildren = (children: any): string => { } // 检查是否包含潜在的 LaTeX 模式 -const containsLatexRegex = /\\\(.*?\\\)|\\\[.*?\\\]|\$.*?\$|\\begin\{equation\}.*?\\end\{equation\}/s +const containsLatexRegex = /\\\(.*?\\\)|\\\[.*?\\\]/s /** * 转换 LaTeX 公式括号 `\[\]` 和 `\(\)` 为 Markdown 格式 `$$...$$` 和 `$...$` @@ -41,7 +40,7 @@ const containsLatexRegex = /\\\(.*?\\\)|\\\[.*?\\\]|\$.*?\$|\\begin\{equation\}. * 目前的实现: * - 保护代码块和链接,避免被 remark-math 处理 * - 支持嵌套括号的平衡匹配 - * - 转义 `\\(x\\)` 会被处理为 `\$x\$`,`\\[x\\]` 会被处理为 `\$$x\$$` + * - 转义括号 `\\(\\)` 或 `\\[\\]` 不会被处理 * * @see https://github.com/remarkjs/remark-math/issues/39 * @param text 输入的 Markdown 文本 @@ -77,7 +76,7 @@ export const processLatexBrackets = (text: string) => { let remaining = content while (remaining.length > 0) { - const match = balanced(openDelim, closeDelim, remaining) + const match = findLatexMatch(remaining, openDelim, closeDelim) if (!match) { result += remaining break @@ -109,6 +108,57 @@ export const processLatexBrackets = (text: string) => { return result } +/** + * 查找 LaTeX 数学公式的匹配括号对 + * + * 使用平衡括号算法处理嵌套结构,正确识别转义字符 + * + * @param text 要搜索的文本 + * @param openDelim 开始分隔符 (如 '\[' 或 '\(') + * @param closeDelim 结束分隔符 (如 '\]' 或 '\)') + * @returns 匹配结果对象或 null + */ +const findLatexMatch = (text: string, openDelim: string, closeDelim: string) => { + // 统计连续反斜杠:奇数个表示转义,偶数个表示未转义 + const escaped = (i: number) => { + let count = 0 + while (--i >= 0 && text[i] === '\\') count++ + return count & 1 + } + + // 查找第一个有效的开始标记 + for (let i = 0, n = text.length; i <= n - openDelim.length; i++) { + // 没有找到开始分隔符或被转义,跳过 + if (!text.startsWith(openDelim, i) || escaped(i)) continue + + // 处理嵌套结构 + for (let j = i + openDelim.length, depth = 1; j <= n - closeDelim.length && depth; j++) { + // 计算当前位置对深度的影响:+1(开始), -1(结束), 0(无关) + const delta = + text.startsWith(openDelim, j) && !escaped(j) ? 1 : text.startsWith(closeDelim, j) && !escaped(j) ? -1 : 0 + + if (delta) { + depth += delta + + // 找到了匹配的结束位置 + if (!depth) + return { + start: i, + end: j + closeDelim.length, + pre: text.slice(0, i), + body: text.slice(i + openDelim.length, j), + post: text.slice(j + closeDelim.length) + } + + // 跳过已处理的分隔符字符,避免重复检查 + j += (delta > 0 ? openDelim : closeDelim).length - 1 + } + } + } + + return null +} + /** * 转换数学公式格式: * - 将 LaTeX 格式的 '\\[' 和 '\\]' 转换为 '$$$$'。 diff --git a/yarn.lock b/yarn.lock index 75f9e2f434..eefde56f9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4115,13 +4115,6 @@ __metadata: languageName: node linkType: hard -"@types/balanced-match@npm:^3": - version: 3.0.2 - resolution: "@types/balanced-match@npm:3.0.2" - checksum: 10c0/833f6499609363537026c4ec2770af5c5a36e71b80f7b5b23884b15296301bfcf974cd40bc75fda940dea4994acd96c9222b284c248383a1ade59bf8835940b0 - languageName: node - linkType: hard - "@types/cacheable-request@npm:^6.0.1": version: 6.0.3 resolution: "@types/cacheable-request@npm:6.0.3" @@ -5646,7 +5639,6 @@ __metadata: "@testing-library/jest-dom": "npm:^6.6.3" "@testing-library/react": "npm:^16.3.0" "@tryfabric/martian": "npm:^1.2.4" - "@types/balanced-match": "npm:^3" "@types/diff": "npm:^7" "@types/fs-extra": "npm:^11" "@types/lodash": "npm:^4.17.5" @@ -5673,7 +5665,6 @@ __metadata: archiver: "npm:^7.0.1" async-mutex: "npm:^0.5.0" axios: "npm:^1.7.3" - balanced-match: "npm:^3.0.1" browser-image-compression: "npm:^2.0.2" color: "npm:^5.0.0" dayjs: "npm:^1.11.11" @@ -6306,13 +6297,6 @@ __metadata: languageName: node linkType: hard -"balanced-match@npm:^3.0.1": - version: 3.0.1 - resolution: "balanced-match@npm:3.0.1" - checksum: 10c0/ac8dd63a5b260610c2cbda982f436e964c1b9ae8764d368a523769da40a31710abd6e19f0fdf1773c4ad7b2ea7ba7b285d547375dc723f6e754369835afc8e9f - languageName: node - linkType: hard - "bare-events@npm:^2.2.0": version: 2.5.4 resolution: "bare-events@npm:2.5.4"