Skip to content

Commit 0938fb7

Browse files
committed
fix: add max_emphasis_depth
1 parent 9946c92 commit 0938fb7

3 files changed

Lines changed: 57 additions & 6 deletions

File tree

src/mistune/inline_parser.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
_REGEX_META_CHARS = set(r"()[]{}?*+|.^$")
2929
_CHARREF_PREFIX = re.compile(r"(#[0-9]{1,7};|#[xX][0-9a-fA-F]+;|[^\t\n\f <&#;]{1,32};)")
30+
DEFAULT_MAX_EMPHASIS_DEPTH = 20
3031

3132
AUTO_EMAIL = (
3233
r"""<[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9]"""
@@ -84,10 +85,11 @@ class InlineParser(Parser[InlineState]):
8485
"linebreak",
8586
)
8687

87-
def __init__(self, hard_wrap: bool = False) -> None:
88+
def __init__(self, hard_wrap: bool = False, max_emphasis_depth: int = DEFAULT_MAX_EMPHASIS_DEPTH) -> None:
8889
super(InlineParser, self).__init__()
8990

9091
self.hard_wrap = hard_wrap
92+
self.max_emphasis_depth = max_emphasis_depth
9193
self._fast_trigger_chars: Optional[Set[str]] = None
9294
self._fast_trigger_re: Optional[re.Pattern[str]] = None
9395
self._fast_trigger_re_chars: Optional[Tuple[str, ...]] = None
@@ -380,7 +382,11 @@ def parse(self, state: InlineState) -> List[Dict[str, Any]]:
380382
self.process_text(state.src, state)
381383
elif pos < len(state.src):
382384
self.process_text(state.src[pos:], state)
383-
state.tokens = _finalize_emphasis_tokens(state.tokens, "emphasis" in self.rules)
385+
state.tokens = _finalize_emphasis_tokens(
386+
state.tokens,
387+
"emphasis" in self.rules,
388+
self.max_emphasis_depth,
389+
)
384390
return state.tokens
385391

386392
def _find_fast_text_end(self, src: str, pos: int) -> Optional[int]:
@@ -542,7 +548,11 @@ class _Delimiter:
542548
orig_length: int = 0
543549

544550

545-
def _finalize_emphasis_tokens(tokens: List[Dict[str, Any]], enabled: bool) -> List[Dict[str, Any]]:
551+
def _finalize_emphasis_tokens(
552+
tokens: List[Dict[str, Any]],
553+
enabled: bool,
554+
max_depth: int = DEFAULT_MAX_EMPHASIS_DEPTH,
555+
) -> List[Dict[str, Any]]:
546556
if not enabled:
547557
return _clean_emphasis_tokens(tokens)
548558
if not _contains_emphasis_marker(tokens):
@@ -559,7 +569,7 @@ def _finalize_emphasis_tokens(tokens: List[Dict[str, Any]], enabled: bool) -> Li
559569
parts.append(_clean_emphasis_token(token))
560570
source_pos += _emphasis_source_length(token)
561571

562-
_process_emphasis_delimiters(parts, delimiters)
572+
_process_emphasis_delimiters(parts, delimiters, max_depth)
563573
return _merge_text_tokens(parts)
564574

565575

@@ -639,7 +649,11 @@ def _next_delimiter_run(text: str, pos: int) -> int:
639649
return pos
640650

641651

642-
def _process_emphasis_delimiters(parts: List[Dict[str, Any]], delimiters: List[_Delimiter]) -> None:
652+
def _process_emphasis_delimiters(
653+
parts: List[Dict[str, Any]],
654+
delimiters: List[_Delimiter],
655+
max_depth: int,
656+
) -> None:
643657
closer_pos = 0
644658
openers_bottom: Dict[Tuple[str, int, bool], int] = {}
645659
while closer_pos < len(delimiters):
@@ -688,9 +702,12 @@ def _process_emphasis_delimiters(parts: List[Dict[str, Any]], delimiters: List[_
688702
closer_pos += 1
689703
continue
690704

705+
children = parts[opener.index + 1 : closer.index]
706+
if max_depth > 0 and _emphasis_depth(children) >= max_depth:
707+
closer_pos += 1
708+
continue
691709
opener_text["raw"] = opener_text["raw"][:-use_length]
692710
closer_text["raw"] = closer_text["raw"][use_length:]
693-
children = parts[opener.index + 1 : closer.index]
694711
if use_length == 2:
695712
node = {"type": "strong", "children": children}
696713
else:
@@ -721,6 +738,21 @@ def _process_emphasis_delimiters(parts: List[Dict[str, Any]], delimiters: List[_
721738
closer_pos += 1
722739

723740

741+
def _emphasis_depth(tokens: List[Dict[str, Any]]) -> int:
742+
max_depth = 0
743+
stack = [(token, 0) for token in tokens]
744+
while stack:
745+
token, depth = stack.pop()
746+
token_type = token["type"]
747+
if token_type in ("emphasis", "strong"):
748+
depth += 1
749+
if depth > max_depth:
750+
max_depth = depth
751+
for child in token.get("children", ()):
752+
stack.append((child, depth))
753+
return max_depth
754+
755+
724756
def _has_strong_enabled(parts: List[Dict[str, Any]], opener: _Delimiter, closer: _Delimiter) -> bool:
725757
return len(_text_raw(parts[opener.index])) >= 2 and len(_text_raw(parts[closer.index])) >= 2
726758

tests/test_misc.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,12 @@ def test_max_nested_level_setting(self):
289289
md = mistune.Markdown(block=mistune.BlockParser(max_nested_level=7))
290290
self.assertEqual(md.block.max_nested_level, 7)
291291

292+
def test_max_emphasis_depth_setting(self):
293+
self.assertEqual(mistune.InlineParser().max_emphasis_depth, 20)
294+
self.assertEqual(mistune.create_markdown().inline.max_emphasis_depth, 20)
295+
md = mistune.Markdown(inline=mistune.InlineParser(max_emphasis_depth=7))
296+
self.assertEqual(md.inline.max_emphasis_depth, 7)
297+
292298
def test_deeply_nested_block_quote_and_list(self):
293299
# Block quotes and lists each capped their own nesting at the limit but
294300
# not each other's, so alternating them recursed without bound. This

tests/test_security_inline.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,16 @@ def test_unmatched_bracket_input_returns_quickly(self):
5959

6060
self.assertLess(elapsed, DEADLINE)
6161
self.assertIn("[", html)
62+
63+
def test_deep_emphasis_input_does_not_recurse_forever(self):
64+
marks = "*" * 1000
65+
text = marks + "a" + marks
66+
67+
start = time.monotonic()
68+
html = create_markdown()(text)
69+
ast = create_markdown(renderer="ast")(text)
70+
elapsed = time.monotonic() - start
71+
72+
self.assertLess(elapsed, DEADLINE)
73+
self.assertLessEqual(html.count("<strong>") + html.count("<em>"), 20)
74+
self.assertTrue(ast)

0 commit comments

Comments
 (0)