Skip to content

fix: write a delimiter run the pairing sum leaves literal without an escape - #383

Merged
Azganoth merged 1 commit into
mainfrom
bug/attention-pairing-sum-escape
Sep 4, 2026
Merged

fix: write a delimiter run the pairing sum leaves literal without an escape#383
Azganoth merged 1 commit into
mainfrom
bug/attention-pairing-sum-escape

Conversation

@Azganoth

@Azganoth Azganoth commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Summary

The serializer escaped a ** run that the parser already reads as literal text, writing _\*\*underscore and one asterisk stay literal* for corpus/commonmark/emphasis.md line 121. The safety check saw a left-flanking run and escaped it as an opener without asking whether any closer on the line could match it. CommonMark declines a pair whose two runs sum to a multiple of three unless both lengths are, so a two-marker opener and the single marker closing it reach nothing and the run is literal without a backslash. This is the class #252, #256, #257, #258, #271, #289, #292, #293, #336, and #353 removed, reaching the one pairing constraint none of them covered.

That rule holds only where one of the two runs can also play the other part, and by flanking alone neither run in this shape can, so the constraint alone does not reach it. Measured with mdast-util-from-markdown 2.0.3 and the GFM extension from the project's dependency tree, **b* and .**b* both pair while _**b* and ~**b* stay literal. micromark's attention tokenizer carries a clause the spec prose does not: a run is read as able to play a part wherever the character on that side is another attention marker, and micromark-extension-gfm puts the tilde into that set. The roles are therefore read as micromark settles them rather than as the flanking rules alone state them, which is what puts this shape within reach of the sum at all. That widening only ever keeps an escape on its own, so the sum is what the relaxation rests on. A tilde run is left on plain flanking, because micromark-extension-gfm-strikethrough tokenizes it without the widening and closes only with a run of its own length.

The sum is applied in both directions of the pairing search, which is what lets the closing run relax too: with the opener no longer escaped, the closer would otherwise have found an opener it cannot actually pair with and kept a backslash of its own.

docs/decisions.md records this beside the merged-run rule under Accept Milkdown GFM preset behavior, since it is that same sum reaching the seam where both runs are in hand rather than one of them belonging to a mark.

Related Issue

Closes #372

Verification

markdownCompatibility.test.ts covers both directions under Escape precision. Relaxation is asserted for _**a*, ~**a*, and x****strong**; retention for a\*\*\*b***, where both lengths are multiples of three and the pair forms on the exception the rule carves out, and for a\*\*b**, where a sum of four is not a multiple of three at all. Both retention cases also show that a backslash on the opening run is what holds the pair apart, and that the closing run needs none once that opener has stopped being a delimiter.

Because closure and convergence both already hold for this class, the corpus round trip and the document-preservation guard cannot see it. _**underscore and one asterisk stay literal* and x****strong** are therefore asserted in the block that writes a source as authored and reopens it as the same document.

Measured by mounting the corpus files and saving them, commonmark/emphasis.md goes from three differing lines to two: line 121 is now written as authored, leaving the two nesting-collapse lines #213 settled, which no serializer change reaches. gfm/strikethrough.md stays at its three, so the widened roles moved nothing there.

pnpm check:frontend passes.

Not verified: no manual pass in the assembled application, and pnpm check:backend was not run. The change is serializer behavior with no rendered surface of its own, and it touches no Rust or Tauri code.

Notes

  • x\*\*\*\*strong** was covered as a run that keeps its escape, under the comment about a mark's far delimiters being decided by text no handler has seen. Its document is a single text node holding both runs and no mark at all, so it was always the same-node path; four markers and the two closing them sum to six and pair with nothing. Both spellings parse to paragraph("x****strong**"), so the row moved to the coverage asserting the bytes and the reopened document together rather than being dropped.
  • The merged-run case keeps answering the sum as though the mark's far delimiters can play both parts. Nothing here narrows it: a handler writing a run beside a mark still cannot see the text past that mark.

The safety check escaped a left-flanking run without asking whether any
closer on the line could match it. CommonMark declines a pair whose two
runs sum to a multiple of three unless both lengths are, so a two-marker
opener and the single marker closing it reach nothing and the run is
already literal without a backslash.

That rule holds only where one of the two runs can also play the other
part, which this shape reaches only once the roles are read as micromark
settles them rather than as the flanking rules alone state them: a run
facing another attention marker can play both, and the GFM extension
puts the tilde in that set. Widening the roles keeps an escape wherever
it reaches anything else, so the sum is what the relaxation rests on.

That sum also reaches a case covered as one that keeps its escape.
`x\*\*\*\*strong**` holds both of its runs in one text node, where four
markers and the two closing them sum to six, so it now writes bare and
moves to the coverage asserting the bytes and the reopened document
together.
@Azganoth Azganoth added the Bug Something isn't working label Sep 4, 2026
@Azganoth Azganoth self-assigned this Sep 4, 2026
@Azganoth
Azganoth enabled auto-merge (squash) September 4, 2026 08:08
@Azganoth
Azganoth merged commit 0665dad into main Sep 4, 2026
3 checks passed
@Azganoth
Azganoth deleted the bug/attention-pairing-sum-escape branch September 4, 2026 08:16
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.

The serializer escapes a run the multiple-of-three rule already leaves literal

1 participant