1use super::base::{ContentType, Part, PartType};
15use crate::core::{escape_xml, ToXml};
16use crate::exc::PptxError;
17
18#[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#[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#[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#[derive(Debug, Clone, Default)]
83pub struct CellBorder {
84 pub width: i32, 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#[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 {
142 width: 0,
143 color: String::new(),
144 style: BorderStyle::None,
145 };
146 CellBorders {
147 left: Some(no_border.clone()),
148 right: Some(no_border.clone()),
149 top: Some(no_border.clone()),
150 bottom: Some(no_border),
151 }
152 }
153
154 pub fn to_xml(&self) -> String {
155 let mut xml = String::new();
156 if let Some(ref b) = self.left {
157 xml.push_str(&b.to_xml("lnL"));
158 }
159 if let Some(ref b) = self.right {
160 xml.push_str(&b.to_xml("lnR"));
161 }
162 if let Some(ref b) = self.top {
163 xml.push_str(&b.to_xml("lnT"));
164 }
165 if let Some(ref b) = self.bottom {
166 xml.push_str(&b.to_xml("lnB"));
167 }
168 xml
169 }
170}
171
172impl ToXml for CellBorders {
173 fn to_xml(&self) -> String {
174 CellBorders::to_xml(self)
175 }
176}
177
178#[derive(Debug, Clone)]
180pub struct CellMargins {
181 pub left: i32, pub right: i32,
183 pub top: i32,
184 pub bottom: i32,
185}
186
187impl Default for CellMargins {
188 fn default() -> Self {
189 CellMargins {
190 left: 91440, right: 91440,
192 top: 45720, bottom: 45720,
194 }
195 }
196}
197
198impl CellMargins {
199 pub fn uniform(margin: i32) -> Self {
200 CellMargins {
201 left: margin,
202 right: margin,
203 top: margin,
204 bottom: margin,
205 }
206 }
207}
208
209#[derive(Debug, Clone)]
211pub struct TableCellPart {
212 pub text: String,
213 pub row_span: u32,
214 pub col_span: u32,
215 pub bold: bool,
216 pub italic: bool,
217 pub underline: bool,
218 pub strikethrough: bool,
219 pub background_color: Option<String>,
220 pub text_color: Option<String>,
221 pub font_size: Option<u32>,
222 pub font_family: Option<String>,
223 pub h_align: HorizontalAlign,
224 pub v_align: VerticalAlign,
225 pub borders: Option<CellBorders>,
226 pub margins: Option<CellMargins>,
227 pub is_merged: bool, }
229
230impl TableCellPart {
231 pub fn new(text: impl Into<String>) -> Self {
233 TableCellPart {
234 text: text.into(),
235 row_span: 1,
236 col_span: 1,
237 bold: false,
238 italic: false,
239 underline: false,
240 strikethrough: false,
241 background_color: None,
242 text_color: None,
243 font_size: None,
244 font_family: None,
245 h_align: HorizontalAlign::default(),
246 v_align: VerticalAlign::default(),
247 borders: None,
248 margins: None,
249 is_merged: false,
250 }
251 }
252
253 pub fn merged() -> Self {
255 let mut cell = Self::new("");
256 cell.is_merged = true;
257 cell
258 }
259
260 pub fn bold(mut self) -> Self {
262 self.bold = true;
263 self
264 }
265
266 pub fn italic(mut self) -> Self {
268 self.italic = true;
269 self
270 }
271
272 pub fn underline(mut self) -> Self {
274 self.underline = true;
275 self
276 }
277
278 pub fn strikethrough(mut self) -> Self {
280 self.strikethrough = true;
281 self
282 }
283
284 pub fn background(mut self, color: impl Into<String>) -> Self {
286 self.background_color = Some(color.into());
287 self
288 }
289
290 pub fn color(mut self, color: impl Into<String>) -> Self {
292 self.text_color = Some(color.into());
293 self
294 }
295
296 pub fn font_size(mut self, size: u32) -> Self {
298 self.font_size = Some(size);
299 self
300 }
301
302 pub fn font(mut self, family: impl Into<String>) -> Self {
304 self.font_family = Some(family.into());
305 self
306 }
307
308 pub fn align(mut self, align: HorizontalAlign) -> Self {
310 self.h_align = align;
311 self
312 }
313
314 pub fn valign(mut self, align: VerticalAlign) -> Self {
316 self.v_align = align;
317 self
318 }
319
320 pub fn center(mut self) -> Self {
322 self.h_align = HorizontalAlign::Center;
323 self.v_align = VerticalAlign::Middle;
324 self
325 }
326
327 pub fn row_span(mut self, span: u32) -> Self {
329 self.row_span = span;
330 self
331 }
332
333 pub fn col_span(mut self, span: u32) -> Self {
335 self.col_span = span;
336 self
337 }
338
339 pub fn borders(mut self, borders: CellBorders) -> Self {
341 self.borders = Some(borders);
342 self
343 }
344
345 pub fn border(mut self, width_pt: f32, color: impl Into<String>) -> Self {
347 self.borders = Some(CellBorders::all(CellBorder::new(width_pt, color)));
348 self
349 }
350
351 pub fn margins(mut self, margins: CellMargins) -> Self {
353 self.margins = Some(margins);
354 self
355 }
356
357 pub fn to_xml(&self) -> String {
359 if self.is_merged {
361 return r#"<a:tc hMerge="1"><a:txBody><a:bodyPr/><a:lstStyle/><a:p/></a:txBody><a:tcPr/></a:tc>"#.to_string();
362 }
363
364 let mut attrs = String::new();
365 if self.row_span > 1 {
366 attrs.push_str(&format!(r#" rowSpan="{}""#, self.row_span));
367 }
368 if self.col_span > 1 {
369 attrs.push_str(&format!(r#" gridSpan="{}""#, self.col_span));
370 }
371
372 let bg_xml = self
374 .background_color
375 .as_ref()
376 .map(|c| {
377 format!(
378 r#"<a:solidFill><a:srgbClr val="{}"/></a:solidFill>"#,
379 c.trim_start_matches('#')
380 )
381 })
382 .unwrap_or_default();
383
384 let mut rpr_attrs = String::new();
386 if self.bold {
387 rpr_attrs.push_str(r#" b="1""#);
388 }
389 if self.italic {
390 rpr_attrs.push_str(r#" i="1""#);
391 }
392 if self.underline {
393 rpr_attrs.push_str(r#" u="sng""#);
394 }
395 if self.strikethrough {
396 rpr_attrs.push_str(r#" strike="sngStrike""#);
397 }
398 if let Some(size) = self.font_size {
399 rpr_attrs.push_str(&format!(r#" sz="{}""#, size * 100));
400 }
401
402 let color_xml = self
404 .text_color
405 .as_ref()
406 .map(|c| {
407 format!(
408 r#"<a:solidFill><a:srgbClr val="{}"/></a:solidFill>"#,
409 c.trim_start_matches('#')
410 )
411 })
412 .unwrap_or_default();
413
414 let font_xml = self
416 .font_family
417 .as_ref()
418 .map(|f| format!(r#"<a:latin typeface="{}"/>"#, f))
419 .unwrap_or_default();
420
421 let p_align = format!(r#" algn="{}""#, self.h_align.as_str());
423
424 let mut tcpr_attrs = format!(r#" anchor="{}""#, self.v_align.as_str());
426 if let Some(ref m) = self.margins {
427 tcpr_attrs.push_str(&format!(
428 r#" marL="{}" marR="{}" marT="{}" marB="{}""#,
429 m.left, m.right, m.top, m.bottom
430 ));
431 }
432
433 let borders_xml = self
435 .borders
436 .as_ref()
437 .map(|b| b.to_xml())
438 .unwrap_or_default();
439
440 format!(
441 r#"<a:tc{}>
442 <a:txBody>
443 <a:bodyPr/>
444 <a:lstStyle/>
445 <a:p{}>
446 <a:r>
447 <a:rPr lang="en-US"{}>{}{}</a:rPr>
448 <a:t>{}</a:t>
449 </a:r>
450 </a:p>
451 </a:txBody>
452 <a:tcPr{}>{}{}</a:tcPr>
453 </a:tc>"#,
454 attrs,
455 p_align,
456 rpr_attrs,
457 color_xml,
458 font_xml,
459 escape_xml(&self.text),
460 tcpr_attrs,
461 borders_xml,
462 bg_xml
463 )
464 }
465}
466
467impl ToXml for TableCellPart {
468 fn to_xml(&self) -> String {
469 TableCellPart::to_xml(self)
470 }
471}
472
473#[derive(Debug, Clone)]
475pub struct TableRowPart {
476 pub cells: Vec<TableCellPart>,
477 pub height: Option<i64>, }
479
480impl TableRowPart {
481 pub fn new(cells: Vec<TableCellPart>) -> Self {
483 TableRowPart {
484 cells,
485 height: None,
486 }
487 }
488
489 pub fn height(mut self, height: i64) -> Self {
491 self.height = Some(height);
492 self
493 }
494
495 pub fn to_xml(&self) -> String {
497 let height_attr = self
498 .height
499 .map(|h| format!(r#" h="{}""#, h))
500 .unwrap_or_default();
501
502 let cells_xml: String = self
503 .cells
504 .iter()
505 .map(|c| c.to_xml())
506 .collect::<Vec<_>>()
507 .join("\n ");
508
509 format!(
510 r#"<a:tr{}>
511 {}
512 </a:tr>"#,
513 height_attr, cells_xml
514 )
515 }
516}
517
518impl ToXml for TableRowPart {
519 fn to_xml(&self) -> String {
520 TableRowPart::to_xml(self)
521 }
522}
523
524#[derive(Debug, Clone)]
526pub struct TablePart {
527 pub rows: Vec<TableRowPart>,
528 pub col_widths: Vec<i64>, pub x: i64,
530 pub y: i64,
531 pub width: i64,
532 pub height: i64,
533}
534
535impl TablePart {
536 pub fn new() -> Self {
538 TablePart {
539 rows: vec![],
540 col_widths: vec![],
541 x: 914400, y: 1828800, width: 7315200, height: 1828800, }
546 }
547
548 pub fn add_row(mut self, row: TableRowPart) -> Self {
550 if self.col_widths.is_empty() && !row.cells.is_empty() {
552 let col_count = row.cells.len();
553 let col_width = self.width / col_count as i64;
554 self.col_widths = vec![col_width; col_count];
555 }
556 self.rows.push(row);
557 self
558 }
559
560 pub fn position(mut self, x: i64, y: i64) -> Self {
562 self.x = x;
563 self.y = y;
564 self
565 }
566
567 pub fn size(mut self, width: i64, height: i64) -> Self {
569 self.width = width;
570 self.height = height;
571 self
572 }
573
574 pub fn col_widths(mut self, widths: Vec<i64>) -> Self {
576 self.col_widths = widths;
577 self
578 }
579
580 pub fn to_slide_xml(&self, shape_id: usize) -> String {
582 let grid_cols: String = self
583 .col_widths
584 .iter()
585 .map(|w| format!(r#"<a:gridCol w="{}"/>"#, w))
586 .collect::<Vec<_>>()
587 .join("\n ");
588
589 let rows_xml: String = self
590 .rows
591 .iter()
592 .map(|r| r.to_xml())
593 .collect::<Vec<_>>()
594 .join("\n ");
595
596 format!(
597 r#"<p:graphicFrame>
598 <p:nvGraphicFramePr>
599 <p:cNvPr id="{}" name="Table {}"/>
600 <p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr>
601 <p:nvPr/>
602 </p:nvGraphicFramePr>
603 <p:xfrm>
604 <a:off x="{}" y="{}"/>
605 <a:ext cx="{}" cy="{}"/>
606 </p:xfrm>
607 <a:graphic>
608 <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
609 <a:tbl>
610 <a:tblPr firstRow="1" bandRow="1">
611 <a:tableStyleId>{{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}}</a:tableStyleId>
612 </a:tblPr>
613 <a:tblGrid>
614 {}
615 </a:tblGrid>
616 {}
617 </a:tbl>
618 </a:graphicData>
619 </a:graphic>
620</p:graphicFrame>"#,
621 shape_id, shape_id, self.x, self.y, self.width, self.height, grid_cols, rows_xml
622 )
623 }
624}
625
626impl Default for TablePart {
627 fn default() -> Self {
628 Self::new()
629 }
630}
631
632impl Part for TablePart {
633 fn path(&self) -> &str {
634 "" }
636
637 fn part_type(&self) -> PartType {
638 PartType::Slide }
640
641 fn content_type(&self) -> ContentType {
642 ContentType::Xml
643 }
644
645 fn to_xml(&self) -> Result<String, PptxError> {
646 Ok(self.to_slide_xml(2))
647 }
648
649 fn from_xml(_xml: &str) -> Result<Self, PptxError> {
650 Ok(TablePart::new())
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657
658 #[test]
659 fn test_table_cell_new() {
660 let cell = TableCellPart::new("Test");
661 assert_eq!(cell.text, "Test");
662 assert!(!cell.bold);
663 }
664
665 #[test]
666 fn test_table_cell_formatting() {
667 let cell = TableCellPart::new("Bold")
668 .bold()
669 .color("FF0000")
670 .font_size(14);
671 assert!(cell.bold);
672 assert_eq!(cell.text_color, Some("FF0000".to_string()));
673 assert_eq!(cell.font_size, Some(14));
674 }
675
676 #[test]
677 fn test_table_cell_span() {
678 let cell = TableCellPart::new("Merged").row_span(2).col_span(3);
679 assert_eq!(cell.row_span, 2);
680 assert_eq!(cell.col_span, 3);
681 }
682
683 #[test]
684 fn test_table_row_new() {
685 let row = TableRowPart::new(vec![TableCellPart::new("A"), TableCellPart::new("B")]);
686 assert_eq!(row.cells.len(), 2);
687 }
688
689 #[test]
690 fn test_table_part_new() {
691 let table = TablePart::new()
692 .add_row(TableRowPart::new(vec![
693 TableCellPart::new("Header 1"),
694 TableCellPart::new("Header 2"),
695 ]))
696 .add_row(TableRowPart::new(vec![
697 TableCellPart::new("Data 1"),
698 TableCellPart::new("Data 2"),
699 ]));
700 assert_eq!(table.rows.len(), 2);
701 assert_eq!(table.col_widths.len(), 2);
702 }
703
704 #[test]
705 fn test_table_to_xml() {
706 let table = TablePart::new().add_row(TableRowPart::new(vec![TableCellPart::new("Test")]));
707 let xml = table.to_slide_xml(5);
708 assert!(xml.contains("p:graphicFrame"));
709 assert!(xml.contains("a:tbl"));
710 assert!(xml.contains("Test"));
711 }
712}