Skip to content

Commit 892b40c

Browse files
author
Bob
committed
fix: ancestor walk for inline <code> to prevent duplication (#849, #884)
- _is_inline_code_element() walks iterancestors() to catch <span><code>, <a><code> etc. inside paragraph-like parents (p, li, td, th, dd, dt) - is_code_block_element() returns False for inline code - recover_wild_text and _extract filter inline code sub-elems - baseline filters inline code via _is_inline_code() ancestor walk - fixes ruff format/CI, addresses review from Sanjays2402 and adbar All 15 custom inline-code tests pass, 176 unit_tests pass, ruff check passes
1 parent 2ba8f62 commit 892b40c

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

trafilatura/baseline.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ def basic_cleaning(tree: HtmlElement) -> HtmlElement:
5151
# <article> must carry more than this to count as content)
5252
_MIN_CONTENT_LENGTH = 100
5353

54+
_INLINE_CODE_PARENTS = ("p", "li", "td", "th", "dd", "dt")
55+
56+
57+
def _is_inline_code(element) -> bool:
58+
# inline <code> inside paragraph-like ancestors (including wrapped in span/a etc.) is not standalone (#849, #884)
59+
if element.tag != "code":
60+
return False
61+
for ancestor in element.iterancestors():
62+
if ancestor.tag in _INLINE_CODE_PARENTS:
63+
return True
64+
return False
65+
5466

5567
def _walk_json(node: Any, bodies: list[str], teasers: list[str]) -> None:
5668
"""Collect schema.org text content from parsed JSON-LD (list-wrapped and @graph-nested
@@ -208,7 +220,12 @@ def baseline(filecontent: Any) -> tuple[_Element, str, int]:
208220

209221
# scrape from text paragraphs, dropping repeats: a nested element (e.g. <p> in
210222
# <blockquote>) duplicates part of its container's text, collected first in document order
211-
paragraphs = (trim(element.text_content()) for element in tree.iter("blockquote", "code", "p", "pre", "q", "quote"))
223+
# skip inline <code> — text already captured by parent <p>/<li>/etc. (#849, #884)
224+
paragraphs = (
225+
trim(element.text_content())
226+
for element in tree.iter("blockquote", "code", "p", "pre", "q", "quote")
227+
if not _is_inline_code(element)
228+
)
212229
if result := _attempt(paragraphs, dedupe=True):
213230
return result
214231

trafilatura/main_extractor.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,24 @@ def handle_lists(element: _Element, options: Extractor) -> _Element | None:
229229
return None
230230

231231

232+
_INLINE_CODE_PARENTS = frozenset(("p", "li", "td", "th", "dd", "dt"))
233+
234+
235+
def _is_inline_code_element(element: _Element) -> bool:
236+
# inline <code> inside paragraph-like ancestors (including wrapped) is not a block (#849, #884)
237+
if element.tag != "code":
238+
return False
239+
for ancestor in element.iterancestors():
240+
if ancestor.tag in _INLINE_CODE_PARENTS:
241+
return True
242+
return False
243+
244+
232245
def is_code_block_element(element: _Element) -> bool:
233246
"Check if it is a code element according to common structural markers."
247+
# inline <code> inside paragraph-like parents is not a block element (#849, #884)
248+
if _is_inline_code_element(element):
249+
return False
234250
# pip
235251
if element.get("lang") or element.tag == "code":
236252
return True
@@ -681,6 +697,8 @@ def recover_wild_text(
681697
unwanted = ("span",) if "ref" in potential_tags else ("a", "ref", "span")
682698
strip_tags(search_tree, *unwanted)
683699
subelems = search_tree.xpath(search_expr)
700+
# filter out inline <code> to prevent duplication (#849, #884)
701+
subelems = [e for e in subelems if not _is_inline_code_element(e)]
684702
# dedup against the pre-main-pass snapshot: skip what the main pass already took -- exact
685703
# match (not length-gated, #634; accepted cost: identical-text elements collapse) or a
686704
# length-gated substring (a <p> folded into its <list> container)
@@ -777,6 +795,9 @@ def _extract(tree: HtmlElement, options: Extractor) -> tuple[_Element, str, set[
777795
LOGGER.debug(sorted(potential_tags))
778796
# proper extraction
779797
subelems = subtree.xpath(".//*")
798+
# filter out inline <code> — already part of their parent's text
799+
# flow; processing them separately duplicates their content (#849, #884)
800+
subelems = [e for e in subelems if not _is_inline_code_element(e)]
780801
# e.g. only lb-elems in a div
781802
if {e.tag for e in subelems} == {"lb"}:
782803
subelems = [subtree]

0 commit comments

Comments
 (0)