diff --git a/blocksuite/affine/all/src/__tests__/adapters/latex-currency-escape.unit.spec.ts b/blocksuite/affine/all/src/__tests__/adapters/latex-currency-escape.unit.spec.ts new file mode 100644 index 0000000000..9feae80c1a --- /dev/null +++ b/blocksuite/affine/all/src/__tests__/adapters/latex-currency-escape.unit.spec.ts @@ -0,0 +1,30 @@ +import { preprocessLatex } from '@blocksuite/affine-block-latex'; +import { describe, expect, test } from 'vitest'; + +describe('latex preprocessor currency escaping', () => { + test.each([ + ['a plain price', 'costs $5 today', 'costs \\$5 today'], + ['a bare amount', '$4', '\\$4'], + ['two prices', 'costs $5 and $10', 'costs \\$5 and \\$10'], + ])('escapes %s', (_, markdown, expected) => { + expect(preprocessLatex(markdown)).toBe(expected); + }); + + describe('a dollar that is already escaped', () => { + // Escaping it again turns the odd backslash run even, which leaves a + // literal backslash followed by an unescaped `$` for remark-math. + test.each([ + ['one backslash', 'costs \\$4 today'], + ['three backslashes', '\\\\\\$4'], + ['several amounts', '\\$4 and \\$10'], + ])('is left alone with %s', (_, markdown) => { + expect(preprocessLatex(markdown)).toBe(markdown); + }); + }); + + test('an even backslash run still has its dollar escaped', () => { + // `\\` is a literal backslash, so the `$` after it is unescaped and is a + // genuine currency candidate. The run itself must survive untouched. + expect(preprocessLatex('costs \\\\$4 today')).toBe('costs \\\\\\$4 today'); + }); +}); diff --git a/blocksuite/affine/blocks/latex/src/adapters/markdown/preprocessor.ts b/blocksuite/affine/blocks/latex/src/adapters/markdown/preprocessor.ts index 7f1128a3bb..39f0d957c2 100644 --- a/blocksuite/affine/blocks/latex/src/adapters/markdown/preprocessor.ts +++ b/blocksuite/affine/blocks/latex/src/adapters/markdown/preprocessor.ts @@ -32,7 +32,7 @@ function escapeMhchem(text: string) { * @param content - The content to preprocess * @returns The preprocessed content */ -function preprocessLatex(content: string) { +export function preprocessLatex(content: string) { // Protect code blocks const codeBlocks: string[] = []; let preprocessedContent = content; @@ -54,8 +54,17 @@ function preprocessLatex(content: string) { } ); - // Escape dollar signs that are likely currency indicators - preprocessedContent = preprocessedContent.replace(/\$(?=\d)/g, '\\$'); + // Escape dollar signs that are likely currency indicators. + // + // A dollar preceded by an odd number of backslashes is already escaped, so + // adding another backslash makes the run even: `\$4` becomes `\\$4`, which is + // a literal backslash followed by an unescaped `$` that remark-math can then + // treat as a delimiter. Only escape a dollar whose preceding backslash run is + // even, and keep that run as it was. + preprocessedContent = preprocessedContent.replace( + /(?