2929TAG_RE = re .compile (r"<([a-zA-Z][a-zA-Z0-9]*)>" )
3030"""Matches an HTML opening tag without attributes."""
3131
32+ MAX_INLINE_DEPTH = 100
33+ """Maximum nesting depth for inline constructs before rejecting input."""
34+
3235
3336class InlineParser :
3437 """Parse inline Markdown constructs into text with style/entity ranges.
@@ -137,6 +140,7 @@ def _resolve_entity(
137140 try :
138141 resolution : EntityResolution = resolve (resolvers , url , label , default )
139142 except MarkdownParseError :
143+ # Propagate without wrapping; unexpected exceptions are wrapped below.
140144 raise
141145 except Exception as err :
142146 raise MarkdownParseError (
@@ -156,8 +160,18 @@ def _resolve_entity(
156160 entity_type , data , resolution .get ("mutability" , "MUTABLE" )
157161 )
158162
159- def _parse (self , text : str ) -> tuple [str , list [Span ]]:
160- """Scan text, returning output characters and annotation spans."""
163+ def _parse (self , text : str , depth : int = 0 ) -> tuple [str , list [Span ]]:
164+ """Scan text, returning output characters and annotation spans.
165+
166+ Parameters:
167+ text: The text to scan.
168+ depth: Current recursion depth for nested constructs.
169+
170+ Raises:
171+ MarkdownParseError: If nesting exceeds ``MAX_INLINE_DEPTH``.
172+ """
173+ if depth > MAX_INLINE_DEPTH :
174+ raise MarkdownParseError ("Inline nesting too deep" )
161175 out : list [str ] = []
162176 spans : list [Span ] = []
163177 i = 0
@@ -207,7 +221,7 @@ def _parse(self, text: str) -> tuple[str, list[Span]]:
207221 result = self ._link_target (text , i )
208222 if result is not None :
209223 label_src , url , end = result
210- label_plain , label_spans = self ._parse (label_src )
224+ label_plain , label_spans = self ._parse (label_src , depth + 1 )
211225 start = len (out )
212226 out .extend (label_plain )
213227 spans .extend (
@@ -223,14 +237,14 @@ def _parse(self, text: str) -> tuple[str, list[Span]]:
223237
224238 # Emphasis: * _ ** __ *** ___
225239 if self .emphasis and ch in "*_" :
226- consumed = self ._parse_emphasis (text , i , out , spans )
240+ consumed = self ._parse_emphasis (text , i , out , spans , depth )
227241 if consumed is not None :
228242 i = consumed
229243 continue
230244
231245 # Whitelisted inline HTML tags.
232246 if ch == "<" and self .inline_html_styles :
233- consumed = self ._parse_inline_html (text , i , out , spans )
247+ consumed = self ._parse_inline_html (text , i , out , spans , depth )
234248 if consumed is not None :
235249 i = consumed
236250 continue
@@ -275,7 +289,7 @@ def _link_target(text: str, i: int) -> tuple[str, str, int] | None:
275289 return text [i + 1 : close ], text [close + 2 : paren ], paren + 1
276290
277291 def _parse_emphasis (
278- self , text : str , i : int , out : list [str ], spans : list [Span ]
292+ self , text : str , i : int , out : list [str ], spans : list [Span ], depth : int
279293 ) -> int | None :
280294 """Parse an emphasis delimiter run at index i.
281295
@@ -287,6 +301,7 @@ def _parse_emphasis(
287301 i: Index of the first delimiter character.
288302 out: Output characters accumulated so far.
289303 spans: Spans accumulated so far.
304+ depth: Current recursion depth for nested constructs.
290305
291306 Returns:
292307 The index after the closing delimiter, or None when the run
@@ -305,7 +320,7 @@ def _parse_emphasis(
305320 end = self ._find_closing (text , i + run , marker )
306321 if end == - 1 :
307322 return None
308- inner_plain , inner_spans = self ._parse (text [i + run : end ])
323+ inner_plain , inner_spans = self ._parse (text [i + run : end ], depth + 1 )
309324 start = len (out )
310325 out .extend (inner_plain )
311326 spans .extend (
@@ -349,7 +364,7 @@ def _find_closing(text: str, start: int, marker: str) -> int:
349364 return end
350365
351366 def _parse_inline_html (
352- self , text : str , i : int , out : list [str ], spans : list [Span ]
367+ self , text : str , i : int , out : list [str ], spans : list [Span ], depth : int
353368 ) -> int | None :
354369 """Parse a whitelisted inline HTML tag at index i.
355370
@@ -362,6 +377,7 @@ def _parse_inline_html(
362377 i: Index of the ``<`` character.
363378 out: Output characters accumulated so far.
364379 spans: Spans accumulated so far.
380+ depth: Current recursion depth for nested constructs.
365381
366382 Returns:
367383 The index after the closing tag, or None when the tag does
@@ -378,7 +394,7 @@ def _parse_inline_html(
378394 end = text .find (closing , match .end ())
379395 if end == - 1 :
380396 return None
381- inner_plain , inner_spans = self ._parse (text [match .end () : end ])
397+ inner_plain , inner_spans = self ._parse (text [match .end () : end ], depth + 1 )
382398 start = len (out )
383399 out .extend (inner_plain )
384400 spans .extend (
0 commit comments