88
99fenced_re = re .compile (r"^[`~]+" , re .M )
1010
11+ #: leading markers that would be parsed as a new block (list, heading, block
12+ #: quote) if they appear unescaped at the start of a line.
13+ _block_prefix_re = re .compile (r"^(\s*)(>|[-+*]|#{1,6}|\d{1,9}[.)])(\s|$)" )
14+
1115
1216class MarkdownRenderer (BaseRenderer ):
1317 """A renderer to re-format Markdown text."""
@@ -35,7 +39,9 @@ def render_children(self, token: Dict[str, Any], state: BlockState) -> str:
3539 return self .render_tokens (children , state )
3640
3741 def text (self , token : Dict [str , Any ], state : BlockState ) -> str :
38- return cast (str , token ["raw" ])
42+ # a backtick always opens a code span, so it must stay escaped to
43+ # survive a re-parse as literal text.
44+ return cast (str , token ["raw" ]).replace ("`" , "\\ `" )
3945
4046 def emphasis (self , token : Dict [str , Any ], state : BlockState ) -> str :
4147 return "*" + self .render_children (token , state ) + "*"
@@ -87,7 +93,7 @@ def inline_html(self, token: Dict[str, Any], state: BlockState) -> str:
8793
8894 def paragraph (self , token : Dict [str , Any ], state : BlockState ) -> str :
8995 text = self .render_children (token , state )
90- return text + "\n \n "
96+ return _escape_block_prefix ( text ) + "\n \n "
9197
9298 def heading (self , token : Dict [str , Any ], state : BlockState ) -> str :
9399 level = cast (int , token ["attrs" ]["level" ])
@@ -99,7 +105,7 @@ def thematic_break(self, token: Dict[str, Any], state: BlockState) -> str:
99105 return "***\n \n "
100106
101107 def block_text (self , token : Dict [str , Any ], state : BlockState ) -> str :
102- return self .render_children (token , state ) + "\n "
108+ return _escape_block_prefix ( self .render_children (token , state ) ) + "\n "
103109
104110 def block_code (self , token : Dict [str , Any ], state : BlockState ) -> str :
105111 attrs = token .get ("attrs" , {})
@@ -129,6 +135,20 @@ def list(self, token: Dict[str, Any], state: BlockState) -> str:
129135 return render_list (self , token , state )
130136
131137
138+ def _escape_block_prefix (text : str ) -> str :
139+ """Backslash-escape a leading block marker on each line so that literal
140+ text is not re-parsed as a list, heading or block quote."""
141+ return "\n " .join (_escape_line_prefix (line ) for line in text .split ("\n " ))
142+
143+
144+ def _escape_line_prefix (line : str ) -> str :
145+ m = _block_prefix_re .match (line )
146+ if not m :
147+ return line
148+ indent_ , marker = m .group (1 ), m .group (2 )
149+ return indent_ + marker [:- 1 ] + "\\ " + marker [- 1 ] + line [m .end (2 ) :]
150+
151+
132152def _get_fenced_marker (code : str ) -> str :
133153 found = fenced_re .findall (code )
134154 if not found :
0 commit comments