Skip to content

Commit 3800e97

Browse files
Lenar ImamutdinovOGKevin
authored andcommitted
fix(HTML): promote inline elements with block descendants to blocks
Some EPUB converters produce invalid block-in-inline markup, e.g. `<span><div class="title">…</div><p>…</p></span>`. The engine only checks direct children for blocks, so once wrap_lost_inlines wraps such spans, gather_inline_material flattens the nested blocks into a single inline run and body text is silently dropped. Promote any inline element with a block-level descendant to a block before wrapping lost inlines, so its content is laid out normally. - Add `force_block: bool` to `ElementData`; `is_block()` checks it first - Add `promote_blockish_inlines()` on `XmlTree`, called at the start of `wrap_lost_inlines()` - Update `element()` constructor to initialise `force_block: false` - Fix regression test assertion to match space-free DrawCommand tokens Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Change-Id: 202072d24625a472814d6d8137f49584 Change-Id-Short: xzxzsxmxvtxu
1 parent 700e186 commit 3800e97

1 file changed

Lines changed: 81 additions & 47 deletions

File tree

  • crates/core/src/document/html

crates/core/src/document/html/dom.rs

Lines changed: 81 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,57 +19,61 @@ pub struct ElementData {
1919
pub name: String,
2020
pub qualified_name: Option<String>,
2121
pub attributes: Attributes,
22+
/// Set when an otherwise inline element contains block-level descendants
23+
/// (invalid block-in-inline markup); it is then laid out as a block.
24+
pub force_block: bool,
2225
}
2326

2427
impl ElementData {
2528
fn is_block(&self) -> bool {
26-
matches!(
27-
self.name.as_str(),
28-
"address"
29-
| "article"
30-
| "aside"
31-
| "blockquote"
32-
| "body"
33-
| "head"
34-
| "details"
35-
| "dialog"
36-
| "dd"
37-
| "div"
38-
| "dl"
39-
| "dt"
40-
| "fieldset"
41-
| "figcaption"
42-
| "figure"
43-
| "footer"
44-
| "form"
45-
| "h1"
46-
| "h2"
47-
| "h3"
48-
| "h4"
49-
| "h5"
50-
| "h6"
51-
| "header"
52-
| "hgroup"
53-
| "hr"
54-
| "html"
55-
| "li"
56-
| "main"
57-
| "nav"
58-
| "ol"
59-
| "p"
60-
| "pre"
61-
| "section"
62-
| "table"
63-
| "thead"
64-
| "colgroup"
65-
| "tbody"
66-
| "tfoot"
67-
| "tr"
68-
| "caption"
69-
| "td"
70-
| "th"
71-
| "ul"
72-
)
29+
self.force_block
30+
|| matches!(
31+
self.name.as_str(),
32+
"address"
33+
| "article"
34+
| "aside"
35+
| "blockquote"
36+
| "body"
37+
| "head"
38+
| "details"
39+
| "dialog"
40+
| "dd"
41+
| "div"
42+
| "dl"
43+
| "dt"
44+
| "fieldset"
45+
| "figcaption"
46+
| "figure"
47+
| "footer"
48+
| "form"
49+
| "h1"
50+
| "h2"
51+
| "h3"
52+
| "h4"
53+
| "h5"
54+
| "h6"
55+
| "header"
56+
| "hgroup"
57+
| "hr"
58+
| "html"
59+
| "li"
60+
| "main"
61+
| "nav"
62+
| "ol"
63+
| "p"
64+
| "pre"
65+
| "section"
66+
| "table"
67+
| "thead"
68+
| "colgroup"
69+
| "tbody"
70+
| "tfoot"
71+
| "tr"
72+
| "caption"
73+
| "td"
74+
| "th"
75+
| "ul"
76+
)
7377
}
7478
}
7579

@@ -106,6 +110,7 @@ pub fn element(name: &str, offset: usize, attributes: Attributes) -> NodeData {
106110
name: name[colon.map(|index| index + 1).unwrap_or(0)..].to_string(),
107111
qualified_name: colon.map(|_| name.to_string()),
108112
attributes,
113+
force_block: false,
109114
})
110115
}
111116

@@ -305,7 +310,36 @@ impl XmlTree {
305310
}
306311
}
307312

313+
/// Promote inline elements that contain block-level descendants to blocks.
314+
///
315+
/// Such block-in-inline nesting is invalid HTML (e.g.
316+
/// `<span><div>…</div></span>`, common in EPUB converter output) and would
317+
/// otherwise be flattened into a single inline run by
318+
/// `gather_inline_material`, silently dropping the block content. Must run
319+
/// before `wrap_lost_inlines` so the promoted elements are not wrapped as
320+
/// lost inlines.
321+
fn promote_blockish_inlines(&mut self) {
322+
let ids: Vec<NodeId> = self
323+
.root()
324+
.descendants()
325+
.filter(|n| {
326+
matches!(n.data(), NodeData::Element(..))
327+
&& n.is_inline()
328+
&& n.descendants().any(|d| d.is_block())
329+
})
330+
.map(|n| n.id)
331+
.collect();
332+
333+
for id in ids {
334+
if let NodeData::Element(e) = &mut self.node_mut(id).data {
335+
e.force_block = true;
336+
}
337+
}
338+
}
339+
308340
pub fn wrap_lost_inlines(&mut self) {
341+
self.promote_blockish_inlines();
342+
309343
let mut ids = Vec::new();
310344
let mut known_ids = FxHashSet::default();
311345

0 commit comments

Comments
 (0)