From 873b2ad5b1b6ef3c6ed808a47ad6ac579ff574fa Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Mon, 20 Jul 2026 03:53:40 +0800 Subject: [PATCH] Lengthen the pre code fence so backtick runs can't close it early `convert_pre` emitted a fixed three-backtick fence, so a `
` whose content contains a ``` run (or longer) produced an ambiguous fenced block that reparses to different HTML: `foo\n```\nbar` closed the fence at the inner ```, splitting one code block into a code block, a paragraph, and an empty code block. Compute the fence from the longest backtick run in the content (at least three, matching the previous default), the same mechanism `convert_code` already uses for inline code. Per CommonMark section 4.5 the closing fence must have at least as many backticks as the opening. --- markdownify/__init__.py | 8 +++++++- tests/test_conversions.py | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/markdownify/__init__.py b/markdownify/__init__.py index 28cdaf6..99b3c63 100644 --- a/markdownify/__init__.py +++ b/markdownify/__init__.py @@ -702,7 +702,13 @@ def convert_pre(self, el, text, parent_tags): else: raise ValueError('Invalid value for strip_pre: %s' % self.options['strip_pre']) - return '\n\n```%s\n%s\n```\n\n' % (code_language, text) + # Use a fence long enough that no backtick run inside the code can close + # it early (CommonMark section 4.5: the closing fence must have at least + # as many backticks as the opening). + max_backticks = max((len(match) for match in re.findall(re_backtick_runs, text)), default=0) + fence = '`' * max(3, max_backticks + 1) + + return '\n\n%s%s\n%s\n%s\n\n' % (fence, code_language, text, fence) def convert_q(self, el, text, parent_tags): return '"' + text + '"' diff --git a/tests/test_conversions.py b/tests/test_conversions.py index c95483c..0d74845 100644 --- a/tests/test_conversions.py +++ b/tests/test_conversions.py @@ -311,6 +311,15 @@ def test_pre(): assert md("foo
\nbar\nbaz", sub_symbol="^") == "\n\nfoo\n\n```\nbar\n```\n\nbaz" +def test_pre_backticks(): + # A backtick run inside the code must not close the fence early: the fence + # needs at least one more backtick than the longest run in the content. + assert md('foo\n```\nbar') == '\n\n````\nfoo\n```\nbar\n````\n\n' + assert md('') == '\n\n````\n```\n````\n\n' + assert md('```a````b') == '\n\n`````\na````b\n`````\n\n' + assert md('plain') == '\n\n```\nplain\n```\n\n' + + def test_q(): assert md('fooquotebar') == 'foo "quote" bar' assert md('fooquotebar') == 'foo "quote" bar'