Skip to content

The serializer escapes tildes that cannot form a strikethrough where they sit #256

Description

@Azganoth

Summary

Saving escapes every ~ in text on the possibility that it could be a strikethrough marker rather than on whether a marker can form where it sits. #252 made that check precise for *, _, [, and (; tildes reach the same seam and were left outside its scope. This is the largest escape class left in the corpus.

Steps to reproduce

  1. Open a document containing ~opening-only single-tilde strikethrough.
  2. Save it.
  3. Read the file.

Expected behavior

~opening-only single-tilde strikethrough. One tilde run with no counterpart cannot form a strikethrough, so no escape is needed.

Text that would parse differently without an escape keeps it. Both directions matter, as in #252: a run that can open beside a run that can close must stay escaped.

Actual behavior

Measured against 9626471d by driving the editor mount used by the plugin tests. Every row below is stable across a second save, so the corpus guard is green on all of them:

Document text Saved
~opening-only single-tilde strikethrough \~opening-only single-tilde strikethrough
closing-only double-tilde strikethrough~~ closing-only double-tilde strikethrough\~\~
~ opening space~ and ~closing space ~ remain literal. \~ opening space\~ and \~closing space \~ remain literal.
Inline ~~~three tildes do not strike~~~ remains literal. Inline \~\~\~three tildes do not strike\~\~\~ remains literal.
Empty candidates: ~~ and ~~~~ remain literal inline text. Empty candidates: \~\~ and \~\~\~\~ remain literal inline text.
This ~~does not cross / a paragraph boundary~~. in two paragraphs both runs escaped

corpus/gfm/strikethrough.md carries 54 backslashes on save and 16 of its lines differ from source by escaping alone. It is the only scoped corpus file whose escaping #252 left untouched.

Related context

Done when

  • The cases above save without an escape they do not need.
  • A tilde run that can reach a counterpart keeps its escape, including \~not single-tilde strikethrough~ and \~\~not double-tilde strikethrough~~.
  • A tilde run that could open a fenced code block at a line start keeps its escape.
  • Regression coverage asserts both directions, alongside the Escape precision cases The serializer escapes characters that cannot be syntax where they sit #252 added.
  • corpus/gfm/strikethrough.md round-trips byte-identically, or the reason it cannot is recorded.

Notes, logs, screenshots

Diagnosis

mdast-util-gfm-strikethrough contributes {character: '~', inConstruct: 'phrasing', notInConstruct: [...]} to state.unsafe, so the escape is emitted through the same state.safe call that serializeMarkdownText in src/features/editor/utils/markdownText.ts already post-processes. That function's attention pass reads ATTENTION_CHARACTERS, which holds * and _ only.

Implementation direction

GFM strikethrough uses the same left- and right-flanking rules as *, so canOpen is left-flanking and canClose is right-flanking, and the existing run analysis carries over unchanged. Three differences to account for:

  • Only a run of one or two tildes can form a strikethrough, so a run of three or more never needs the escape at all. The corpus exercises this directly.
  • Strikethrough does not cross a paragraph boundary. The existing sibling scan already respects that, because it never leaves the phrasing parent.
  • The core unsafe list also carries {atBreak: true, character: '~'} for a tilde code fence, so the opensBlockConstruct guard must cover ~ rather than being skipped for it.

Out of scope

Metadata

Metadata

Assignees

Labels

BugSomething isn't working

Projects

Status
Ready

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions