Repository navigation
HTML backend: <pre> loses preformatted semantics when it contains inline elements (sibling <code> children destroy the block; text node beside an element loses its newline) #4549
Description
Bug
<pre> is whitespace-preserving, but the HTML backend only honours that when the <pre> contains a bare text node. As soon as it contains inline elements, preformatted semantics are lost in one of two ways:
- Sibling
<code>children destroy the block entirely. A<pre>whose children are several<code>elements (no single wrapping<code>) is emitted as a run of inline code fragments instead of a code block — and spaces are introduced at the element boundaries, so string literals are corrupted. - A bare text node beside an inline element loses the newline between them. The
\nseparating them is collapsed to a space, while newlines between two elements survive. A wrapping<code>makes no difference.
Steps to reproduce
Five inputs, each run with docling convert --to md --output . <file>.html:
a-text-only.html — ✅ correct
<html><body><pre><code>line one
line two
line three</code></pre></body></html>Output:
```
line one
line two
line three
```
b-span-children.html — ✅ correct
<html><body><pre><code><span>line one</span>
<span>line two</span>
<span>line three</span></code></pre></body></html>Output: same correct fenced block as (a).
c-sibling-codes.html — ❌ bug 1
<html><body><pre><code>line one</code>
<code>line two</code>
<code>line three</code></pre></body></html>Expected: a fenced code block with three lines.
Actual — no block at all, inline code with injected spaces:
`line one` `` `line two` `` `line three`
d-mixed-text-and-spans.html — ❌ bug 2
<html><body><pre><code></code><span class="gp">$</span> vagrant ssh host001
<span class="go">Last login: Tue Jan 20 19:18:45 2015</span>
<span class="gp">[vagrant@host001 ~]$</span>
</pre></body></html>Expected: three lines. Actual — the first newline is collapsed, the second survives:
```
$ vagrant ssh host001 Last login: Tue Jan 20 19:18:45 2015
[vagrant@host001 ~]$
```
e-wrapped-mixed.html — same as (d) but with the whole content inside one <code>: output is identical to (d), so the wrapper does not change the behaviour.
Why this matters in practice
This is the markup a real published technical EPUB ships. Learning CFEngine (O'Reilly, 2nd ed.) uses Pygments token classes with every <span> rewritten to <code> — up to 207 sibling <code> elements inside a single <pre>, 270 <pre> blocks, 10,486 <code> elements against 17 <span>.
Converting that EPUB, docling recovers 12 code blocks across a ~500 page programming book. Every policy listing arrives as a line of backtick fragments, with spaces inserted inside string literals — " s1 " where the source says "s1", which silently changes the meaning of the code. Flattening each <pre> to a single plain-text <code> first (reducing the input to case (a)) recovers 210 code blocks with correct indentation.
Case (2) is why a spec-correct repair of such an EPUB is still not sufficient: rewriting the <code> tokens back to <span> fixes case (1), but any line with a bare text run next to a highlighted token still loses its break.
Expectation
Inside <pre>, the element's whitespace-preserving semantics should hold regardless of which inline elements appear in it: emit one code block and preserve the source newlines, stripping inline markup rather than letting it dictate block structure.
Prior art checked
Open and closed issues plus PRs; none of these covers either case:
- Unable to extract code block in HTML page #222 → PR fix: Added handling of code blocks in html with <pre> tag #302 added
<pre>handling (2024) - Unable to extract text inside <code> tag #1302 fixed text extraction inside
<code>(2025) - HTML backend: Source whitespace inside <p> tags split into separate paragraphs #3608 was whitespace inside
<p>— the opposite direction (2026)
Docling version
Docling version: 2.133.0
Docling Core version: 2.99.0
Docling IBM Models version: 4.0.3
Docling Parse version: 7.22.1
Python: cpython-312 (3.12.15)
Platform: macOS 27.0, arm64 (Apple Silicon)
Activity
teachershuang commentedon Oct 4, 2026
I can take the
<pre>-specific part of this. On current main (51fe9ff4), I reproduced the sibling-<code>example becoming five code items (including two empty items), and the mixed text/<span>example losing its first newline. The wrapped mixed example also fails; the plain-text and span-only examples remain correct.My proposed scope is to emit one code item for a
<pre>and preserve text-node boundaries without inserting separators. Normal-flow paragraphs, inline code outside<pre>, table extraction, and the docling-core whitespace rollout would stay out of scope. I would test both reported failures alongside the working cases, language hints, suppressed content, and the EPUB delegation path.Before implementation, could a maintainer confirm one compatibility point? Today
<pre>See <a href="https://example.com/reference">reference</a> first</pre>produces separate code items and retains the link on the middle fragment. Should a single preformatted code item treat such inline markup as presentation only, retaining its literal text but not per-fragment links/formatting? I would retain the current block-edge trimming in this fix unless a different contract is preferred.I checked #3891: its description explicitly leaves
<pre>fragment structure out of scope, but its extraction changes touch this area. I would keep this fix independent of that pending serializer change and wait for the compatibility direction above before changing code.L4XB commentedon Oct 21, 2026
Two more inputs from the same family, measured on current
main(85dcebc), in case they help with the scope:Pygments output, as Sphinx, MkDocs and nbconvert write it. It is probably the most common
<pre>markup on documentation sites:The code item is
def f (): return 1: the newline and the indentation are gone, and a space appears before():. The same code as one text node comes back asdef f():\n return 1.The indentation of the first line. Even a bare text node loses it, because the code text is
.strip()ped (html_backend.py, theprebranch):<pre> return x\nprint(1)</pre>givesreturn x\nprint(1).<pre><code> if ok:\n run()\n</code></pre>givesif ok:\n run().Only the leading and trailing blank lines should go.
@teachershuang are you still working on this? If not, I'm happy to take it.
teachershuang commentedon Oct 23, 2026
Yes, I’m still tracking this issue. I have not started an implementation branch because I’m waiting for maintainer guidance on the compatibility question: should inline markup inside
<pre>be treated as presentation-only while preserving literal text, or should fragment links/formatting remain attached?The Pygments and first-line-indentation cases you added fit the same narrow
<pre>whitespace/fragment-preservation scope and are useful repros. @docling-project maintainers, could you confirm that contract? If you prefer @L4XB to take this or want me to narrow the scope, I’m happy to coordinate so we avoid duplicate work.L4XB commentedon Oct 23, 2026
@teachershuang Please go ahead, it's yours. I only meant to add the inputs and won't open a PR for this issue.