1use crate::oxml::color::Color;
36use crate::oxml::simpletypes::{
37 Alignment, MsoAnchor, MsoAutoSize, TabAlignment, TextWrapping, Underline,
38};
39use crate::units::{Emu, Pt, RGBColor};
40
41#[derive(Copy, Clone, Debug, Default)]
46pub struct TabStop {
47 pub pos: Emu,
49 pub alignment: TabAlignment,
51}
52
53#[derive(Copy, Clone, Debug, Default)]
55pub struct Indent {
56 pub left: Option<Emu>,
58 pub right: Option<Emu>,
60 pub first_line: Option<Emu>,
62 pub hanging: Option<i32>,
64}
65
66#[derive(Clone, Debug, Default)]
70pub enum BulletStyle {
71 #[default]
73 None,
74 Char {
76 char: String,
78 },
79 AutoNum {
81 auto_num_type: String,
83 start_at: Option<u32>,
85 },
86}
87
88#[derive(Clone, Debug, Default)]
90pub struct ParagraphProperties {
91 pub alignment: Option<Alignment>,
93 pub indent: Indent,
95 pub line_spacing: Option<i32>,
97 pub line_spacing_pct: Option<i32>,
99 pub space_before: Option<Emu>,
101 pub space_after: Option<Emu>,
103 pub bullet: bool,
105 pub bullet_style: Option<BulletStyle>,
107 pub level: u8,
109 pub default_run_properties: Option<RunProperties>,
111 pub tab_stops: Vec<TabStop>,
116}
117
118impl ParagraphProperties {
119 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
121 if self.alignment.is_none()
122 && self.indent.left.is_none()
123 && self.indent.right.is_none()
124 && self.indent.first_line.is_none()
125 && self.indent.hanging.is_none()
126 && self.line_spacing.is_none()
127 && self.line_spacing_pct.is_none()
128 && self.space_before.is_none()
129 && self.space_after.is_none()
130 && !self.bullet
131 && self.bullet_style.is_none()
132 && self.level == 0
133 && self.default_run_properties.is_none()
134 && self.tab_stops.is_empty()
135 {
136 return;
137 }
138 let mut attrs: Vec<(&str, &str)> = Vec::new();
139 if let Some(a) = self.alignment {
140 attrs.push(("algn", a.as_str()));
141 }
142 let lvl_s = if self.level != 0 {
144 Some(self.level.to_string())
145 } else {
146 None
147 };
148 if let Some(s) = &lvl_s {
149 attrs.push(("lvl", s));
150 }
151 w.open_with("a:pPr", &attrs);
152 if self.indent.left.is_some()
154 || self.indent.right.is_some()
155 || self.indent.first_line.is_some()
156 || self.indent.hanging.is_some()
157 {
158 let l_s = self.indent.left.map(|v| v.value().to_string());
160 let r_s = self.indent.right.map(|v| v.value().to_string());
161 let first_s = self.indent.first_line.map(|v| v.value().to_string());
162 let hang_s = self.indent.hanging.map(|v| v.to_string());
163 let mut iattrs: Vec<(&str, &str)> = Vec::new();
164 if let Some(s) = &l_s {
165 iattrs.push(("l", s));
166 }
167 if let Some(s) = &r_s {
168 iattrs.push(("r", s));
169 }
170 if let Some(s) = &first_s {
171 iattrs.push(("firstLine", s));
172 }
173 if let Some(s) = &hang_s {
174 iattrs.push(("hanging", s));
175 }
176 w.empty_with("a:indent", &iattrs);
177 }
178 if self.line_spacing.is_some() || self.line_spacing_pct.is_some() {
179 if let Some(p) = self.line_spacing_pct {
183 let s = p.to_string();
184 w.open("a:lnSpc");
185 w.empty_with("a:spcPct", &[("val", &s)]);
186 w.close("a:lnSpc");
187 }
188 if let Some(sp) = self.line_spacing {
189 let s = sp.to_string();
190 w.open("a:lnSpc");
191 w.empty_with("a:spcPts", &[("val", &s)]);
192 w.close("a:lnSpc");
193 }
194 }
195 if self.space_before.is_some() || self.space_after.is_some() {
196 let sb_s = self.space_before.map(|v| v.value().to_string());
198 let sa_s = self.space_after.map(|v| v.value().to_string());
199 let mut sattrs: Vec<(&str, &str)> = Vec::new();
200 if let Some(s) = &sb_s {
201 sattrs.push(("spcBef", s));
202 }
203 if let Some(s) = &sa_s {
204 sattrs.push(("spcAft", s));
205 }
206 w.empty_with("a:spcBef", &sattrs);
207 }
208 if let Some(bs) = &self.bullet_style {
210 match bs {
211 BulletStyle::None => {
212 w.empty("a:buNone");
213 }
214 BulletStyle::Char { char } => {
215 w.empty_with("a:buChar", &[("char", char.as_str())]);
216 }
217 BulletStyle::AutoNum {
218 auto_num_type,
219 start_at,
220 } => {
221 if let Some(sa) = start_at {
222 let sa_s = sa.to_string();
223 w.empty_with(
224 "a:buAutoNum",
225 &[("type", auto_num_type.as_str()), ("startAt", sa_s.as_str())],
226 );
227 } else {
228 w.empty_with("a:buAutoNum", &[("type", auto_num_type.as_str())]);
229 }
230 }
231 }
232 }
233 if !self.tab_stops.is_empty() {
235 w.open("a:tabLst");
236 for tab in &self.tab_stops {
237 let pos_s = tab.pos.value().to_string();
238 w.empty_with(
239 "a:tab",
240 &[("pos", pos_s.as_str()), ("algn", tab.alignment.as_str())],
241 );
242 }
243 w.close("a:tabLst");
244 }
245 if let Some(rpr) = &self.default_run_properties {
246 rpr.write_xml(w, "a:defRPr");
247 }
248 w.close("a:pPr");
249 }
250}
251
252#[derive(Clone, Debug, Default)]
256pub struct Hyperlink {
257 pub rid: Option<String>,
259 pub tooltip: Option<String>,
261 pub action: Option<String>,
263 pub invalid: bool,
265}
266
267impl Hyperlink {
268 pub fn new(rid: impl Into<String>) -> Self {
270 Self {
271 rid: Some(rid.into()),
272 ..Default::default()
273 }
274 }
275
276 pub fn new_slide_jump() -> Self {
278 Self {
279 action: Some("ppaction://hlinksldjump".to_string()),
280 ..Default::default()
281 }
282 }
283}
284
285#[derive(Clone, Debug, Default)]
287pub struct RunProperties {
288 pub size: Option<Pt>,
290 pub bold: bool,
292 pub italic: bool,
294 pub underline: Option<Underline>,
296 pub strike: bool,
298 pub strike_dbl: bool,
300 pub color: Color,
302 pub highlight: Option<Color>,
304 pub latin_font: Option<String>,
306 pub eastasia_font: Option<String>,
308 pub cs_font: Option<String>,
310 pub baseline: Option<i32>,
312 pub kerning: Option<i32>,
314 pub spc: Option<i32>,
316 pub caps: Caps,
318 pub lang: Option<String>,
320 pub alpha: Option<i32>,
325 pub hlink_click: Option<Hyperlink>,
327 pub hlink_hover: Option<Hyperlink>,
329}
330
331#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
332pub enum Caps {
333 #[default]
334 None,
335 Small,
336 All,
337}
338
339impl Caps {
340 pub fn as_str(self) -> &'static str {
341 match self {
342 Caps::None => "none",
343 Caps::Small => "small",
344 Caps::All => "all",
345 }
346 }
347}
348
349impl RunProperties {
350 pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
352 let sz_s = self
354 .size
355 .map(|sz| ((sz.value() * 100.0) as i32).to_string());
356 let baseline_s = self.baseline.map(|v| v.to_string());
357 let kerning_s = self.kerning.map(|v| v.to_string());
358 let spc_s = self.spc.map(|v| v.to_string());
359
360 let mut attrs: Vec<(&str, &str)> = Vec::new();
361 if let Some(s) = &sz_s {
362 attrs.push(("sz", s));
363 }
364 if self.bold {
365 attrs.push(("b", "1"));
366 }
367 if self.italic {
368 attrs.push(("i", "1"));
369 }
370 if let Some(u) = self.underline {
371 attrs.push(("u", u.as_str()));
372 }
373 if self.strike {
374 attrs.push(("strike", "sngStrike"));
375 }
376 if self.strike_dbl {
377 attrs.push(("strike", "dblStrike"));
378 }
379 if self.caps != Caps::None {
380 attrs.push(("cap", self.caps.as_str()));
381 }
382 if let Some(s) = &baseline_s {
383 attrs.push(("baseline", s));
384 }
385 if let Some(s) = &kerning_s {
386 attrs.push(("kern", s));
387 }
388 if let Some(s) = &spc_s {
389 attrs.push(("spc", s));
390 }
391 if let Some(l) = &self.lang {
392 attrs.push(("lang", l));
393 }
394 w.open_with(tag, &attrs);
396 if !matches!(self.color, Color::None) {
397 self.color.write_solid_fill_with_alpha(w, self.alpha);
398 } else if self.alpha.is_some() {
399 w.open("a:solidFill");
401 w.empty_with("a:srgbClr", &[("val", "000000")]);
402 if let Some(a) = self.alpha {
403 w.empty_with("a:alpha", &[("val", a.to_string().as_str())]);
404 }
405 w.close("a:srgbClr");
406 w.close("a:solidFill");
407 }
408 if let Some(h) = &self.highlight {
409 h.write_solid_fill(w);
410 }
411 if let Some(latin) = &self.latin_font {
412 w.empty_with("a:latin", &[("typeface", latin)]);
413 }
414 if let Some(ea) = &self.eastasia_font {
415 w.empty_with("a:ea", &[("typeface", ea)]);
416 }
417 if let Some(cs) = &self.cs_font {
418 w.empty_with("a:cs", &[("typeface", cs)]);
419 }
420 if let Some(h) = &self.hlink_click {
422 let mut hattrs: Vec<(&str, &str)> = Vec::new();
423 if let Some(rid) = &h.rid {
424 hattrs.push(("r:id", rid.as_str()));
425 }
426 if let Some(tip) = &h.tooltip {
427 hattrs.push(("tooltip", tip.as_str()));
428 }
429 if let Some(act) = &h.action {
430 hattrs.push(("action", act.as_str()));
431 }
432 if hattrs.is_empty() {
433 w.empty("a:hlinkClick");
434 } else {
435 w.empty_with("a:hlinkClick", &hattrs);
436 }
437 }
438 if let Some(h) = &self.hlink_hover {
439 let mut hattrs: Vec<(&str, &str)> = Vec::new();
440 if let Some(rid) = &h.rid {
441 hattrs.push(("r:id", rid.as_str()));
442 }
443 if let Some(tip) = &h.tooltip {
444 hattrs.push(("tooltip", tip.as_str()));
445 }
446 if hattrs.is_empty() {
447 w.empty("a:hlinkHover");
448 } else {
449 w.empty_with("a:hlinkHover", &hattrs);
450 }
451 }
452 w.close(tag);
453 }
454
455 #[doc(hidden)]
457 pub fn from_attrs_unused(_attrs: &super::parser::AttrMap) -> Self {
458 RunProperties::default()
460 }
461
462 pub fn copy(&self) -> Self {
466 self.clone()
467 }
468}
469
470#[derive(Clone, Debug, Default)]
472pub struct Run {
473 pub text: String,
475 pub properties: RunProperties,
477}
478
479impl Run {
480 pub fn new(text: impl Into<String>) -> Self {
482 Run {
483 text: text.into(),
484 properties: RunProperties::default(),
485 }
486 }
487 pub fn line_break() -> Self {
489 Run {
490 text: String::from("\n"),
491 properties: RunProperties::default(),
492 }
493 }
494
495 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
497 if self.text == "\n" {
499 if !is_default_rpr(&self.properties) {
500 w.empty("a:br");
502 } else {
503 w.empty("a:br");
504 }
505 return;
506 }
507 w.open("a:r");
508 if !is_default_rpr(&self.properties) {
509 self.properties.write_xml(w, "a:rPr");
510 }
511 w.open("a:t");
512 w.text(&self.text);
513 w.close("a:t");
514 w.close("a:r");
515 }
516
517 pub fn text(&self) -> &str {
521 &self.text
522 }
523 pub fn set_text(&mut self, t: impl Into<String>) {
525 self.text = t.into();
526 }
527
528 pub fn size(&self) -> Option<Pt> {
530 self.properties.size
531 }
532 pub fn set_size(&mut self, v: Pt) {
534 self.properties.size = Some(v);
535 }
536
537 pub fn bold(&self) -> bool {
539 self.properties.bold
540 }
541 pub fn set_bold(&mut self, v: bool) {
543 self.properties.bold = v;
544 }
545
546 pub fn italic(&self) -> bool {
548 self.properties.italic
549 }
550 pub fn set_italic(&mut self, v: bool) {
552 self.properties.italic = v;
553 }
554
555 pub fn color(&self) -> Color {
557 self.properties.color.clone()
558 }
559 pub fn set_color(&mut self, c: impl Into<Color>) {
561 self.properties.color = c.into();
562 }
563
564 pub fn font_name(&self) -> Option<&str> {
566 self.properties.latin_font.as_deref()
567 }
568 pub fn set_font_name(&mut self, name: impl Into<String>) {
570 self.properties.latin_font = Some(name.into());
571 }
572
573 pub fn eastasia_name(&self) -> Option<&str> {
579 self.properties.eastasia_font.as_deref()
580 }
581 pub fn set_eastasia_name(&mut self, name: impl Into<String>) {
586 self.properties.eastasia_font = Some(name.into());
587 }
588
589 pub fn complex_script_name(&self) -> Option<&str> {
593 self.properties.cs_font.as_deref()
594 }
595 pub fn set_complex_script_name(&mut self, name: impl Into<String>) {
600 self.properties.cs_font = Some(name.into());
601 }
602
603 pub fn underline(&self) -> Option<Underline> {
605 self.properties.underline
606 }
607 pub fn set_underline(&mut self, v: Underline) {
609 self.properties.underline = Some(v);
610 }
611
612 pub fn strike(&self) -> bool {
614 self.properties.strike
615 }
616 pub fn set_strike(&mut self, v: bool) {
618 self.properties.strike = v;
619 }
620
621 pub fn double_strike(&self) -> bool {
623 self.properties.strike_dbl
624 }
625 pub fn set_double_strike(&mut self, v: bool) {
627 self.properties.strike_dbl = v;
628 }
629
630 pub fn highlight(&self) -> Option<&Color> {
634 self.properties.highlight.as_ref()
635 }
636 pub fn set_highlight(&mut self, color: Option<Color>) {
640 self.properties.highlight = color;
641 }
642 pub fn clear_highlight(&mut self) {
644 self.properties.highlight = None;
645 }
646
647 pub fn hlink_click(&self) -> Option<&Hyperlink> {
649 self.properties.hlink_click.as_ref()
650 }
651 pub fn set_hlink_click(&mut self, hl: Hyperlink) {
653 self.properties.hlink_click = Some(hl);
654 }
655 pub fn clear_hlink_click(&mut self) {
657 self.properties.hlink_click = None;
658 }
659
660 pub fn hlink_hover(&self) -> Option<&Hyperlink> {
662 self.properties.hlink_hover.as_ref()
663 }
664 pub fn set_hlink_hover(&mut self, hl: Hyperlink) {
666 self.properties.hlink_hover = Some(hl);
667 }
668 pub fn clear_hlink_hover(&mut self) {
670 self.properties.hlink_hover = None;
671 }
672
673 pub fn set_hyperlink(&mut self, rid: impl Into<String>, tooltip: Option<&str>) {
690 let mut hl = Hyperlink::new(rid);
691 if let Some(t) = tooltip {
692 hl.tooltip = Some(t.to_string());
693 }
694 self.properties.hlink_click = Some(hl);
695 }
696
697 pub fn set_slide_jump(&mut self) {
706 self.properties.hlink_click = Some(Hyperlink::new_slide_jump());
707 }
708
709 pub fn font(&mut self) -> Font<'_> {
713 Font::new(&mut self.properties)
714 }
715}
716
717fn is_default_rpr(p: &RunProperties) -> bool {
718 p.size.is_none()
719 && !p.bold
720 && !p.italic
721 && p.underline.is_none()
722 && !p.strike
723 && !p.strike_dbl
724 && matches!(p.color, Color::None)
725 && p.highlight.is_none()
726 && p.latin_font.is_none()
727 && p.eastasia_font.is_none()
728 && p.cs_font.is_none()
729 && p.baseline.is_none()
730 && p.kerning.is_none()
731 && p.spc.is_none()
732 && p.caps == Caps::None
733 && p.lang.is_none()
734 && p.alpha.is_none()
735 && p.hlink_click.is_none()
736 && p.hlink_hover.is_none()
737}
738
739#[derive(Clone, Debug, Default, PartialEq)]
744pub enum FieldType {
745 #[default]
747 SlideNumber,
748 DateTime,
750 DateTime1,
752 DateTime2,
754 DateTime3,
756 Footer,
758 Custom(String),
760}
761
762impl FieldType {
763 pub fn as_str(&self) -> &str {
765 match self {
766 FieldType::SlideNumber => "slidenum",
767 FieldType::DateTime => "datetime",
768 FieldType::DateTime1 => "datetime1",
769 FieldType::DateTime2 => "datetime2",
770 FieldType::DateTime3 => "datetime3",
771 FieldType::Footer => "footer",
772 FieldType::Custom(s) => s.as_str(),
773 }
774 }
775
776 pub fn from_str_value(s: &str) -> Self {
778 match s {
779 "slidenum" => FieldType::SlideNumber,
780 "datetime" => FieldType::DateTime,
781 "datetime1" => FieldType::DateTime1,
782 "datetime2" => FieldType::DateTime2,
783 "datetime3" => FieldType::DateTime3,
784 "footer" => FieldType::Footer,
785 other => FieldType::Custom(other.to_string()),
786 }
787 }
788}
789
790#[derive(Clone, Debug, Default)]
803pub struct Field {
804 pub id: String,
806 pub field_type: FieldType,
808 pub properties: RunProperties,
810 pub text: String,
812}
813
814impl Field {
815 pub fn new(field_type: FieldType, text: impl Into<String>) -> Self {
821 Field {
822 id: format!("{{{}}}", uuid_like()),
823 field_type,
824 properties: RunProperties::default(),
825 text: text.into(),
826 }
827 }
828
829 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
831 w.open_with(
832 "a:fld",
833 &[("id", self.id.as_str()), ("type", self.field_type.as_str())],
834 );
835 self.properties.write_xml(w, "a:rPr");
836 w.open("a:t");
837 w.text(&self.text);
838 w.close("a:t");
839 w.close("a:fld");
840 }
841}
842
843fn uuid_like() -> String {
849 use std::sync::atomic::{AtomicU64, Ordering};
850 static COUNTER: AtomicU64 = AtomicU64::new(1);
851 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
852 let ts = std::time::SystemTime::now()
853 .duration_since(std::time::UNIX_EPOCH)
854 .map(|d| d.as_nanos() as u64)
855 .unwrap_or(0);
856 format!("{:016x}-{:016x}", ts, n)
857}
858
859#[derive(Clone, Debug, Default)]
861pub struct Paragraph {
862 pub properties: ParagraphProperties,
864 pub runs: Vec<Run>,
866 pub fields: Vec<Field>,
871 pub end_properties: Option<RunProperties>,
873}
874
875impl Paragraph {
876 pub fn new() -> Self {
878 Paragraph::default()
879 }
880
881 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
883 w.open("a:p");
884 self.properties.write_xml(w);
885 for r in &self.runs {
886 r.write_xml(w);
887 }
888 for f in &self.fields {
889 f.write_xml(w);
890 }
891 if let Some(rpr) = &self.end_properties {
892 rpr.write_xml(w, "a:endParaRPr");
893 }
894 w.close("a:p");
895 }
896
897 pub fn add_run(&mut self) -> &mut Run {
903 self.runs.push(Run::default());
904 let idx = self.runs.len() - 1;
906 &mut self.runs[idx]
907 }
908
909 pub fn add_run_with_text(&mut self, text: impl Into<String>) -> &mut Run {
913 self.runs.push(Run::new(text));
914 let idx = self.runs.len() - 1;
916 &mut self.runs[idx]
917 }
918
919 pub fn add_line_break(&mut self) -> &mut Run {
924 self.runs.push(Run {
925 text: String::from("\n"),
926 properties: RunProperties::default(),
927 });
928 let idx = self.runs.len() - 1;
930 &mut self.runs[idx]
931 }
932
933 pub fn clear_runs(&mut self) {
937 self.runs.clear();
938 }
939
940 pub fn add_field(&mut self, field_type: FieldType, text: impl Into<String>) -> &mut Field {
948 self.fields.push(Field::new(field_type, text));
949 let idx = self.fields.len() - 1;
950 &mut self.fields[idx]
951 }
952
953 pub fn clear_fields(&mut self) {
955 self.fields.clear();
956 }
957
958 pub fn set_text(&mut self, text: impl Into<String>) -> &mut Run {
963 self.runs.clear();
964 self.runs.push(Run::new(text));
965 let idx = self.runs.len() - 1;
967 &mut self.runs[idx]
968 }
969
970 pub fn text(&self) -> String {
972 let mut out = String::new();
973 for r in &self.runs {
974 out.push_str(&r.text);
976 }
977 out
978 }
979
980 pub fn alignment(&self) -> Option<Alignment> {
982 self.properties.alignment
983 }
984 pub fn set_alignment(&mut self, v: Alignment) {
986 self.properties.alignment = Some(v);
987 }
988
989 pub fn level(&self) -> u8 {
991 self.properties.level
992 }
993 pub fn set_level(&mut self, lvl: u8) {
995 self.properties.level = lvl;
996 }
997
998 pub fn line_spacing(&self) -> Option<Pt> {
1000 self.properties
1002 .line_spacing
1003 .map(|emu| Pt(emu as f64 / 12_700.0))
1004 }
1005 pub fn set_line_spacing(&mut self, v: Pt) {
1007 let emu = (v.value() * 12_700.0) as i32;
1008 self.properties.line_spacing = Some(emu);
1009 self.properties.line_spacing_pct = None;
1010 }
1011
1012 pub fn line_spacing_pct(&self) -> Option<f32> {
1014 self.properties.line_spacing_pct.map(|v| v as f32 / 1000.0)
1015 }
1016 pub fn set_line_spacing_pct(&mut self, v: f32) {
1018 self.properties.line_spacing_pct = Some((v * 1000.0) as i32);
1020 self.properties.line_spacing = None;
1021 }
1022
1023 pub fn space_before(&self) -> Option<Emu> {
1025 self.properties.space_before
1026 }
1027 pub fn set_space_before(&mut self, emu: Emu) {
1029 self.properties.space_before = Some(emu);
1030 }
1031
1032 pub fn space_after(&self) -> Option<Emu> {
1034 self.properties.space_after
1035 }
1036 pub fn set_space_after(&mut self, emu: Emu) {
1038 self.properties.space_after = Some(emu);
1039 }
1040
1041 pub fn indent(&self) -> Indent {
1043 self.properties.indent
1044 }
1045 pub fn set_indent(
1047 &mut self,
1048 left: Option<Emu>,
1049 right: Option<Emu>,
1050 first_line: Option<Emu>,
1051 hanging: Option<i32>,
1052 ) {
1053 self.properties.indent = Indent {
1054 left,
1055 right,
1056 first_line,
1057 hanging,
1058 };
1059 }
1060
1061 pub fn end_para_rpr(&self) -> Option<&RunProperties> {
1070 self.end_properties.as_ref()
1071 }
1072
1073 pub fn end_para_rpr_mut(&mut self) -> Option<&mut RunProperties> {
1075 self.end_properties.as_mut()
1076 }
1077
1078 pub fn set_end_para_rpr(&mut self, rpr: RunProperties) {
1094 self.end_properties = Some(rpr);
1095 }
1096
1097 pub fn clear_end_para_rpr(&mut self) {
1099 self.end_properties = None;
1100 }
1101}
1102
1103#[derive(Clone, Debug, Default)]
1108pub struct TextBody {
1109 pub body_properties: Option<BodyProperties>,
1110 pub paragraphs: Vec<Paragraph>,
1111}
1112
1113impl TextBody {
1114 pub fn new() -> Self {
1115 TextBody::default()
1116 }
1117
1118 pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
1123 w.open(tag);
1124 if let Some(bp) = &self.body_properties {
1125 bp.write_xml(w);
1126 }
1127 for p in &self.paragraphs {
1128 p.write_xml(w);
1129 }
1130 w.close(tag);
1131 }
1132
1133 pub fn text(&self) -> String {
1137 let mut out = String::new();
1138 for (i, p) in self.paragraphs.iter().enumerate() {
1139 if i > 0 {
1140 out.push('\n');
1141 }
1142 for r in &p.runs {
1143 out.push_str(&r.text);
1144 }
1145 }
1146 out
1147 }
1148
1149 pub fn first_paragraph(&self) -> Option<&Paragraph> {
1151 self.paragraphs.first()
1152 }
1153
1154 pub fn first_paragraph_mut(&mut self) -> Option<&mut Paragraph> {
1156 self.paragraphs.first_mut()
1157 }
1158
1159 pub fn paragraph(&self, idx: usize) -> Option<&Paragraph> {
1161 self.paragraphs.get(idx)
1162 }
1163
1164 pub fn paragraph_mut(&mut self, idx: usize) -> Option<&mut Paragraph> {
1166 self.paragraphs.get_mut(idx)
1167 }
1168
1169 pub fn add_paragraph(&mut self) -> &mut Paragraph {
1173 self.paragraphs.push(Paragraph::new());
1174 let idx = self.paragraphs.len() - 1;
1176 &mut self.paragraphs[idx]
1177 }
1178
1179 pub fn add_paragraph_with_text(&mut self, text: &str) -> &mut Paragraph {
1183 for line in text.split('\n') {
1184 let mut p = Paragraph::new();
1185 p.runs.push(Run::new(line));
1186 self.paragraphs.push(p);
1187 }
1188 let idx = self.paragraphs.len() - 1;
1190 &mut self.paragraphs[idx]
1191 }
1192
1193 pub fn remove_paragraph(&mut self, idx: usize) -> Option<Paragraph> {
1195 if idx < self.paragraphs.len() {
1196 Some(self.paragraphs.remove(idx))
1197 } else {
1198 None
1199 }
1200 }
1201
1202 pub fn clear(&mut self) {
1204 self.paragraphs.clear();
1205 self.paragraphs.push(Paragraph::new());
1206 }
1207
1208 pub fn set_text(&mut self, text: &str) {
1212 self.paragraphs.clear();
1213 for line in text.split('\n') {
1214 let mut p = Paragraph::new();
1215 p.runs.push(Run::new(line));
1216 self.paragraphs.push(p);
1217 }
1218 }
1219
1220 fn ensure_body_properties(&mut self) -> &mut BodyProperties {
1224 if self.body_properties.is_none() {
1225 self.body_properties = Some(BodyProperties::default());
1226 }
1227 match &mut self.body_properties {
1229 Some(bp) => bp,
1230 None => unreachable!("body_properties was just initialized above"),
1231 }
1232 }
1233
1234 pub fn auto_size(&self) -> MsoAutoSize {
1236 self.body_properties
1237 .as_ref()
1238 .and_then(|b| b.auto_size())
1239 .unwrap_or(MsoAutoSize::None)
1240 }
1241 pub fn set_auto_size(&mut self, v: MsoAutoSize) {
1243 self.ensure_body_properties().set_auto_size(v);
1244 }
1245
1246 pub fn vertical_anchor(&self) -> Option<MsoAnchor> {
1248 self.body_properties.as_ref().and_then(|b| b.anchor)
1249 }
1250 pub fn set_vertical_anchor(&mut self, v: MsoAnchor) {
1252 self.ensure_body_properties().anchor = Some(v);
1253 }
1254
1255 pub fn word_wrap(&self) -> Option<bool> {
1259 self.body_properties.as_ref().and_then(|b| match b.wrap {
1260 Some(TextWrapping::Square) => Some(true),
1261 Some(TextWrapping::None) => Some(false),
1262 None => None,
1263 })
1264 }
1265 pub fn set_word_wrap(&mut self, v: bool) {
1267 self.ensure_body_properties().wrap = Some(if v {
1268 TextWrapping::Square
1269 } else {
1270 TextWrapping::None
1271 });
1272 }
1273
1274 pub fn margin_left(&self) -> Option<Emu> {
1276 self.body_properties
1277 .as_ref()
1278 .and_then(|b| b.insets.as_ref().map(|i| i.left))
1279 }
1280 pub fn margin_right(&self) -> Option<Emu> {
1282 self.body_properties
1283 .as_ref()
1284 .and_then(|b| b.insets.as_ref().map(|i| i.right))
1285 }
1286 pub fn margin_top(&self) -> Option<Emu> {
1288 self.body_properties
1289 .as_ref()
1290 .and_then(|b| b.insets.as_ref().map(|i| i.top))
1291 }
1292 pub fn margin_bottom(&self) -> Option<Emu> {
1294 self.body_properties
1295 .as_ref()
1296 .and_then(|b| b.insets.as_ref().map(|i| i.bottom))
1297 }
1298 pub fn set_margins(&mut self, left: Emu, top: Emu, right: Emu, bottom: Emu) {
1300 let bp = self.ensure_body_properties();
1301 bp.insets = Some(Inset {
1302 left,
1303 top,
1304 right,
1305 bottom,
1306 });
1307 }
1308 pub fn set_margin_left(&mut self, emu: Emu) {
1310 let bp = self.ensure_body_properties();
1311 let i = bp.insets.get_or_insert(Inset::default());
1312 i.left = emu;
1313 }
1314 pub fn set_margin_right(&mut self, emu: Emu) {
1316 let bp = self.ensure_body_properties();
1317 let i = bp.insets.get_or_insert(Inset::default());
1318 i.right = emu;
1319 }
1320 pub fn set_margin_top(&mut self, emu: Emu) {
1322 let bp = self.ensure_body_properties();
1323 let i = bp.insets.get_or_insert(Inset::default());
1324 i.top = emu;
1325 }
1326 pub fn set_margin_bottom(&mut self, emu: Emu) {
1328 let bp = self.ensure_body_properties();
1329 let i = bp.insets.get_or_insert(Inset::default());
1330 i.bottom = emu;
1331 }
1332
1333 pub fn num_cols(&self) -> Option<u32> {
1337 self.body_properties.as_ref().and_then(|b| b.num_cols)
1338 }
1339 pub fn set_num_cols(&mut self, count: u32) {
1341 let bp = self.ensure_body_properties();
1342 bp.num_cols = Some(count);
1343 }
1344 pub fn col_spacing(&self) -> Option<Emu> {
1346 self.body_properties.as_ref().and_then(|b| b.col_spacing)
1347 }
1348 pub fn set_col_spacing(&mut self, emu: Emu) {
1350 let bp = self.ensure_body_properties();
1351 bp.col_spacing = Some(emu);
1352 }
1353}
1354
1355#[derive(Clone, Debug, Default)]
1357pub struct BodyProperties {
1358 pub insets: Option<Inset>,
1360 pub vertical: Option<String>,
1362 pub rotation: Option<i32>,
1364 pub wrap: Option<TextWrapping>,
1366 pub sp_auto_fit: bool,
1368 pub norm_autofit: bool,
1370 pub anchor: Option<MsoAnchor>,
1372 pub anchor_ctr: bool,
1374 pub num_cols: Option<u32>,
1379 pub col_spacing: Option<Emu>,
1383}
1384
1385#[derive(Copy, Clone, Debug, Default)]
1390pub struct Inset {
1391 pub left: Emu,
1393 pub top: Emu,
1395 pub right: Emu,
1397 pub bottom: Emu,
1399}
1400
1401impl BodyProperties {
1402 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1404 let l_s = self.insets.as_ref().map(|i| i.left.value().to_string());
1406 let t_s = self.insets.as_ref().map(|i| i.top.value().to_string());
1407 let r_s = self.insets.as_ref().map(|i| i.right.value().to_string());
1408 let b_s = self.insets.as_ref().map(|i| i.bottom.value().to_string());
1409 let rot_s = self.rotation.map(|v| v.to_string());
1410 let wrap_s = self.wrap.map(|v| v.as_str());
1411 let anchor_s = self.anchor.map(|v| v.as_str());
1412 let numcol_s = self.num_cols.map(|v| v.to_string());
1413 let spccol_s = self.col_spacing.map(|v| v.value().to_string());
1414
1415 let mut attrs: Vec<(&str, &str)> = Vec::new();
1416 if let Some(s) = &l_s {
1417 attrs.push(("lIns", s));
1418 }
1419 if let Some(s) = &t_s {
1420 attrs.push(("tIns", s));
1421 }
1422 if let Some(s) = &r_s {
1423 attrs.push(("rIns", s));
1424 }
1425 if let Some(s) = &b_s {
1426 attrs.push(("bIns", s));
1427 }
1428 if let Some(v) = &self.vertical {
1429 attrs.push(("vert", v));
1430 }
1431 if let Some(s) = &rot_s {
1432 attrs.push(("rot", s));
1433 }
1434 if let Some(s) = &wrap_s {
1435 attrs.push(("wrap", s));
1436 }
1437 if let Some(s) = &anchor_s {
1438 attrs.push(("anchor", s));
1439 }
1440 if let Some(s) = &numcol_s {
1441 attrs.push(("numCol", s));
1442 }
1443 if let Some(s) = &spccol_s {
1444 attrs.push(("spcCol", s));
1445 }
1446 w.open_with("a:bodyPr", &attrs);
1447 if self.sp_auto_fit {
1449 w.empty("a:spAutoFit");
1450 } else if self.norm_autofit {
1451 w.empty("a:normAutofit");
1452 }
1453 w.close("a:bodyPr");
1454 }
1455
1456 pub fn auto_size(&self) -> Option<MsoAutoSize> {
1458 if self.sp_auto_fit {
1459 Some(MsoAutoSize::ShapeToFitText)
1460 } else if self.norm_autofit {
1461 Some(MsoAutoSize::TextToFitShape)
1462 } else {
1463 None
1464 }
1465 }
1466 pub fn set_auto_size(&mut self, v: MsoAutoSize) {
1468 match v {
1469 MsoAutoSize::None => {
1470 self.sp_auto_fit = false;
1471 self.norm_autofit = false;
1472 }
1473 MsoAutoSize::ShapeToFitText => {
1474 self.sp_auto_fit = true;
1475 self.norm_autofit = false;
1476 }
1477 MsoAutoSize::TextToFitShape => {
1478 self.sp_auto_fit = false;
1479 self.norm_autofit = true;
1480 }
1481 }
1482 }
1483}
1484
1485impl From<RGBColor> for Color {
1486 fn from(c: RGBColor) -> Self {
1487 Color::RGB(c)
1488 }
1489}
1490
1491use crate::oxml::color::ColorFormat;
1496
1497#[derive(Debug)]
1512pub struct Font<'a> {
1513 rpr: &'a mut RunProperties,
1515}
1516
1517impl<'a> Font<'a> {
1518 pub fn new(rpr: &'a mut RunProperties) -> Self {
1520 Font { rpr }
1521 }
1522 pub fn rpr(&self) -> &RunProperties {
1524 self.rpr
1525 }
1526 pub fn rpr_mut(&mut self) -> &mut RunProperties {
1528 self.rpr
1529 }
1530
1531 pub fn color(&mut self) -> ColorFormat<'_> {
1533 ColorFormat::new(&mut self.rpr.color)
1534 }
1535
1536 pub fn size(&self) -> Option<Pt> {
1538 self.rpr.size
1539 }
1540 pub fn set_size(&mut self, v: Pt) {
1542 self.rpr.size = Some(v);
1543 }
1544 pub fn clear_size(&mut self) {
1546 self.rpr.size = None;
1547 }
1548
1549 pub fn bold(&self) -> bool {
1551 self.rpr.bold
1552 }
1553 pub fn set_bold(&mut self, v: bool) {
1555 self.rpr.bold = v;
1556 }
1557
1558 pub fn italic(&self) -> bool {
1560 self.rpr.italic
1561 }
1562 pub fn set_italic(&mut self, v: bool) {
1564 self.rpr.italic = v;
1565 }
1566
1567 pub fn strike(&self) -> bool {
1569 self.rpr.strike
1570 }
1571 pub fn set_strike(&mut self, v: bool) {
1573 self.rpr.strike = v;
1574 }
1575
1576 pub fn double_strike(&self) -> bool {
1581 self.rpr.strike_dbl
1582 }
1583 pub fn set_double_strike(&mut self, v: bool) {
1585 self.rpr.strike_dbl = v;
1586 }
1587
1588 pub fn highlight(&self) -> Option<&Color> {
1593 self.rpr.highlight.as_ref()
1594 }
1595 pub fn set_highlight(&mut self, color: Option<Color>) {
1599 match color {
1600 None => self.rpr.highlight = None,
1601 Some(Color::None) => self.rpr.highlight = None,
1602 Some(c) => self.rpr.highlight = Some(c),
1603 }
1604 }
1605
1606 pub fn underline(&self) -> Option<Underline> {
1608 self.rpr.underline
1609 }
1610 pub fn set_underline(&mut self, v: Option<Underline>) {
1612 self.rpr.underline = v;
1613 }
1614
1615 pub fn name(&self) -> Option<&str> {
1617 self.rpr.latin_font.as_deref()
1618 }
1619 pub fn set_name(&mut self, n: impl Into<String>) {
1621 self.rpr.latin_font = Some(n.into());
1622 }
1623 pub fn clear_name(&mut self) {
1625 self.rpr.latin_font = None;
1626 }
1627
1628 pub fn eastasia_name(&self) -> Option<&str> {
1630 self.rpr.eastasia_font.as_deref()
1631 }
1632 pub fn set_eastasia_name(&mut self, n: impl Into<String>) {
1634 self.rpr.eastasia_font = Some(n.into());
1635 }
1636 pub fn clear_eastasia_name(&mut self) {
1641 self.rpr.eastasia_font = None;
1642 }
1643
1644 pub fn complex_script_name(&self) -> Option<&str> {
1646 self.rpr.cs_font.as_deref()
1647 }
1648 pub fn set_complex_script_name(&mut self, n: impl Into<String>) {
1650 self.rpr.cs_font = Some(n.into());
1651 }
1652 pub fn clear_complex_script_name(&mut self) {
1657 self.rpr.cs_font = None;
1658 }
1659
1660 pub fn baseline(&self) -> Option<i32> {
1662 self.rpr.baseline
1663 }
1664 pub fn set_baseline(&mut self, v: i32) {
1666 self.rpr.baseline = Some(v);
1667 }
1668
1669 pub fn spacing(&self) -> Option<i32> {
1671 self.rpr.spc
1672 }
1673 pub fn set_spacing(&mut self, v: i32) {
1675 self.rpr.spc = Some(v);
1676 }
1677
1678 pub fn hlink_click(&self) -> Option<&Hyperlink> {
1682 self.rpr.hlink_click.as_ref()
1683 }
1684 pub fn set_hlink_click(&mut self, hl: Hyperlink) {
1686 self.rpr.hlink_click = Some(hl);
1687 }
1688 pub fn clear_hlink_click(&mut self) {
1690 self.rpr.hlink_click = None;
1691 }
1692
1693 pub fn hlink_hover(&self) -> Option<&Hyperlink> {
1695 self.rpr.hlink_hover.as_ref()
1696 }
1697 pub fn set_hlink_hover(&mut self, hl: Hyperlink) {
1699 self.rpr.hlink_hover = Some(hl);
1700 }
1701 pub fn clear_hlink_hover(&mut self) {
1703 self.rpr.hlink_hover = None;
1704 }
1705
1706 pub fn set_hyperlink(&mut self, rid: impl Into<String>, tooltip: Option<&str>) {
1719 let mut hl = Hyperlink::new(rid);
1720 if let Some(t) = tooltip {
1721 hl.tooltip = Some(t.to_string());
1722 }
1723 self.rpr.hlink_click = Some(hl);
1724 }
1725
1726 pub fn set_slide_jump(&mut self) {
1730 self.rpr.hlink_click = Some(Hyperlink::new_slide_jump());
1731 }
1732}
1733
1734impl<'a> From<&'a mut RunProperties> for Font<'a> {
1735 fn from(r: &'a mut RunProperties) -> Self {
1736 Font::new(r)
1737 }
1738}
1739
1740#[derive(Debug)]
1759pub struct ParagraphFormat<'a> {
1760 ppr: &'a mut ParagraphProperties,
1762}
1763
1764impl<'a> ParagraphFormat<'a> {
1765 pub fn new(ppr: &'a mut ParagraphProperties) -> Self {
1767 ParagraphFormat { ppr }
1768 }
1769 pub fn ppr(&self) -> &ParagraphProperties {
1771 self.ppr
1772 }
1773 pub fn ppr_mut(&mut self) -> &mut ParagraphProperties {
1775 self.ppr
1776 }
1777
1778 pub fn alignment(&self) -> Option<Alignment> {
1780 self.ppr.alignment
1781 }
1782 pub fn set_alignment(&mut self, v: Alignment) {
1784 self.ppr.alignment = Some(v);
1785 }
1786 pub fn clear_alignment(&mut self) {
1788 self.ppr.alignment = None;
1789 }
1790
1791 pub fn level(&self) -> u8 {
1793 self.ppr.level
1794 }
1795 pub fn set_level(&mut self, lvl: u8) {
1797 self.ppr.level = lvl;
1798 }
1799
1800 pub fn line_spacing(&self) -> Option<Pt> {
1802 self.ppr.line_spacing.map(|emu| Pt(emu as f64 / 12_700.0))
1803 }
1804 pub fn set_line_spacing(&mut self, v: Pt) {
1806 let emu = (v.value() * 12_700.0) as i32;
1807 self.ppr.line_spacing = Some(emu);
1808 self.ppr.line_spacing_pct = None;
1809 }
1810 pub fn line_spacing_pct(&self) -> Option<f32> {
1812 self.ppr.line_spacing_pct.map(|v| v as f32 / 1000.0)
1813 }
1814 pub fn set_line_spacing_pct(&mut self, v: f32) {
1816 self.ppr.line_spacing_pct = Some((v * 1000.0) as i32);
1817 self.ppr.line_spacing = None;
1818 }
1819 pub fn clear_line_spacing(&mut self) {
1821 self.ppr.line_spacing = None;
1822 self.ppr.line_spacing_pct = None;
1823 }
1824
1825 pub fn space_before(&self) -> Option<Emu> {
1827 self.ppr.space_before
1828 }
1829 pub fn set_space_before(&mut self, emu: Emu) {
1831 self.ppr.space_before = Some(emu);
1832 }
1833 pub fn space_after(&self) -> Option<Emu> {
1835 self.ppr.space_after
1836 }
1837 pub fn set_space_after(&mut self, emu: Emu) {
1839 self.ppr.space_after = Some(emu);
1840 }
1841
1842 pub fn set_indent(
1844 &mut self,
1845 left: Option<Emu>,
1846 right: Option<Emu>,
1847 first_line: Option<Emu>,
1848 hanging: Option<i32>,
1849 ) {
1850 self.ppr.indent = Indent {
1851 left,
1852 right,
1853 first_line,
1854 hanging,
1855 };
1856 }
1857
1858 pub fn indent(&self) -> Indent {
1860 self.ppr.indent
1861 }
1862
1863 pub fn bullet_style(&self) -> Option<&BulletStyle> {
1869 self.ppr.bullet_style.as_ref()
1870 }
1871
1872 pub fn set_bullet_char(&mut self, ch: impl Into<String>) {
1877 self.ppr.bullet = true;
1878 self.ppr.bullet_style = Some(BulletStyle::Char { char: ch.into() });
1879 }
1880
1881 pub fn set_bullet_numbered(&mut self, auto_num_type: impl Into<String>, start_at: Option<u32>) {
1890 self.ppr.bullet = true;
1891 self.ppr.bullet_style = Some(BulletStyle::AutoNum {
1892 auto_num_type: auto_num_type.into(),
1893 start_at,
1894 });
1895 }
1896
1897 pub fn clear_bullet(&mut self) {
1899 self.ppr.bullet = false;
1900 self.ppr.bullet_style = Some(BulletStyle::None);
1901 }
1902
1903 pub fn has_bullet(&self) -> bool {
1905 self.ppr.bullet
1906 && self
1907 .ppr
1908 .bullet_style
1909 .as_ref()
1910 .map(|bs| !matches!(bs, BulletStyle::None))
1911 .unwrap_or(false)
1912 }
1913
1914 pub fn tab_stops(&self) -> &[TabStop] {
1920 &self.ppr.tab_stops
1921 }
1922
1923 pub fn add_tab_stop(&mut self, pos: Emu, alignment: TabAlignment) {
1931 self.ppr.tab_stops.push(TabStop { pos, alignment });
1932 }
1933
1934 pub fn clear_tab_stops(&mut self) {
1936 self.ppr.tab_stops.clear();
1937 }
1938}
1939
1940impl<'a> From<&'a mut ParagraphProperties> for ParagraphFormat<'a> {
1941 fn from(p: &'a mut ParagraphProperties) -> Self {
1942 ParagraphFormat::new(p)
1943 }
1944}
1945
1946#[derive(Debug)]
1969pub struct TextFrame<'a> {
1970 body: &'a mut TextBody,
1972}
1973
1974impl<'a> TextFrame<'a> {
1975 pub fn new(body: &'a mut TextBody) -> Self {
1977 TextFrame { body }
1978 }
1979
1980 pub fn body(&self) -> &TextBody {
1982 self.body
1983 }
1984 pub fn body_mut(&mut self) -> &mut TextBody {
1986 self.body
1987 }
1988
1989 pub fn len(&self) -> usize {
1993 self.body.paragraphs.len()
1994 }
1995 pub fn is_empty(&self) -> bool {
1997 self.body.paragraphs.is_empty()
1998 }
1999 pub fn paragraphs(&self) -> std::slice::Iter<'_, Paragraph> {
2001 self.body.paragraphs.iter()
2002 }
2003 pub fn paragraphs_mut(&mut self) -> std::slice::IterMut<'_, Paragraph> {
2005 self.body.paragraphs.iter_mut()
2006 }
2007 pub fn paragraph(&self, idx: usize) -> Option<&Paragraph> {
2009 self.body.paragraphs.get(idx)
2010 }
2011 pub fn paragraph_mut(&mut self, idx: usize) -> Option<&mut Paragraph> {
2013 self.body.paragraphs.get_mut(idx)
2014 }
2015 pub fn first_paragraph(&self) -> Option<&Paragraph> {
2017 self.body.paragraphs.first()
2018 }
2019 pub fn first_paragraph_mut(&mut self) -> Option<&mut Paragraph> {
2021 self.body.paragraphs.first_mut()
2022 }
2023
2024 pub fn add_paragraph(&mut self) -> &mut Paragraph {
2026 self.body.add_paragraph()
2027 }
2028
2029 pub fn add_paragraph_with_text(&mut self, text: &str) -> &mut Paragraph {
2031 self.body.add_paragraph_with_text(text)
2032 }
2033
2034 pub fn remove_paragraph(&mut self, idx: usize) -> Option<Paragraph> {
2036 self.body.remove_paragraph(idx)
2037 }
2038
2039 pub fn clear(&mut self) {
2041 self.body.clear();
2042 }
2043
2044 pub fn text_getter(&self) -> String {
2048 self.body.text()
2049 }
2050
2051 pub fn set_text(&mut self, text: &str) {
2053 self.body.set_text(text);
2054 }
2055
2056 pub fn auto_size(&self) -> MsoAutoSize {
2060 self.body.auto_size()
2061 }
2062 pub fn set_auto_size(&mut self, v: MsoAutoSize) {
2064 self.body.set_auto_size(v);
2065 }
2066
2067 pub fn vertical_anchor(&self) -> Option<MsoAnchor> {
2069 self.body.vertical_anchor()
2070 }
2071 pub fn set_vertical_anchor(&mut self, v: MsoAnchor) {
2073 self.body.set_vertical_anchor(v);
2074 }
2075
2076 pub fn word_wrap(&self) -> Option<bool> {
2078 self.body.word_wrap()
2079 }
2080 pub fn set_word_wrap(&mut self, v: bool) {
2082 self.body.set_word_wrap(v);
2083 }
2084
2085 pub fn margin_left(&self) -> Option<Emu> {
2087 self.body.margin_left()
2088 }
2089 pub fn margin_right(&self) -> Option<Emu> {
2091 self.body.margin_right()
2092 }
2093 pub fn margin_top(&self) -> Option<Emu> {
2095 self.body.margin_top()
2096 }
2097 pub fn margin_bottom(&self) -> Option<Emu> {
2099 self.body.margin_bottom()
2100 }
2101 pub fn set_margins(&mut self, l: Emu, t: Emu, r: Emu, b: Emu) {
2103 self.body.set_margins(l, t, r, b);
2104 }
2105 pub fn set_margin_left(&mut self, emu: Emu) {
2107 self.body.set_margin_left(emu);
2108 }
2109 pub fn set_margin_right(&mut self, emu: Emu) {
2111 self.body.set_margin_right(emu);
2112 }
2113 pub fn set_margin_top(&mut self, emu: Emu) {
2115 self.body.set_margin_top(emu);
2116 }
2117 pub fn set_margin_bottom(&mut self, emu: Emu) {
2119 self.body.set_margin_bottom(emu);
2120 }
2121
2122 pub fn num_cols(&self) -> Option<u32> {
2128 self.body.num_cols()
2129 }
2130
2131 pub fn set_num_cols(&mut self, count: u32) {
2133 self.body.set_num_cols(count);
2134 }
2135
2136 pub fn col_spacing(&self) -> Option<Emu> {
2138 self.body.col_spacing()
2139 }
2140
2141 pub fn set_col_spacing(&mut self, emu: Emu) {
2143 self.body.set_col_spacing(emu);
2144 }
2145
2146 pub fn set_columns(&mut self, count: u32, spacing: Option<Emu>) {
2151 self.body.set_num_cols(count);
2152 if let Some(s) = spacing {
2153 self.body.set_col_spacing(s);
2154 }
2155 }
2156}
2157
2158impl<'a> From<&'a mut TextBody> for TextFrame<'a> {
2159 fn from(b: &'a mut TextBody) -> Self {
2160 TextFrame::new(b)
2161 }
2162}
2163
2164#[cfg(test)]
2165mod tests {
2166 use super::*;
2167
2168 #[test]
2169 fn write_paragraph_simple() {
2170 let mut p = Paragraph::new();
2171 let mut r = Run::new("Hello");
2172 r.properties.size = Some(Pt(24.0));
2173 r.properties.bold = true;
2174 r.properties.color = RGBColor(0xFF, 0, 0).into();
2175 p.runs.push(r);
2176 let mut w = super::super::writer::XmlWriter::new();
2177 p.write_xml(&mut w);
2178 let s = w.into_string();
2179 assert!(s.contains("Hello"));
2180 assert!(s.contains("sz=\"2400\""));
2181 assert!(s.contains("b=\"1\""));
2182 assert!(s.contains("a:srgbClr"));
2183 }
2184
2185 #[test]
2187 fn textframe_view_mirrors_body() {
2188 let mut tb = TextBody::new();
2189 {
2190 let mut tf = TextFrame::new(&mut tb);
2191 let p = tf.add_paragraph();
2193 p.add_run_with_text("first").set_bold(true);
2194 tf.add_paragraph_with_text("second\nthird");
2195 tf.set_word_wrap(false);
2196 tf.set_margins(Emu(91440), Emu(45720), Emu(91440), Emu(45720));
2197 }
2198 assert_eq!(tb.paragraphs.len(), 3);
2200 assert_eq!(tb.paragraphs[0].runs[0].text, "first");
2201 assert!(tb.paragraphs[0].runs[0].properties.bold);
2202 assert_eq!(tb.paragraphs[1].runs[0].text, "second");
2203 assert_eq!(tb.paragraphs[2].runs[0].text, "third");
2204 assert_eq!(tb.word_wrap(), Some(false));
2206 }
2207
2208 #[test]
2210 fn paragraph_format_line_spacing_mutex() {
2211 let mut p = Paragraph::new();
2212 p.set_line_spacing(Pt(20.0));
2213 assert!(p.line_spacing().is_some());
2214 assert!(p.line_spacing_pct().is_none());
2215 p.set_line_spacing_pct(1.5);
2217 assert!(p.line_spacing().is_none());
2218 assert_eq!(p.line_spacing_pct(), Some(1.5));
2219 let mut p2 = Paragraph::new();
2221 {
2222 let mut pf = ParagraphFormat::new(&mut p2.properties);
2223 pf.set_line_spacing(Pt(15.0));
2224 }
2225 assert_eq!(p2.line_spacing(), Some(Pt(15.0)));
2226 {
2227 let mut pf = ParagraphFormat::new(&mut p2.properties);
2228 pf.set_line_spacing_pct(2.0);
2229 }
2230 assert!(p2.line_spacing().is_none());
2231 assert_eq!(p2.line_spacing_pct(), Some(2.0));
2232 }
2233
2234 #[test]
2238 fn font_strikethrough_api() {
2239 let mut r = Run::new("text");
2240 {
2241 let mut f = Font::new(&mut r.properties);
2242 f.set_strike(true);
2243 }
2244 assert!(r.properties.strike);
2245 assert!(!r.properties.strike_dbl);
2246
2247 {
2249 let mut f = Font::new(&mut r.properties);
2250 f.set_double_strike(true);
2251 }
2252 assert!(r.properties.strike_dbl);
2253
2254 let f = Font::new(&mut r.properties);
2256 assert!(f.strike());
2257 assert!(f.double_strike());
2258 }
2259
2260 #[test]
2264 fn font_highlight_api() {
2265 let mut r = Run::new("text");
2266 assert!(r.properties.highlight.is_none());
2268
2269 {
2271 let mut f = Font::new(&mut r.properties);
2272 f.set_highlight(Some(Color::RGB(RGBColor(0xFF, 0xFF, 0x00))));
2273 }
2274 assert!(r.properties.highlight.is_some());
2275
2276 let f = Font::new(&mut r.properties);
2278 let hl = f.highlight().expect("应有高亮色");
2279 assert!(matches!(hl, Color::RGB(c) if c.0 == 0xFF && c.1 == 0xFF && c.2 == 0x00));
2280
2281 {
2283 let mut f = Font::new(&mut r.properties);
2284 f.set_highlight(None);
2285 }
2286 assert!(r.properties.highlight.is_none());
2287
2288 {
2290 let mut f = Font::new(&mut r.properties);
2291 f.set_highlight(Some(Color::RGB(RGBColor(0xFF, 0x00, 0x00))));
2292 f.set_highlight(Some(Color::None));
2293 }
2294 assert!(r.properties.highlight.is_none());
2295 }
2296
2297 #[test]
2301 fn text_body_multi_column_api() {
2302 let mut tb = TextBody::new();
2303 assert!(tb.num_cols().is_none());
2305 assert!(tb.col_spacing().is_none());
2306
2307 tb.set_num_cols(3);
2309 tb.set_col_spacing(Emu(91440));
2310 assert_eq!(tb.num_cols(), Some(3));
2311 assert_eq!(tb.col_spacing(), Some(Emu(91440)));
2312
2313 let mut w = super::super::writer::XmlWriter::new();
2315 tb.body_properties
2316 .as_ref()
2317 .expect("应有 body_properties")
2318 .write_xml(&mut w);
2319 let s = w.into_string();
2320 assert!(s.contains("numCol=\"3\""), "应输出 numCol=\"3\",实际: {s}");
2321 assert!(
2322 s.contains("spcCol=\"91440\""),
2323 "应输出 spcCol=\"91440\",实际: {s}"
2324 );
2325 }
2326
2327 #[test]
2331 fn text_frame_set_columns() {
2332 let mut tb = TextBody::new();
2333 {
2334 let mut tf = TextFrame::new(&mut tb);
2335 tf.set_columns(2, Some(Emu(45720)));
2337 }
2338 assert_eq!(tb.num_cols(), Some(2));
2339 assert_eq!(tb.col_spacing(), Some(Emu(45720)));
2340
2341 {
2343 let mut tf = TextFrame::new(&mut tb);
2344 tf.set_columns(4, None);
2345 }
2346 assert_eq!(tb.num_cols(), Some(4));
2347 assert_eq!(tb.col_spacing(), Some(Emu(45720)));
2349
2350 {
2352 let tf = TextFrame::new(&mut tb);
2353 assert_eq!(tf.num_cols(), Some(4));
2354 assert_eq!(tf.col_spacing(), Some(Emu(45720)));
2355 }
2356 }
2357
2358 #[test]
2362 fn paragraph_format_bullet_char() {
2363 let mut ppr = ParagraphProperties::default();
2364 {
2365 let mut pf = ParagraphFormat::new(&mut ppr);
2366 pf.set_bullet_char("•");
2367 }
2368 assert!(ppr.bullet, "bullet 应为 true");
2369 assert!(pf_has_bullet(&ppr), "has_bullet 应为 true");
2370 let mut w = super::super::writer::XmlWriter::new();
2372 ppr.write_xml(&mut w);
2373 let s = w.into_string();
2374 assert!(s.contains("buChar"), "应输出 buChar,实际: {s}");
2375 assert!(s.contains("char=\"•\""), "应包含 char=\"•\",实际: {s}");
2376 }
2377
2378 #[test]
2382 fn paragraph_format_bullet_numbered() {
2383 let mut ppr = ParagraphProperties::default();
2384 {
2385 let mut pf = ParagraphFormat::new(&mut ppr);
2386 pf.set_bullet_numbered("arabicPeriod", Some(3));
2387 }
2388 assert!(ppr.bullet);
2389 match &ppr.bullet_style {
2390 Some(BulletStyle::AutoNum {
2391 auto_num_type,
2392 start_at,
2393 }) => {
2394 assert_eq!(auto_num_type, "arabicPeriod");
2395 assert_eq!(*start_at, Some(3));
2396 }
2397 other => panic!("期望 AutoNum,实际: {other:?}"),
2398 }
2399 let mut w = super::super::writer::XmlWriter::new();
2401 ppr.write_xml(&mut w);
2402 let s = w.into_string();
2403 assert!(s.contains("buAutoNum"), "应输出 buAutoNum,实际: {s}");
2404 assert!(
2405 s.contains("type=\"arabicPeriod\""),
2406 "应包含 type,实际: {s}"
2407 );
2408 assert!(s.contains("startAt=\"3\""), "应包含 startAt,实际: {s}");
2409 }
2410
2411 #[test]
2415 fn paragraph_format_clear_bullet() {
2416 let mut ppr = ParagraphProperties::default();
2417 {
2418 let mut pf = ParagraphFormat::new(&mut ppr);
2419 pf.set_bullet_char("•");
2420 }
2421 assert!(pf_has_bullet(&ppr));
2422 {
2423 let mut pf = ParagraphFormat::new(&mut ppr);
2424 pf.clear_bullet();
2425 }
2426 assert!(!ppr.bullet, "bullet 应为 false");
2427 assert!(!pf_has_bullet(&ppr), "has_bullet 应为 false");
2428 let mut w = super::super::writer::XmlWriter::new();
2430 ppr.write_xml(&mut w);
2431 let s = w.into_string();
2432 assert!(s.contains("buNone"), "应输出 buNone,实际: {s}");
2433 }
2434
2435 fn pf_has_bullet(ppr: &ParagraphProperties) -> bool {
2437 ppr.bullet
2438 && ppr
2439 .bullet_style
2440 .as_ref()
2441 .map(|bs| !matches!(bs, BulletStyle::None))
2442 .unwrap_or(false)
2443 }
2444
2445 #[test]
2449 fn paragraph_format_tab_stops() {
2450 let mut ppr = ParagraphProperties::default();
2451 {
2452 let mut pf = ParagraphFormat::new(&mut ppr);
2453 pf.add_tab_stop(Emu(914400), TabAlignment::Left);
2454 pf.add_tab_stop(Emu(1828800), TabAlignment::Right);
2455 pf.add_tab_stop(Emu(2743200), TabAlignment::Center);
2456 }
2457 assert_eq!(ppr.tab_stops.len(), 3);
2458 assert_eq!(ppr.tab_stops[0].pos.value(), 914400);
2459 assert_eq!(ppr.tab_stops[0].alignment, TabAlignment::Left);
2460 assert_eq!(ppr.tab_stops[1].pos.value(), 1828800);
2461 assert_eq!(ppr.tab_stops[1].alignment, TabAlignment::Right);
2462 assert_eq!(ppr.tab_stops[2].pos.value(), 2743200);
2463 assert_eq!(ppr.tab_stops[2].alignment, TabAlignment::Center);
2464
2465 let mut w = super::super::writer::XmlWriter::new();
2467 ppr.write_xml(&mut w);
2468 let s = w.into_string();
2469 assert!(s.contains("tabLst"), "应输出 tabLst,实际: {s}");
2470 assert!(s.contains("pos=\"914400\""), "应包含 pos=914400,实际: {s}");
2471 assert!(s.contains("algn=\"l\""), "应包含 algn=l,实际: {s}");
2472 assert!(s.contains("algn=\"r\""), "应包含 algn=r,实际: {s}");
2473 assert!(s.contains("algn=\"ctr\""), "应包含 algn=ctr,实际: {s}");
2474
2475 {
2477 let mut pf = ParagraphFormat::new(&mut ppr);
2478 pf.clear_tab_stops();
2479 }
2480 assert!(ppr.tab_stops.is_empty());
2481 }
2482
2483 #[test]
2487 fn paragraph_field_serialization() {
2488 let mut p = Paragraph::new();
2489 p.add_field(FieldType::SlideNumber, "1");
2490 p.add_field(FieldType::DateTime, "1/1/2024");
2491
2492 assert_eq!(p.fields.len(), 2);
2493 assert_eq!(p.fields[0].field_type, FieldType::SlideNumber);
2494 assert_eq!(p.fields[0].text, "1");
2495 assert_eq!(p.fields[1].field_type, FieldType::DateTime);
2496 assert_eq!(p.fields[1].text, "1/1/2024");
2497
2498 let mut w = super::super::writer::XmlWriter::new();
2500 p.write_xml(&mut w);
2501 let s = w.into_string();
2502 assert!(s.contains("a:fld"), "应输出 a:fld,实际: {s}");
2503 assert!(
2504 s.contains("type=\"slidenum\""),
2505 "应包含 type=slidenum,实际: {s}"
2506 );
2507 assert!(
2508 s.contains("type=\"datetime\""),
2509 "应包含 type=datetime,实际: {s}"
2510 );
2511 assert!(s.contains(">1<"), "应包含文本 1,实际: {s}");
2512 assert!(s.contains(">1/1/2024<"), "应包含文本 1/1/2024,实际: {s}");
2513 }
2514
2515 #[test]
2519 fn field_type_conversion() {
2520 assert_eq!(FieldType::SlideNumber.as_str(), "slidenum");
2522 assert_eq!(FieldType::DateTime.as_str(), "datetime");
2523 assert_eq!(FieldType::DateTime1.as_str(), "datetime1");
2524 assert_eq!(FieldType::Footer.as_str(), "footer");
2525 assert_eq!(FieldType::Custom("custom1".to_string()).as_str(), "custom1");
2526
2527 assert_eq!(
2529 FieldType::from_str_value("slidenum"),
2530 FieldType::SlideNumber
2531 );
2532 assert_eq!(FieldType::from_str_value("datetime"), FieldType::DateTime);
2533 assert_eq!(FieldType::from_str_value("datetime1"), FieldType::DateTime1);
2534 assert_eq!(FieldType::from_str_value("footer"), FieldType::Footer);
2535 assert_eq!(
2536 FieldType::from_str_value("unknown"),
2537 FieldType::Custom("unknown".to_string())
2538 );
2539 }
2540
2541 #[test]
2545 fn font_hyperlink_api() {
2546 let mut run = Run::new("链接文本");
2547 run.font().set_hyperlink("rId3", Some("点击访问"));
2549 let hl = run
2550 .properties
2551 .hlink_click
2552 .as_ref()
2553 .expect("hlink_click 应存在");
2554 assert_eq!(hl.rid.as_deref(), Some("rId3"));
2555 assert_eq!(hl.tooltip.as_deref(), Some("点击访问"));
2556
2557 run.font().set_slide_jump();
2559 let hl = run
2560 .properties
2561 .hlink_click
2562 .as_ref()
2563 .expect("hlink_click 应存在");
2564 assert_eq!(hl.action.as_deref(), Some("ppaction://hlinksldjump"));
2565
2566 run.font().clear_hlink_click();
2568 assert!(run.properties.hlink_click.is_none());
2569 }
2570
2571 #[test]
2575 fn hyperlink_serialization_roundtrip() {
2576 let mut run = Run::new("点击这里");
2577 run.font().set_hyperlink("rId7", Some("提示文字"));
2578
2579 let mut w = super::super::writer::XmlWriter::new();
2581 run.write_xml(&mut w);
2582 let s = w.into_string();
2583 assert!(s.contains("a:hlinkClick"), "应输出 a:hlinkClick,实际: {s}");
2584 assert!(s.contains("r:id=\"rId7\""), "应包含 r:id=rId7,实际: {s}");
2585 assert!(
2586 s.contains("tooltip=\"提示文字\""),
2587 "应包含 tooltip,实际: {s}"
2588 );
2589 }
2590
2591 #[test]
2597 fn end_para_rpr_set_and_serialize() {
2598 let mut p = Paragraph::new();
2599 let rpr = RunProperties {
2600 size: Some(Pt(24.0)),
2601 latin_font: Some("Calibri".to_string()),
2602 lang: Some("en-US".to_string()),
2603 ..Default::default()
2604 };
2605 p.set_end_para_rpr(rpr);
2606
2607 assert!(p.end_para_rpr().is_some());
2609 let got = p.end_para_rpr().unwrap();
2610 assert_eq!(got.size, Some(Pt(24.0)));
2611 assert_eq!(got.latin_font.as_deref(), Some("Calibri"));
2612 assert_eq!(got.lang.as_deref(), Some("en-US"));
2613
2614 let mut w = super::super::writer::XmlWriter::new();
2616 p.write_xml(&mut w);
2617 let s = w.into_string();
2618 assert!(s.contains("a:endParaRPr"), "应输出 a:endParaRPr,实际: {s}");
2619 assert!(s.contains("sz=\"2400\""), "应包含 sz=2400,实际: {s}");
2620 assert!(s.contains("lang=\"en-US\""), "应包含 lang=en-US,实际: {s}");
2621 assert!(s.contains("a:latin"), "应包含 a:latin,实际: {s}");
2622 assert!(
2623 s.contains("typeface=\"Calibri\""),
2624 "应包含 typeface=Calibri,实际: {s}"
2625 );
2626 }
2627
2628 #[test]
2630 fn end_para_rpr_clear() {
2631 let mut p = Paragraph::new();
2632 let rpr = RunProperties {
2633 size: Some(Pt(18.0)),
2634 ..Default::default()
2635 };
2636 p.set_end_para_rpr(rpr);
2637 assert!(p.end_para_rpr().is_some());
2638
2639 p.clear_end_para_rpr();
2640 assert!(p.end_para_rpr().is_none());
2641
2642 let mut w = super::super::writer::XmlWriter::new();
2644 p.write_xml(&mut w);
2645 let s = w.into_string();
2646 assert!(
2647 !s.contains("a:endParaRPr"),
2648 "不应输出 a:endParaRPr,实际: {s}"
2649 );
2650 }
2651
2652 #[test]
2654 fn end_para_rpr_mut_modify() {
2655 let mut p = Paragraph::new();
2656 let rpr = RunProperties::default();
2657 p.set_end_para_rpr(rpr);
2658
2659 if let Some(rpr) = p.end_para_rpr_mut() {
2661 rpr.size = Some(Pt(32.0));
2662 rpr.bold = true;
2663 }
2664
2665 let got = p.end_para_rpr().expect("end_para_rpr 应存在");
2666 assert_eq!(got.size, Some(Pt(32.0)));
2667 assert!(got.bold);
2668 }
2669
2670 #[test]
2678 fn run_eastasia_name_setter_and_getter() {
2679 let mut r = Run::new("你好");
2680 assert!(r.eastasia_name().is_none());
2681 r.set_eastasia_name("宋体");
2682 assert_eq!(r.eastasia_name(), Some("宋体"));
2683 assert_eq!(r.properties.eastasia_font.as_deref(), Some("宋体"));
2684 }
2685
2686 #[test]
2688 fn run_complex_script_name_setter_and_getter() {
2689 let mut r = Run::new("مرحبا");
2690 assert!(r.complex_script_name().is_none());
2691 r.set_complex_script_name("Traditional Arabic");
2692 assert_eq!(r.complex_script_name(), Some("Traditional Arabic"));
2693 assert_eq!(r.properties.cs_font.as_deref(), Some("Traditional Arabic"));
2694 }
2695
2696 #[test]
2698 fn run_three_fonts_independent() {
2699 let mut r = Run::new("Hello 你好 مرحبا");
2700 r.set_font_name("Arial");
2701 r.set_eastasia_name("Microsoft YaHei");
2702 r.set_complex_script_name("Tahoma");
2703 assert_eq!(r.font_name(), Some("Arial"));
2704 assert_eq!(r.eastasia_name(), Some("Microsoft YaHei"));
2705 assert_eq!(r.complex_script_name(), Some("Tahoma"));
2706 assert_eq!(r.properties.latin_font.as_deref(), Some("Arial"));
2708 assert_eq!(
2709 r.properties.eastasia_font.as_deref(),
2710 Some("Microsoft YaHei")
2711 );
2712 assert_eq!(r.properties.cs_font.as_deref(), Some("Tahoma"));
2713 }
2714
2715 #[test]
2717 fn font_clear_eastasia_name() {
2718 let mut r = Run::new("文本");
2719 r.set_eastasia_name("宋体");
2720 assert_eq!(r.eastasia_name(), Some("宋体"));
2721 {
2723 let mut f = Font::new(&mut r.properties);
2724 f.clear_eastasia_name();
2725 }
2726 assert!(r.eastasia_name().is_none());
2727 assert!(r.properties.eastasia_font.is_none());
2728 }
2729
2730 #[test]
2732 fn font_clear_complex_script_name() {
2733 let mut r = Run::new("text");
2734 r.set_complex_script_name("Arial");
2735 assert_eq!(r.complex_script_name(), Some("Arial"));
2736 {
2738 let mut f = Font::new(&mut r.properties);
2739 f.clear_complex_script_name();
2740 }
2741 assert!(r.complex_script_name().is_none());
2742 assert!(r.properties.cs_font.is_none());
2743 }
2744
2745 #[test]
2747 fn font_view_eastasia_cs_consistent_with_run() {
2748 let mut r = Run::new("混合文本");
2749 r.set_eastasia_name("黑体");
2750 r.set_complex_script_name("Tahoma");
2751 let run_ea = r.eastasia_name().map(|s| s.to_string());
2753 let run_cs = r.complex_script_name().map(|s| s.to_string());
2754 let f = Font::new(&mut r.properties);
2756 assert_eq!(f.eastasia_name(), run_ea.as_deref());
2757 assert_eq!(f.complex_script_name(), run_cs.as_deref());
2758 }
2759
2760 #[test]
2762 fn eastasia_font_serialization_order() {
2763 let mut r = Run::new("你好");
2764 r.set_font_name("Arial");
2765 r.set_eastasia_name("宋体");
2766 r.set_complex_script_name("Tahoma");
2767 let mut p = Paragraph::new();
2768 p.runs.push(r);
2769 let mut w = crate::oxml::writer::XmlWriter::new();
2770 p.write_xml(&mut w);
2771 let xml = w.into_string();
2772 assert!(
2774 xml.contains(r#"<a:latin typeface="Arial"/>"#),
2775 "xml: {}",
2776 xml
2777 );
2778 assert!(xml.contains(r#"<a:ea typeface="宋体"/>"#), "xml: {}", xml);
2779 assert!(xml.contains(r#"<a:cs typeface="Tahoma"/>"#), "xml: {}", xml);
2780 let pos_latin = xml.find("<a:latin").expect("latin should exist");
2782 let pos_ea = xml.find("<a:ea").expect("ea should exist");
2783 let pos_cs = xml.find("<a:cs").expect("cs should exist");
2784 assert!(pos_latin < pos_ea, "latin must come before ea: {}", xml);
2785 assert!(pos_ea < pos_cs, "ea must come before cs: {}", xml);
2786 }
2787}