Skip to main content

oxidize_html/
table.rs

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}