diff --git a/src/mistune/renderers/markdown.py b/src/mistune/renderers/markdown.py index de7ba3a..cd49a9d 100644 --- a/src/mistune/renderers/markdown.py +++ b/src/mistune/renderers/markdown.py @@ -7,6 +7,7 @@ from ._list import render_list, render_list_item fenced_re = re.compile(r"^[`~]+", re.M) +_backtick_run_re = re.compile(r"`+") #: leading markers that would be parsed as a new block (list, heading, block #: quote) if they appear unescaped at the start of a line. @@ -77,7 +78,17 @@ def image(self, token: Dict[str, Any], state: BlockState) -> str: return "!" + self.link(token, state) def codespan(self, token: Dict[str, Any], state: BlockState) -> str: - return "`" + cast(str, token["raw"]) + "`" + raw = cast(str, token["raw"]) + # fence with one more backtick than the longest run in the content so + # an inner run can never be mistaken for the closing delimiter + longest = max((len(m.group()) for m in _backtick_run_re.finditer(raw)), default=0) + marker = "`" * (longest + 1) + # a re-parse strips one leading and trailing space when the content + # both begins and ends with a space (and is not all spaces); pad so a + # leading/trailing backtick or wrapping space survives that strip + if raw.strip() and (raw[0] == "`" or raw[-1] == "`" or (raw[0] == " " and raw[-1] == " ")): + raw = " " + raw + " " + return marker + raw + marker def linebreak(self, token: Dict[str, Any], state: BlockState) -> str: return " \n" diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 530fa98..fa34515 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -46,6 +46,18 @@ def test_escaped_marker_inside_list_item(self): def test_escaped_backtick(self): self.assert_round_trip(r"\`not code\`" + "\n") + def test_codespan_with_backticks(self): + # a code span whose content contains backticks (or is wrapped in + # spaces) must be re-fenced so the content survives a re-parse + for text in ( + "``a`b``\n", + "`` `code` ``\n", + "`` ` ``\n", + "` x `\n", + "```a``b```\n", + ): + self.assert_round_trip(text) + def test_real_markers_preserved(self): for text in ( "* bullet\n",