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
- Open a document containing
~opening-only single-tilde strikethrough.
- Save it.
- 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
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
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
~opening-only single-tilde strikethrough.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
9626471dby 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:~opening-only single-tilde strikethrough\~opening-only single-tilde strikethroughclosing-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 paragraphscorpus/gfm/strikethrough.mdcarries 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
~; Determine whether the Markdown corpus can drive automated round-trip tests #135 measured the identity gap; test: assert corpus round-trip convergence for supported syntax #249 owns the corpus guard this tightens.Outcomesections of Determine whether the Markdown corpus can drive automated round-trip tests #135 and How a Markdown escape should be represented in the editor #245.Done when
\~not single-tilde strikethrough~and\~\~not double-tilde strikethrough~~.Escape precisioncases The serializer escapes characters that cannot be syntax where they sit #252 added.corpus/gfm/strikethrough.mdround-trips byte-identically, or the reason it cannot is recorded.Notes, logs, screenshots
Diagnosis
mdast-util-gfm-strikethroughcontributes{character: '~', inConstruct: 'phrasing', notInConstruct: [...]}tostate.unsafe, so the escape is emitted through the samestate.safecall thatserializeMarkdownTextinsrc/features/editor/utils/markdownText.tsalready post-processes. That function's attention pass readsATTENTION_CHARACTERS, which holds*and_only.Implementation direction
GFM strikethrough uses the same left- and right-flanking rules as
*, socanOpenis left-flanking andcanCloseis right-flanking, and the existing run analysis carries over unchanged. Three differences to account for:{atBreak: true, character: '~'}for a tilde code fence, so theopensBlockConstructguard must cover~rather than being skipped for it.Out of scope
\@,\., and\:, which belong to A bare URL is escaped on save with an escape that does not hold #241.~strike~rewritten to~~strike~~on save. That is a marker rewrite rather than an escape, and Preserve the authored form of a Markdown file on save #251'sScopealready lists it.