Skip to content

Add MD_FLAG_STRIKETHROUGH_DOUBLE_ONLY - #429

Open
bevali wants to merge 1 commit into
mity:masterfrom
bevali:strikethrough-double-only
Open

bevali wants to merge 1 commit into
mity:masterfrom
bevali:strikethrough-double-only

Conversation

@bevali

@bevali bevali commented Sep 16, 2026

Copy link
Copy Markdown

What/Why?

GFM accepts one or two tildes as strike-through delimiters, and MD4C
implements that faithfully. This PR does not change that — it adds an opt-in
flag for callers who need ~~ only.

The motivating case is LLM-generated text, where a lone ~ is very commonly
used as shorthand for "approximately". Two of those in the same block pair up
and strike out everything between them.

A concrete example from a production AI assistant:

Low end: ~$16/month ($192/year) by downgrading Netflix and cutting All Access. Mid range: ~$32/month ($384/year).

renders today as:

<p>Low end: <del>$16/month ($192/year) by downgrading Netflix and cutting All Access. Mid range: </del>$32/month ($384/year).</p>

79 characters struck out mid-sentence, swallowing the "Mid range:" label, so
the sentence reads as though a figure had been retracted.

Currency amounts are especially prone to this because of the flanking rules:
in ~$1,359 the tilde is preceded by whitespace (so it may open) and followed
by punctuation (so it may also close), which makes every such tilde eligible
for both roles.

How common this is, measured over 576 full-text responses from one production
assistant surface:

  • 128 responses contained at least one ~
  • 21 had two or more in the same block, and therefore rendered an unintended
    strike-through
  • 0 contained a deliberate ~~…~~ span

So on that surface every strike-through the renderer produced was unintended.
The damage is bounded — pairing is block-scoped, so a span can cross a soft
line break inside a paragraph but not a blank line, heading, or list item —
but within a block it is still disfiguring.

Turning MD_FLAG_STRIKETHROUGH off entirely is the only lever available today,
and that also gives up deliberate ~~…~~. Hence a flag.

The change

MD_FLAG_STRIKETHROUGH_DOUBLE_ONLY makes length-1 tilde runs inert, so a
single ~ is emitted as literal text. It is a one-condition change in
md_collect_marks: the mark is simply never created, so md_analyze_tilde
and the pairing logic are untouched.

  • Opt-in. Default behavior, MD_DIALECT_GITHUB, and the existing
    spec-strikethrough.txt suite are all unchanged.
  • No effect unless MD_FLAG_STRIKETHROUGH is also set.
  • MD_FLAG_SUBSCRIPTS still claims single tildes when enabled, since
    subscripts are recognized before strike-through.
  • Mismatched and over-long runs (~text~~, ~~~bar~~~) behave as before.

md2html gains --fstrikethrough-double-only, and README documents the flag.

I did not add a CHANGELOG entry, since there is no unreleased section right
now — happy to add one wherever you prefer.

Testing

New test/spec-strikethrough-double-only.txt with 8 examples, picked up
automatically by scripts/run-tests.py. It covers: the real-world
approximation case both with and without the flag, ~~x~~ still striking,
~x~ not striking, mismatched and over-long runs, the flag being inert
without MD_FLAG_STRIKETHROUGH, and the MD_FLAG_SUBSCRIPTS interaction.

Full suite passes, including the 30 pathological-input tests:

Testing spec-strikethrough-double-only.txt
8 passed, 0 failed, 0 errored, 0 skipped

Testing spec-strikethrough.txt
5 passed, 0 failed, 0 errored, 0 skipped

30 passed, 0 failed, 0 errored     (pathological inputs)

Built with -Wall -Werror on clang (macOS). I compiled the sources directly
rather than through CMake, as CMake was not available in my environment, so
the CMake and Windows build paths are only exercised by CI here.

PR Checklist

  • Opt-in flag; default behavior unchanged
  • Existing test suite passes
  • New spec tests added
  • README updated
  • md2html option and --help updated

GFM accepts both one and two tildes as strike-through delimiters. For input
where a lone '~' is used as a shorthand for "approximately", two such tildes
in the same block pair up and strike out everything between them.

With the new flag, only ~~two tildes~~ delimit a strike-through span and a
single tilde is left as literal text. The flag is opt-in, so the default GFM
behavior is unchanged. Single tildes claimed by MD_FLAG_SUBSCRIPTS are
unaffected, as subscripts are recognized first.
@mity

mity commented Sep 16, 2026

Copy link
Copy Markdown
Owner

I'll come back to review this later, we're now in feature freeze and approaching release.

Are we inventing a new syntax rule with this, or is there some notable prior implementation(s) which recognize only the two-char marks?

@znewt99

znewt99 commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Are we inventing a new syntax rule with this, or is there some notable prior implementation(s) which recognize only the two-char marks?

Requiring double tilde for strikethrough is how GitHub Flavored Markdown originally worked. When GitHub switched from redcarpet to cmark, they accidently switch strikethrough from two to one tildes. See github/cmark-gfm#71

The GFM spec wasn't updated until 2022 to say,

Strikethrough text is any text wrapped in a matching pair of one or two tildes (~).

Before the change in 2022, the spec said,

Strikethrough text is any text wrapped in two tildes (~).

cmark-gfm includes an option for only parsing strikethroughs if surrounded by exactly 2 tildes.

@mity

mity commented Sep 20, 2026

Copy link
Copy Markdown
Owner

After some thinking I'm wondering whether this really has to be added into the parser code.
(Admittedly, my motivation is keeping the precious bits in MD_PARSER::flags for something more worthy.)

Consider this as a workaround:

  1. Enable both MD_FLAG_STRIKETHROUGH and MD_FLAG_SUBSCRIPT. MD_FLAG_SUBSCRIPT then actually "steals" the single-tilde marks from MD_FLAG_STRIKETHROUGH.

  2. If you're using your own renderer (and don't support the subscripts), you may then just ignore the md_enter/leave_span(MD_SPAN_SUBSCRIPT). If you're using the libmd4c-html or the md2html, we might teach those to do exactly that with some command line option instead of emitting <sub>...</sub>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants