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