fix(desktop): make source mode math highlighting follow the editor (#5483) by Jocs · Pull Request #5484 · marktext/marktext · GitHub
Skip to content

fix(desktop): make source mode math highlighting follow the editor (#5483) - #5484

Merged
Jocs merged 8 commits into
developfrom
fix/source-mode-math-highlighting
Sep 21, 2026
Merged

Jocs merged 8 commits into
developfrom
fix/source-mode-math-highlighting

Conversation

@Jocs

@Jocs Jocs commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Closes #5483.

Source mode now highlights exactly the math syntaxes the editor is currently
reading. Three things stood between it and that, all in the same file and all
the same missing wiring.

1. The mode never read a preference

codeMirror/index.ts registers markdown-math at module load and
sourceCode.vue asked for it by name, so the source view delegated $…$ to
stex whether or not texMathDollars was on. That went unnoticed because the
preference ships enabled — but #5446 added three more math syntaxes and they
ship disabled, so the same shortcut would be wrong in the opposite direction
for them.

The mode now takes its regions from the mode spec. A fresh CodeMirror instance
reads the preferences at mount (index.vue builds the source view behind a
v-if, so every entry is a new one); a watch covers a preference changed while
the view is already open, mirroring the one #5111 added for
sourceCodeLineNumbers. The factory's defaults reproduce the old behaviour, so
a caller still passing the bare string 'markdown-math' is unaffected.

Visible change, and the intended one: with texMathDollars off, source mode
stops treating $ as a delimiter — matching what the editor has done since
#5450, and what #2002 and #5243 asked for.

2. The $ open guard had drifted

It claimed to mirror Muya's inline_math rule and pointed at
src/muya/lib/parser/rules.js — a path belonging to the engine this editor no
longer uses. It drifted further when #5449 gave the editor pandoc's three
tex_math_dollars constraints, which the guard never gained. So source mode
still opened a math span on Revenue rose from $13B to $24B. while the editor
read it as prose — the currency case #5446 used to describe the problem in the
first place.

The guard now carries the same three constraints. It still requires a closer on
the line, which is this view's own rule rather than pandoc's: CodeMirror
tokenizes per line and the multiplexer's state survives across them, so an
unclosed opener would render the rest of the file as LaTeX.

Worth knowing: the existing currency test passed throughout. The old guard
opened its span at $5 and closed it before only, which is exactly what the
assertion looked for. It now checks what is inside the spans.

3. The backslash delimiters had no regions

\(…\), \[…\], \\(…\\) and \\[…\\] were not just unhighlighted but
actively mis-highlighted, since the markdown mode reads the underscores in
\(\text{F}_\text{A}\) as emphasis — the #4121 bug in its backslash spelling.

Each pair now gets a stex region behind its own preference. The double-backslash
regions are registered first, because the multiplexer takes the first spec
matching at the cursor and the literal \( matches at the second character of
\\(. The inline forms need their own guards rather than INLINE_MATH_OPEN:
part of that one requires the character before the closer not to be a backslash,
and for \) it is one. Their content rules come from Muya's own regexes,
including the two escape rules that differ between the
extensions
.

Two behaviours that look wrong and are not

  • - \[TODO\] item becomes a formula once texMathSingleBackslash is on. That
    is pandoc's documented drawback — the extension "precludes escaping ( and
    [" — the editor agrees, and it is why the preference ships disabled. Pinned
    by a test so nobody "fixes" it.
  • \\(x\) — a double-backslash opener closed with a single-backslash closer —
    is read as math when only the single extension is on, where the editor sees an
    escaped backslash. The multiplexer searches a regexp against the rest of the
    line, so a guard cannot see the backslash in front of the opener. The shape is
    invalid in both dialects and not worth reworking shared code for; the
    well-formed \\(x\\) is unaffected, because its closer reads as an escaped
    backslash under the single-backslash rule and leaves the span unclosed.
    Commented at the guards.

Also here: a comment pass over the same file

The mode file had grown to 42% comments, several restating the code or
recording archaeology. Trimmed to what the code cannot say, and the three
delimiter pairs folded into one region helper instead of three copies of the
same literals: 156 lines to 111, no behaviour change. It edits the file this PR
rewrites, so it belongs here rather than on its own branch.

A separate bug found while verifying this — the two backslash preferences
rendering the same label, because vue-i18n collapses \\ — is its own PR,
#5485.

4. GitHub inline math had no region either

$`e=mc^2`$ opens on $, so the dollar rule claimed the span and handed
the backticks to stex as part of the formula — the mis-reading #5457 fixed in
the editor, still on show in the source view and unmoved by texMathGfm. It
now has its own region, offered ahead of the dollar ones because its opener is
the longer of the two.

5. The delimiters are set back from the formula

$, \(, \[ and their closers carried the multiplexer's delimStyle
classes but nothing ever painted them, so a delimiter was as loud as the
formula it wrapped. They now keep the surrounding colour at reduced opacity —
fading what is inherited rather than naming a hue is what makes this work
across the three CodeMirror themes and the thirty-odd palettes behind them.

0.65 is the lowest value clearing 3:1 in every shipped theme. Measured: light
7.0, graphite 6.85, ulysses 6.72, tokyo-night-light 6.0, oxocarbon-dark 7.28,
synthwave-84 6.57, material-dark 5.43, one-dark 3.67. one-dark sets the floor —
its body text only reaches 6.57:1.

Not in scope

```math is a fenced code block rather than a delimiter pair, so it reaches
stex by a different route than a multiplexer region.

\\[…\\] with texMathDoubleBackslash off is styled as a markdown link: \\
is an escaped backslash and CodeMirror's markdown mode speculatively opens a
link on the [ that follows, closing at the ]. Not math-specific — any
unclosed [ does it — and identical on develop.

Test plan

  • pnpm run lint and pnpm run typecheck
  • test/e2e/source-math.spec.ts — 13 cases: the four existing $ ones
    (one strengthened), the currency regression, the backslash forms per
    preference, the unclosed opener, the escaped bracket, and both the
    live-toggle and remount paths for every preference
  • source-code-line-numbers, source-mode-flush, issue-781-source-undo,
    issue-2372-source-selection-color — 18 passing together
  • Both open guards checked against the case tables they mirror before being
    written in: the new $ guard agrees with fix(muya): apply pandoc's tex_math_dollars constraints to $…$ (#5446) #5449 on all ten rows, where the
    old one was wrong on four

🤖 Generated with Claude Code

Jocs and others added 3 commits September 21, 2026 15:07
The `markdown-math` mode has never read a preference. `codeMirror/index.ts`
registers it at module load and `sourceCode.vue` asked for it by name, so the
source view delegated `$…$` to stex whether or not `texMathDollars` was on.

That went unnoticed because the preference ships enabled, but #5446 added
three more math syntaxes and they ship disabled — the same shortcut would be
wrong in the opposite direction for them. So the mode now takes its regions
from the mode spec, which is where the delimiters that follow can join.

Visible change, and the intended one: with `texMathDollars` off, the source
view stops treating `$` as a delimiter, matching what the editor has done
since #5450. That is what #2002 and #5243 asked for — the latter reported
patching app.asar to get it.

The factory's defaults reproduce the old behaviour, so a caller still passing
the bare string `'markdown-math'` is unaffected. A fresh CodeMirror instance
reads the preferences at mount (`index.vue` builds the source view behind a
`v-if`, so every entry is a new one); the watch covers a preference changed
while the view is already open, mirroring the one #5111 added for
`sourceCodeLineNumbers`.

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

The inline `$…$` open guard said it mirrored Muya's `inline_math` rule, and
pointed at `src/muya/lib/parser/rules.js` — a path that belongs to the engine
this editor no longer uses. It drifted further when #5449 gave the editor
pandoc's three `tex_math_dollars` constraints, which the guard never gained.

So the source view still opened a math span on `Revenue rose from $13B to
$24B.` while the editor read it as prose — the currency case #2002 and #5243
reported, on the very input #5446 used to describe the problem.

The guard now carries the same three constraints: a non-space right after the
opener, a non-space right before the closer, and no digit right after it. It
still requires a closer on the line, which is this view's own rule rather than
pandoc's: CodeMirror tokenizes per line and the multiplexer's state survives
across them, so an unclosed opener would render the rest of the file as LaTeX.

The guard only decides whether to enter stex. The region's closer is the plain
string `$` and leaves on the next one without re-checking; a closer regexp
would need the text before it, which `StringStream.match` does not expose.

The existing currency case passed throughout: the old guard opened its span at
`$5` and closed it before `only`, which is what the assertion looked for. It
now checks the span contents instead.

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

`\(…\)`, `\[…\]`, `\\(…\\)` and `\\[…\\]` render as formulas in the WYSIWYG
view since #5481 but were plain prose in the source view — and worse than
plain: `markdown-math` exists because the markdown mode reads the underscores
in `\(\text{F}_\text{A}\)` as emphasis (#4121), which it was still doing for
these.

Each pair now gets a stex region, riding the preference that governs it.

Notes:

* The double-backslash regions are registered first. The multiplexer takes the
  first spec matching at the cursor, and the literal `\(` matches at the second
  character of `\\(`.
* The inline forms need their own open guards, not `INLINE_MATH_OPEN`: part of
  that one requires the character before the closer not to be a backslash, and
  for `\)` it is one. The content rules come from Muya's own regexes, including
  the two escape rules that differ between the extensions, so both views draw
  the same span. Display math legitimately spans lines, so like `$$` it carries
  no guard.
* Known limit, commented at the guards: `\\(x\)` — a double-backslash opener
  closed with a single-backslash closer — is read as math when only the single
  extension is on. The multiplexer searches a regexp against the rest of the
  line, so a guard cannot see the backslash in front of the opener. The shape
  is invalid in both dialects, and the well-formed `\\(x\\)` is unaffected.
* `- \[TODO\] item` becomes a formula once the extension is on. That is pandoc's
  documented drawback — the extension "precludes escaping `(` and `[`" — the
  editor agrees, and it is why both preferences ship disabled.

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

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

…arn it

The file had grown to 42% comments, several of them restating the code or
recording archaeology (why the outer mode is gfm, what `setMode` used to
resolve to) rather than anything a reader needs.

The three delimiter pairs also carried three copies of the same two region
literals. Folding them into one helper removes the duplication and, with it,
the comments that existed to explain it — including the note about display
regions preceding inline ones, which was never load-bearing.

What stays is what the code cannot say: why this mode exists at all, the
constraints each open guard encodes and where they come from, why the
backslash guards cannot reuse the dollar one, the ordering the multiplexer
demands, and the one shape it reads differently from the editor.

156 lines to 111; no behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Jocs
Jocs force-pushed the fix/source-mode-math-highlighting branch from 0c4a079 to df2b050 Compare September 21, 2026 10:15
Jocs and others added 4 commits September 21, 2026 19:47
…lock (#5483)

With only `texMathSingleBackslash` on, a `\\[…\\]` block highlighted as math —
the very syntax that preference does not cover. The editor reads `\\` there as
an escaped backslash and finds no formula, so the two views disagreed exactly
where this change set out to make them agree.

The display openers span lines, so unlike the inline ones they carry no closer
lookahead. `\[` was a plain string, and `\\[` contains it one character in: the
multiplexer found it at that offset and opened a region that ran to the
matching `\]`, swallowing the block whichever way the double-backslash
preference was set.

Every opener now refuses a preceding backslash. That needs the openers to be
regexps, and regexps could not see what precedes them: `indexOf` in our
vendored `multiplexingMode` searched `string.slice(from)`, hiding the text
before the cursor. It now searches the whole line from an offset, which keeps
lookbehind working — at the cost of `^` in an opener anchoring to the start of
the line rather than to the cursor, noted there. No opener in this codebase is
anchored.

The same guard retires the limitation the previous commit documented: `\\(x\)`
is no longer read as single-backslash math either.

The regression test sets its own preferences rather than leaning on the test
before it — with the extension off the assertion is vacuous, which is how a
first version of it passed against the unfixed code.

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

`$`, `\(`, `\[` and their closers carried the multiplexer's `delimStyle`
classes but no rule ever painted them, so a delimiter was as loud as the
formula it wrapped — unlike the WYSIWYG view, which greys its own markers.

They now keep the surrounding colour at reduced opacity. Fading what is
inherited rather than naming a hue is what makes this work across the three
CodeMirror themes the app ships and the thirty-odd MarkText palettes behind
them; no single colour could sit on both `#ffffff` and `#161616`.

0.65 is the lowest value that clears 3:1 — the threshold this sort of
de-emphasised punctuation is held to — in every shipped theme. Measured:
light 7.0, graphite 6.85, ulysses 6.72, tokyo-night-light 6.0, oxocarbon-dark
7.28, synthwave-84 6.57, material-dark 5.43, one-dark 3.67. one-dark sets the
floor because its body text only reaches 6.57:1, so anything fainter
disappears there long before it does anywhere else.

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

`` $`e=mc^2`$ `` opens on `$`, so the dollar rule claimed the span and handed
the backticks to stex as part of the formula — the same mis-reading #5457 fixed
in the editor, still on show in the source view, and unaffected by the
`texMathGfm` preference either way.

It now has its own region, offered ahead of the dollar ones because its opener
is the longer of the two. With the extension off the dollar rule keeps the span
exactly as before, which is what `tex_math_dollars` alone prescribes and what
the editor does.

The three delimiter pairs were building their regions through one helper that
took a display pair and an inline pair together; gfm has no display half, so
the helper splits into `displayRegion` and `inlineRegion` and each extension
pushes what it actually has.

Out of scope, and unchanged: ```` ```math ```` is a fenced code block rather
than a delimiter pair, so it reaches stex through a different route than a
multiplexer region.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
144 comment lines across the four files this PR touches, down to 102. What
went was restatement and detail a reader does not need to act on: how
`StringStream.match` hides context, why display openers need the guard more
than inline ones, that inner modes are imported eagerly.

Two comments had drifted from the code they describe — later edits inserted
the gfm opener between the backslash comment and its constants, and the
delimiter-contrast note between the currency comment and its test. Both are
back above what they explain, and one had picked up doubled backslashes on the
way through a script.

Comments only; no behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Jocs
Jocs merged commit d701114 into develop Sep 21, 2026
11 checks passed
@Jocs
Jocs deleted the fix/source-mode-math-highlighting branch September 21, 2026 14:03
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.

Source mode math highlighting ignores the backslash delimiters and every math preference

1 participant