Skip to main content

ppt_rs/oxml/
table.rs

1//! Table XML elements for OOXML
2//!
3//! Provides types for parsing and generating DrawingML table elements.
4
5use super::xmlchemy::XmlElement;
6
7/// Table cell properties
8#[derive(Debug, Clone, Default)]
9pub struct TableCellProperties {
10    pub margin_left: Option<u32>,
11    pub margin_right: Option<u32>,
12    pub margin_top: Option<u32>,
13    pub margin_bottom: Option<u32>,
14    pub anchor: Option<String>,
15}
16
17impl TableCellProperties {
18    pub fn parse(elem: &XmlElement) -> Self {
19        TableCellProperties {
20            margin_left: elem.attr("marL").and_then(|v| v.parse().ok()),
21            margin_right: elem.attr("marR").and_then(|v| v.parse().ok()),
22            margin_top: elem.attr("marT").and_then(|v| v.parse().ok()),
23            margin_bottom: elem.attr("marB").and_then(|v| v.parse().ok()),
24            anchor: elem.attr("anchor").map(|s| s.to_string()),
25        }
26    }
27
28    pub fn to_xml(&self) -> String {
29        let mut attrs = Vec::new();
30        
31        if let Some(l) = self.margin_left {
32            attrs.push(format!(r#"marL="{l}""#));
33        }
34        if let Some(r) = self.margin_right {
35            attrs.push(format!(r#"marR="{r}""#));
36        }
37        if let Some(t) = self.margin_top {
38            attrs.push(format!(r#"marT="{t}""#));
39        }
40        if let Some(b) = self.margin_bottom {
41            attrs.push(format!(r#"marB="{b}""#));
42        }
43        if let Some(ref anchor) = self.anchor {
44            attrs.push(format!(r#"anchor="{anchor}""#));
45        }
46
47        if attrs.is_empty() {
48            "<a:tcPr/>".to_string()
49        } else {
50            format!("<a:tcPr {}/>", attrs.join(" "))
51        }
52    }
53}
54
55/// Table cell (a:tc)
56#[derive(Debug, Clone)]
57pub struct TableCell {
58    pub text: String,
59    pub row_span: u32,
60    pub col_span: u32,
61    pub properties: TableCellProperties,
62}
63
64impl TableCell {
65    pub fn new(text: &str) -> Self {
66        TableCell {
67            text: text.to_string(),
68            row_span: 1,
69            col_span: 1,
70            properties: TableCellProperties::default(),
71        }
72    }
73
74    pub fn parse(elem: &XmlElement) -> Self {
75        let text = elem.find_descendant("t")
76            .map(|t| t.text_content())
77            .unwrap_or_default();
78
79        let row_span = elem.attr("rowSpan").and_then(|v| v.parse().ok()).unwrap_or(1);
80        let col_span = elem.attr("gridSpan").and_then(|v| v.parse().ok()).unwrap_or(1);
81
82        let properties = elem.find("tcPr")
83            .map(|tc| TableCellProperties::parse(tc))
84            .unwrap_or_default();
85
86        TableCell { text, row_span, col_span, properties }
87    }
88
89    pub fn to_xml(&self) -> String {
90        let mut attrs = Vec::new();
91        if self.row_span > 1 {
92            let row_span = self.row_span;
93            attrs.push(format!(r#"rowSpan="{row_span}""#));
94        }
95        if self.col_span > 1 {
96            let col_span = self.col_span;
97            attrs.push(format!(r#"gridSpan="{col_span}""#));
98        }
99
100        let attr_str = if attrs.is_empty() { String::new() } else { format!(" {}", attrs.join(" ")) };
101
102        format!(
103            r#"<a:tc{}><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:rPr lang="en-US"/><a:t>{}</a:t></a:r></a:p></a:txBody>{}</a:tc>"#,
104            attr_str,
105            escape_xml(&self.text),
106            self.properties.to_xml()
107        )
108    }
109}
110
111/// Table row (a:tr)
112#[derive(Debug, Clone)]
113pub struct TableRow {
114    pub cells: Vec<TableCell>,
115    pub height: u32,
116}
117
118impl TableRow {
119    pub fn new() -> Self {
120        TableRow {
121            cells: Vec::new(),
122            height: 370840, // Default row height
123        }
124    }
125
126    pub fn parse(elem: &XmlElement) -> Self {
127        let height = elem.attr("h").and_then(|v| v.parse().ok()).unwrap_or(370840);
128        let cells = elem.find_all("tc")
129            .into_iter()
130            .map(|tc| TableCell::parse(tc))
131            .collect();
132
133        TableRow { cells, height }
134    }
135
136    pub fn to_xml(&self) -> String {
137        let height = self.height;
138        let mut xml = format!(r#"<a:tr h="{height}">"#);
139        for cell in &self.cells {
140            xml.push_str(&cell.to_xml());
141        }
142        xml.push_str("</a:tr>");
143        xml
144    }
145
146    pub fn add_cell(mut self, cell: TableCell) -> Self {
147        self.cells.push(cell);
148        self
149    }
150}
151
152impl Default for TableRow {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158/// Table grid column (a:gridCol)
159#[derive(Debug, Clone)]
160pub struct GridColumn {
161    pub width: u32,
162}
163
164impl GridColumn {
165    pub fn new(width: u32) -> Self {
166        GridColumn { width }
167    }
168
169    pub fn to_xml(&self) -> String {
170        let width = self.width;
171        format!(r#"<a:gridCol w="{width}"/>"#)
172    }
173}
174
175/// Table (a:tbl)
176#[derive(Debug, Clone)]
177pub struct Table {
178    pub rows: Vec<TableRow>,
179    pub grid_columns: Vec<GridColumn>,
180}
181
182impl Table {
183    pub fn new() -> Self {
184        Table {
185            rows: Vec::new(),
186            grid_columns: Vec::new(),
187        }
188    }
189
190    pub fn parse(elem: &XmlElement) -> Self {
191        let mut table = Table::new();
192
193        // Parse grid columns
194        if let Some(tbl_grid) = elem.find("tblGrid") {
195            table.grid_columns = tbl_grid.find_all("gridCol")
196                .into_iter()
197                .map(|gc| {
198                    let width = gc.attr("w").and_then(|v| v.parse().ok()).unwrap_or(914400);
199                    GridColumn::new(width)
200                })
201                .collect();
202        }
203
204        // Parse rows
205        table.rows = elem.find_all("tr")
206            .into_iter()
207            .map(|tr| TableRow::parse(tr))
208            .collect();
209
210        table
211    }
212
213    pub fn to_xml(&self) -> String {
214        let mut xml = String::from("<a:tbl>");
215        
216        // Table properties
217        xml.push_str("<a:tblPr firstRow=\"1\" bandRow=\"1\"/>");
218        
219        // Grid
220        xml.push_str("<a:tblGrid>");
221        for col in &self.grid_columns {
222            xml.push_str(&col.to_xml());
223        }
224        xml.push_str("</a:tblGrid>");
225        
226        // Rows
227        for row in &self.rows {
228            xml.push_str(&row.to_xml());
229        }
230        
231        xml.push_str("</a:tbl>");
232        xml
233    }
234
235    pub fn row_count(&self) -> usize {
236        self.rows.len()
237    }
238
239    pub fn col_count(&self) -> usize {
240        self.grid_columns.len()
241    }
242
243    pub fn add_row(mut self, row: TableRow) -> Self {
244        self.rows.push(row);
245        self
246    }
247
248    pub fn add_column(mut self, width: u32) -> Self {
249        self.grid_columns.push(GridColumn::new(width));
250        self
251    }
252}
253
254impl Default for Table {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260fn escape_xml(s: &str) -> String {
261    s.replace('&', "&amp;")
262        .replace('<', "&lt;")
263        .replace('>', "&gt;")
264        .replace('"', "&quot;")
265        .replace('\'', "&apos;")
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_table_cell_to_xml() {
274        let cell = TableCell::new("Hello");
275        let xml = cell.to_xml();
276        
277        assert!(xml.contains("<a:tc>"));
278        assert!(xml.contains("Hello"));
279    }
280
281    #[test]
282    fn test_table_row_to_xml() {
283        let row = TableRow::new()
284            .add_cell(TableCell::new("A"))
285            .add_cell(TableCell::new("B"));
286        
287        let xml = row.to_xml();
288        assert!(xml.contains("<a:tr"));
289        assert!(xml.contains("A"));
290        assert!(xml.contains("B"));
291    }
292
293    #[test]
294    fn test_table_to_xml() {
295        let table = Table::new()
296            .add_column(914400)
297            .add_column(914400)
298            .add_row(TableRow::new()
299                .add_cell(TableCell::new("A1"))
300                .add_cell(TableCell::new("B1")));
301        
302        let xml = table.to_xml();
303        assert!(xml.contains("<a:tbl>"));
304        assert!(xml.contains("<a:tblGrid>"));
305        assert!(xml.contains("A1"));
306    }
307}