Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/x-markdown/src/plugins/Latex/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,43 @@ describe('LaTeX Plugin', () => {
expect(container).toMatchSnapshot();
});

it.each([
'Downgrade Max ($100) to Pro ($20)',
'把 Max 5x($100)降级到 Pro($20)',
'The items cost $10, $20, and $30, respectively.',
])('should not interpret currency amounts as LaTeX: %s', (content) => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { container } = render(
<XMarkdown config={{ extensions: latexPlugin() }}>{`**${content}**`}</XMarkdown>,
);

expect(container.querySelector('.katex')).not.toBeInTheDocument();
expect(container.querySelector('strong')).toHaveTextContent(content);
expect(warnSpy.mock.calls.flat().join(' ')).not.toContain('unicodeTextInMathMode');
warnSpy.mockRestore();
});

it('should still render formulas that start with a number', () => {
const { container } = render(
<XMarkdown config={{ extensions: latexPlugin() }}>{'$2x + 1$'}</XMarkdown>,
);

expect(container.querySelector('.katex')).toBeInTheDocument();
});

it.each([
'$ x$',
'$x $',
'$x$2',
])('should follow single-dollar delimiter rules: %s', (content) => {
const { container } = render(
<XMarkdown config={{ extensions: latexPlugin() }}>{content}</XMarkdown>,
);

expect(container.querySelector('.katex')).not.toBeInTheDocument();
expect(container).toHaveTextContent(content);
});

it('should render inline LaTeX with $$\n..\n$$ syntax', () => {
const { container } = render(
<XMarkdown config={{ extensions: latexPlugin() }}>
Expand Down
22 changes: 17 additions & 5 deletions packages/x-markdown/src/plugins/Latex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { TokenizerAndRendererExtension } from 'marked';

import 'katex/dist/katex.min.css';

const inlineRuleNonStandard =
/^(?:\${1,2}([^$]{1,10000}?)\${1,2}|\\\(([\s\S]{1,10000}?)\\\)|\\\[((?:\\.|[^\\]){1,10000}?)\\\])/;
const inlineDollarRule = /^(\${1,2})([^$]{1,10000}?)\1/;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

支持公式内容中的转义美元符。

[^$] 会把 $\text{\$100}$ 中的 \$ 误当作关闭定界符,导致公式被截断并产生 KaTeX 解析错误。允许反斜杠转义的字符,并补充该回归用例。

建议修改
-const inlineDollarRule = /^(\${1,2})([^$]{1,10000}?)\1/;
+const inlineDollarRule = /^(\${1,2})((?:\\[\s\S]|[^$]){1,10000}?)\1/;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const inlineDollarRule = /^(\${1,2})([^$]{1,10000}?)\1/;
const inlineDollarRule = /^(\${1,2})((?:\\[\s\S]|[^$]){1,10000}?)\1/;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/x-markdown/src/plugins/Latex/index.ts` at line 6, Update
inlineDollarRule to recognize escaped dollar signs within LaTeX content, so
sequences such as \${} are not treated as closing delimiters; preserve the
existing single- and double-dollar delimiter handling and length limit, and add
a regression test covering $\text{\$100}$.

const inlineRuleNonStandard = /^(?:\\\(([\s\S]{1,10000}?)\\\)|\\\[((?:\\.|[^\\]){1,10000}?)\\\])/;
const blockRule =
/^(\${1,2})\n([\s\S]{1,10000}?)\n\1(?:\s*(?:\n|$))|^\\\[((?:\\.|[^\\]){1,10000}?)\\\]/;

Expand All @@ -28,6 +28,14 @@ function replaceAlign(text: string) {
return text ? text.replace(/\{align\*\}/g, '{aligned}') : text;
}

// Follow Pandoc's single-dollar delimiter rules to avoid parsing currency as math.
function isValidInlineDollarMatch(match: RegExpMatchArray, src: string) {
const [, delimiter, text] = match;
if (delimiter === '$$') return true;

return !/^\s|\s$/.test(text) && !/^\d/.test(src.slice(match[0].length));
}

function createRenderer(options: KatexOptions, newlineAfter: boolean) {
return (token: Token) =>
katex.renderToString(token.text, {
Expand All @@ -49,15 +57,19 @@ function inlineKatex(renderer: Render, replaceAlignStart: boolean) {
return indices.length > 0 ? Math.min(...indices) : undefined;
},
tokenizer(src: string) {
const match = src.match(inlineRuleNonStandard);
const dollarMatch = src.match(inlineDollarRule);
if (dollarMatch && !isValidInlineDollarMatch(dollarMatch, src)) return;

const nonStandardMatch = src.match(inlineRuleNonStandard);
const match = dollarMatch || nonStandardMatch;
if (!match) return;

const rawText = match[1] || match[2] || match[3] || '';
const rawText = dollarMatch?.[2] || nonStandardMatch?.[1] || nonStandardMatch?.[2] || '';
const text = replaceAlignStart ? replaceAlign(rawText.trim()) : rawText.trim();

// 对于 \[...\] 语法,如果内容包含换行,标记为块级公式
// 注意:换行检测必须在 trim 之前进行
const isBracketSyntax = match[3] !== undefined;
const isBracketSyntax = nonStandardMatch?.[2] !== undefined;
const hasNewline = rawText.includes('\n');

return {
Expand Down
Loading