ppt_rs/parts/
table.rs

1//! Table part
2//!
3//! Represents table data embedded in slides with advanced formatting.
4//!
5//! # Features
6//! - Cell merging (row span, column span)
7//! - Text formatting (bold, italic, underline, strikethrough)
8//! - Cell alignment (horizontal and vertical)
9//! - Borders (all sides, individual sides)
10//! - Background colors and gradients
11//! - Font customization (size, color, family)
12//! - Table styles
13
14use super::base::{Part, PartType, ContentType};
15use crate::exc::PptxError;
16use crate::core::escape_xml;
17
18/// Horizontal alignment
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum HorizontalAlign {
21    #[default]
22    Left,
23    Center,
24    Right,
25    Justify,
26}
27
28impl HorizontalAlign {
29    pub fn as_str(&self) -> &'static str {
30        match self {
31            HorizontalAlign::Left => "l",
32            HorizontalAlign::Center => "ctr",
33            HorizontalAlign::Right => "r",
34            HorizontalAlign::Justify => "just",
35        }
36    }
37}
38
39/// Vertical alignment
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub enum VerticalAlign {
42    Top,
43    #[default]
44    Middle,
45    Bottom,
46}
47
48impl VerticalAlign {
49    pub fn as_str(&self) -> &'static str {
50        match self {
51            VerticalAlign::Top => "t",
52            VerticalAlign::Middle => "ctr",
53            VerticalAlign::Bottom => "b",
54        }
55    }
56}
57
58/// Border style
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum BorderStyle {
61    #[default]
62    Solid,
63    Dashed,
64    Dotted,
65    Double,
66    None,
67}
68
69impl BorderStyle {
70    pub fn as_str(&self) -> &'static str {
71        match self {
72            BorderStyle::Solid => "solid",
73            BorderStyle::Dashed => "dash",
74            BorderStyle::Dotted => "dot",
75            BorderStyle::Double => "dbl",
76            BorderStyle::None => "none",
77        }
78    }
79}
80
81/// Cell border
82#[derive(Debug, Clone, Default)]
83pub struct CellBorder {
84    pub width: i32,        // in EMU (12700 = 1pt)
85    pub color: String,
86    pub style: BorderStyle,
87}
88
89impl CellBorder {
90    pub fn new(width_pt: f32, color: impl Into<String>) -> Self {
91        CellBorder {
92            width: (width_pt * 12700.0) as i32,
93            color: color.into(),
94            style: BorderStyle::Solid,
95        }
96    }
97
98    pub fn style(mut self, style: BorderStyle) -> Self {
99        self.style = style;
100        self
101    }
102
103    pub fn to_xml(&self, tag: &str) -> String {
104        if self.style == BorderStyle::None {
105            return format!("<a:{}/>\n", tag);
106        }
107        format!(
108            r#"<a:{} w="{}" cap="flat" cmpd="sng" algn="ctr">
109              <a:solidFill><a:srgbClr val="{}"/></a:solidFill>
110              <a:prstDash val="{}"/>
111            </a:{}>"#,
112            tag,
113            self.width,
114            self.color.trim_start_matches('#'),
115            self.style.as_str(),
116            tag
117        )
118    }
119}
120
121/// Cell borders (all four sides)
122#[derive(Debug, Clone, Default)]
123pub struct CellBorders {
124    pub left: Option<CellBorder>,
125    pub right: Option<CellBorder>,
126    pub top: Option<CellBorder>,
127    pub bottom: Option<CellBorder>,
128}
129
130impl CellBorders {
131    pub fn all(border: CellBorder) -> Self {
132        CellBorders {
133            left: Some(border.clone()),
134            right: Some(border.clone()),
135            top: Some(border.clone()),
136            bottom: Some(border),
137        }
138    }
139
140    pub fn none() -> Self {
141        let no_border = CellBorder { width: 0, color: String::new(), style: BorderStyle::None };
142        CellBorders {
143            left: Some(no_border.clone()),
144            right: Some(no_border.clone()),
145            top: Some(no_border.clone()),
146            bottom: Some(no_border),
147        }
148    }
149
150    pub fn to_xml(&self) -> String {
151        let mut xml = String::new();
152        if let Some(ref b) = self.left { xml.push_str(&b.to_xml("lnL")); }
153        if let Some(ref b) = self.right { xml.push_str(&b.to_xml("lnR")); }
154        if let Some(ref b) = self.top { xml.push_str(&b.to_xml("lnT")); }
155        if let Some(ref b) = self.bottom { xml.push_str(&b.to_xml("lnB")); }
156        xml
157    }
158}
159
160/// Cell margins
161#[derive(Debug, Clone)]
162pub struct CellMargins {
163    pub left: i32,   // in EMU
164    pub right: i32,
165    pub top: i32,
166    pub bottom: i32,
167}
168
169impl Default for CellMargins {
170    fn default() -> Self {
171        CellMargins {
172            left: 91440,   // 0.1 inch
173            right: 91440,
174            top: 45720,    // 0.05 inch
175            bottom: 45720,
176        }
177    }
178}
179
180impl CellMargins {
181    pub fn uniform(margin: i32) -> Self {
182        CellMargins { left: margin, right: margin, top: margin, bottom: margin }
183    }
184}
185
186/// Table cell with advanced formatting
187#[derive(Debug, Clone)]
188pub struct TableCellPart {
189    pub text: String,
190    pub row_span: u32,
191    pub col_span: u32,
192    pub bold: bool,
193    pub italic: bool,
194    pub underline: bool,
195    pub strikethrough: bool,
196    pub background_color: Option<String>,
197    pub text_color: Option<String>,
198    pub font_size: Option<u32>,
199    pub font_family: Option<String>,
200    pub h_align: HorizontalAlign,
201    pub v_align: VerticalAlign,
202    pub borders: Option<CellBorders>,
203    pub margins: Option<CellMargins>,
204    pub is_merged: bool,  // For cells that are part of a merge (not the anchor)
205}
206
207impl TableCellPart {
208    /// Create a new table cell
209    pub fn new(text: impl Into<String>) -> Self {
210        TableCellPart {
211            text: text.into(),
212            row_span: 1,
213            col_span: 1,
214            bold: false,
215            italic: false,
216            underline: false,
217            strikethrough: false,
218            background_color: None,
219            text_color: None,
220            font_size: None,
221            font_family: None,
222            h_align: HorizontalAlign::default(),
223            v_align: VerticalAlign::default(),
224            borders: None,
225            margins: None,
226            is_merged: false,
227        }
228    }
229
230    /// Create a merged placeholder cell (for cells covered by a span)
231    pub fn merged() -> Self {
232        let mut cell = Self::new("");
233        cell.is_merged = true;
234        cell
235    }
236
237    /// Set bold
238    pub fn bold(mut self) -> Self {
239        self.bold = true;
240        self
241    }
242
243    /// Set italic
244    pub fn italic(mut self) -> Self {
245        self.italic = true;
246        self
247    }
248
249    /// Set underline
250    pub fn underline(mut self) -> Self {
251        self.underline = true;
252        self
253    }
254
255    /// Set strikethrough
256    pub fn strikethrough(mut self) -> Self {
257        self.strikethrough = true;
258        self
259    }
260
261    /// Set background color
262    pub fn background(mut self, color: impl Into<String>) -> Self {
263        self.background_color = Some(color.into());
264        self
265    }
266
267    /// Set text color
268    pub fn color(mut self, color: impl Into<String>) -> Self {
269        self.text_color = Some(color.into());
270        self
271    }
272
273    /// Set font size (in points)
274    pub fn font_size(mut self, size: u32) -> Self {
275        self.font_size = Some(size);
276        self
277    }
278
279    /// Set font family
280    pub fn font(mut self, family: impl Into<String>) -> Self {
281        self.font_family = Some(family.into());
282        self
283    }
284
285    /// Set horizontal alignment
286    pub fn align(mut self, align: HorizontalAlign) -> Self {
287        self.h_align = align;
288        self
289    }
290
291    /// Set vertical alignment
292    pub fn valign(mut self, align: VerticalAlign) -> Self {
293        self.v_align = align;
294        self
295    }
296
297    /// Center text (horizontal and vertical)
298    pub fn center(mut self) -> Self {
299        self.h_align = HorizontalAlign::Center;
300        self.v_align = VerticalAlign::Middle;
301        self
302    }
303
304    /// Set row span
305    pub fn row_span(mut self, span: u32) -> Self {
306        self.row_span = span;
307        self
308    }
309
310    /// Set column span
311    pub fn col_span(mut self, span: u32) -> Self {
312        self.col_span = span;
313        self
314    }
315
316    /// Set all borders
317    pub fn borders(mut self, borders: CellBorders) -> Self {
318        self.borders = Some(borders);
319        self
320    }
321
322    /// Set uniform border on all sides
323    pub fn border(mut self, width_pt: f32, color: impl Into<String>) -> Self {
324        self.borders = Some(CellBorders::all(CellBorder::new(width_pt, color)));
325        self
326    }
327
328    /// Set cell margins
329    pub fn margins(mut self, margins: CellMargins) -> Self {
330        self.margins = Some(margins);
331        self
332    }
333
334    /// Generate XML for this cell
335    pub fn to_xml(&self) -> String {
336        // Handle merged cells (placeholders)
337        if self.is_merged {
338            return r#"<a:tc hMerge="1"><a:txBody><a:bodyPr/><a:lstStyle/><a:p/></a:txBody><a:tcPr/></a:tc>"#.to_string();
339        }
340
341        let mut attrs = String::new();
342        if self.row_span > 1 {
343            attrs.push_str(&format!(r#" rowSpan="{}""#, self.row_span));
344        }
345        if self.col_span > 1 {
346            attrs.push_str(&format!(r#" gridSpan="{}""#, self.col_span));
347        }
348
349        // Background fill
350        let bg_xml = self.background_color.as_ref()
351            .map(|c| format!(r#"<a:solidFill><a:srgbClr val="{}"/></a:solidFill>"#, c.trim_start_matches('#')))
352            .unwrap_or_default();
353
354        // Text run properties
355        let mut rpr_attrs = String::new();
356        if self.bold { rpr_attrs.push_str(r#" b="1""#); }
357        if self.italic { rpr_attrs.push_str(r#" i="1""#); }
358        if self.underline { rpr_attrs.push_str(r#" u="sng""#); }
359        if self.strikethrough { rpr_attrs.push_str(r#" strike="sngStrike""#); }
360        if let Some(size) = self.font_size {
361            rpr_attrs.push_str(&format!(r#" sz="{}""#, size * 100));
362        }
363
364        // Text color
365        let color_xml = self.text_color.as_ref()
366            .map(|c| format!(r#"<a:solidFill><a:srgbClr val="{}"/></a:solidFill>"#, c.trim_start_matches('#')))
367            .unwrap_or_default();
368
369        // Font family
370        let font_xml = self.font_family.as_ref()
371            .map(|f| format!(r#"<a:latin typeface="{}"/>"#, f))
372            .unwrap_or_default();
373
374        // Paragraph alignment
375        let p_align = format!(r#" algn="{}""#, self.h_align.as_str());
376
377        // Cell properties
378        let mut tcpr_attrs = format!(r#" anchor="{}""#, self.v_align.as_str());
379        if let Some(ref m) = self.margins {
380            tcpr_attrs.push_str(&format!(r#" marL="{}" marR="{}" marT="{}" marB="{}""#, 
381                m.left, m.right, m.top, m.bottom));
382        }
383
384        // Borders
385        let borders_xml = self.borders.as_ref()
386            .map(|b| b.to_xml())
387            .unwrap_or_default();
388
389        format!(
390            r#"<a:tc{}>
391          <a:txBody>
392            <a:bodyPr/>
393            <a:lstStyle/>
394            <a:p{}>
395              <a:r>
396                <a:rPr lang="en-US"{}>{}{}</a:rPr>
397                <a:t>{}</a:t>
398              </a:r>
399            </a:p>
400          </a:txBody>
401          <a:tcPr{}>{}{}</a:tcPr>
402        </a:tc>"#,
403            attrs,
404            p_align,
405            rpr_attrs,
406            color_xml,
407            font_xml,
408            escape_xml(&self.text),
409            tcpr_attrs,
410            borders_xml,
411            bg_xml
412        )
413    }
414}
415
416/// Table row
417#[derive(Debug, Clone)]
418pub struct TableRowPart {
419    pub cells: Vec<TableCellPart>,
420    pub height: Option<i64>, // in EMU
421}
422
423impl TableRowPart {
424    /// Create a new table row
425    pub fn new(cells: Vec<TableCellPart>) -> Self {
426        TableRowPart {
427            cells,
428            height: None,
429        }
430    }
431
432    /// Set row height in EMU
433    pub fn height(mut self, height: i64) -> Self {
434        self.height = Some(height);
435        self
436    }
437
438    /// Generate XML for this row
439    pub fn to_xml(&self) -> String {
440        let height_attr = self.height
441            .map(|h| format!(r#" h="{}""#, h))
442            .unwrap_or_default();
443
444        let cells_xml: String = self.cells.iter()
445            .map(|c| c.to_xml())
446            .collect::<Vec<_>>()
447            .join("\n        ");
448
449        format!(
450            r#"<a:tr{}>
451        {}
452      </a:tr>"#,
453            height_attr,
454            cells_xml
455        )
456    }
457}
458
459/// Table part for embedding in slides
460#[derive(Debug, Clone)]
461pub struct TablePart {
462    pub rows: Vec<TableRowPart>,
463    pub col_widths: Vec<i64>, // in EMU
464    pub x: i64,
465    pub y: i64,
466    pub width: i64,
467    pub height: i64,
468}
469
470impl TablePart {
471    /// Create a new table part
472    pub fn new() -> Self {
473        TablePart {
474            rows: vec![],
475            col_widths: vec![],
476            x: 914400,      // 1 inch
477            y: 1828800,     // 2 inches
478            width: 7315200, // 8 inches
479            height: 1828800, // 2 inches
480        }
481    }
482
483    /// Add a row
484    pub fn add_row(mut self, row: TableRowPart) -> Self {
485        // Auto-calculate column widths if not set
486        if self.col_widths.is_empty() && !row.cells.is_empty() {
487            let col_count = row.cells.len();
488            let col_width = self.width / col_count as i64;
489            self.col_widths = vec![col_width; col_count];
490        }
491        self.rows.push(row);
492        self
493    }
494
495    /// Set position
496    pub fn position(mut self, x: i64, y: i64) -> Self {
497        self.x = x;
498        self.y = y;
499        self
500    }
501
502    /// Set size
503    pub fn size(mut self, width: i64, height: i64) -> Self {
504        self.width = width;
505        self.height = height;
506        self
507    }
508
509    /// Set column widths
510    pub fn col_widths(mut self, widths: Vec<i64>) -> Self {
511        self.col_widths = widths;
512        self
513    }
514
515    /// Generate table XML for embedding in a slide
516    pub fn to_slide_xml(&self, shape_id: usize) -> String {
517        let grid_cols: String = self.col_widths.iter()
518            .map(|w| format!(r#"<a:gridCol w="{}"/>"#, w))
519            .collect::<Vec<_>>()
520            .join("\n        ");
521
522        let rows_xml: String = self.rows.iter()
523            .map(|r| r.to_xml())
524            .collect::<Vec<_>>()
525            .join("\n      ");
526
527        format!(
528            r#"<p:graphicFrame>
529  <p:nvGraphicFramePr>
530    <p:cNvPr id="{}" name="Table {}"/>
531    <p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr>
532    <p:nvPr/>
533  </p:nvGraphicFramePr>
534  <p:xfrm>
535    <a:off x="{}" y="{}"/>
536    <a:ext cx="{}" cy="{}"/>
537  </p:xfrm>
538  <a:graphic>
539    <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
540      <a:tbl>
541        <a:tblPr firstRow="1" bandRow="1">
542          <a:tableStyleId>{{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}}</a:tableStyleId>
543        </a:tblPr>
544        <a:tblGrid>
545        {}
546        </a:tblGrid>
547      {}
548      </a:tbl>
549    </a:graphicData>
550  </a:graphic>
551</p:graphicFrame>"#,
552            shape_id,
553            shape_id,
554            self.x,
555            self.y,
556            self.width,
557            self.height,
558            grid_cols,
559            rows_xml
560        )
561    }
562}
563
564impl Default for TablePart {
565    fn default() -> Self {
566        Self::new()
567    }
568}
569
570impl Part for TablePart {
571    fn path(&self) -> &str {
572        "" // Tables are embedded in slides, not separate parts
573    }
574
575    fn part_type(&self) -> PartType {
576        PartType::Slide // Tables are part of slides
577    }
578
579    fn content_type(&self) -> ContentType {
580        ContentType::Xml
581    }
582
583    fn to_xml(&self) -> Result<String, PptxError> {
584        Ok(self.to_slide_xml(2))
585    }
586
587    fn from_xml(_xml: &str) -> Result<Self, PptxError> {
588        Ok(TablePart::new())
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595
596    #[test]
597    fn test_table_cell_new() {
598        let cell = TableCellPart::new("Test");
599        assert_eq!(cell.text, "Test");
600        assert!(!cell.bold);
601    }
602
603    #[test]
604    fn test_table_cell_formatting() {
605        let cell = TableCellPart::new("Bold")
606            .bold()
607            .color("FF0000")
608            .font_size(14);
609        assert!(cell.bold);
610        assert_eq!(cell.text_color, Some("FF0000".to_string()));
611        assert_eq!(cell.font_size, Some(14));
612    }
613
614    #[test]
615    fn test_table_cell_span() {
616        let cell = TableCellPart::new("Merged")
617            .row_span(2)
618            .col_span(3);
619        assert_eq!(cell.row_span, 2);
620        assert_eq!(cell.col_span, 3);
621    }
622
623    #[test]
624    fn test_table_row_new() {
625        let row = TableRowPart::new(vec![
626            TableCellPart::new("A"),
627            TableCellPart::new("B"),
628        ]);
629        assert_eq!(row.cells.len(), 2);
630    }
631
632    #[test]
633    fn test_table_part_new() {
634        let table = TablePart::new()
635            .add_row(TableRowPart::new(vec![
636                TableCellPart::new("Header 1"),
637                TableCellPart::new("Header 2"),
638            ]))
639            .add_row(TableRowPart::new(vec![
640                TableCellPart::new("Data 1"),
641                TableCellPart::new("Data 2"),
642            ]));
643        assert_eq!(table.rows.len(), 2);
644        assert_eq!(table.col_widths.len(), 2);
645    }
646
647    #[test]
648    fn test_table_to_xml() {
649        let table = TablePart::new()
650            .add_row(TableRowPart::new(vec![
651                TableCellPart::new("Test"),
652            ]));
653        let xml = table.to_slide_xml(5);
654        assert!(xml.contains("p:graphicFrame"));
655        assert!(xml.contains("a:tbl"));
656        assert!(xml.contains("Test"));
657    }
658}