Skip to main content

ppt_rs/generator/table/
format.rs

1//! Shared table cell formatting and XML generation
2
3use super::cell::{CellAlign, CellVAlign, TableCell};
4use crate::core::escape_xml;
5use crate::generator::slide_content::table_merge::CellMergeState;
6use crate::generator::text::{color_to_xml, TextFormat};
7
8impl TableCell {
9    /// Convert cell formatting to the shared text format model.
10    pub fn to_text_format(&self) -> TextFormat {
11        let mut format = TextFormat::new();
12        if self.bold {
13            format = format.bold();
14        }
15        if self.italic {
16            format = format.italic();
17        }
18        if self.underline {
19            format = format.underline();
20        }
21        if let Some(ref color) = self.text_color {
22            format = format.color(color);
23        }
24        if let Some(size) = self.font_size {
25            format = format.font_size(size);
26        }
27        if let Some(ref family) = self.font_family {
28            format = format.font_family(family);
29        }
30        format
31    }
32}
33
34/// Merge attributes for `<a:tc>` from cell state.
35pub fn merge_attrs_from_cell(cell: &TableCell) -> String {
36    if cell.h_merge {
37        return CellMergeState::HMerge.to_xml_attrs();
38    }
39    if cell.v_merge {
40        return CellMergeState::VMerge.to_xml_attrs();
41    }
42
43    let col_span = cell.grid_span.unwrap_or(1) as usize;
44    let row_span = cell.row_span.unwrap_or(1) as usize;
45    CellMergeState::Anchor { row_span, col_span }.to_xml_attrs()
46}
47
48/// Generate `<a:rPr>` for a table cell text run.
49pub fn run_properties_xml(cell: &TableCell) -> String {
50    let format = cell.to_text_format();
51    let attrs = format.to_xml_attrs();
52    let color_xml = cell
53        .text_color
54        .as_ref()
55        .map(|color| color_to_xml(color))
56        .unwrap_or_default();
57    let font_xml = cell
58        .font_family
59        .as_ref()
60        .map(|font| format!(r#"<a:latin typeface="{}"/>"#, escape_xml(font)))
61        .unwrap_or_default();
62
63    if color_xml.is_empty() && font_xml.is_empty() {
64        format!(r#"<a:rPr lang="en-US" dirty="0"{attrs}/>"#)
65    } else {
66        format!(
67            r#"<a:rPr lang="en-US" dirty="0"{attrs}>{color_xml}{font_xml}</a:rPr>"#
68        )
69    }
70}
71
72/// Paragraph alignment attributes for `<a:p>`.
73pub fn paragraph_props_xml(cell: &TableCell) -> String {
74    if cell.align == CellAlign::Center {
75        String::new()
76    } else {
77        format!(r#" algn="{}""#, cell.align.as_str())
78    }
79}
80
81/// Body properties for `<a:bodyPr>` inside table cells.
82pub fn body_props_xml(cell: &TableCell) -> String {
83    if cell.wrap_text {
84        r#"<a:bodyPr wrap="square"/>"#.to_string()
85    } else {
86        r#"<a:bodyPr wrap="none"/>"#.to_string()
87    }
88}
89
90/// Cell properties (`<a:tcPr>`) including background and vertical alignment.
91pub fn tc_properties_xml(cell: &TableCell) -> String {
92    let anchor = if cell.valign == CellVAlign::Middle {
93        String::new()
94    } else {
95        format!(r#" anchor="{}""#, cell.valign.as_str())
96    };
97
98    if let Some(ref background) = cell.background_color {
99        format!(
100            r#"<a:tcPr{anchor}>{}</a:tcPr>"#,
101            color_to_xml(background)
102        )
103    } else if anchor.is_empty() {
104        "<a:tcPr/>".to_string()
105    } else {
106        format!(r#"<a:tcPr{anchor}/>"#)
107    }
108}
109
110/// Generate cell XML with formatting.
111/// Based on reference PPTX structure: `txBody` comes before `tcPr`.
112pub fn generate_cell_xml(cell: &TableCell) -> String {
113    let merge_attrs = merge_attrs_from_cell(cell);
114
115    if cell.h_merge || cell.v_merge {
116        return format!(
117            r#"<a:tc{merge_attrs}><a:txBody><a:bodyPr/><a:lstStyle/><a:p/></a:txBody><a:tcPr/></a:tc>"#
118        );
119    }
120
121    let body_pr = body_props_xml(cell);
122    let paragraph_props = paragraph_props_xml(cell);
123    let run_props = run_properties_xml(cell);
124    let text = escape_xml(&cell.text);
125    let tc_pr = tc_properties_xml(cell);
126
127    format!(
128        r#"<a:tc{merge_attrs}><a:txBody>{body_pr}<a:lstStyle/><a:p{paragraph_props}><a:r>{run_props}<a:t>{text}</a:t></a:r></a:p></a:txBody>{tc_pr}</a:tc>"#
129    )
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_run_properties_bold() {
138        let cell = TableCell::new("Bold").bold();
139        let xml = run_properties_xml(&cell);
140        assert!(xml.contains(r#"b="1""#));
141    }
142
143    #[test]
144    fn test_run_properties_uses_color_helper() {
145        let cell = TableCell::new("Red").text_color("FF0000");
146        let xml = run_properties_xml(&cell);
147        assert!(xml.contains("FF0000"));
148        assert!(xml.contains("solidFill"));
149    }
150
151    #[test]
152    fn test_tc_properties_background_and_valign() {
153        let cell = TableCell::new("Top").background_color("0000FF").valign_top();
154        let xml = tc_properties_xml(&cell);
155        assert!(xml.contains("0000FF"));
156        assert!(xml.contains(r#"anchor="t""#));
157    }
158
159    #[test]
160    fn test_generate_cell_alignment() {
161        let cell = TableCell::new("Left").align_left();
162        let xml = generate_cell_xml(&cell);
163        assert!(xml.contains(r#"algn="l""#));
164    }
165
166    #[test]
167    fn test_generate_cell_wrap_disabled() {
168        let cell = TableCell::new("No wrap").wrap(false);
169        let xml = generate_cell_xml(&cell);
170        assert!(xml.contains(r#"wrap="none""#));
171    }
172
173    #[test]
174    fn test_merge_attrs_uses_cell_merge_state() {
175        let cell = TableCell::new("").h_merge();
176        assert!(merge_attrs_from_cell(&cell).contains("hMerge"));
177    }
178}