Skip to content

feat(muya): support same-line display math - #4935

Merged
Jocs merged 17 commits into
marktext:developfrom
Renakoni:feature/same-line-display-math
Sep 16, 2026
Merged

Jocs merged 17 commits into
marktext:developfrom
Renakoni:feature/same-line-display-math

Conversation

@Renakoni

@Renakoni Renakoni commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Closes #4904, closes #5364, closes #5365

Summary

MarkText only rendered $$...$$ display math when the $$ fences were on their own lines. This PR lets $$...$$ render as display math on the same line as surrounding text, matching pandoc and GitHub — so markdown copied from those sources (or from tools that emit inline $$) renders correctly on paste instead of showing raw $$.

For example, both of these now render:

$$ E=MC^2 $$

Energy is $$E=mc^2$$ in physics.

What this PR does

  • Tokenizes single-line $$...$$ as inline display math. A dedicated rule recognizes non-empty $$...$$ spans and is tried before the (unchanged) single-dollar $...$ rule. Empty $$$$, unclosed openers, and multiline $$ fences are not treated as inline display math — multiline fences still go through the existing block parser, and single-dollar math is untouched (including soft line breaks inside $...$).
  • Keeps the document model and markdown unchanged. Same-line $$ stays an inline token in paragraph state; it is not promoted to a math-block. Source round-trips losslessly and paste behavior is preserved, including adjacent formulas like $$a$$$$b$$.
  • Renders display mode with the correct layout. $$ spans render through KaTeX in display mode; a new .mu-display-math class shows the hidden preview as a full-width, centered block, while the editing state (caret inside) stays inline-block so the source is editable in place.
  • Uses the real marker width for offsets. Cursor/format offsets and the preview's content range are derived from the actual marker length (2 for $$) instead of a hardcoded 1, so selecting or toggling a $$x$$ span targets the content correctly.

Type of change

  • Bug fix (non-breaking, fixes an issue)
  • New feature (non-breaking, adds functionality)
  • Breaking change (causes existing functionality to change)
  • Documentation update

Test plan

  • New tests added
  • Manually tested on: Windows (Chromium)

Passing checks:

  • 48 Muya unit tests covering tokenization (adjacent formulas, empty/unclosed/multiline edge cases, escaping, single-dollar newlines), markdown round-trip, paste, and format/cursor offsets
  • 2 real-Chromium E2E tests for the display-block layout and the block → inline-block editing interaction
  • Muya tsc type check and ESLint (0 errors), plus targeted inlineSyntax.css stylelint

Notes for reviewers


By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.

@Renakoni

Copy link
Copy Markdown
Contributor Author

Pushed 7cf518c: the display-math rule is now a linear-time scanner instead of a regex.

A downstream review of this change flagged that inlineDisplayMathRule backtracked quadratically on malformed input — the \. content pairs overlap the (\*) trailing group, so $$ followed by 131,072 backslashes and a stray $ blocked the thread for ~10s (2.5s at 65,536). Since tryChunks runs synchronously during paragraph tokenization, one crafted line could freeze the window on open or paste, so this seemed worth fixing before merge rather than shipping another regexp/no-super-linear-backtracking suppression.

execInlineDisplayMath keeps the exact semantics: content ends at the first unescaped same-line $$; a backslash always escapes the next character (the regex's greedy pair-first behavior, so an escaped closer extends the content); a returned match always has an unescaped closer, so the isLengthEven(to[3]) gate is untouched — the parity group is always empty.

Evidence:

  • exhaustive parity sweep of all 349,525 strings of length ≤ 9 over {$, \, a, \n} (prefixed with $$), old regex (through the parity gate) vs scanner: 0 mismatches on both the accepted set and the matched content;
  • 131,072 backslashes: 10,045ms → 0ms (65,536: 2,581ms → 0ms);
  • 5 new unit tests, including the adversarial input asserted under 500ms and never tokenized as display math.

Re-verified: the touched suites 53/53, tsc --noEmit clean, ESLint 0 errors, CommonMark/GFM conformance unchanged (1344 passed; the same three #4934 round-trip baseline failures as clean develop).

niyo-oyin added a commit to niyo-oyin/marktext-ja that referenced this pull request Aug 13, 2026
Port of upstream PR marktext#4935 (closes marktext#4904 upstream).

`$$...$$` now renders as display math on the same line as
surrounding text, matching pandoc and GitHub, instead of showing raw
markers. A linear-time scanner (execInlineDisplayMath) tokenizes
non-empty single-line $$ spans ahead of the unchanged single-dollar
rule; multiline fences still go through the block parser, and the
token stays inline in paragraph state so markdown round-trips
losslessly. The preview renders KaTeX in display mode as a full-width
centered block; offsets derive from the real marker length.

Deviations from the upstream PR, integrating with marktext#5004 (29d010a)
which landed here first:
- The inlineMath toggle gates only single-dollar math. The WYSIWYG
  lexer tries execInlineDisplayMath before the gated $ rule, and the
  marked inlineKatex extension now rejects single-dollar matches
  instead of being dropped wholesale — its start() scans past rejected
  candidates so a later $$ span on the same line is still found.
  Interaction specs cover both engines.
- The renderer merge keeps this fork's lazy KaTeX loading; the new
  spec preloads KaTeX so the display assertion sees real output.
@Jocs

Jocs commented Sep 4, 2026

Copy link
Copy Markdown
Member

Could we achieve this by modifying the regular expression rather than adding a new rule (inlineDisplayMathRule)?

Jocs and others added 10 commits September 15, 2026 11:52
…uns (marktext#5365)

The trailing (\\*) group never changes an accepted match, but with no
closing $ the engine tried every split of a backslash run between it and
the content group, so `$` + 131,072 backslashes took ~6.6s to tokenize.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fold `$$...$$` into the `inline_math` regex instead of a scanner that runs
before it in `tryChunks`. Being a regular rule, it is also part of
`validateRules`, so emphasis that closes inside a `$$` span no longer takes
the formula (`*note $$a*b$$`). Like `$...$`, `$$...$$` may now span a soft
line break; directly adjacent formulas such as `$$a$$$$b$$` need a space.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ktext#5364)

`MATH_BLOCK_REG` matched any paragraph that started with `$$`, so Enter in
`$$ E=MC^2 $$`, `$$x` or `- $$a$$ b` replaced the paragraph with an empty
math block and deleted its text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With auto-pair on, typing `$$x$$` saved `$$x$$$`: the second `$` overtypes
the auto-inserted closer, so the final `$` completes the formula with the
caret at its end, where the in-math check (`offset < end`) no longer applied
and a closing `$` was paired.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Once the hidden wrapper became `display: block`, its zero-size `$$` marker
spans were left in anonymous line boxes of their own, so a paragraph that
only held `$$E=mc^2$$` was 84px tall around a 33px formula. Tag the markers
of display math and take them out of flow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hidden display wrapper is as wide as its container, but the preview
kept the editor-wide `max-width`, so in `> - > - $$...$$` a long formula
stuck out 150px past the quote instead of scrolling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Without KaTeX rendering (copy as HTML / rich text), the math extension
wrote every inline token as `$text$`, so copying `$$ E=MC^2 $$` produced
inline math `$E=MC^2$`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`getOffset` hard-coded marker widths (2 for strong/del, 1 otherwise) and
special-cased `inline_math`. Inline code can be fenced by up to three
backticks, so toggling code off in ``` ``word`` ``` shifted the selection
by one; the token's own marker length covers every case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rewrite said only single-dollar errors stay compact, but `$$` parse
errors take the same compact path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Jocs

Jocs commented Sep 15, 2026

Copy link
Copy Markdown
Member

Thanks @Renakoni, same-line $$ is a feature people have been asking for. I reviewed the PR and pushed maintainer commits to the branch: a merge of the current develop, plus fixes for what the review found. Summary below.

Design: one inline_math regex instead of a scanner

This follows up on my earlier question. $$...$$ is now matched by the existing rule, and execInlineDisplayMath plus the special case in tryChunks are gone:

// `$$...$$` is display math and may contain a lone `$`; `$...$` may not.
inline_math: /^(\$\$(?!\$)|\$)((?:(?!\1)[^\\]|\\.)+)\1(?!\1)/,
  • Linear time. The quadratic backtracking you found came from the trailing (\\*) group, not from $$ itself. The single-dollar rule on develop has the same problem: $ plus 131,072 backslashes takes about 6.6s to tokenize. That is now [Bug] A $ followed by a long run of backslashes freezes the editor #5365. The group never changed an accepted match, so dropping it fixes both markers. I checked this over all 1,048,575 strings of up to 11 characters over $, \, a and \n.
  • Emphasis priority. Because it is a regular rule, it is also in validateRules. *note $$a*b$$ now keeps the formula, as *note $a*b$ already did. The scanner sat outside the rule tables, so lowerPriority never saw it and the emphasis won.
  • Behavior changes compared with the scanner. Over the same input set, every difference between the regex and the scanner (0 unexplained out of 1,048,575) falls into one of two cases:
    • Like $...$, $$...$$ may span a soft line break, so hard-wrapped Energy $$E =\nmc^2$$ renders, as in pandoc.
    • Directly adjacent formulas such as $$a$$$$b$$ need a space between them. The HTML export does not render that form either.

Commits pushed

Commit Change
dd5a434 Inline math rule no longer backtracks on backslash runs (#5365)
e4ea1fc Same-line display math matched by the inline_math rule (above)
e32beef Enter in a paragraph that starts with $$ (for example $$ E=MC^2 $$) replaced it with an empty math block and deleted the formula. Only a bare $$ line converts now (#5364; MATH_BLOCK_REG was /^\$\$/)
9dfd439 With auto-pair on, typing $$x$$ saved $$x$$$. The $ that closes the formula is no longer paired
7b40db1 A paragraph holding only $$E=mc^2$$ was 84px tall around a 33px formula: the zero-size hidden $$ markers each got an anonymous line box. They are taken out of flow now
dec70b2 In > - > - $$<long>$$ the preview kept the editor-wide max-width and stuck out 150px past the quote. It is capped at the wrapper width now and scrolls
a34b0e8 Copy as HTML / rich text wrote display math as $E=MC^2$. It keeps $$ now
432ec15 getOffset reads token.marker.length for all markered formats. This replaces the nested ternary and also fixes the selection shift when toggling off ``code``
ab5f35c Restored the inline-math error comment. $$ errors take the same compact path
150d66b sameLineDisplayMath.spec.ts restores window.MUYA_VERSION, like its sibling specs

Not changed here

  • Export parity. The marked math extension used by HTML/PDF export and the clipboard still needs whitespace before the opener and whitespace or ?!.,: after the closer, and does not cross newlines. So ($$x$$), 中文$$x$$中文 and a $$ span with a soft break render in the editor but come out raw in export. $...$ has the same gap on develop today, so it belongs in a separate change. The PR description's note that export "already renders $$" holds for whitespace-delimited formulas.
  • Caret next to the formula. A caret at either edge of the formula reveals its source, and the paragraph reflows from the centered block to inline source. This is the same reveal-on-caret model as $...$.

Verification

The description still says multiline $$ is left to the block parser and that $$a$$$$b$$ tokenizes as two formulas. This comment supersedes those two points.

Jocs and others added 2 commits September 15, 2026 17:15
Follow GitHub: `$$...$$` renders as display math only in a paragraph that
holds nothing but such formulas, and never inside a list item. With text
around it in the same paragraph, or in a heading, table cell or list item,
it renders inline like `$...$`.

A content renders before it is attached, so a list item re-renders its
`$$` contents once its children are in place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The marked math extension rendered every `$$...$$` in display mode. A
walkTokens pass now marks it display only in a paragraph that holds nothing
but `$$` formulas, outside list items, matching the editor and GitHub. The
clipboard source keeps the authored marker instead of deriving it from the
display mode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Jocs

Jocs commented Sep 15, 2026

Copy link
Copy Markdown
Member

Follow-up: $$ now picks display versus inline the way GitHub does. This replaces the "every $$...$$ renders as a centered block" behavior described in my previous comment.

Rule. $$...$$ renders as display math only when it is in a paragraph that holds nothing but $$ formulas (whitespace and line breaks are fine), and that paragraph is not inside a list item. Anywhere else it renders inline, exactly like $...$ (KaTeX displayMode: false). The Markdown source is unchanged.

I checked each case below against GitHub's Markdown API (POST /markdown, which returns js-display-math or js-inline-math):

Markdown GitHub MarkText (editor and HTML export)
$$a \ne b$$, $$a$$ , $$a$$ $$b$$, $$a$$⏎$$b$$ display display
> $$a$$, > > $$a$$ display display
text $$a$$ text, $$a$$ text, text $$a$$ inline inline
line one⏎$$a$$⏎line three (same paragraph) inline inline
- $$c$$, 1. $$d$$, - [ ] $$t$$, - item⏎⏎ $$c$$, - > $$x$$, > - $$y$$ inline inline
# $$a$$, table cell | $$d$$ | inline inline
Commit Change
8df0561 Editor: isDisplayMath() in the inline math renderer. A paragraph content renders before it is attached and so cannot yet see a list item around it; ListItem.create / TaskListItem.create therefore re-render their $$ contents once the children are attached
4826c4a HTML export: the marked math extension now defaults $$ to inline, and a walkTokens pass marks it display under the same rule. Copy as HTML / rich text keeps the authored marker ($$ stays $$ even when inline)

Tests: 21 editor render cases and 20 export cases. Every inline case failed on 150d66b and passes now. The e2e sentence test now checks that Energy is $$E=mc^2$$ conserved. stays on one line, and the nested-overflow test uses > > > $$...$$, since a list item would render it inline. pnpm -C packages/muya test (1732 passed), test:spec (1347 passed), lint, lint:types and check-circular are clean. Local Chromium e2e failures match those on 150d66b (keyboard-focus specs only).

Jocs and others added 2 commits September 15, 2026 20:58
Drop the list-item exception: a paragraph that holds nothing but `$$`
formulas renders them as display math wherever it sits, so the decision
depends only on the paragraph's own text. That removes the post-attach
re-render the ancestor check needed (contents render before they are
attached) and keeps a paragraph correct when it moves in or out of a list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Match the editor: drop the list-item exception from the export rule. A
tight list item's paragraph comes from marked as a block-level `text` token,
so it is checked like a `paragraph`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Jocs

Jocs commented Sep 15, 2026

Copy link
Copy Markdown
Member

Follow-up on the $$ display rule: this update drops the list-item exception from my previous comment.

Rule now. $$...$$ renders as display math when its paragraph holds nothing but $$ formulas (whitespace and line breaks are fine). That includes paragraphs inside block quotes and list items. Next to other text in the same paragraph, or in a heading or table cell, it renders inline like $...$.

Markdown Result
$$a \ne b$$, $$a$$ $$b$$, $$a$$⏎$$b$$, > $$a$$ display
- $$c$$, 1. $$d$$, - [ ] $$t$$, - item⏎⏎ $$c$$, - > $$x$$, > - $$y$$ display (GitHub renders these inline)
text $$a$$ text, line one⏎$$a$$⏎line three, - see $$c$$, **$$a$$** inline
# $$a$$, table cell | $$d$$ | inline

Why diverge from GitHub in lists. A paragraph content renders when it is created, before it is attached to its list item. Keeping the list exception meant re-rendering $$ contents after attach, and a paragraph moved into or out of a list would stay stale until its next render. Now the decision depends only on the paragraph's own text, so the special re-render hook (Parent.rerenderDisplayMath) is gone and Parent, ListItem and TaskListItem are back to their develop versions.

I also checked how GitHub renders in Chrome. Its inline $$ in text and in list items produces the same MathML as $...$ (27px tall, limits beside ∑), so a CSS-only override that keeps display typesetting would not have matched GitHub either.

Commit Change
db1af93 Editor: drop the list-item check and the post-attach re-render
1456a7b Export: drop the list_item branch. A tight list item's paragraph comes from marked as a block-level text token, so it is checked like a paragraph (5 tight-list cases fail without this)

Tests: 22 editor render cases and 22 export cases, including nested and task lists and - see $$c$$. pnpm -C packages/muya test (1735 passed), test:spec (1347 passed), lint, lint:types and check-circular are clean. Local Chromium e2e for the math specs is 13/13.

@Jocs
Jocs merged commit bec5f47 into marktext:develop Sep 16, 2026
6 checks passed
@Renakoni
Renakoni deleted the feature/same-line-display-math branch September 16, 2026 01:33
Jocs added a commit that referenced this pull request Sep 24, 2026
KaTeX positions `\tag` absolutely at the right edge of `.katex-html`, a block
as wide as whatever holds it, so a formula wider than the column was drawn
underneath the tag rather than beside it — the overlap reported in #4339 and
still open upstream as KaTeX/KaTeX#1983.

Size that box to the formula and float the tag instead: the tag then follows
the formula inside the scrollable extent, and `min-width: 100%` keeps it at
the right margin whenever the formula fits, which is the numbered-equation
look. Same-line display math (#4935) is covered by the same rules — its popup
shrinks to fit the formula, so the tag sat over the tail of even a short one.

The one deliberate cost: a formula that fits is now centred in the space left
of its tag, half the tag's width off the box centre. Centring the formula and
flowing the tag after it cannot both hold, and the overlap is the bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jocs added a commit that referenced this pull request Sep 24, 2026
…) (#5540)

KaTeX positions `\tag` absolutely at the right edge of `.katex-html`, a block
as wide as whatever holds it, so a formula wider than the column was drawn
underneath the tag rather than beside it — the overlap reported in #4339 and
still open upstream as KaTeX/KaTeX#1983.

Size that box to the formula and float the tag instead: the tag then follows
the formula inside the scrollable extent, and `min-width: 100%` keeps it at
the right margin whenever the formula fits, which is the numbered-equation
look. Same-line display math (#4935) is covered by the same rules — its popup
shrinks to fit the formula, so the tag sat over the tail of even a short one.

The one deliberate cost: a formula that fits is now centred in the space left
of its tag, half the tag's width off the box centre. Centring the formula and
flowing the tag after it cannot both hold, and the overlap is the bug.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wangqkoe pushed a commit to Wangqkoe/marktext that referenced this pull request Oct 21, 2026
, marktext#5365) (marktext#4935)

Render `$$...$$` on the same line as the surrounding text (marktext#4904). A `$$` span
renders as display math when its paragraph holds nothing but such formulas,
and inline otherwise — matching GitHub, except inside list items where
MarkText keeps the block layout.

Also fixes Enter on a paragraph starting with `$$` replacing it with an empty
math block (marktext#5364), and the quadratic backtracking of the inline math rule
(marktext#5365).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit bec5f47)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants