Repository navigation
fix(latex): keep inline math that starts with a digit (#15588) - #15596
Conversation
…5588) The markdown preprocessor escapes `$` when it is followed by a digit, on the assumption that it is a price. It protects `$$...$$`, `\[...\]` and `\(...\)` from that escape first, but never single-dollar inline math, so `$4\vee 6=12$` had its opening `$` escaped and never parsed as math. The now-unbalanced closing `$` then threw off everything after it, which is why one such expression breaks the rest of the document. Wrapping the number in braces avoided it only because `${` is not `$` followed by a digit. Protect single-dollar inline math as well, using the usual rule for telling math from prices: the opening `$` is not followed by whitespace, the closing `$` is not preceded by whitespace, and the closing `$` is not followed by a digit. `costs $5 and $10` and `$100$200` still escape as currency because their closing candidate is preceded by a space or followed by a digit. `preprocessLatex` is exported so the rule can be tested directly.
📝 WalkthroughWalkthroughThe PR updates single-dollar LaTeX detection so even-length backslash runs can precede an opening delimiter. It adds unit tests for math, currency, delimiters, whitespace, display syntax, and code spans. ChangesLaTeX preprocessing
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix · Severity of issue fixed: Medium Suggested reviewers: Merge Risk: 🔵 Low · up to Escaped currency followed by a digit can be parsed as inline math. Preserve odd backslash runs before merging. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Advanced
Run ID: 689f86a4-bf11-4c02-a34c-f55ac2ccdf8e
📒 Files selected for processing (2)
blocksuite/affine/all/src/__tests__/adapters/latex-preprocessor.unit.spec.tsblocksuite/affine/blocks/latex/src/adapters/markdown/preprocessor.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
Review feedback on toeverything#15596: the inline-math rule only checked for whitespace before the closing `$`, so a backslash satisfied it and `$5\$ and $10` was captured as math. That stopped `$5` being escaped as currency, changing behaviour that the previous code got right. The same gap let an escaped `\$` act as an opening delimiter, so `\$5 and x$` was also captured. Consume backslash escapes as a unit inside the expression and refuse to open on an escaped dollar, so `\$` is neither delimiter. Every escaped-dollar input now preprocesses byte-identically to the behaviour before this branch; only digit-first inline math changes.
Review feedback on toeverything#15596: the inline-math rule only checked for whitespace before the closing `$`, so a backslash satisfied it and `$5\$ and $10` was captured as math. That stopped `$5` being escaped as currency, changing behaviour that the previous code got right. The same gap let an escaped `\$` act as an opening delimiter, so `\$5 and x$` was also captured. Consume backslash escapes as a unit inside the expression and refuse to open on an escaped dollar, so `\$` is neither delimiter. Every escaped-dollar input now preprocesses byte-identically to the behaviour before this branch; only digit-first inline math changes. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01XWr9UtfXxdTdfHMydFh4V8
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Advanced
Run ID: dce1a33d-1d67-4f66-831c-de4e01d12a69
📒 Files selected for processing (2)
blocksuite/affine/all/src/__tests__/adapters/latex-preprocessor.unit.spec.tsblocksuite/affine/blocks/latex/src/adapters/markdown/preprocessor.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.
Follow-up on review: `(?<!\)` rejected an opening `$` after any backslash, including an even-length run. An even run is a literal backslash rather than an escape, so `\\$4\vee 6=12$` was left unprotected and the currency escape still broke it — the same bug as toeverything#15588, just one variant short of the fix. Match the even run along with the expression it precedes. An odd run still means the dollar is escaped and is left alone.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
⚠️ Outside diff range comments (1)
blocksuite/affine/blocks/latex/src/adapters/markdown/preprocessor.ts (1)
69-78: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winPreserve odd backslash runs before currency
MarkdownPreprocessorManager.processrunspreprocessLatexbefore_markdownToAst, which usesremark-math. An odd-length backslash run before$4is excluded from protection, then/\$(?=\d)/gadds a backslash and makes the run even.remark-mathcan then treat$4as an active delimiter. Make the currency replacement add a backslash only for even-length runs, and add an odd-run regression test without changing the existing even-run behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Advanced
Run ID: 624c5211-5da7-4ec6-acc1-78987c7b582b
📒 Files selected for processing (2)
blocksuite/affine/all/src/__tests__/adapters/latex-preprocessor.unit.spec.tsblocksuite/affine/blocks/latex/src/adapters/markdown/preprocessor.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- blocksuite/affine/blocks/latex/src/adapters/markdown/preprocessor.ts
- blocksuite/affine/all/src/tests/adapters/latex-preprocessor.unit.spec.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
|
On the outside-diff comment about odd backslash runs before currency ( Checked before splitting: #15602 escapes only dollars whose preceding backslash run is even, leaving an already-escaped |
Review feedback on toeverything#15596: the inline-math rule only checked for whitespace before the closing `$`, so a backslash satisfied it and `$5\$ and $10` was captured as math. That stopped `$5` being escaped as currency, changing behaviour that the previous code got right. The same gap let an escaped `\$` act as an opening delimiter, so `\$5 and x$` was also captured. Consume backslash escapes as a unit inside the expression and refuse to open on an escaped dollar, so `\$` is neither delimiter. Every escaped-dollar input now preprocesses byte-identically to the behaviour before this branch; only digit-first inline math changes. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01XWr9UtfXxdTdfHMydFh4V8
09b505f to
d7e3d03
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## canary #15596 +/- ##
==========================================
+ Coverage 52.79% 54.33% +1.53%
==========================================
Files 4001 4004 +3
Lines 191593 191876 +283
Branches 30455 30908 +453
==========================================
+ Hits 101155 104257 +3102
+ Misses 86560 83753 -2807
+ Partials 3878 3866 -12
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
d7e3d03 to
448ada7
Compare
| * @returns The preprocessed content | ||
| */ | ||
| function preprocessLatex(content: string) { | ||
| export function preprocessLatex(content: string) { |
There was a problem hiding this comment.
Please keep preprocessLatex private. Exporting it only for tests adds an unnecessary public API. Extend the existing MarkdownAdapter tests instead.
| ['relation', '$2<3$'], | ||
| ['single digit', '$5$'], | ||
| ])('leaves %s untouched', (_, markdown) => { | ||
| expect(preprocessLatex(markdown)).toBe(markdown); |
There was a problem hiding this comment.
Please test the final MarkdownAdapter output. A string check does not verify the parsing failure reported in #15588.
## Description Split out of #15596 on review feedback — this is an independent, pre-existing bug in the currency pass, so it does not belong in that PR. The currency pass escapes any `$` followed by a digit without looking at what precedes it: ```ts preprocessedContent = preprocessedContent.replace(/\$(?=\d)/g, '\\$'); ``` When the dollar is **already escaped**, that turns an odd backslash run even: ``` input: \$4 (an escaped dollar — the author wants a literal "$4") output: \\$4 (a literal backslash, then an unescaped `$`) ``` `MarkdownPreprocessorManager.process` runs this before `_markdownToAst`, so remark-math sees that bare `$` and can take it as an active delimiter — the opposite of what escaping it was for. ### The fix Escape only a dollar whose preceding backslash run is **even**, keeping the run itself: ```ts preprocessedContent = preprocessedContent.replace( /(?<!\\)((?:\\\\)*)\$(?=\d)/g, '$1\\$' ); ``` `(?<!\\)` anchors the run to its start and `((?:\\\\)*)` takes backslashes in pairs, so an odd run never matches. Behaviour that was already correct is untouched: | input | before | after | |---|---|---| | `costs $5 today` | `costs \$5 today` | unchanged | | `costs \\$4 today` (even run — literal `\` then a real `$`) | `costs \\\$4 today` | unchanged | | `costs \$4 today` (odd run — already escaped) | `costs \\$4 today` | **`costs \$4 today`** | ### Relationship to #15596 Both touch this file, so whichever lands second will need a trivial rebase: - Both add `export` to `preprocessLatex` (the same one-word change). - They edit **different** statements — #15596 changes the *protection* regex above, this one changes the *currency* regex below. - Test files are deliberately named differently (`latex-preprocessor.unit.spec.ts` there, `latex-currency-escape.unit.spec.ts` here) so they do not collide. Happy to fold them into one file once both are in, if you'd prefer that. I verified this bug is not introduced by #15596: `\$4` preprocesses to `\\$4` identically with and without that branch. ## Checklist - [ ] I have signed the [AFFiNE Contributor License Agreement](https://cla-assistant.io/toeverything/AFFiNE) — required before merge; the `license/cla` check must be green ([how it works](https://github.com/toeverything/AFFiNE/blob/canary/docs/BUILDING.md#sign-the-cla-first)) - [x] The PR targets the `canary` branch and its title follows [Conventional Commits](https://www.conventionalcommits.org/) - [x] Tests are added or updated where it makes sense - [x] `yarn lint` and `yarn typecheck` pass locally - [x] If the PR code includes AI-generated edits, I have carefully reviewed it to ensure that the scope of the PR is consistent with what is claimed in the title and description, and that there are no redundant or invalid implementations. - [x] I agree that maintainers may close the PR without discussion if they consider the code quality to be too low. ### Notes on the checklist - **CLA:** not signed yet — the repository owner is signing it; the `license/cla` check will go green once that happens. - **Tests:** 7 cases in `latex-currency-escape.unit.spec.ts`. Reverting only the regex fails exactly the three already-escaped cases and leaves the four guards (plain price, bare amount, two prices, even run) green — they are regression guards, not props. Full `blocksuite/affine/all` suite: 230 passing. - **Lint/format:** `oxlint --deny-warnings` and `oxfmt --check` are both clean on the two changed files. - **Typecheck:** clean for these files. For transparency, a full `yarn typecheck` on my machine also reports pre-existing `@prisma/client` errors under `packages/backend/server`; those come from my installing with `--mode=skip-build` (so `prisma generate` never ran) and are unrelated to this change. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved LaTeX handling for currency values by escaping dollar signs only when required. - Preserved already-escaped dollar signs and backslash sequences correctly. - **Tests** - Added coverage for plain currency amounts, already-escaped dollar signs, and even backslash runs. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
|
Thanks for merging, and for cleaning up the export and moving the tests onto MarkdownAdapter in #15622. Noted for next time. |
Fixes #15588.
Root cause
preprocessLatexescapes any$that is followed by a digit, assuming it is a price:The step above it shields existing LaTeX from that escape — but only
$$…$$,\[…\]and\(…\). Single-dollar inline math was never protected.So for
$4\vee 6=12$:$4and rewrites it to\$4,$is left orphaned, which throws off the parse of everything after it.That last part is why a single such expression breaks the rest of the document, as the report describes. It also explains the reporter's
{ }workaround:${4}is$followed by{, not a digit, so the escape never fires.The fix
Protect single-dollar inline math too, using the usual rule for telling math from prices:
$is not followed by whitespace,$is not preceded by whitespace,$is not followed by a digit./(\$\$[\s\S]*?\$\$|\\\[[\s\S]*?\\\]|\\\(.*?\\\)|\$(?!\s)[^\n$]*?(?<!\s)\$(?!\d))/gCurrency is unaffected, which is the part worth checking:
costs $5 and $10 today— the only closing candidate is the$before10, and it is preceded by a space, so no match; both escape as before.$100$200— the closing candidate is followed by2, so no match; both escape as before.it costs $5.00 total— no closing$at all.$$…$$stays first in the alternation, so display math is still matched as display math.Verification
New
blocksuite/affine/all/src/__tests__/adapters/latex-preprocessor.unit.spec.ts, 11 cases: the digit-first expressions from the issue, the currency cases above, and guards for display math, a$followed by whitespace,\(…\)rewriting, and code spans.Reverting only the regex (keeping the export so the module still loads) fails exactly the four digit-first tests and leaves the other seven green:
That the currency and existing-behaviour cases pass without the fix is the point — they are regression guards, not props for it.
With the fix:
(the whole
blocksuite/affine/allsuite, which includes the 193 existing adapter tests).Notes
preprocessLatexis now exported so the delimiter rule can be tested directly. It was previously module-private with only the extension exported;adapters/markdown/index.tsalready doesexport * from './preprocessor.js', so nothing else changed.(?<!\s)is ES2018; the file already targets modern runtimes.Summary by CodeRabbit
Bug Fixes
\( \)expressions, whitespace, mixed content, and code spans.Tests