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:endParaRPr/></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_pr = if self.h_align == HorizontalAlign::Center {
423 String::new()
424 } else {
425 format!(r#"<a:pPr algn="{}"/>"#, self.h_align.as_str())
426 };
427
428 let mut tcpr_attrs = format!(r#" anchor="{}""#, self.v_align.as_str());
430 if let Some(ref m) = self.margins {
431 tcpr_attrs.push_str(&format!(
432 r#" marL="{}" marR="{}" marT="{}" marB="{}""#,
433 m.left, m.right, m.top, m.bottom
434 ));
435 }
436
437 let borders_xml = self
439 .borders
440 .as_ref()
441 .map(|b| b.to_xml())
442 .unwrap_or_default();
443
444 format!(
445 r#"<a:tc{}>
446 <a:txBody>
447 <a:bodyPr/>
448 <a:lstStyle/>
449 <a:p>
450 {}
451 <a:r>
452 <a:rPr lang="en-US"{}>{}{}</a:rPr>
453 <a:t>{}</a:t>
454 </a:r>
455 </a:p>
456 </a:txBody>
457 <a:tcPr{}>{}{}</a:tcPr>
458 </a:tc>"#,
459 attrs,
460 p_pr,
461 rpr_attrs,
462 color_xml,
463 font_xml,
464 escape_xml(&self.text),
465 tcpr_attrs,
466 borders_xml,
467 bg_xml
468 )
469 }
470}
471
472impl ToXml for TableCellPart {
473 fn to_xml(&self) -> String {
474 TableCellPart::to_xml(self)
475 }
476}
477
478#[derive(Debug, Clone)]
480pub struct TableRowPart {
481 pub cells: Vec<TableCellPart>,
482 pub height: Option<i64>, }
484
485impl TableRowPart {
486 pub fn new(cells: Vec<TableCellPart>) -> Self {
488 TableRowPart {
489 cells,
490 height: None,
491 }
492 }
493
494 pub fn height(mut self, height: i64) -> Self {
496 self.height = Some(height);
497 self
498 }
499
500 pub fn to_xml(&self) -> String {
502 let height_attr = self
503 .height
504 .map(|h| format!(r#" h="{}""#, h))
505 .unwrap_or_default();
506
507 let cells_xml: String = self
508 .cells
509 .iter()
510 .map(|c| c.to_xml())
511 .collect::<Vec<_>>()
512 .join("\n ");
513
514 format!(
515 r#"<a:tr{}>
516 {}
517 </a:tr>"#,
518 height_attr, cells_xml
519 )
520 }
521}
522
523impl ToXml for TableRowPart {
524 fn to_xml(&self) -> String {
525 TableRowPart::to_xml(self)
526 }
527}
528
529#[derive(Debug, Clone)]
531pub struct TablePart {
532 pub rows: Vec<TableRowPart>,
533 pub col_widths: Vec<i64>, pub x: i64,
535 pub y: i64,
536 pub width: i64,
537 pub height: i64,
538}
539
540impl TablePart {
541 pub fn new() -> Self {
543 TablePart {
544 rows: vec![],
545 col_widths: vec![],
546 x: 914400, y: 1828800, width: 7315200, height: 1828800, }
551 }
552
553 pub fn add_row(mut self, row: TableRowPart) -> Self {
555 if self.col_widths.is_empty() && !row.cells.is_empty() {
557 let col_count = row.cells.len();
558 let col_width = self.width / col_count as i64;
559 self.col_widths = vec![col_width; col_count];
560 }
561 self.rows.push(row);
562 self
563 }
564
565 pub fn position(mut self, x: i64, y: i64) -> Self {
567 self.x = x;
568 self.y = y;
569 self
570 }
571
572 pub fn size(mut self, width: i64, height: i64) -> Self {
574 self.width = width;
575 self.height = height;
576 self
577 }
578
579 pub fn col_widths(mut self, widths: Vec<i64>) -> Self {
581 self.col_widths = widths;
582 self
583 }
584
585 pub fn to_slide_xml(&self, shape_id: usize) -> String {
587 let grid_cols: String = self
588 .col_widths
589 .iter()
590 .map(|w| format!(r#"<a:gridCol w="{}"/>"#, w))
591 .collect::<Vec<_>>()
592 .join("\n ");
593
594 let rows_xml: String = self
595 .rows
596 .iter()
597 .map(|r| r.to_xml())
598 .collect::<Vec<_>>()
599 .join("\n ");
600
601 format!(
602 r#"<p:graphicFrame>
603 <p:nvGraphicFramePr>
604 <p:cNvPr id="{}" name="Table {}"/>
605 <p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr>
606 <p:nvPr/>
607 </p:nvGraphicFramePr>
608 <p:xfrm>
609 <a:off x="{}" y="{}"/>
610 <a:ext cx="{}" cy="{}"/>
611 </p:xfrm>
612 <a:graphic>
613 <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
614 <a:tbl>
615 <a:tblPr firstRow="1" bandRow="1">
616 <a:tableStyleId>{{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}}</a:tableStyleId>
617 </a:tblPr>
618 <a:tblGrid>
619 {}
620 </a:tblGrid>
621 {}
622 </a:tbl>
623 </a:graphicData>
624 </a:graphic>
625</p:graphicFrame>"#,
626 shape_id, shape_id, self.x, self.y, self.width, self.height, grid_cols, rows_xml
627 )
628 }
629}
630
631impl Default for TablePart {
632 fn default() -> Self {
633 Self::new()
634 }
635}
636
637impl Part for TablePart {
638 fn path(&self) -> &str {
639 "" }
641
642 fn part_type(&self) -> PartType {
643 PartType::Slide }
645
646 fn content_type(&self) -> ContentType {
647 ContentType::Xml
648 }
649
650 fn to_xml(&self) -> Result<String, PptxError> {
651 Ok(self.to_slide_xml(2))
652 }
653
654 fn from_xml(_xml: &str) -> Result<Self, PptxError> {
655 Ok(TablePart::new())
656 }
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662
663 #[test]
664 fn test_table_cell_new() {
665 let cell = TableCellPart::new("Test");
666 assert_eq!(cell.text, "Test");
667 assert!(!cell.bold);
668 }
669
670 #[test]
671 fn test_table_cell_formatting() {
672 let cell = TableCellPart::new("Bold")
673 .bold()
674 .color("FF0000")
675 .font_size(14);
676 assert!(cell.bold);
677 assert_eq!(cell.text_color, Some("FF0000".to_string()));
678 assert_eq!(cell.font_size, Some(14));
679 }
680
681 #[test]
682 fn test_table_cell_span() {
683 let cell = TableCellPart::new("Merged").row_span(2).col_span(3);
684 assert_eq!(cell.row_span, 2);
685 assert_eq!(cell.col_span, 3);
686 }
687
688 #[test]
689 fn test_table_row_new() {
690 let row = TableRowPart::new(vec![TableCellPart::new("A"), TableCellPart::new("B")]);
691 assert_eq!(row.cells.len(), 2);
692 }
693
694 #[test]
695 fn test_table_part_new() {
696 let table = TablePart::new()
697 .add_row(TableRowPart::new(vec![
698 TableCellPart::new("Header 1"),
699 TableCellPart::new("Header 2"),
700 ]))
701 .add_row(TableRowPart::new(vec![
702 TableCellPart::new("Data 1"),
703 TableCellPart::new("Data 2"),
704 ]));
705 assert_eq!(table.rows.len(), 2);
706 assert_eq!(table.col_widths.len(), 2);
707 }
708
709 #[test]
710 fn test_table_to_xml() {
711 let table = TablePart::new().add_row(TableRowPart::new(vec![TableCellPart::new("Test")]));
712 let xml = table.to_slide_xml(5);
713 assert!(xml.contains("p:graphicFrame"));
714 assert!(xml.contains("a:tbl"));
715 assert!(xml.contains("Test"));
716 }
717}