Skip to content

Port upstream 4935: render same-line double-dollar display math#141

Merged
Renakoni merged 3 commits into
mainfrom
sync/upstream-4935-same-line-display-math
Jul 16, 2026
Merged

Port upstream 4935: render same-line double-dollar display math#141
Renakoni merged 3 commits into
mainfrom
sync/upstream-4935-same-line-display-math

Conversation

@Renakoni

Copy link
Copy Markdown
Owner

Summary

Port of marktext#4935 (issue marktext#4904, authored upstream by this repo's owner) into the vendored Muya tree. $$...$$ display math only rendered when the fences sat on their own lines; markdown copied from pandoc/GitHub-style sources showed raw $$ instead. On Android, paste-from-other-apps is a primary input path, so this lands as a paste-fidelity fix.

Changes (adapted onto our vendored tree, byte-equivalent to upstream)

  • inlineRenderer/rules.ts + lexer.ts — a dedicated inlineDisplayMathRule recognizes non-empty single-line $$...$$ spans, tried before the unchanged $...$ rule. Empty $$$$, unclosed openers, and multiline fences are excluded — multiline $$ still goes through the block parser, and single-dollar math (including soft line breaks) is untouched.
  • inlineRenderer/renderer/inlineMath.ts$$ tokens render through KaTeX in display mode; the KaTeX cache key now includes the mode; preview content offsets use the real marker length instead of a hardcoded 1.
  • config/index.ts + assets/styles/inlineSyntax.css — new mu-display-math class: the hidden preview renders as a full-width centered block, while the editing state (caret inside) stays inline-block so the source is editable in place.
  • block/base/format.ts — format/cursor offsets for inline_math derive from token.marker.length, so selecting or toggling a $$x$$ span targets the content correctly.
  • Same-line $$ stays an inline token (never promoted to math-block): source round-trips losslessly and paste behavior is preserved.

Tests: upstream's Muya unit specs ported (sameLineDisplayMath.spec.ts, plus formatToggle/pasteBlockMerge additions). Upstream's real-Chromium e2e (our vendored tree has no Muya e2e harness) is replaced by an app-side Playwright spec (mobile-display-math.spec.ts) that verifies what happy-dom cannot — the shipped CSS lays the preview out as a full-width centered block in the real editor, and $x$ stays inline.

node_modules/@muyajs/core synced per the vendor contract (the contract test passes in the app suite).

Verification

  • Focused Muya specs: 3 files / 48 tests passed.
  • Full Muya app gate: 212 files / 1435 tests passed. Full app unit suite: 75 files / 577 tests passed (includes the vendor-sync contract).
  • pnpm typecheck and focused ESLint passed. Muya tsc --noEmit reports only the two pre-existing import.meta.glob errors in utils/prism/loadLanguage.ts (verified identical on a clean tree — zero new type errors). Muya's lint:css cannot run in the vendored tree (the monorepo-root stylelint config was never vendored); the CSS addition is byte-identical to upstream's, which passed stylelint there.
  • New e2e passed. Failure path proven: with node_modules reverted to main's Muya files, the spec fails (.mu-math.mu-display-math never appears — the raw-$$ behavior); restored, it passes.

Notes

  • Upstream PR is still open; divergence from any post-review upstream changes is accepted (Android product first).
  • Labeled "feature" upstream; adopted here as a paste-fidelity fix per the repo owner's scope decision.

@Renakoni

Copy link
Copy Markdown
Owner Author

Addressed the Codex P1 (super-linear backtracking in the display-math rule) in 92c86bb.

Fix. inlineDisplayMathRule is replaced by execInlineDisplayMath, a linear-time scanner with the same semantics: content ends at the first unescaped same-line $$, a backslash always escapes the following character (the regex's greedy pair-first behavior, so an escaped closer extends the content), and a returned match always has an unescaped closer — the escape-parity group is always empty, so the lexer's isLengthEven gate is unchanged. The eslint-disable regexp/no-super-linear-backtracking suppression is gone.

Equivalence evidence. An exhaustive parity sweep over all 349,525 strings of length ≤ 9 from the alphabet {$, \, a, \n} (prefixed with $$) compares the old regex (through the parity gate) against the scanner: 0 mismatches on both the accepted set and the matched content.

Performance evidence. Reproduced the reported blow-up, then eliminated it: $$ + 131,072 backslashes + $ took the old regex 10,045ms; the scanner takes 0ms. (65,536: 2,581ms → 0ms.)

Regression tests added (spec now 15 tests): the adversarial 131,072-backslash input asserted under 500ms and never matched as display math; escaped-closer-extends-content; even-backslash-run content; escaped-only-closer rejected; dangling backslash / backslash-before-newline rejected.

Full re-verification: Muya app gate 212 files / 1440 tests, app suite 75 files / 577 tests (vendor-sync contract included), typecheck, and the display-math e2e all pass.

Note: the pre-existing single-dollar inline_math rule (and upstream's copy of both) shares the same backtracking shape; that's out of this PR's scope but worth carrying upstream together with this fix.

@Renakoni
Renakoni merged commit 4dfb416 into main Jul 16, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant