Skip to content

fix: pair a delimiter typed beside a mark with the literal one across it - #345

Merged
Azganoth merged 1 commit into
mainfrom
bug/pair-a-delimiter-typed-beside-a-mark
Sep 1, 2026
Merged

fix: pair a delimiter typed beside a mark with the literal one across it#345
Azganoth merged 1 commit into
mainfrom
bug/pair-a-delimiter-typed-beside-a-mark

Conversation

@Azganoth

@Azganoth Azganoth commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Summary

A *, _, or ~ typed against a marked run now pairs with the matching literal run on the other side of it, so the two become the construct the line spells instead of both staying as content.

Typing the closing _ of _**text** reached a document of three siblings — a literal _, the strong run, and another literal _ — which the file recorded faithfully as \_**text**\_. Opening a file holding _**text**_ gives emphasis wrapping strong emphasis, so the parser and the editing path disagreed about the same characters.

The character reaches Milkdown's emphasis input rule while the source projection is open, so the rule matches the projected literal _**text**_ and offers emphasis over a literal **text**. The projection then commits: its session covers only the mark's own source, a character typed outside it leaves that source unchanged, and the clean finalize restores the mark's original content over the range, discarding what the rule built. Both answers are wrong on their own, and the commit lands a dispatch later than the character arrives in.

attentionPairing decides the pairing once the document has settled and the run exists rather than its projection. The typed character is held in plugin state until hasActiveSourceProjection is false, then mapped forward and answered against the real marks.

Three constraints it holds:

  • It answers the character typed in this session, not the shape of the document. Escapes do not survive a load, so \_**text**\_ opens as the same three siblings a paired document would have started from; only the typed character separates them.
  • It checks flanking, so a_**text** keeps its markers literal. The run always faces the mark's own delimiters, which are punctuation, so both CommonMark tests collapse to the character on the outer side and the intraword rule _ adds falls out of the same neighbour.
  • It reaches only the typed delimiter and its counterpart. Reinterpreting the literal text beside the run would turn other characters into syntax, a literal backtick pair being the clear case.

Related Issue

Closes #344

Verification

attentionPairing.test.ts covers the four rows the issue measures — both delimiter characters, closing after the run and opening before it — asserting the document once the projection settles, the bytes, and the document the saved file reloads as. Reading the projection instead of the document is what produced that issue's previous table, so the assertions park the caret in a following paragraph first.

The rows that must not pair are covered beside them: a file that already escapes a delimiter on each side of a run, a run that cannot open (a_**text**), one that cannot close (_**text**a), runs of unequal length, a span already carrying the mark it would apply, and an ordinary _text_ with no mark between, which the existing input rule still owns.

sourceProjection.test.ts held one expectation encoding the behavior this replaces, for the issue's second row, and it was updated rather than worked around. What it exists to prove is unchanged and still asserted: the input rule must not swallow the run into literal text, which getEditorTextContent now pins as text rather than __text__. Its caret moves to a following paragraph because the pairing carries the run to the end of its own.

pnpm check:frontend passes. The backend is untouched, so pnpm check:backend was not run.

Not verified: the manual pass over corpus/ in the running application, which covers rendering, interaction, and navigator behavior beyond the automated round-trip.

Notes

The saved nesting order is the schema's, not the order the delimiters were typed in: __*text*__ is written *__text__*, and ~~**text**~~ is written **~~text~~**. Measured with this plugin disabled, a plain open of either form produces one identical document and the same output, so the order is lost when the file is read and no serializer change reaches it. It is the cross-type sibling of the same-type collapse #251 already excludes, and that issue now records it there. The tests assert the mark set and the reload rather than a nesting order.

A delimiter separated from the run by text, such as _a**b**_, is not covered. That is general delimiter recognition, which #232 owns.

Typing the delimiter on the inner side of a closing run remains out of scope, as #344 and #232 both record: it becomes part of the run's content, and CommonMark does not require the editor to relocate it.

The typed character reaches the emphasis input rule while the source
projection is open, so the rule matches the projected literal and the
projection's later commit discards what it built. The pairing is
therefore decided once the projection has settled and the run exists,
which is a dispatch later than the character arrives in, so the typed
character is held in plugin state until then.

It is driven by the character typed in this session rather than by the
shape of the document: escapes do not survive a load, so a file holding
a delimiter on each side of a run opens as the same three siblings, and
only the typed character tells the two apart.
@Azganoth Azganoth added the Bug Something isn't working label Sep 1, 2026
@Azganoth Azganoth self-assigned this Sep 1, 2026
@Azganoth
Azganoth merged commit 8c6fe9a into main Sep 1, 2026
3 checks passed
@Azganoth
Azganoth deleted the bug/pair-a-delimiter-typed-beside-a-mark branch September 1, 2026 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A delimiter typed beside a strong run does not pair with the literal delimiter across it

1 participant