Skip to content

fix: do not leak lxml's synthetic fragment wrapper into the text output - #135

Merged
AlbertWeichselbraun merged 1 commit into
weblyzard:masterfrom
OsamaAnsar:fix/fragment-wrapper-leak
Oct 23, 2026
Merged

AlbertWeichselbraun merged 1 commit into
weblyzard:masterfrom
OsamaAnsar:fix/fragment-wrapper-leak

Conversation

@OsamaAnsar

Copy link
Copy Markdown
Contributor

Problem

inscriptis._get_html_tree parses the input with lxml.html.fromstring. For fragments that consist of several root elements, or of top-level text next to elements, fromstring returns a synthetic <div> (if a block-level tag is present) or <span> as the root. The relaxed CSS profile styles those tags (div: padding_inline=2, span: prefix/suffix " "), so markup that is not in the input shows up in the text. In particular, words get split: <b>un</b>believable is converted to un believable.

Observed on master (a863dd7, 2.7.5, lxml 6.1.3 and 5.4.0, default/relaxed profile), compared with the same markup inside an explicit <html><body>...</body></html>:

input before (get_text) after same markup in explicit <body>
<p>a</p>x ' a\n\n x' 'a\n\nx' 'a\n\nx'
<p>a</p><p>b</p> ' a\n\n b\n' 'a\n\nb\n' 'a\n\nb\n'
<div>a</div><div>b</div> ' a\n b' ' a\n b' ' a\n b'
<b>a</b>b 'a b' 'ab' 'ab'
<b>bold</b>text 'bold text' 'boldtext' 'boldtext'
<b>un</b>believable 'un believable' 'unbelievable' 'unbelievable'
<td>a</td><td>b</td> ' ab' 'ab' 'ab'

get_annotated_text('<b>un</b>believable', ParserConfig(annotation_rules={'b': ['bold']})) returned text 'un believable' before and returns 'unbelievable' with label (0, 2, 'bold') now.

Change

_get_html_tree uses lxml.html.document_fromstring instead of fromstring (also in the <pre> fallback). document_fromstring always returns the <html> root, so no synthetic wrapper is created; this is the same tree fromstring already returns for full documents.

Behaviour changes

  • Output for fragment inputs with multiple roots or top-level text changes (see table above). This is the intended fix, but anyone relying on the old indentation/spacing for such inputs will see a difference. Only the relaxed (default) profile is visibly affected; I found no difference with the strict profile.
  • Existing expectation changed: tests/test_broken_table_handling.py::test_forgotten_td_close_tag parses <body>hallo<table>...</table>echo</body>. fromstring turned that <body> into a <div>, which gave "hallo\n1 2\n3 4\n\necho". With the real <body> the expected text is now "hallo1 2\n3 4\n\necho". This is the same output that <html><body>hallo<table>... already produces on master, so I treated the old "hallo\n" as an artifact of the wrapper rather than intended table behaviour (the test is about the missing </td> handling). Please say if you would rather keep the line break and handle this differently.
  • No other existing expectation changed. All other existing tests pass unmodified.

Not changed

  • Single-element fragments (<p>a</p>, <div>a</div>, <span>a</span>, <li>a</li><li>b</li>), full documents, empty/whitespace-only/comment-only input: output identical before and after.
  • Explicit <span> elements keep the relaxed profile's intended padding (e.g. fi<span>r</span>st is still fi r st, see Presentation of internal spans seems a bit odd #75).
  • Fragments that start with bare text (Hello <b>world</b>!, plain text) already ended in a \n before this change and still do; libxml2 puts such text into an implied <p>, which is separate from the lxml wrapper.

Tests

New tests/test_fragment_wrapper.py (29 cases): exact expected text for the fragments above, equality with the same markup inside <html><body> for the relaxed and strict profiles, annotation offsets, and unchanged outputs.

With only the src/inscriptis/__init__.py change reverted, 15 tests fail (14 in the new file, plus the updated test_forgotten_td_close_tag); with the change restored, all 109 tests pass.

Verified

  • pytest (full suite): 109 passed on Python 3.12 with lxml 6.1.3 and with lxml 5.4.0 (the lower bound in pyproject.toml).
  • ruff check and ruff format --check: clean.
  • Compared old and new output on all 44 tests/html/*.html files passed in raw, without a <body> wrapper (88 comparisons, text and annotations, both profiles): 16 relaxed-profile results differ (the leading indentation of the synthetic wrapper is gone); strict-profile output is identical.

Not tested

  • tox: there is no tox.ini in the repository, so I ran what CI runs (ruff check) plus ruff format --check and pytest. I did not run uv build, ty, or safety.
  • Python versions other than 3.12, and lxml versions other than 5.4.0 and 6.1.3.
  • On Windows, two existing tests (test_html_snippets, test_html_snippets_annotations) fail on master unless PYTHONUTF8=1 is set, because they open() UTF-8 files without an encoding. Unrelated to this change; I ran with PYTHONUTF8=1.
  • Direct use of Inscriptis(fromstring(...)) (as in the docs and examples) is not affected by this change and still has the old behaviour.

_get_html_tree used lxml.html.fromstring, which wraps fragments that
consist of multiple root elements or top-level text in a synthetic <div>
(or <span>). The relaxed CSS profile styles these tags, so the wrapper
added indentation (div) or surrounding spaces (span) to the output, e.g.
'<b>un</b>believable' was converted to 'un believable'.

Use lxml.html.document_fromstring instead, which always returns the
<html> root and never creates a synthetic wrapper. Output for single
elements, full documents and the strict profile is unchanged.

tests/test_broken_table_handling.py: the input starts with <body>, which
fromstring turned into a <div>; the expected text is updated accordingly.
@AlbertWeichselbraun
AlbertWeichselbraun merged commit 09e7252 into weblyzard:master Oct 23, 2026
1 check passed
AlbertWeichselbraun added a commit that referenced this pull request Oct 23, 2026
This prevents test failures under Windows as reported by @OsamaAnsar in #135.
@AlbertWeichselbraun

Copy link
Copy Markdown
Contributor

Thank you for spotting and fixing this. Your pull request has been merged and included in the Inscriptis 2.7.6 release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants