Skip to content

fix(latex): keep inline math that starts with a digit (#15588) - #15596

Merged
darkskygit merged 3 commits into
toeverything:canaryfrom
anandghegde:fix/15588-latex-dollar-digit
Sep 19, 2026
Merged

darkskygit merged 3 commits into
toeverything:canaryfrom
anandghegde:fix/15588-latex-dollar-digit

Conversation

@anandghegde

@anandghegde anandghegde commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #15588.

Root cause

preprocessLatex escapes any $ that is followed by a digit, assuming it is a price:

// Escape dollar signs that are likely currency indicators
preprocessedContent = preprocessedContent.replace(/\$(?=\d)/g, '\\$');

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$:

  1. the protection pass does not match it,
  2. the currency pass sees $4 and rewrites it to \$4,
  3. the expression never parses as math, and the closing $ 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:

  • the opening $ is not followed by whitespace,
  • the closing $ is not preceded by whitespace,
  • the closing $ is not followed by a digit.
/(\$\$[\s\S]*?\$\$|\\\[[\s\S]*?\\\]|\\\(.*?\\\)|\$(?!\s)[^\n$]*?(?<!\s)\$(?!\d))/g

Currency is unaffected, which is the part worth checking:

  • costs $5 and $10 today — the only closing candidate is the $ before 10, and it is preceded by a space, so no match; both escape as before.
  • $100$200 — the closing candidate is followed by 2, 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:

× leaves binary operators untouched
× leaves relation untouched
× leaves single digit untouched
× keeps every expression in a sentence mixing math and prose
  Tests  4 failed | 7 passed (11)

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:

 Test Files  14 passed (14)
      Tests  234 passed (234)

(the whole blocksuite/affine/all suite, which includes the 193 existing adapter tests).

Notes

  • preprocessLatex is now exported so the delimiter rule can be tested directly. It was previously module-private with only the extension exported; adapters/markdown/index.ts already does export * from './preprocessor.js', so nothing else changed.
  • I kept the change to the one regex and did not reformat the rest of the file — there is no Prettier config in the repo, and running Prettier with its defaults rewrites quotes and trailing commas across the whole file.
  • The lookbehind (?<!\s) is ES2018; the file already targets modern runtimes.

Summary by CodeRabbit

  • Bug Fixes

    • Improved Markdown LaTeX preprocessing for inline expressions preceded by even-length backslash runs.
    • Continued to distinguish escaped dollar signs from valid math delimiters, while preserving currency values, display math, \( \) expressions, whitespace, mixed content, and code spans.
  • Tests

    • Added comprehensive coverage for inline and display math, delimiter escaping, currency values, whitespace, conversions, mixed content, digit-leading expressions, and code-span protection.

…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.
@anandghegde
anandghegde requested a review from a team as a code owner September 13, 2026 11:06
@github-actions github-actions Bot added the test Related to test cases label Sep 13, 2026
@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

LaTeX preprocessing

Layer / File(s) Summary
Single-dollar protection and validation
blocksuite/affine/blocks/latex/src/adapters/markdown/preprocessor.ts, blocksuite/affine/all/src/__tests__/adapters/latex-preprocessor.unit.spec.ts
The regex now consumes even-length backslash runs before an inline-math delimiter while preserving escaped-dollar handling. Unit tests cover the updated and existing preprocessing behavior.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: l-sun

Merge Risk: 🔵 Low · up to d7e3d

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)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: preserving inline LaTeX math expressions that start with a digit. This matches the pull request objectives and changes.
Linked Issues check ✅ Passed The changes satisfy issue #15588. preprocessLatex protects digit-leading single-dollar expressions before currency escaping. The delimiter pattern preserves escaped-dollar behavior and supports even…
Out of Scope Changes check ✅ Passed The changes stay within issue #15588. The exported preprocessLatex function enables direct regression tests. The added tests verify the reported parsing conflict and related delimiter behavior. No u…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 689f86a4-bf11-4c02-a34c-f55ac2ccdf8e

📥 Commits

Reviewing files that changed from the base of the PR and between 868acf8 and 4b8b9a9.

📒 Files selected for processing (2)
  • blocksuite/affine/all/src/__tests__/adapters/latex-preprocessor.unit.spec.ts
  • blocksuite/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.
anandghegde pushed a commit to anandghegde/AFFiNE that referenced this pull request Sep 13, 2026
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
@CLAassistant

CLAassistant commented Sep 13, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: dce1a33d-1d67-4f66-831c-de4e01d12a69

📥 Commits

Reviewing files that changed from the base of the PR and between 4b8b9a9 and d10e9e7.

📒 Files selected for processing (2)
  • blocksuite/affine/all/src/__tests__/adapters/latex-preprocessor.unit.spec.ts
  • blocksuite/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.

@coderabbitai coderabbitai Bot left a comment

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.

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 win

Preserve odd backslash runs before currency

MarkdownPreprocessorManager.process runs preprocessLatex before _markdownToAst, which uses remark-math. An odd-length backslash run before $4 is excluded from protection, then /\$(?=\d)/g adds a backslash and makes the run even. remark-math can then treat $4 as 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

📥 Commits

Reviewing files that changed from the base of the PR and between d10e9e7 and 09b505f.

📒 Files selected for processing (2)
  • blocksuite/affine/all/src/__tests__/adapters/latex-preprocessor.unit.spec.ts
  • blocksuite/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.

@anandghegde

Copy link
Copy Markdown
Contributor Author

On the outside-diff comment about odd backslash runs before currency (preprocessor.ts 69-78) — it's a real bug, but it is pre-existing and independent of this PR, so I've split it out rather than widening the scope here: #15602.

Checked before splitting: \$4 preprocesses to \\$4 identically with and without this branch, and the line involved (/\$(?=\d)/g) is untouched by these commits. So it isn't a regression introduced here — it's a separate defect in the currency pass that happens to sit in the same function.

#15602 escapes only dollars whose preceding backslash run is even, leaving an already-escaped \$4 alone. The two PRs edit different statements in this file; the only overlap is that both add export to preprocessLatex, so whichever lands second needs a trivial rebase.

anandghegde added a commit to anandghegde/AFFiNE that referenced this pull request Sep 14, 2026
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
@anandghegde
anandghegde force-pushed the fix/15588-latex-dollar-digit branch from 09b505f to d7e3d03 Compare September 14, 2026 04:45
@codecov

codecov Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 54.33%. Comparing base (868acf8) to head (d7e3d03).
⚠️ Report is 1 commits behind head on canary.

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     
Flag Coverage Δ
server-test 78.31% <ø> (+3.66%) ⬆️
unittest 33.30% <ø> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@anandghegde
anandghegde force-pushed the fix/15588-latex-dollar-digit branch from d7e3d03 to 448ada7 Compare September 15, 2026 16:40
* @returns The preprocessed content
*/
function preprocessLatex(content: string) {
export function preprocessLatex(content: string) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please test the final MarkdownAdapter output. A string check does not verify the parsing failure reported in #15588.

darkskygit pushed a commit that referenced this pull request Sep 19, 2026
## 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 -->
@darkskygit
darkskygit merged commit 42457ef into toeverything:canary Sep 19, 2026
3 checks passed
@anandghegde

Copy link
Copy Markdown
Contributor Author

Thanks for merging, and for cleaning up the export and moving the tests onto MarkdownAdapter in #15622. Noted for next time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test Related to test cases

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Bug]: LaTeX parsing conflict for the $

3 participants