Skip to content

fix: pair a delimiter run typed into literal text with the run that text opens - #346

Merged
Azganoth merged 1 commit into
mainfrom
bug/recognize-typed-emphasis-delimiter-runs
Sep 1, 2026
Merged

fix: pair a delimiter run typed into literal text with the run that text opens#346
Azganoth merged 1 commit into
mainfrom
bug/recognize-typed-emphasis-delimiter-runs

Conversation

@Azganoth

@Azganoth Azganoth commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Summary

A * or _ run typed into literal text now pairs with the run that same text opens, over as many delimiters as the shorter of the two spells, leaving the surplus literal. ***text* reads as two literal asterisks before italic text, _**text** as a literal underscore before bold text, and _**text**_ as italic bold.

The preset input rules read one content group between delimiters of equal length, so a run closed against an unequal or differently spelled one matched nothing and stayed literal. _**text** failed for a second reason: the strong rule's guard counts _ as a word character, where CommonMark counts it as punctuation.

The pass that already answers a delimiter typed beside a mark now answers one typed beside literal text as well. It acts only where the preset declined, because a rule that matches consumes the typed character before the plugin sees it, so the preset's word, colon, and slash guard is reproduced rather than replaced and lead**text** still types literal.

Where the closing run is the shorter one, the pair is read once the caret leaves that run rather than as the character lands. The rows this issue lists are continuations of each other — _**text* is a prefix of _**text** — and an input rule reads only the text before the caret, so firing on the keystroke serves one row at the cost of the other. Deferring serves both, and is how typed link source already reaches the object it spells.

Three constraints the pairing had to meet, each measured rather than assumed:

  • Stored marks are reset after the mark is added, not before, because any later step clears them. Without it the next character extends the construct just closed.
  • The transaction suppresses source-projection entry, as an input rule already does, or the projection takes the next character into that construct. Without it _**text**_ committed as _**text_**.
  • A closing run that could equally open a construct is refused outright rather than deferred. Without that guard **a*b** reached the projection commit path and lost two typed delimiters, which is worse than the literal text it replaced.

Related Issue

Closes #232

Verification

attentionPairing.test.ts gains a typing a delimiter run into literal text block. Ten rows type a run into an empty paragraph and assert the document, the saved Markdown, and the document the saved file reopens as; each row also asserts that stripping the backslashes from the saved Markdown reproduces exactly the characters typed, which is where No input inserts or duplicates a delimiter character that was not typed is pinned. Four further tests cover the behaviors that carry the fix: a run held until the caret leaves it and then extended into bold, a run closed against a letter left literal, a run following a word left literal, and an unequal tilde run left literal.

Measured by driving the editor mount across a twenty-six input matrix and comparing the document and saved bytes before and after: three inputs beyond the issue's table change behavior, and each moves to the reading CommonMark gives the same source — ****text*, ____text_, and ***text* rest. Nothing else in the matrix moves, including **text**, __text__, ***text**, *__text__, ***text***, ~~text~~, a**b**, and snake_case_name.

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

Reading a pair once the caret leaves the run is new behavior rather than a narrower rule, and is recorded in docs/specification.md and docs/decisions.md. It qualifies the conservatism recorded for the strikethrough input rule rather than contradicting it: that decision reasons from an input rule not being able to know another delimiter is coming, which is the limitation deferring removes. The conservatism itself is kept where deferring cannot help, which is why a closer that could also open is refused rather than held.

A run still open to growth when the document is saved is written as the literal text it is. What the file holds matches what the editor shows, and it reopens as the same document, so the two agree; it does mean the same keystrokes save differently depending on whether the caret moved first. No save-time finalize was added for it.

A tilde is left out of this pass. A strikethrough is spelled only by runs of equal length, so an unequal tilde run has nothing to pair, and the input rule Leafdown owns for it already holds a run literal until the author closes it.

@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 8780c58 into main Sep 1, 2026
3 checks passed
@Azganoth
Azganoth deleted the bug/recognize-typed-emphasis-delimiter-runs branch September 1, 2026 08:31
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.

Typing nested emphasis delimiter runs leaves literal text or duplicates runs

1 participant