Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions markdownify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,17 +225,30 @@ def convert(self, html):
def convert_soup(self, soup):
return self.process_tag(soup, parent_tags=set())

def process_element(self, node, parent_tags=None):
def process_element(self, node, parent_tags=None, _visited=None):
if isinstance(node, NavigableString):
return self.process_text(node, parent_tags=parent_tags)
else:
return self.process_tag(node, parent_tags=parent_tags)
return self.process_tag(node, parent_tags=parent_tags, _visited=_visited)

def process_tag(self, node, parent_tags=None):
def process_tag(self, node, parent_tags=None, _visited=None):
# For the top-level element, initialize the parent context with an empty set.
if parent_tags is None:
parent_tags = set()

# Guard against cyclic trees. A well-formed BeautifulSoup tree is acyclic,
# but some HTML producers (e.g. certain PDF-to-HTML pipelines) can yield a
# graph where a descendant references an ancestor, which would otherwise
# send process_tag/process_element into unbounded mutual recursion and a
# RecursionError. Track the ids of the tags on the current descent path and
# stop if one repeats.
if _visited is None:
_visited = set()
node_id = id(node)
if node_id in _visited:
return ''
_visited = _visited | {node_id}

# Collect child elements to process, ignoring whitespace-only text elements
# adjacent to the inner/outer boundaries of block elements.
should_remove_inside = should_remove_whitespace_inside(node)
Expand Down Expand Up @@ -285,7 +298,7 @@ def _can_ignore(el):

# Convert the children elements into a list of result strings.
child_strings = [
self.process_element(el, parent_tags=parent_tags_for_children)
self.process_element(el, parent_tags=parent_tags_for_children, _visited=_visited)
for el in children_to_convert
]

Expand Down
24 changes: 24 additions & 0 deletions tests/test_advanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,27 @@ def test_code_with_tricky_content():
def test_special_tags():
assert md('<!DOCTYPE html>') == ''
assert md('<![CDATA[foobar]]>') == 'foobar'


def test_cyclic_tree_does_not_recurse():
"""A cyclic BeautifulSoup tree (a descendant referencing an ancestor, as
some PDF-to-HTML pipelines can produce) must not send process_tag /
process_element into unbounded recursion. Regression test for #256."""
import sys
from bs4 import BeautifulSoup
from markdownify import MarkdownConverter

soup = BeautifulSoup('<div><p>hello</p></div>', 'html.parser')
div = soup.find('div')
p = soup.find('p')
# Introduce a cycle: p now contains div, which already contains p.
p.contents.append(div)

original_limit = sys.getrecursionlimit()
sys.setrecursionlimit(300)
try:
# Must complete without raising RecursionError.
result = MarkdownConverter().convert_soup(soup)
finally:
sys.setrecursionlimit(original_limit)
assert 'hello' in result