1use super::base::{Part, PartType, ContentType};
15use crate::exc::PptxError;
16use crate::core::escape_xml;
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
160#[derive(Debug, Clone)]
162pub struct CellMargins {
163 pub left: i32, 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, right: 91440,
174 top: 45720, 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#[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, }
206
207impl TableCellPart {
208 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 pub fn merged() -> Self {
232 let mut cell = Self::new("");
233 cell.is_merged = true;
234 cell
235 }
236
237 pub fn bold(mut self) -> Self {
239 self.bold = true;
240 self
241 }
242
243 pub fn italic(mut self) -> Self {
245 self.italic = true;
246 self
247 }
248
249 pub fn underline(mut self) -> Self {
251 self.underline = true;
252 self
253 }
254
255 pub fn strikethrough(mut self) -> Self {
257 self.strikethrough = true;
258 self
259 }
260
261 pub fn background(mut self, color: impl Into<String>) -> Self {
263 self.background_color = Some(color.into());
264 self
265 }
266
267 pub fn color(mut self, color: impl Into<String>) -> Self {
269 self.text_color = Some(color.into());
270 self
271 }
272
273 pub fn font_size(mut self, size: u32) -> Self {
275 self.font_size = Some(size);
276 self
277 }
278
279 pub fn font(mut self, family: impl Into<String>) -> Self {
281 self.font_family = Some(family.into());
282 self
283 }
284
285 pub fn align(mut self, align: HorizontalAlign) -> Self {
287 self.h_align = align;
288 self
289 }
290
291 pub fn valign(mut self, align: VerticalAlign) -> Self {
293 self.v_align = align;
294 self
295 }
296
297 pub fn center(mut self) -> Self {
299 self.h_align = HorizontalAlign::Center;
300 self.v_align = VerticalAlign::Middle;
301 self
302 }
303
304 pub fn row_span(mut self, span: u32) -> Self {
306 self.row_span = span;
307 self
308 }
309
310 pub fn col_span(mut self, span: u32) -> Self {
312 self.col_span = span;
313 self
314 }
315
316 pub fn borders(mut self, borders: CellBorders) -> Self {
318 self.borders = Some(borders);
319 self
320 }
321
322 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 pub fn margins(mut self, margins: CellMargins) -> Self {
330 self.margins = Some(margins);
331 self
332 }
333
334 pub fn to_xml(&self) -> String {
336 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 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 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 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 let font_xml = self.font_family.as_ref()
371 .map(|f| format!(r#"<a:latin typeface="{}"/>"#, f))
372 .unwrap_or_default();
373
374 let p_align = format!(r#" algn="{}""#, self.h_align.as_str());
376
377 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 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#[derive(Debug, Clone)]
418pub struct TableRowPart {
419 pub cells: Vec<TableCellPart>,
420 pub height: Option<i64>, }
422
423impl TableRowPart {
424 pub fn new(cells: Vec<TableCellPart>) -> Self {
426 TableRowPart {
427 cells,
428 height: None,
429 }
430 }
431
432 pub fn height(mut self, height: i64) -> Self {
434 self.height = Some(height);
435 self
436 }
437
438 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#[derive(Debug, Clone)]
461pub struct TablePart {
462 pub rows: Vec<TableRowPart>,
463 pub col_widths: Vec<i64>, pub x: i64,
465 pub y: i64,
466 pub width: i64,
467 pub height: i64,
468}
469
470impl TablePart {
471 pub fn new() -> Self {
473 TablePart {
474 rows: vec![],
475 col_widths: vec![],
476 x: 914400, y: 1828800, width: 7315200, height: 1828800, }
481 }
482
483 pub fn add_row(mut self, row: TableRowPart) -> Self {
485 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 pub fn position(mut self, x: i64, y: i64) -> Self {
497 self.x = x;
498 self.y = y;
499 self
500 }
501
502 pub fn size(mut self, width: i64, height: i64) -> Self {
504 self.width = width;
505 self.height = height;
506 self
507 }
508
509 pub fn col_widths(mut self, widths: Vec<i64>) -> Self {
511 self.col_widths = widths;
512 self
513 }
514
515 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 "" }
574
575 fn part_type(&self) -> PartType {
576 PartType::Slide }
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}