1use crate::{Display, SizeValue, StyledNode};
2
3pub fn normalize_tables(root: &mut StyledNode, available_width: f32) {
4 visit(root, available_width);
5}
6
7fn visit(node: &mut StyledNode, parent_width: f32) {
8 if node.tag.as_deref() == Some("table") {
9 normalize_table(node, parent_width);
10 }
11
12 let width = node_width(&node.style.width, parent_width).unwrap_or(parent_width);
13 for child in &mut node.children {
14 visit(child, width);
15 }
16}
17
18fn normalize_table(table: &mut StyledNode, available_width: f32) {
19 table.style.display = Display::Table;
20 let table_width = node_width(&table.style.width, available_width).unwrap_or(available_width);
21 if matches!(table.style.width, SizeValue::Auto) {
22 table.style.width = SizeValue::Px(table_width);
23 }
24
25 let mut rows = Vec::new();
26 collect_rows(table, &mut rows);
27
28 if rows.is_empty() {
29 return;
30 }
31
32 let col_count = rows
33 .iter()
34 .map(|r| {
35 r.children
36 .iter()
37 .filter(|n| matches!(n.tag.as_deref(), Some("td" | "th")))
38 .count()
39 })
40 .max()
41 .unwrap_or(0);
42 if col_count == 0 {
43 return;
44 }
45
46 let mut widths: Vec<Option<f32>> = vec![None; col_count];
47 for row in rows {
48 let mut col = 0;
49 for cell in row
50 .children
51 .iter()
52 .filter(|n| matches!(n.tag.as_deref(), Some("td" | "th")))
53 {
54 let span = colspan(cell);
55 if let Some(width) = node_width(&cell.style.width, table_width) {
56 let each = width / span as f32;
57 for i in col..(col + span).min(col_count) {
58 widths[i] = Some(widths[i].unwrap_or(each).max(each));
59 }
60 }
61 col += span;
62 }
63 }
64
65 let explicit_total: f32 = widths.iter().flatten().sum();
66 let remaining_cols = widths.iter().filter(|w| w.is_none()).count();
67 let leftover = (table_width - explicit_total).max(0.0);
68 let fallback = if remaining_cols > 0 {
69 leftover / remaining_cols as f32
70 } else {
71 table_width / col_count as f32
72 };
73 let resolved: Vec<f32> = widths.into_iter().map(|w| w.unwrap_or(fallback)).collect();
74
75 for_each_row_mut(table, &mut |row| {
76 let mut col = 0;
77 for cell in row
78 .children
79 .iter_mut()
80 .filter(|n| matches!(n.tag.as_deref(), Some("td" | "th")))
81 {
82 cell.style.display = Display::TableCell;
83 let span = colspan(cell).max(1);
84 let end = (col + span).min(resolved.len());
85 let width = resolved[col..end].iter().sum();
86 cell.style.width = SizeValue::Px(width);
87 col = end;
88 }
89 });
90}
91
92fn colspan(cell: &StyledNode) -> usize {
93 cell.attrs
94 .get("colspan")
95 .and_then(|v| v.parse::<usize>().ok())
96 .filter(|v| *v > 0)
97 .unwrap_or(1)
98}
99
100fn node_width(size: &SizeValue, parent_width: f32) -> Option<f32> {
101 match size {
102 SizeValue::Px(px) => Some(*px),
103 SizeValue::Percent(pct) => Some((pct / 100.0) * parent_width),
104 SizeValue::Auto => None,
105 }
106}
107
108fn collect_rows<'a>(node: &'a StyledNode, rows: &mut Vec<&'a StyledNode>) {
109 if node.tag.as_deref() == Some("tr") {
110 rows.push(node);
111 return;
112 }
113 for child in &node.children {
114 if child.tag.as_deref() == Some("table") {
115 continue;
116 }
117 collect_rows(child, rows);
118 }
119}
120
121fn for_each_row_mut(node: &mut StyledNode, f: &mut impl FnMut(&mut StyledNode)) {
122 if node.tag.as_deref() == Some("tr") {
123 f(node);
124 return;
125 }
126 for child in &mut node.children {
127 if child.tag.as_deref() == Some("table") {
128 continue;
129 }
130 for_each_row_mut(child, f);
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use crate::{HtmlRenderer, SizeValue, StyledNode};
137
138 fn find_first_tag<'a>(node: &'a StyledNode, tag: &str) -> Option<&'a StyledNode> {
139 if node.tag.as_deref() == Some(tag) {
140 return Some(node);
141 }
142 for child in &node.children {
143 if let Some(found) = find_first_tag(child, tag) {
144 return Some(found);
145 }
146 }
147 None
148 }
149
150 #[test]
151 fn distributes_table_width_to_missing_columns() {
152 let html =
153 r#"<table width="600"><tr><td width="200">A</td><td>B</td><td>C</td></tr></table>"#;
154 let mut renderer = HtmlRenderer::default();
155 let mut tree = renderer.style_tree(html);
156 super::normalize_tables(&mut tree, 600.0);
157 let row = find_first_tag(&tree, "tr").expect("row");
158 let widths: Vec<f32> = row
159 .children
160 .iter()
161 .map(|c| match c.style.width {
162 SizeValue::Px(px) => px,
163 _ => 0.0,
164 })
165 .collect();
166 assert_eq!(widths, vec![200.0, 200.0, 200.0]);
167 }
168
169 #[test]
170 fn nested_table_columns_not_corrupted() {
171 let html = r#"
172 <table width="600">
173 <tr>
174 <td width="300">Left</td>
175 <td width="300">
176 <table width="200">
177 <tr><td width="100">A</td><td width="100">B</td></tr>
178 </table>
179 </td>
180 </tr>
181 </table>
182 "#;
183
184 let mut renderer = HtmlRenderer::default();
185 let mut tree = renderer.style_tree(html);
186 super::normalize_tables(&mut tree, 600.0);
187
188 fn find_tables<'a>(node: &'a StyledNode, out: &mut Vec<&'a StyledNode>) {
189 if node.tag.as_deref() == Some("table") {
190 out.push(node);
191 }
192 for child in &node.children {
193 find_tables(child, out);
194 }
195 }
196 let mut tables = Vec::new();
197 find_tables(&tree, &mut tables);
198 assert!(tables.len() >= 2);
199 let inner = tables[1];
200 let inner_row = find_first_tag(inner, "tr").expect("inner row");
201 let widths: Vec<f32> = inner_row
202 .children
203 .iter()
204 .filter(|n| matches!(n.tag.as_deref(), Some("td" | "th")))
205 .map(|c| match c.style.width {
206 SizeValue::Px(px) => px,
207 _ => 0.0,
208 })
209 .collect();
210 assert_eq!(widths, vec![100.0, 100.0]);
211 }
212}