1use crate::types::CTShapeProperties;
6#[cfg(feature = "dml-text")]
7use crate::types::*;
8
9#[cfg(feature = "dml-text")]
11pub trait TextBodyExt {
12 fn paragraphs(&self) -> &[TextParagraph];
14
15 fn text(&self) -> String;
17}
18
19#[cfg(feature = "dml-text")]
20impl TextBodyExt for TextBody {
21 fn paragraphs(&self) -> &[TextParagraph] {
22 &self.p
23 }
24
25 fn text(&self) -> String {
26 self.p
27 .iter()
28 .map(|p| p.text())
29 .collect::<Vec<_>>()
30 .join("\n")
31 }
32}
33
34#[cfg(feature = "dml-text")]
36pub trait TextParagraphExt {
37 fn runs(&self) -> Vec<&TextRun>;
39
40 fn text(&self) -> String;
42
43 fn level(&self) -> Option<i32>;
45
46 fn alignment(&self) -> Option<STTextAlignType>;
48}
49
50#[cfg(feature = "dml-text")]
51impl TextParagraphExt for TextParagraph {
52 fn runs(&self) -> Vec<&TextRun> {
53 self.text_run
54 .iter()
55 .filter_map(|tr| match tr {
56 EGTextRun::R(run) => Some(run.as_ref()),
57 _ => None,
58 })
59 .collect()
60 }
61
62 fn text(&self) -> String {
63 self.text_run
64 .iter()
65 .filter_map(|tr| match tr {
66 EGTextRun::R(run) => Some(run.t.as_str()),
67 EGTextRun::Br(_) => Some("\n"),
68 EGTextRun::Fld(fld) => fld.t.as_deref(),
69 })
70 .collect()
71 }
72
73 fn level(&self) -> Option<i32> {
74 self.p_pr.as_ref().and_then(|p| p.lvl)
75 }
76
77 fn alignment(&self) -> Option<STTextAlignType> {
78 self.p_pr.as_ref().and_then(|p| p.algn)
79 }
80}
81
82#[cfg(feature = "dml-text")]
84pub trait TextRunExt {
85 fn text(&self) -> &str;
87
88 fn is_bold(&self) -> bool;
90
91 fn is_italic(&self) -> bool;
93
94 fn is_underlined(&self) -> bool;
96
97 fn is_strikethrough(&self) -> bool;
99
100 fn font_size(&self) -> Option<i32>;
102
103 fn has_hyperlink(&self) -> bool;
105
106 fn hyperlink_rel_id(&self) -> Option<&str>;
108
109 fn is_all_caps(&self) -> bool;
111
112 fn is_small_caps(&self) -> bool;
114
115 fn language(&self) -> Option<&str>;
117
118 fn baseline_shift_pct(&self) -> Option<i32>;
120}
121
122#[cfg(feature = "dml-text")]
123impl TextRunExt for TextRun {
124 fn text(&self) -> &str {
125 &self.t
126 }
127
128 fn is_bold(&self) -> bool {
129 self.r_pr.as_ref().and_then(|p| p.b).unwrap_or(false)
130 }
131
132 fn is_italic(&self) -> bool {
133 self.r_pr.as_ref().and_then(|p| p.i).unwrap_or(false)
134 }
135
136 fn is_underlined(&self) -> bool {
137 self.r_pr
138 .as_ref()
139 .and_then(|p| p.u.as_ref())
140 .is_some_and(|u| *u != STTextUnderlineType::None)
141 }
142
143 fn is_strikethrough(&self) -> bool {
144 self.r_pr
145 .as_ref()
146 .and_then(|p| p.strike.as_ref())
147 .is_some_and(|s| *s != STTextStrikeType::NoStrike)
148 }
149
150 fn font_size(&self) -> Option<i32> {
151 self.r_pr.as_ref().and_then(|p| p.sz)
152 }
153
154 fn has_hyperlink(&self) -> bool {
155 self.r_pr
156 .as_ref()
157 .and_then(|p| p.hlink_click.as_ref())
158 .is_some()
159 }
160
161 fn hyperlink_rel_id(&self) -> Option<&str> {
162 self.r_pr
163 .as_ref()
164 .and_then(|p| p.hlink_click.as_ref())
165 .and_then(|h| h.id.as_deref())
166 }
167
168 fn is_all_caps(&self) -> bool {
169 self.r_pr
170 .as_ref()
171 .and_then(|p| p.cap.as_ref())
172 .is_some_and(|c| *c == STTextCapsType::All)
173 }
174
175 fn is_small_caps(&self) -> bool {
176 self.r_pr
177 .as_ref()
178 .and_then(|p| p.cap.as_ref())
179 .is_some_and(|c| *c == STTextCapsType::Small)
180 }
181
182 fn language(&self) -> Option<&str> {
183 self.r_pr.as_ref().and_then(|p| p.lang.as_deref())
184 }
185
186 fn baseline_shift_pct(&self) -> Option<i32> {
187 self.r_pr
188 .as_ref()
189 .and_then(|p| p.baseline.as_deref())
190 .and_then(|s| s.parse().ok())
191 }
192}
193
194#[cfg(feature = "dml-tables")]
196pub trait TableExt {
197 fn rows(&self) -> &[CTTableRow];
199
200 fn row_count(&self) -> usize;
202
203 fn col_count(&self) -> usize;
205
206 fn cell(&self, row: usize, col: usize) -> Option<&CTTableCell>;
208
209 fn to_text_grid(&self) -> Vec<Vec<String>>;
211
212 fn text(&self) -> String;
214}
215
216#[cfg(feature = "dml-tables")]
217impl TableExt for CTTable {
218 fn rows(&self) -> &[CTTableRow] {
219 &self.tr
220 }
221
222 fn row_count(&self) -> usize {
223 self.tr.len()
224 }
225
226 fn col_count(&self) -> usize {
227 self.tbl_grid.grid_col.len()
228 }
229
230 fn cell(&self, row: usize, col: usize) -> Option<&CTTableCell> {
231 self.tr.get(row).and_then(|r| r.tc.get(col))
232 }
233
234 fn to_text_grid(&self) -> Vec<Vec<String>> {
235 self.tr
236 .iter()
237 .map(|row| row.tc.iter().map(|c| c.text()).collect())
238 .collect()
239 }
240
241 fn text(&self) -> String {
242 self.tr
243 .iter()
244 .map(|row| {
245 row.tc
246 .iter()
247 .map(|c| c.text())
248 .collect::<Vec<_>>()
249 .join("\t")
250 })
251 .collect::<Vec<_>>()
252 .join("\n")
253 }
254}
255
256#[cfg(feature = "dml-tables")]
258pub trait TableRowExt {
259 fn cells(&self) -> &[CTTableCell];
261
262 fn cell(&self, col: usize) -> Option<&CTTableCell>;
264
265 fn height_emu(&self) -> Option<i64>;
267}
268
269#[cfg(feature = "dml-tables")]
270impl TableRowExt for CTTableRow {
271 fn cells(&self) -> &[CTTableCell] {
272 &self.tc
273 }
274
275 fn cell(&self, col: usize) -> Option<&CTTableCell> {
276 self.tc.get(col)
277 }
278
279 fn height_emu(&self) -> Option<i64> {
280 self.height.parse::<i64>().ok()
281 }
282}
283
284#[cfg(feature = "dml-tables")]
286pub trait TableCellExt {
287 fn text_body(&self) -> Option<&TextBody>;
289
290 fn text(&self) -> String;
292
293 fn row_span(&self) -> u32;
295
296 fn col_span(&self) -> u32;
298
299 fn has_row_span(&self) -> bool;
301
302 fn has_col_span(&self) -> bool;
304
305 fn is_h_merge(&self) -> bool;
307
308 fn is_v_merge(&self) -> bool;
310}
311
312#[cfg(feature = "dml-tables")]
313impl TableCellExt for CTTableCell {
314 fn text_body(&self) -> Option<&TextBody> {
315 self.tx_body.as_deref()
316 }
317
318 fn text(&self) -> String {
319 self.tx_body
320 .as_ref()
321 .map(|tb| tb.text())
322 .unwrap_or_default()
323 }
324
325 fn row_span(&self) -> u32 {
326 self.row_span.map(|s| s.max(1) as u32).unwrap_or(1)
327 }
328
329 fn col_span(&self) -> u32 {
330 self.grid_span.map(|s| s.max(1) as u32).unwrap_or(1)
331 }
332
333 fn has_row_span(&self) -> bool {
334 self.row_span.is_some_and(|s| s > 1)
335 }
336
337 fn has_col_span(&self) -> bool {
338 self.grid_span.is_some_and(|s| s > 1)
339 }
340
341 fn is_h_merge(&self) -> bool {
342 self.h_merge.unwrap_or(false)
343 }
344
345 fn is_v_merge(&self) -> bool {
346 self.v_merge.unwrap_or(false)
347 }
348}
349
350#[cfg(feature = "dml-charts")]
354#[derive(Debug, Clone, PartialEq, Eq)]
355pub enum ChartKind {
356 Bar,
358 Bar3D,
360 Line,
362 Line3D,
364 Pie,
366 Pie3D,
368 Scatter,
370 Area,
372 Area3D,
374 Bubble,
376 Doughnut,
378 Radar,
380 Stock,
382 Surface,
384 Surface3D,
386 OfPie,
388}
389
390#[cfg(feature = "dml-charts")]
394pub trait ChartSpaceExt {
395 fn chart(&self) -> &crate::types::Chart;
397
398 fn chart_types(&self) -> Vec<ChartKind>;
402
403 fn title_text(&self) -> Option<String>;
408}
409
410#[cfg(feature = "dml-charts")]
411impl ChartSpaceExt for crate::types::ChartSpace {
412 fn chart(&self) -> &crate::types::Chart {
413 &self.chart
414 }
415
416 fn chart_types(&self) -> Vec<ChartKind> {
417 self.chart.chart_types()
418 }
419
420 fn title_text(&self) -> Option<String> {
421 self.chart.title_text()
422 }
423}
424
425#[cfg(feature = "dml-charts")]
429pub trait ChartExt {
430 fn plot_area(&self) -> &crate::types::PlotArea;
432
433 fn legend(&self) -> Option<&crate::types::Legend>;
435
436 fn chart_types(&self) -> Vec<ChartKind>;
438
439 fn title_text(&self) -> Option<String>;
441}
442
443#[cfg(feature = "dml-charts")]
444impl ChartExt for crate::types::Chart {
445 fn plot_area(&self) -> &crate::types::PlotArea {
446 &self.plot_area
447 }
448
449 fn legend(&self) -> Option<&crate::types::Legend> {
450 self.legend.as_deref()
451 }
452
453 fn chart_types(&self) -> Vec<ChartKind> {
454 self.plot_area.chart_types()
455 }
456
457 fn title_text(&self) -> Option<String> {
458 self.title.as_deref().and_then(|t| t.title_text())
459 }
460}
461
462#[cfg(feature = "dml-charts")]
466pub trait PlotAreaExt {
467 fn chart_types(&self) -> Vec<ChartKind>;
472}
473
474#[cfg(feature = "dml-charts")]
475impl PlotAreaExt for crate::types::PlotArea {
476 fn chart_types(&self) -> Vec<ChartKind> {
477 let mut kinds = Vec::new();
478 if !self.bar_chart.is_empty() {
479 kinds.push(ChartKind::Bar);
480 }
481 if !self.bar3_d_chart.is_empty() {
482 kinds.push(ChartKind::Bar3D);
483 }
484 if !self.line_chart.is_empty() {
485 kinds.push(ChartKind::Line);
486 }
487 if !self.line3_d_chart.is_empty() {
488 kinds.push(ChartKind::Line3D);
489 }
490 if !self.pie_chart.is_empty() {
491 kinds.push(ChartKind::Pie);
492 }
493 if !self.pie3_d_chart.is_empty() {
494 kinds.push(ChartKind::Pie3D);
495 }
496 if !self.scatter_chart.is_empty() {
497 kinds.push(ChartKind::Scatter);
498 }
499 if !self.area_chart.is_empty() {
500 kinds.push(ChartKind::Area);
501 }
502 if !self.area3_d_chart.is_empty() {
503 kinds.push(ChartKind::Area3D);
504 }
505 if !self.bubble_chart.is_empty() {
506 kinds.push(ChartKind::Bubble);
507 }
508 if !self.doughnut_chart.is_empty() {
509 kinds.push(ChartKind::Doughnut);
510 }
511 if !self.radar_chart.is_empty() {
512 kinds.push(ChartKind::Radar);
513 }
514 if !self.stock_chart.is_empty() {
515 kinds.push(ChartKind::Stock);
516 }
517 if !self.surface_chart.is_empty() {
518 kinds.push(ChartKind::Surface);
519 }
520 if !self.surface3_d_chart.is_empty() {
521 kinds.push(ChartKind::Surface3D);
522 }
523 if !self.of_pie_chart.is_empty() {
524 kinds.push(ChartKind::OfPie);
525 }
526 kinds
527 }
528}
529
530#[cfg(feature = "dml-charts")]
534pub trait ChartTitleExt {
535 fn title_text(&self) -> Option<String>;
541}
542
543#[cfg(all(feature = "dml-charts", feature = "dml-text"))]
544impl ChartTitleExt for crate::types::ChartTitle {
545 fn title_text(&self) -> Option<String> {
546 self.tx.as_deref().and_then(|tx| {
547 tx.rich.as_deref().map(|body| {
548 body.p
549 .iter()
550 .map(|p| {
551 p.text_run
552 .iter()
553 .filter_map(|tr| match tr {
554 EGTextRun::R(run) => Some(run.t.as_str()),
555 EGTextRun::Br(_) => Some("\n"),
556 EGTextRun::Fld(fld) => fld.t.as_deref(),
557 })
558 .collect::<String>()
559 })
560 .collect::<Vec<_>>()
561 .join("\n")
562 })
563 })
564 }
565}
566
567#[cfg(all(feature = "dml-charts", not(feature = "dml-text")))]
569impl ChartTitleExt for crate::types::ChartTitle {
570 fn title_text(&self) -> Option<String> {
571 None
572 }
573}
574
575#[cfg(feature = "dml-diagrams")]
579pub trait DataModelExt {
580 fn content_points(&self) -> Vec<&crate::types::DiagramPoint>;
585
586 fn connections(&self) -> Vec<&crate::types::DiagramConnection>;
588
589 fn text(&self) -> Vec<String>;
594}
595
596#[cfg(feature = "dml-diagrams")]
597fn diagram_content_points(model: &crate::types::DataModel) -> Vec<&crate::types::DiagramPoint> {
598 use crate::types::STPtType;
599 model
600 .pt_lst
601 .pt
602 .iter()
603 .filter(|pt| {
604 matches!(
607 pt.r#type,
608 None | Some(STPtType::Node) | Some(STPtType::Asst) | Some(STPtType::Doc)
609 )
610 })
611 .collect()
612}
613
614#[cfg(all(feature = "dml-diagrams", feature = "dml-text"))]
615impl DataModelExt for crate::types::DataModel {
616 fn content_points(&self) -> Vec<&crate::types::DiagramPoint> {
617 diagram_content_points(self)
618 }
619
620 fn connections(&self) -> Vec<&crate::types::DiagramConnection> {
621 self.cxn_lst
622 .as_deref()
623 .map(|lst| lst.cxn.iter().collect())
624 .unwrap_or_default()
625 }
626
627 fn text(&self) -> Vec<String> {
628 self.content_points()
629 .iter()
630 .filter_map(|pt| {
631 pt.t.as_deref().map(|body| {
632 body.p
633 .iter()
634 .map(|p| {
635 p.text_run
636 .iter()
637 .filter_map(|tr| match tr {
638 crate::types::EGTextRun::R(run) => Some(run.t.as_str()),
639 crate::types::EGTextRun::Br(_) => Some("\n"),
640 crate::types::EGTextRun::Fld(fld) => fld.t.as_deref(),
641 })
642 .collect::<String>()
643 })
644 .collect::<Vec<_>>()
645 .join("\n")
646 })
647 })
648 .filter(|s| !s.is_empty())
649 .collect()
650 }
651}
652
653#[cfg(all(feature = "dml-diagrams", not(feature = "dml-text")))]
655impl DataModelExt for crate::types::DataModel {
656 fn content_points(&self) -> Vec<&crate::types::DiagramPoint> {
657 diagram_content_points(self)
658 }
659
660 fn connections(&self) -> Vec<&crate::types::DiagramConnection> {
661 self.cxn_lst
662 .as_deref()
663 .map(|lst| lst.cxn.iter().collect())
664 .unwrap_or_default()
665 }
666
667 fn text(&self) -> Vec<String> {
668 Vec::new()
669 }
670}
671
672#[cfg(feature = "dml-text")]
682pub trait TextCharacterPropertiesExt {
683 fn language(&self) -> Option<&str>;
685 fn font_size_pt(&self) -> Option<f64>;
687 fn is_bold(&self) -> bool;
689 fn is_italic(&self) -> bool;
691 fn is_all_caps(&self) -> bool;
693 fn is_small_caps(&self) -> bool;
695 fn underline_style(&self) -> Option<STTextUnderlineType>;
697 fn strike_type(&self) -> Option<STTextStrikeType>;
699 fn kerning_pt(&self) -> Option<f64>;
701 fn baseline_shift_pct(&self) -> Option<i32>;
703 fn character_spacing_pt(&self) -> Option<f64>;
705}
706
707#[cfg(feature = "dml-text")]
708impl TextCharacterPropertiesExt for TextCharacterProperties {
709 fn language(&self) -> Option<&str> {
710 self.lang.as_deref()
711 }
712
713 fn font_size_pt(&self) -> Option<f64> {
714 self.sz.map(|s| s as f64 / 100.0)
715 }
716
717 fn is_bold(&self) -> bool {
718 self.b.unwrap_or(false)
719 }
720
721 fn is_italic(&self) -> bool {
722 self.i.unwrap_or(false)
723 }
724
725 fn is_all_caps(&self) -> bool {
726 self.cap.as_ref().is_some_and(|c| *c == STTextCapsType::All)
727 }
728
729 fn is_small_caps(&self) -> bool {
730 self.cap
731 .as_ref()
732 .is_some_and(|c| *c == STTextCapsType::Small)
733 }
734
735 fn underline_style(&self) -> Option<STTextUnderlineType> {
736 self.u
737 }
738
739 fn strike_type(&self) -> Option<STTextStrikeType> {
740 self.strike
741 }
742
743 fn kerning_pt(&self) -> Option<f64> {
744 self.kern.map(|k| k as f64 / 100.0)
745 }
746
747 fn baseline_shift_pct(&self) -> Option<i32> {
748 self.baseline.as_deref().and_then(|s| s.parse().ok())
749 }
750
751 fn character_spacing_pt(&self) -> Option<f64> {
752 self.spc
754 .as_deref()
755 .and_then(|s| s.parse::<f64>().ok().map(|v| v / 100.0))
756 }
757}
758
759#[cfg(feature = "dml-text")]
765pub trait TextParagraphPropertiesExt {
766 fn margin_left_emu(&self) -> Option<i32>;
768 fn margin_right_emu(&self) -> Option<i32>;
770 fn indent_emu(&self) -> Option<i32>;
772 fn line_spacing(&self) -> Option<&CTTextSpacing>;
774 fn space_before(&self) -> Option<&CTTextSpacing>;
776 fn space_after(&self) -> Option<&CTTextSpacing>;
778 fn paragraph_level(&self) -> i32;
780 fn bullet_char(&self) -> Option<&str>;
782 fn text_alignment(&self) -> Option<STTextAlignType>;
784}
785
786#[cfg(feature = "dml-text")]
787impl TextParagraphPropertiesExt for TextParagraphProperties {
788 fn margin_left_emu(&self) -> Option<i32> {
789 self.mar_l
790 }
791
792 fn margin_right_emu(&self) -> Option<i32> {
793 self.mar_r
794 }
795
796 fn indent_emu(&self) -> Option<i32> {
797 self.indent
798 }
799
800 fn line_spacing(&self) -> Option<&CTTextSpacing> {
801 self.ln_spc.as_deref()
802 }
803
804 fn space_before(&self) -> Option<&CTTextSpacing> {
805 self.spc_bef.as_deref()
806 }
807
808 fn space_after(&self) -> Option<&CTTextSpacing> {
809 self.spc_aft.as_deref()
810 }
811
812 fn paragraph_level(&self) -> i32 {
813 self.lvl.unwrap_or(0)
814 }
815
816 fn bullet_char(&self) -> Option<&str> {
817 self.text_bullet.as_ref().and_then(|b| {
818 if let EGTextBullet::BuChar(bc) = b.as_ref() {
819 Some(bc.char.as_str())
820 } else {
821 None
822 }
823 })
824 }
825
826 fn text_alignment(&self) -> Option<STTextAlignType> {
827 self.algn
828 }
829}
830
831pub trait ShapePropertiesExt {
836 fn offset_emu(&self) -> Option<(i64, i64)>;
840 fn extent_emu(&self) -> Option<(i64, i64)>;
844 fn rotation_angle_deg(&self) -> Option<f64>;
846 fn is_flip_h(&self) -> bool;
848 fn is_flip_v(&self) -> bool;
850 fn has_line(&self) -> bool;
852}
853
854impl ShapePropertiesExt for CTShapeProperties {
855 fn offset_emu(&self) -> Option<(i64, i64)> {
856 let xfrm = self.transform.as_ref()?;
857 let off = xfrm.offset.as_ref()?;
858 let x = off.x.parse::<i64>().ok()?;
859 let y = off.y.parse::<i64>().ok()?;
860 Some((x, y))
861 }
862
863 fn extent_emu(&self) -> Option<(i64, i64)> {
864 let xfrm = self.transform.as_ref()?;
865 let ext = xfrm.extents.as_ref()?;
866 Some((ext.cx, ext.cy))
867 }
868
869 fn rotation_angle_deg(&self) -> Option<f64> {
870 self.transform
871 .as_ref()
872 .and_then(|xfrm| xfrm.rot)
873 .map(|rot| rot as f64 / 60000.0)
874 }
875
876 fn is_flip_h(&self) -> bool {
877 self.transform
878 .as_ref()
879 .and_then(|xfrm| xfrm.flip_h)
880 .unwrap_or(false)
881 }
882
883 fn is_flip_v(&self) -> bool {
884 self.transform
885 .as_ref()
886 .and_then(|xfrm| xfrm.flip_v)
887 .unwrap_or(false)
888 }
889
890 #[cfg(feature = "dml-lines")]
891 fn has_line(&self) -> bool {
892 self.line.is_some()
893 }
894
895 #[cfg(not(feature = "dml-lines"))]
896 fn has_line(&self) -> bool {
897 false
898 }
899}
900
901#[cfg(test)]
902mod tests {
903 use super::*;
904
905 #[test]
906 fn test_text_paragraph_text() {
907 let para = TextParagraph {
908 #[cfg(feature = "dml-text")]
909 p_pr: None,
910 text_run: vec![
911 EGTextRun::R(Box::new(TextRun {
912 #[cfg(feature = "dml-text")]
913 r_pr: None,
914 #[cfg(feature = "dml-text")]
915 t: "Hello ".to_string(),
916 #[cfg(feature = "extra-children")]
917 extra_children: Vec::new(),
918 })),
919 EGTextRun::R(Box::new(TextRun {
920 #[cfg(feature = "dml-text")]
921 r_pr: None,
922 #[cfg(feature = "dml-text")]
923 t: "World".to_string(),
924 #[cfg(feature = "extra-children")]
925 extra_children: Vec::new(),
926 })),
927 ],
928 #[cfg(feature = "dml-text")]
929 end_para_r_pr: None,
930 #[cfg(feature = "extra-children")]
931 extra_children: Vec::new(),
932 };
933
934 assert_eq!(para.text(), "Hello World");
935 assert_eq!(para.runs().len(), 2);
936 }
937
938 #[cfg(feature = "dml-text")]
939 #[test]
940 fn test_text_run_formatting() {
941 let run = TextRun {
942 r_pr: Some(Box::new(TextCharacterProperties {
943 b: Some(true),
944 i: Some(true),
945 ..Default::default()
946 })),
947 t: "Bold Italic".to_string(),
948 #[cfg(feature = "extra-children")]
949 extra_children: Vec::new(),
950 };
951
952 assert!(run.is_bold());
953 assert!(run.is_italic());
954 assert!(!run.is_underlined());
955 assert_eq!(run.text(), "Bold Italic");
956 }
957
958 #[cfg(feature = "dml-tables")]
959 fn make_cell(text: &str) -> CTTableCell {
960 CTTableCell {
961 row_span: None,
962 grid_span: None,
963 h_merge: None,
964 v_merge: None,
965 id: None,
966 tx_body: Some(Box::new(TextBody {
967 body_pr: Box::new(CTTextBodyProperties::default()),
968 lst_style: None,
969 p: vec![TextParagraph {
970 #[cfg(feature = "dml-text")]
971 p_pr: None,
972 text_run: vec![EGTextRun::R(Box::new(TextRun {
973 #[cfg(feature = "dml-text")]
974 r_pr: None,
975 #[cfg(feature = "dml-text")]
976 t: text.to_string(),
977 #[cfg(feature = "extra-children")]
978 extra_children: Vec::new(),
979 }))],
980 #[cfg(feature = "dml-text")]
981 end_para_r_pr: None,
982 #[cfg(feature = "extra-children")]
983 extra_children: Vec::new(),
984 }],
985 #[cfg(feature = "extra-children")]
986 extra_children: Vec::new(),
987 })),
988 tc_pr: None,
989 ext_lst: None,
990 #[cfg(feature = "extra-attrs")]
991 extra_attrs: Default::default(),
992 #[cfg(feature = "extra-children")]
993 extra_children: Vec::new(),
994 }
995 }
996
997 #[cfg(feature = "dml-tables")]
998 fn make_table(data: &[&[&str]]) -> CTTable {
999 let rows: Vec<CTTableRow> = data
1000 .iter()
1001 .map(|row_data| CTTableRow {
1002 height: "370840".to_string(), tc: row_data.iter().map(|&text| make_cell(text)).collect(),
1004 ext_lst: None,
1005 #[cfg(feature = "extra-attrs")]
1006 extra_attrs: Default::default(),
1007 #[cfg(feature = "extra-children")]
1008 extra_children: Vec::new(),
1009 })
1010 .collect();
1011
1012 let col_count = data.first().map(|r| r.len()).unwrap_or(0);
1013 let grid_cols: Vec<CTTableCol> = (0..col_count)
1014 .map(|_| CTTableCol {
1015 width: "914400".to_string(), ext_lst: None,
1017 #[cfg(feature = "extra-attrs")]
1018 extra_attrs: Default::default(),
1019 #[cfg(feature = "extra-children")]
1020 extra_children: Vec::new(),
1021 })
1022 .collect();
1023
1024 CTTable {
1025 tbl_pr: None,
1026 tbl_grid: Box::new(CTTableGrid {
1027 grid_col: grid_cols,
1028 #[cfg(feature = "extra-children")]
1029 extra_children: Vec::new(),
1030 }),
1031 tr: rows,
1032 #[cfg(feature = "extra-children")]
1033 extra_children: Vec::new(),
1034 }
1035 }
1036
1037 #[cfg(feature = "dml-tables")]
1038 #[test]
1039 fn test_table_ext() {
1040 let table = make_table(&[&["A", "B", "C"], &["1", "2", "3"], &["X", "Y", "Z"]]);
1041
1042 assert_eq!(table.row_count(), 3);
1043 assert_eq!(table.col_count(), 3);
1044 assert_eq!(table.rows().len(), 3);
1045
1046 assert_eq!(table.cell(0, 0).unwrap().text(), "A");
1048 assert_eq!(table.cell(1, 1).unwrap().text(), "2");
1049 assert_eq!(table.cell(2, 2).unwrap().text(), "Z");
1050 assert!(table.cell(3, 0).is_none()); let grid = table.to_text_grid();
1054 assert_eq!(
1055 grid,
1056 vec![
1057 vec!["A", "B", "C"],
1058 vec!["1", "2", "3"],
1059 vec!["X", "Y", "Z"],
1060 ]
1061 );
1062
1063 assert_eq!(table.text(), "A\tB\tC\n1\t2\t3\nX\tY\tZ");
1065 }
1066
1067 #[cfg(feature = "dml-tables")]
1068 #[test]
1069 fn test_table_row_ext() {
1070 let table = make_table(&[&["Hello", "World"]]);
1071 let row = &table.tr[0];
1072
1073 assert_eq!(row.cells().len(), 2);
1074 assert_eq!(row.cell(0).unwrap().text(), "Hello");
1075 assert_eq!(row.cell(1).unwrap().text(), "World");
1076 assert!(row.cell(2).is_none());
1077 assert_eq!(row.height_emu(), Some(370840));
1078 }
1079
1080 #[cfg(feature = "dml-tables")]
1081 #[test]
1082 fn test_table_cell_ext() {
1083 let cell = make_cell("Test Content");
1084
1085 assert_eq!(cell.text(), "Test Content");
1086 assert!(cell.text_body().is_some());
1087 assert_eq!(cell.row_span(), 1);
1088 assert_eq!(cell.col_span(), 1);
1089 assert!(!cell.has_row_span());
1090 assert!(!cell.has_col_span());
1091 assert!(!cell.is_h_merge());
1092 assert!(!cell.is_v_merge());
1093 }
1094
1095 #[cfg(feature = "dml-tables")]
1096 #[test]
1097 fn test_table_cell_spanning() {
1098 let mut cell = make_cell("Merged");
1099 cell.row_span = Some(2);
1100 cell.grid_span = Some(3);
1101 cell.h_merge = Some(true);
1102
1103 assert_eq!(cell.row_span(), 2);
1104 assert_eq!(cell.col_span(), 3);
1105 assert!(cell.has_row_span());
1106 assert!(cell.has_col_span());
1107 assert!(cell.is_h_merge());
1108 assert!(!cell.is_v_merge());
1109 }
1110
1111 #[cfg(feature = "dml-charts")]
1116 fn make_chart_with_bar() -> crate::types::Chart {
1117 use crate::types::*;
1118 Chart {
1119 #[cfg(feature = "dml-charts")]
1120 title: None,
1121 #[cfg(feature = "dml-charts")]
1122 auto_title_deleted: None,
1123 #[cfg(feature = "dml-charts")]
1124 pivot_fmts: None,
1125 #[cfg(feature = "dml-charts")]
1126 view3_d: None,
1127 #[cfg(feature = "dml-charts")]
1128 floor: None,
1129 #[cfg(feature = "dml-charts")]
1130 side_wall: None,
1131 #[cfg(feature = "dml-charts")]
1132 back_wall: None,
1133 #[cfg(feature = "dml-charts")]
1134 plot_area: Box::new(PlotArea {
1135 #[cfg(feature = "dml-charts")]
1136 bar_chart: vec![BarChart {
1137 #[cfg(feature = "dml-charts")]
1138 bar_dir: Box::new(BarDirection::default()),
1139 #[cfg(feature = "dml-charts")]
1140 grouping: None,
1141 #[cfg(feature = "dml-charts")]
1142 vary_colors: None,
1143 #[cfg(feature = "dml-charts")]
1144 ser: Vec::new(),
1145 #[cfg(feature = "dml-charts")]
1146 d_lbls: None,
1147 #[cfg(feature = "dml-charts")]
1148 gap_width: None,
1149 #[cfg(feature = "dml-charts")]
1150 overlap: None,
1151 #[cfg(feature = "dml-charts")]
1152 ser_lines: Vec::new(),
1153 #[cfg(feature = "dml-charts")]
1154 ax_id: Vec::new(),
1155 #[cfg(feature = "dml-charts")]
1156 ext_lst: None,
1157 #[cfg(feature = "extra-children")]
1158 extra_children: Vec::new(),
1159 }],
1160 ..Default::default()
1161 }),
1162 #[cfg(feature = "dml-charts")]
1163 legend: None,
1164 #[cfg(feature = "dml-charts")]
1165 plot_vis_only: None,
1166 #[cfg(feature = "dml-charts")]
1167 disp_blanks_as: None,
1168 #[cfg(feature = "dml-charts")]
1169 show_d_lbls_over_max: None,
1170 #[cfg(feature = "dml-charts")]
1171 ext_lst: None,
1172 #[cfg(feature = "extra-children")]
1173 extra_children: Vec::new(),
1174 }
1175 }
1176
1177 #[cfg(feature = "dml-charts")]
1178 #[test]
1179 fn test_chart_kind_from_bar_chart() {
1180 use crate::ext::{ChartExt, ChartKind};
1181 let chart = make_chart_with_bar();
1182 let kinds = chart.chart_types();
1183 assert_eq!(kinds, vec![ChartKind::Bar]);
1184 }
1185
1186 #[cfg(feature = "dml-charts")]
1187 #[test]
1188 fn test_plot_area_empty_has_no_kinds() {
1189 use crate::ext::PlotAreaExt;
1190 use crate::types::PlotArea;
1191 let area = PlotArea::default();
1192 assert!(area.chart_types().is_empty());
1193 }
1194
1195 #[cfg(feature = "dml-charts")]
1196 #[test]
1197 fn test_chart_space_delegates_to_chart() {
1198 use crate::ext::{ChartKind, ChartSpaceExt};
1199 use crate::types::ChartSpace;
1200 let space = ChartSpace {
1201 #[cfg(feature = "dml-charts")]
1202 date1904: None,
1203 #[cfg(feature = "dml-charts")]
1204 lang: None,
1205 #[cfg(feature = "dml-charts")]
1206 rounded_corners: None,
1207 #[cfg(feature = "dml-charts")]
1208 style: None,
1209 #[cfg(feature = "dml-charts")]
1210 clr_map_ovr: None,
1211 #[cfg(feature = "dml-charts")]
1212 pivot_source: None,
1213 #[cfg(feature = "dml-charts")]
1214 protection: None,
1215 #[cfg(feature = "dml-charts")]
1216 chart: Box::new(make_chart_with_bar()),
1217 #[cfg(feature = "dml-charts")]
1218 sp_pr: None,
1219 #[cfg(feature = "dml-charts")]
1220 tx_pr: None,
1221 #[cfg(feature = "dml-charts")]
1222 external_data: None,
1223 #[cfg(feature = "dml-charts")]
1224 print_settings: None,
1225 #[cfg(feature = "dml-charts")]
1226 user_shapes: None,
1227 #[cfg(feature = "dml-charts")]
1228 ext_lst: None,
1229 #[cfg(feature = "extra-children")]
1230 extra_children: Vec::new(),
1231 };
1232 assert_eq!(space.chart_types(), vec![ChartKind::Bar]);
1233 assert!(space.title_text().is_none());
1234 }
1235
1236 #[cfg(all(feature = "dml-charts", feature = "dml-text"))]
1237 #[test]
1238 fn test_chart_title_text_rich() {
1239 use crate::ext::ChartTitleExt;
1240 use crate::types::*;
1241
1242 let title = ChartTitle {
1244 #[cfg(feature = "dml-charts")]
1245 tx: Some(Box::new(ChartText {
1246 #[cfg(feature = "dml-charts")]
1247 str_ref: None,
1248 #[cfg(feature = "dml-charts")]
1249 rich: Some(Box::new(TextBody {
1250 body_pr: Box::new(CTTextBodyProperties::default()),
1251 lst_style: None,
1252 p: vec![TextParagraph {
1253 #[cfg(feature = "dml-text")]
1254 p_pr: None,
1255 text_run: vec![EGTextRun::R(Box::new(TextRun {
1256 #[cfg(feature = "dml-text")]
1257 r_pr: None,
1258 #[cfg(feature = "dml-text")]
1259 t: "Sales Report".to_string(),
1260 #[cfg(feature = "extra-children")]
1261 extra_children: Vec::new(),
1262 }))],
1263 #[cfg(feature = "dml-text")]
1264 end_para_r_pr: None,
1265 #[cfg(feature = "extra-children")]
1266 extra_children: Vec::new(),
1267 }],
1268 #[cfg(feature = "extra-children")]
1269 extra_children: Vec::new(),
1270 })),
1271 #[cfg(feature = "extra-children")]
1272 extra_children: Vec::new(),
1273 })),
1274 #[cfg(feature = "dml-charts")]
1275 layout: None,
1276 #[cfg(feature = "dml-charts")]
1277 overlay: None,
1278 #[cfg(feature = "dml-charts")]
1279 sp_pr: None,
1280 #[cfg(feature = "dml-charts")]
1281 tx_pr: None,
1282 #[cfg(feature = "dml-charts")]
1283 ext_lst: None,
1284 #[cfg(feature = "extra-children")]
1285 extra_children: Vec::new(),
1286 };
1287
1288 assert_eq!(title.title_text(), Some("Sales Report".to_string()));
1289 }
1290
1291 #[cfg(feature = "dml-diagrams")]
1296 fn make_data_model(points: Vec<crate::types::DiagramPoint>) -> crate::types::DataModel {
1297 use crate::types::*;
1298 DataModel {
1299 #[cfg(feature = "dml-diagrams")]
1300 pt_lst: Box::new(DiagramPointList {
1301 pt: points,
1302 #[cfg(feature = "extra-children")]
1303 extra_children: Vec::new(),
1304 }),
1305 #[cfg(feature = "dml-diagrams")]
1306 cxn_lst: None,
1307 #[cfg(feature = "dml-diagrams")]
1308 bg: None,
1309 #[cfg(feature = "dml-diagrams")]
1310 whole: None,
1311 #[cfg(feature = "dml-diagrams")]
1312 ext_lst: None,
1313 #[cfg(feature = "extra-children")]
1314 extra_children: Vec::new(),
1315 }
1316 }
1317
1318 #[cfg(feature = "dml-diagrams")]
1319 fn make_diagram_point(
1320 id: &str,
1321 pt_type: Option<crate::types::STPtType>,
1322 ) -> crate::types::DiagramPoint {
1323 use crate::types::*;
1324 DiagramPoint {
1325 #[cfg(feature = "dml-diagrams")]
1326 model_id: id.to_string(),
1327 #[cfg(feature = "dml-diagrams")]
1328 r#type: pt_type,
1329 #[cfg(feature = "dml-diagrams")]
1330 cxn_id: None,
1331 #[cfg(feature = "dml-diagrams")]
1332 pr_set: None,
1333 #[cfg(feature = "dml-diagrams")]
1334 sp_pr: None,
1335 #[cfg(feature = "dml-diagrams")]
1336 t: None,
1337 #[cfg(feature = "dml-diagrams")]
1338 ext_lst: None,
1339 #[cfg(feature = "extra-attrs")]
1340 extra_attrs: Default::default(),
1341 #[cfg(feature = "extra-children")]
1342 extra_children: Vec::new(),
1343 }
1344 }
1345
1346 #[cfg(feature = "dml-diagrams")]
1347 #[test]
1348 fn test_data_model_content_points() {
1349 use crate::ext::DataModelExt;
1350 use crate::types::STPtType;
1351
1352 let points = vec![
1353 make_diagram_point("1", None), make_diagram_point("2", Some(STPtType::Node)), make_diagram_point("3", Some(STPtType::Asst)), make_diagram_point("4", Some(STPtType::ParTrans)), make_diagram_point("5", Some(STPtType::SibTrans)), make_diagram_point("6", Some(STPtType::Pres)), ];
1360
1361 let model = make_data_model(points);
1362 let content = model.content_points();
1363
1364 assert_eq!(content.len(), 3);
1366 assert_eq!(content[0].model_id, "1");
1367 assert_eq!(content[1].model_id, "2");
1368 assert_eq!(content[2].model_id, "3");
1369 }
1370
1371 #[cfg(feature = "dml-diagrams")]
1372 #[test]
1373 fn test_data_model_connections_empty() {
1374 use crate::ext::DataModelExt;
1375
1376 let model = make_data_model(vec![]);
1377 assert!(model.connections().is_empty());
1378 }
1379
1380 #[cfg(all(feature = "dml-diagrams", feature = "dml-text"))]
1381 #[test]
1382 fn test_data_model_text() {
1383 use crate::ext::DataModelExt;
1384 use crate::types::*;
1385
1386 let mut pt = make_diagram_point("1", None);
1387 pt.t = Some(Box::new(TextBody {
1388 body_pr: Box::new(CTTextBodyProperties::default()),
1389 lst_style: None,
1390 p: vec![TextParagraph {
1391 #[cfg(feature = "dml-text")]
1392 p_pr: None,
1393 text_run: vec![EGTextRun::R(Box::new(TextRun {
1394 #[cfg(feature = "dml-text")]
1395 r_pr: None,
1396 #[cfg(feature = "dml-text")]
1397 t: "SmartArt Node".to_string(),
1398 #[cfg(feature = "extra-children")]
1399 extra_children: Vec::new(),
1400 }))],
1401 #[cfg(feature = "dml-text")]
1402 end_para_r_pr: None,
1403 #[cfg(feature = "extra-children")]
1404 extra_children: Vec::new(),
1405 }],
1406 #[cfg(feature = "extra-children")]
1407 extra_children: Vec::new(),
1408 }));
1409
1410 let model = make_data_model(vec![pt]);
1411 let texts = model.text();
1412
1413 assert_eq!(texts.len(), 1);
1414 assert_eq!(texts[0], "SmartArt Node");
1415 }
1416
1417 #[cfg(feature = "dml-text")]
1422 #[test]
1423 fn test_text_char_props_language_and_size() {
1424 let props = TextCharacterProperties {
1425 lang: Some("en-US".to_string()),
1426 sz: Some(2400), b: Some(true),
1428 i: Some(false),
1429 cap: Some(STTextCapsType::All),
1430 ..Default::default()
1431 };
1432
1433 use crate::ext::TextCharacterPropertiesExt;
1434 assert_eq!(props.language(), Some("en-US"));
1435 assert_eq!(props.font_size_pt(), Some(24.0));
1436 assert!(props.is_bold());
1437 assert!(!props.is_italic());
1438 assert!(props.is_all_caps());
1439 assert!(!props.is_small_caps());
1440 }
1441
1442 #[cfg(feature = "dml-text")]
1443 #[test]
1444 fn test_text_char_props_defaults() {
1445 let props = TextCharacterProperties::default();
1446 use crate::ext::TextCharacterPropertiesExt;
1447 assert!(props.language().is_none());
1448 assert!(props.font_size_pt().is_none());
1449 assert!(!props.is_bold());
1450 assert!(!props.is_italic());
1451 assert!(!props.is_all_caps());
1452 assert!(!props.is_small_caps());
1453 assert!(props.underline_style().is_none());
1454 assert!(props.strike_type().is_none());
1455 assert!(props.kerning_pt().is_none());
1456 assert!(props.baseline_shift_pct().is_none());
1457 assert!(props.character_spacing_pt().is_none());
1458 }
1459
1460 #[cfg(feature = "dml-text")]
1461 #[test]
1462 fn test_text_run_caps_and_language() {
1463 let run = TextRun {
1464 r_pr: Some(Box::new(TextCharacterProperties {
1465 cap: Some(STTextCapsType::Small),
1466 lang: Some("fr-FR".to_string()),
1467 ..Default::default()
1468 })),
1469 t: "test".to_string(),
1470 #[cfg(feature = "extra-children")]
1471 extra_children: Vec::new(),
1472 };
1473
1474 assert!(!run.is_all_caps());
1475 assert!(run.is_small_caps());
1476 assert_eq!(run.language(), Some("fr-FR"));
1477 assert!(run.baseline_shift_pct().is_none());
1478 }
1479
1480 #[cfg(feature = "dml-text")]
1485 #[test]
1486 fn test_text_paragraph_properties_ext() {
1487 let props = TextParagraphProperties {
1488 mar_l: Some(457200), mar_r: Some(0),
1490 lvl: Some(2),
1491 algn: Some(STTextAlignType::Ctr),
1492 text_bullet: Some(Box::new(EGTextBullet::BuChar(Box::new(CTTextCharBullet {
1493 char: "•".to_string(),
1494 #[cfg(feature = "extra-attrs")]
1495 extra_attrs: Default::default(),
1496 })))),
1497 ..Default::default()
1498 };
1499
1500 use crate::ext::TextParagraphPropertiesExt;
1501 assert_eq!(props.margin_left_emu(), Some(457200));
1502 assert_eq!(props.margin_right_emu(), Some(0));
1503 assert_eq!(props.paragraph_level(), 2);
1504 assert_eq!(props.text_alignment(), Some(STTextAlignType::Ctr));
1505 assert_eq!(props.bullet_char(), Some("•"));
1506 assert!(props.line_spacing().is_none());
1507 assert!(props.space_before().is_none());
1508 }
1509
1510 #[cfg(feature = "dml-text")]
1511 #[test]
1512 fn test_text_paragraph_properties_defaults() {
1513 let props = TextParagraphProperties::default();
1514 use crate::ext::TextParagraphPropertiesExt;
1515 assert!(props.margin_left_emu().is_none());
1516 assert!(props.margin_right_emu().is_none());
1517 assert!(props.indent_emu().is_none());
1518 assert_eq!(props.paragraph_level(), 0);
1519 assert!(props.text_alignment().is_none());
1520 assert!(props.bullet_char().is_none());
1521 }
1522
1523 #[test]
1528 fn test_shape_properties_no_transform() {
1529 let sp = CTShapeProperties::default();
1530 use crate::ext::ShapePropertiesExt;
1531 assert!(sp.offset_emu().is_none());
1532 assert!(sp.extent_emu().is_none());
1533 assert!(sp.rotation_angle_deg().is_none());
1534 assert!(!sp.is_flip_h());
1535 assert!(!sp.is_flip_v());
1536 assert!(!sp.has_line());
1537 }
1538
1539 #[test]
1540 fn test_shape_properties_with_transform() {
1541 use crate::types::{Point2D, PositiveSize2D, Transform2D};
1542 let sp = CTShapeProperties {
1543 transform: Some(Box::new(Transform2D {
1544 rot: Some(1800000), flip_h: Some(true),
1546 flip_v: None,
1547 offset: Some(Box::new(Point2D {
1548 x: "914400".to_string(), y: "457200".to_string(), #[cfg(feature = "extra-attrs")]
1551 extra_attrs: Default::default(),
1552 })),
1553 extents: Some(Box::new(PositiveSize2D {
1554 cx: 2743200, cy: 1828800, #[cfg(feature = "extra-attrs")]
1557 extra_attrs: Default::default(),
1558 })),
1559 ..Default::default()
1560 })),
1561 ..Default::default()
1562 };
1563
1564 use crate::ext::ShapePropertiesExt;
1565 assert_eq!(sp.offset_emu(), Some((914400, 457200)));
1566 assert_eq!(sp.extent_emu(), Some((2743200, 1828800)));
1567 assert!((sp.rotation_angle_deg().unwrap() - 30.0).abs() < 0.001);
1568 assert!(sp.is_flip_h());
1569 assert!(!sp.is_flip_v());
1570 }
1571
1572 #[cfg(feature = "dml-charts")]
1573 #[test]
1574 fn test_chart_title_none_when_no_tx() {
1575 use crate::ext::ChartTitleExt;
1576 use crate::types::ChartTitle;
1577 let title = ChartTitle {
1578 #[cfg(feature = "dml-charts")]
1579 tx: None,
1580 #[cfg(feature = "dml-charts")]
1581 layout: None,
1582 #[cfg(feature = "dml-charts")]
1583 overlay: None,
1584 #[cfg(feature = "dml-charts")]
1585 sp_pr: None,
1586 #[cfg(feature = "dml-charts")]
1587 tx_pr: None,
1588 #[cfg(feature = "dml-charts")]
1589 ext_lst: None,
1590 #[cfg(feature = "extra-children")]
1591 extra_children: Vec::new(),
1592 };
1593 assert!(title.title_text().is_none());
1594 }
1595}