Skip to content

Commit 26e43de

Browse files
Stop definition list nesting short of the recursion limit
DefListProcessor.run calls parseBlocks on the definition content, which can contain further definitions, so a document of flat term/definition pairs without blank lines nests once per pair and raises an uncaught RecursionError from only ~5 KB of input. DefListProcessor.test now declines to run near the recursion limit, the same guard BlockquoteProcessor already uses; the remaining markers are kept as text. Fixes #1700.
1 parent 86d1e46 commit 26e43de

3 files changed

Lines changed: 53 additions & 2 deletions

File tree

‎docs/changelog.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ See the [Contributing Guide](contributing.md) for details.
2424
placeholder instead of the text that preceded it.
2525
* Fix quadratic processing time in the `fenced_code` extension for documents
2626
with many fenced code blocks (#1660).
27+
* Stop definition list nesting short of the recursion limit, so deeply nested
28+
definitions are kept as text instead of raising an uncaught
29+
`RecursionError` (#1700).
2730

2831
## [3.11.3] - 2026-11-05
2932

‎markdown/extensions/def_list.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from . import Extension
2525
from ..blockprocessors import BlockProcessor, ListIndentProcessor
26+
from .. import util
2627
import xml.etree.ElementTree as etree
2728
import re
2829

@@ -34,7 +35,10 @@ class DefListProcessor(BlockProcessor):
3435
NO_INDENT_RE = re.compile(r'^[ ]{0,3}[^ :]')
3536

3637
def test(self, parent: etree.Element, block: str) -> bool:
37-
return bool(self.RE.search(block))
38+
# A definition calls `parseBlocks` on its content, which can contain
39+
# further definitions, so the nesting has to stop short of the
40+
# interpreter's limit, as it does for blockquotes and lists.
41+
return bool(self.RE.search(block)) and not util.nearing_recursion_limit()
3842

3943
def run(self, parent: etree.Element, blocks: list[str]) -> bool | None:
4044

‎tests/test_syntax/extensions/test_def_list.py‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
License: BSD (see LICENSE.md for details).
2020
"""
2121

22-
from markdown.test_tools import TestCase
22+
import markdown
23+
from markdown.test_tools import TestCase, recursionlimit
2324

2425

2526
class TestDefList(TestCase):
@@ -321,3 +322,46 @@ def test_def_list_nested_admontions(self):
321322
),
322323
extensions=['def_list', 'admonition']
323324
)
325+
326+
327+
class TestDefListNestingLimit(TestCase):
328+
329+
# A definition calls `parseBlocks` on its content, which can contain
330+
# further definitions, so the nesting has to stop short of the
331+
# interpreter's limit. The remaining markers are kept as text, the same
332+
# way a blockquote or a list keeps its markers.
333+
334+
def test_nesting_stops_short_of_the_recursion_limit(self):
335+
# Ten levels of definitions is more than the limit below allows to nest.
336+
with recursionlimit(120):
337+
result = markdown.markdown('term\n: definition\n' * 10, extensions=['def_list'])
338+
339+
self.assertLess(result.count('<dl>'), 10)
340+
self.assertIn('term', result)
341+
342+
def test_deeply_nested_definitions_do_not_raise(self):
343+
# Around 5 KB of input, which raised `RecursionError` before the limit was applied.
344+
result = markdown.markdown('term\n: definition\n' * 600, extensions=['def_list'])
345+
346+
self.assertIn('<dl>', result)
347+
self.assertIn('term', result)
348+
349+
def test_shallow_definition_nesting_still_nests(self):
350+
self.assertMarkdownRenders(
351+
'term\n: definition\nterm2\n: definition2',
352+
self.dedent(
353+
"""
354+
<dl>
355+
<dt>term</dt>
356+
<dd>
357+
<dl>
358+
<dt>definition</dt>
359+
<dt>term2</dt>
360+
<dd>definition2</dd>
361+
</dl>
362+
</dd>
363+
</dl>
364+
"""
365+
),
366+
extensions=['def_list']
367+
)

0 commit comments

Comments
 (0)