Skip to content

🐛 fix(markdown): preserve links after bangs - #1289

Merged
gaborbernat merged 1 commit into
tox-dev:mainfrom
gaborbernat:fix/markdown-bang-link
Oct 10, 2026
Merged

gaborbernat merged 1 commit into
tox-dev:mainfrom
gaborbernat:fix/markdown-bang-link

Conversation

@gaborbernat

@gaborbernat gaborbernat commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

to_markdown() turns !<a href="/x">x</a> into ![x](/x), which CommonMark reads as an image instead of a literal ! followed by a link. CommonMark 6.4, Example 593 gives \![x](/x) for this case.

Fix

Escape a live ! only when the next emitted marker opens a link. This also covers empty and reference-style links while leaving spaces and already escaped punctuation alone. Escaping only ! keeps the link in ordinary Markdown and avoids a raw HTML fallback for valid link syntax. Single-byte marker opens use a direct append, keeping normal Markdown rendering below the prior instruction count.

Turbohtml: literal ! and link preserved. The before and after files below contain the exact to_markdown() output for the same input. This hunk comes from git diff --no-index --unified=0:

diff --git a/before.md b/after.md
index 77cdcb9..671a6d1 100644
--- a/before.md
+++ b/after.md
@@ -1 +1 @@
-![x](/x)
\ No newline at end of file
+\![x](/x)
\ No newline at end of file

Competitors

markdownify 1.2.3 and html2text 2025.4.15: image instead of link

markdownify output:

diff --git a/after.md b/markdownify.md
index 671a6d1..77cdcb9 100644
--- a/after.md
+++ b/markdownify.md
@@ -1 +1 @@
-\![x](/x)
\ No newline at end of file
+![x](/x)
\ No newline at end of file

markdownify emits ![x](/x), which md4c and markdown-it-py parse as an image. The source has a literal exclamation mark followed by a link, so we escape !.

html2text output:

diff --git a/after.md b/html2text.md
index 671a6d1..9541679 100644
--- a/after.md
+++ b/html2text.md
@@ -1 +1,2 @@
-\![x](/x)
\ No newline at end of file
+![x](/x)
+

html2text produces the same image syntax with an extra blank line. The blank line does not change the parse; the escaped ! form preserves the source structure and follows CommonMark Example 593.

Performance

On CPython 3.15.0 with GCC 16.2.1, Valgrind Callgrind 3.27.1, and a fixed hash seed, 20,000 representative Markdown renders changed from 221,941,063 to 221,561,063 instructions (−0.17%). A five-link paragraph changed from 255,841,056 to 253,921,056 (−0.75%).

@gaborbernat gaborbernat added the bug Something isn't working label Oct 10, 2026
CommonMark treats a literal ! beside a link bracket as an image. Escape
that bang when it is still live, retaining both the link and the text.

Append one-byte markers in one step to offset the check on the normal
rendering path.
@gaborbernat
gaborbernat force-pushed the fix/markdown-bang-link branch from 4d356b4 to 38cf8ed Compare October 10, 2026 17:29
@codspeed

codspeed Bot commented Oct 10, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 582 untouched benchmarks
⏩ 32 skipped benchmarks1


Comparing gaborbernat:fix/markdown-bang-link (38cf8ed) with main (d4701ec)

Open in CodSpeed

Footnotes

  1. 32 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@gaborbernat
gaborbernat merged commit 96ea4ec into tox-dev:main Oct 10, 2026
57 checks passed
@gaborbernat
gaborbernat deleted the fix/markdown-bang-link branch October 10, 2026 21:56
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.

1 participant