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) {
1120 w.open("p:txBody");
1121 if let Some(bp) = &self.body_properties {
1122 bp.write_xml(w);
1123 }
1124 for p in &self.paragraphs {
1125 p.write_xml(w);
1126 }
1127 w.close("p:txBody");
1128 }
1129
1130 pub fn text(&self) -> String {
1134 let mut out = String::new();
1135 for (i, p) in self.paragraphs.iter().enumerate() {
1136 if i > 0 {
1137 out.push('\n');
1138 }
1139 for r in &p.runs {
1140 out.push_str(&r.text);
1141 }
1142 }
1143 out
1144 }
1145
1146 pub fn first_paragraph(&self) -> Option<&Paragraph> {
1148 self.paragraphs.first()
1149 }
1150
1151 pub fn first_paragraph_mut(&mut self) -> Option<&mut Paragraph> {
1153 self.paragraphs.first_mut()
1154 }
1155
1156 pub fn paragraph(&self, idx: usize) -> Option<&Paragraph> {
1158 self.paragraphs.get(idx)
1159 }
1160
1161 pub fn paragraph_mut(&mut self, idx: usize) -> Option<&mut Paragraph> {
1163 self.paragraphs.get_mut(idx)
1164 }
1165
1166 pub fn add_paragraph(&mut self) -> &mut Paragraph {
1170 self.paragraphs.push(Paragraph::new());
1171 let idx = self.paragraphs.len() - 1;
1173 &mut self.paragraphs[idx]
1174 }
1175
1176 pub fn add_paragraph_with_text(&mut self, text: &str) -> &mut Paragraph {
1180 for line in text.split('\n') {
1181 let mut p = Paragraph::new();
1182 p.runs.push(Run::new(line));
1183 self.paragraphs.push(p);
1184 }
1185 let idx = self.paragraphs.len() - 1;
1187 &mut self.paragraphs[idx]
1188 }
1189
1190 pub fn remove_paragraph(&mut self, idx: usize) -> Option<Paragraph> {
1192 if idx < self.paragraphs.len() {
1193 Some(self.paragraphs.remove(idx))
1194 } else {
1195 None
1196 }
1197 }
1198
1199 pub fn clear(&mut self) {
1201 self.paragraphs.clear();
1202 self.paragraphs.push(Paragraph::new());
1203 }
1204
1205 pub fn set_text(&mut self, text: &str) {
1209 self.paragraphs.clear();
1210 for line in text.split('\n') {
1211 let mut p = Paragraph::new();
1212 p.runs.push(Run::new(line));
1213 self.paragraphs.push(p);
1214 }
1215 }
1216
1217 fn ensure_body_properties(&mut self) -> &mut BodyProperties {
1221 if self.body_properties.is_none() {
1222 self.body_properties = Some(BodyProperties::default());
1223 }
1224 match &mut self.body_properties {
1226 Some(bp) => bp,
1227 None => unreachable!("body_properties was just initialized above"),
1228 }
1229 }
1230
1231 pub fn auto_size(&self) -> MsoAutoSize {
1233 self.body_properties
1234 .as_ref()
1235 .and_then(|b| b.auto_size())
1236 .unwrap_or(MsoAutoSize::None)
1237 }
1238 pub fn set_auto_size(&mut self, v: MsoAutoSize) {
1240 self.ensure_body_properties().set_auto_size(v);
1241 }
1242
1243 pub fn vertical_anchor(&self) -> Option<MsoAnchor> {
1245 self.body_properties.as_ref().and_then(|b| b.anchor)
1246 }
1247 pub fn set_vertical_anchor(&mut self, v: MsoAnchor) {
1249 self.ensure_body_properties().anchor = Some(v);
1250 }
1251
1252 pub fn word_wrap(&self) -> Option<bool> {
1256 self.body_properties.as_ref().and_then(|b| match b.wrap {
1257 Some(TextWrapping::Square) => Some(true),
1258 Some(TextWrapping::None) => Some(false),
1259 None => None,
1260 })
1261 }
1262 pub fn set_word_wrap(&mut self, v: bool) {
1264 self.ensure_body_properties().wrap = Some(if v {
1265 TextWrapping::Square
1266 } else {
1267 TextWrapping::None
1268 });
1269 }
1270
1271 pub fn margin_left(&self) -> Option<Emu> {
1273 self.body_properties
1274 .as_ref()
1275 .and_then(|b| b.insets.as_ref().map(|i| i.left))
1276 }
1277 pub fn margin_right(&self) -> Option<Emu> {
1279 self.body_properties
1280 .as_ref()
1281 .and_then(|b| b.insets.as_ref().map(|i| i.right))
1282 }
1283 pub fn margin_top(&self) -> Option<Emu> {
1285 self.body_properties
1286 .as_ref()
1287 .and_then(|b| b.insets.as_ref().map(|i| i.top))
1288 }
1289 pub fn margin_bottom(&self) -> Option<Emu> {
1291 self.body_properties
1292 .as_ref()
1293 .and_then(|b| b.insets.as_ref().map(|i| i.bottom))
1294 }
1295 pub fn set_margins(&mut self, left: Emu, top: Emu, right: Emu, bottom: Emu) {
1297 let bp = self.ensure_body_properties();
1298 bp.insets = Some(Inset {
1299 left,
1300 top,
1301 right,
1302 bottom,
1303 });
1304 }
1305 pub fn set_margin_left(&mut self, emu: Emu) {
1307 let bp = self.ensure_body_properties();
1308 let i = bp.insets.get_or_insert(Inset::default());
1309 i.left = emu;
1310 }
1311 pub fn set_margin_right(&mut self, emu: Emu) {
1313 let bp = self.ensure_body_properties();
1314 let i = bp.insets.get_or_insert(Inset::default());
1315 i.right = emu;
1316 }
1317 pub fn set_margin_top(&mut self, emu: Emu) {
1319 let bp = self.ensure_body_properties();
1320 let i = bp.insets.get_or_insert(Inset::default());
1321 i.top = emu;
1322 }
1323 pub fn set_margin_bottom(&mut self, emu: Emu) {
1325 let bp = self.ensure_body_properties();
1326 let i = bp.insets.get_or_insert(Inset::default());
1327 i.bottom = emu;
1328 }
1329
1330 pub fn num_cols(&self) -> Option<u32> {
1334 self.body_properties.as_ref().and_then(|b| b.num_cols)
1335 }
1336 pub fn set_num_cols(&mut self, count: u32) {
1338 let bp = self.ensure_body_properties();
1339 bp.num_cols = Some(count);
1340 }
1341 pub fn col_spacing(&self) -> Option<Emu> {
1343 self.body_properties.as_ref().and_then(|b| b.col_spacing)
1344 }
1345 pub fn set_col_spacing(&mut self, emu: Emu) {
1347 let bp = self.ensure_body_properties();
1348 bp.col_spacing = Some(emu);
1349 }
1350}
1351
1352#[derive(Clone, Debug, Default)]
1354pub struct BodyProperties {
1355 pub insets: Option<Inset>,
1357 pub vertical: Option<String>,
1359 pub rotation: Option<i32>,
1361 pub wrap: Option<TextWrapping>,
1363 pub sp_auto_fit: bool,
1365 pub norm_autofit: bool,
1367 pub anchor: Option<MsoAnchor>,
1369 pub anchor_ctr: bool,
1371 pub num_cols: Option<u32>,
1376 pub col_spacing: Option<Emu>,
1380}
1381
1382#[derive(Copy, Clone, Debug, Default)]
1387pub struct Inset {
1388 pub left: Emu,
1390 pub top: Emu,
1392 pub right: Emu,
1394 pub bottom: Emu,
1396}
1397
1398impl BodyProperties {
1399 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1401 let l_s = self.insets.as_ref().map(|i| i.left.value().to_string());
1403 let t_s = self.insets.as_ref().map(|i| i.top.value().to_string());
1404 let r_s = self.insets.as_ref().map(|i| i.right.value().to_string());
1405 let b_s = self.insets.as_ref().map(|i| i.bottom.value().to_string());
1406 let rot_s = self.rotation.map(|v| v.to_string());
1407 let wrap_s = self.wrap.map(|v| v.as_str());
1408 let anchor_s = self.anchor.map(|v| v.as_str());
1409 let numcol_s = self.num_cols.map(|v| v.to_string());
1410 let spccol_s = self.col_spacing.map(|v| v.value().to_string());
1411
1412 let mut attrs: Vec<(&str, &str)> = Vec::new();
1413 if let Some(s) = &l_s {
1414 attrs.push(("lIns", s));
1415 }
1416 if let Some(s) = &t_s {
1417 attrs.push(("tIns", s));
1418 }
1419 if let Some(s) = &r_s {
1420 attrs.push(("rIns", s));
1421 }
1422 if let Some(s) = &b_s {
1423 attrs.push(("bIns", s));
1424 }
1425 if let Some(v) = &self.vertical {
1426 attrs.push(("vert", v));
1427 }
1428 if let Some(s) = &rot_s {
1429 attrs.push(("rot", s));
1430 }
1431 if let Some(s) = &wrap_s {
1432 attrs.push(("wrap", s));
1433 }
1434 if let Some(s) = &anchor_s {
1435 attrs.push(("anchor", s));
1436 }
1437 if let Some(s) = &numcol_s {
1438 attrs.push(("numCol", s));
1439 }
1440 if let Some(s) = &spccol_s {
1441 attrs.push(("spcCol", s));
1442 }
1443 w.open_with("a:bodyPr", &attrs);
1444 if self.sp_auto_fit {
1446 w.empty("a:spAutoFit");
1447 } else if self.norm_autofit {
1448 w.empty("a:normAutofit");
1449 }
1450 w.close("a:bodyPr");
1451 }
1452
1453 pub fn auto_size(&self) -> Option<MsoAutoSize> {
1455 if self.sp_auto_fit {
1456 Some(MsoAutoSize::ShapeToFitText)
1457 } else if self.norm_autofit {
1458 Some(MsoAutoSize::TextToFitShape)
1459 } else {
1460 None
1461 }
1462 }
1463 pub fn set_auto_size(&mut self, v: MsoAutoSize) {
1465 match v {
1466 MsoAutoSize::None => {
1467 self.sp_auto_fit = false;
1468 self.norm_autofit = false;
1469 }
1470 MsoAutoSize::ShapeToFitText => {
1471 self.sp_auto_fit = true;
1472 self.norm_autofit = false;
1473 }
1474 MsoAutoSize::TextToFitShape => {
1475 self.sp_auto_fit = false;
1476 self.norm_autofit = true;
1477 }
1478 }
1479 }
1480}
1481
1482impl From<RGBColor> for Color {
1483 fn from(c: RGBColor) -> Self {
1484 Color::RGB(c)
1485 }
1486}
1487
1488use crate::oxml::color::ColorFormat;
1493
1494#[derive(Debug)]
1509pub struct Font<'a> {
1510 rpr: &'a mut RunProperties,
1512}
1513
1514impl<'a> Font<'a> {
1515 pub fn new(rpr: &'a mut RunProperties) -> Self {
1517 Font { rpr }
1518 }
1519 pub fn rpr(&self) -> &RunProperties {
1521 self.rpr
1522 }
1523 pub fn rpr_mut(&mut self) -> &mut RunProperties {
1525 self.rpr
1526 }
1527
1528 pub fn color(&mut self) -> ColorFormat<'_> {
1530 ColorFormat::new(&mut self.rpr.color)
1531 }
1532
1533 pub fn size(&self) -> Option<Pt> {
1535 self.rpr.size
1536 }
1537 pub fn set_size(&mut self, v: Pt) {
1539 self.rpr.size = Some(v);
1540 }
1541 pub fn clear_size(&mut self) {
1543 self.rpr.size = None;
1544 }
1545
1546 pub fn bold(&self) -> bool {
1548 self.rpr.bold
1549 }
1550 pub fn set_bold(&mut self, v: bool) {
1552 self.rpr.bold = v;
1553 }
1554
1555 pub fn italic(&self) -> bool {
1557 self.rpr.italic
1558 }
1559 pub fn set_italic(&mut self, v: bool) {
1561 self.rpr.italic = v;
1562 }
1563
1564 pub fn strike(&self) -> bool {
1566 self.rpr.strike
1567 }
1568 pub fn set_strike(&mut self, v: bool) {
1570 self.rpr.strike = v;
1571 }
1572
1573 pub fn double_strike(&self) -> bool {
1578 self.rpr.strike_dbl
1579 }
1580 pub fn set_double_strike(&mut self, v: bool) {
1582 self.rpr.strike_dbl = v;
1583 }
1584
1585 pub fn highlight(&self) -> Option<&Color> {
1590 self.rpr.highlight.as_ref()
1591 }
1592 pub fn set_highlight(&mut self, color: Option<Color>) {
1596 match color {
1597 None => self.rpr.highlight = None,
1598 Some(Color::None) => self.rpr.highlight = None,
1599 Some(c) => self.rpr.highlight = Some(c),
1600 }
1601 }
1602
1603 pub fn underline(&self) -> Option<Underline> {
1605 self.rpr.underline
1606 }
1607 pub fn set_underline(&mut self, v: Option<Underline>) {
1609 self.rpr.underline = v;
1610 }
1611
1612 pub fn name(&self) -> Option<&str> {
1614 self.rpr.latin_font.as_deref()
1615 }
1616 pub fn set_name(&mut self, n: impl Into<String>) {
1618 self.rpr.latin_font = Some(n.into());
1619 }
1620 pub fn clear_name(&mut self) {
1622 self.rpr.latin_font = None;
1623 }
1624
1625 pub fn eastasia_name(&self) -> Option<&str> {
1627 self.rpr.eastasia_font.as_deref()
1628 }
1629 pub fn set_eastasia_name(&mut self, n: impl Into<String>) {
1631 self.rpr.eastasia_font = Some(n.into());
1632 }
1633 pub fn clear_eastasia_name(&mut self) {
1638 self.rpr.eastasia_font = None;
1639 }
1640
1641 pub fn complex_script_name(&self) -> Option<&str> {
1643 self.rpr.cs_font.as_deref()
1644 }
1645 pub fn set_complex_script_name(&mut self, n: impl Into<String>) {
1647 self.rpr.cs_font = Some(n.into());
1648 }
1649 pub fn clear_complex_script_name(&mut self) {
1654 self.rpr.cs_font = None;
1655 }
1656
1657 pub fn baseline(&self) -> Option<i32> {
1659 self.rpr.baseline
1660 }
1661 pub fn set_baseline(&mut self, v: i32) {
1663 self.rpr.baseline = Some(v);
1664 }
1665
1666 pub fn spacing(&self) -> Option<i32> {
1668 self.rpr.spc
1669 }
1670 pub fn set_spacing(&mut self, v: i32) {
1672 self.rpr.spc = Some(v);
1673 }
1674
1675 pub fn hlink_click(&self) -> Option<&Hyperlink> {
1679 self.rpr.hlink_click.as_ref()
1680 }
1681 pub fn set_hlink_click(&mut self, hl: Hyperlink) {
1683 self.rpr.hlink_click = Some(hl);
1684 }
1685 pub fn clear_hlink_click(&mut self) {
1687 self.rpr.hlink_click = None;
1688 }
1689
1690 pub fn hlink_hover(&self) -> Option<&Hyperlink> {
1692 self.rpr.hlink_hover.as_ref()
1693 }
1694 pub fn set_hlink_hover(&mut self, hl: Hyperlink) {
1696 self.rpr.hlink_hover = Some(hl);
1697 }
1698 pub fn clear_hlink_hover(&mut self) {
1700 self.rpr.hlink_hover = None;
1701 }
1702
1703 pub fn set_hyperlink(&mut self, rid: impl Into<String>, tooltip: Option<&str>) {
1716 let mut hl = Hyperlink::new(rid);
1717 if let Some(t) = tooltip {
1718 hl.tooltip = Some(t.to_string());
1719 }
1720 self.rpr.hlink_click = Some(hl);
1721 }
1722
1723 pub fn set_slide_jump(&mut self) {
1727 self.rpr.hlink_click = Some(Hyperlink::new_slide_jump());
1728 }
1729}
1730
1731impl<'a> From<&'a mut RunProperties> for Font<'a> {
1732 fn from(r: &'a mut RunProperties) -> Self {
1733 Font::new(r)
1734 }
1735}
1736
1737#[derive(Debug)]
1756pub struct ParagraphFormat<'a> {
1757 ppr: &'a mut ParagraphProperties,
1759}
1760
1761impl<'a> ParagraphFormat<'a> {
1762 pub fn new(ppr: &'a mut ParagraphProperties) -> Self {
1764 ParagraphFormat { ppr }
1765 }
1766 pub fn ppr(&self) -> &ParagraphProperties {
1768 self.ppr
1769 }
1770 pub fn ppr_mut(&mut self) -> &mut ParagraphProperties {
1772 self.ppr
1773 }
1774
1775 pub fn alignment(&self) -> Option<Alignment> {
1777 self.ppr.alignment
1778 }
1779 pub fn set_alignment(&mut self, v: Alignment) {
1781 self.ppr.alignment = Some(v);
1782 }
1783 pub fn clear_alignment(&mut self) {
1785 self.ppr.alignment = None;
1786 }
1787
1788 pub fn level(&self) -> u8 {
1790 self.ppr.level
1791 }
1792 pub fn set_level(&mut self, lvl: u8) {
1794 self.ppr.level = lvl;
1795 }
1796
1797 pub fn line_spacing(&self) -> Option<Pt> {
1799 self.ppr.line_spacing.map(|emu| Pt(emu as f64 / 12_700.0))
1800 }
1801 pub fn set_line_spacing(&mut self, v: Pt) {
1803 let emu = (v.value() * 12_700.0) as i32;
1804 self.ppr.line_spacing = Some(emu);
1805 self.ppr.line_spacing_pct = None;
1806 }
1807 pub fn line_spacing_pct(&self) -> Option<f32> {
1809 self.ppr.line_spacing_pct.map(|v| v as f32 / 1000.0)
1810 }
1811 pub fn set_line_spacing_pct(&mut self, v: f32) {
1813 self.ppr.line_spacing_pct = Some((v * 1000.0) as i32);
1814 self.ppr.line_spacing = None;
1815 }
1816 pub fn clear_line_spacing(&mut self) {
1818 self.ppr.line_spacing = None;
1819 self.ppr.line_spacing_pct = None;
1820 }
1821
1822 pub fn space_before(&self) -> Option<Emu> {
1824 self.ppr.space_before
1825 }
1826 pub fn set_space_before(&mut self, emu: Emu) {
1828 self.ppr.space_before = Some(emu);
1829 }
1830 pub fn space_after(&self) -> Option<Emu> {
1832 self.ppr.space_after
1833 }
1834 pub fn set_space_after(&mut self, emu: Emu) {
1836 self.ppr.space_after = Some(emu);
1837 }
1838
1839 pub fn set_indent(
1841 &mut self,
1842 left: Option<Emu>,
1843 right: Option<Emu>,
1844 first_line: Option<Emu>,
1845 hanging: Option<i32>,
1846 ) {
1847 self.ppr.indent = Indent {
1848 left,
1849 right,
1850 first_line,
1851 hanging,
1852 };
1853 }
1854
1855 pub fn indent(&self) -> Indent {
1857 self.ppr.indent
1858 }
1859
1860 pub fn bullet_style(&self) -> Option<&BulletStyle> {
1866 self.ppr.bullet_style.as_ref()
1867 }
1868
1869 pub fn set_bullet_char(&mut self, ch: impl Into<String>) {
1874 self.ppr.bullet = true;
1875 self.ppr.bullet_style = Some(BulletStyle::Char { char: ch.into() });
1876 }
1877
1878 pub fn set_bullet_numbered(&mut self, auto_num_type: impl Into<String>, start_at: Option<u32>) {
1887 self.ppr.bullet = true;
1888 self.ppr.bullet_style = Some(BulletStyle::AutoNum {
1889 auto_num_type: auto_num_type.into(),
1890 start_at,
1891 });
1892 }
1893
1894 pub fn clear_bullet(&mut self) {
1896 self.ppr.bullet = false;
1897 self.ppr.bullet_style = Some(BulletStyle::None);
1898 }
1899
1900 pub fn has_bullet(&self) -> bool {
1902 self.ppr.bullet
1903 && self
1904 .ppr
1905 .bullet_style
1906 .as_ref()
1907 .map(|bs| !matches!(bs, BulletStyle::None))
1908 .unwrap_or(false)
1909 }
1910
1911 pub fn tab_stops(&self) -> &[TabStop] {
1917 &self.ppr.tab_stops
1918 }
1919
1920 pub fn add_tab_stop(&mut self, pos: Emu, alignment: TabAlignment) {
1928 self.ppr.tab_stops.push(TabStop { pos, alignment });
1929 }
1930
1931 pub fn clear_tab_stops(&mut self) {
1933 self.ppr.tab_stops.clear();
1934 }
1935}
1936
1937impl<'a> From<&'a mut ParagraphProperties> for ParagraphFormat<'a> {
1938 fn from(p: &'a mut ParagraphProperties) -> Self {
1939 ParagraphFormat::new(p)
1940 }
1941}
1942
1943#[derive(Debug)]
1966pub struct TextFrame<'a> {
1967 body: &'a mut TextBody,
1969}
1970
1971impl<'a> TextFrame<'a> {
1972 pub fn new(body: &'a mut TextBody) -> Self {
1974 TextFrame { body }
1975 }
1976
1977 pub fn body(&self) -> &TextBody {
1979 self.body
1980 }
1981 pub fn body_mut(&mut self) -> &mut TextBody {
1983 self.body
1984 }
1985
1986 pub fn len(&self) -> usize {
1990 self.body.paragraphs.len()
1991 }
1992 pub fn is_empty(&self) -> bool {
1994 self.body.paragraphs.is_empty()
1995 }
1996 pub fn paragraphs(&self) -> std::slice::Iter<'_, Paragraph> {
1998 self.body.paragraphs.iter()
1999 }
2000 pub fn paragraphs_mut(&mut self) -> std::slice::IterMut<'_, Paragraph> {
2002 self.body.paragraphs.iter_mut()
2003 }
2004 pub fn paragraph(&self, idx: usize) -> Option<&Paragraph> {
2006 self.body.paragraphs.get(idx)
2007 }
2008 pub fn paragraph_mut(&mut self, idx: usize) -> Option<&mut Paragraph> {
2010 self.body.paragraphs.get_mut(idx)
2011 }
2012 pub fn first_paragraph(&self) -> Option<&Paragraph> {
2014 self.body.paragraphs.first()
2015 }
2016 pub fn first_paragraph_mut(&mut self) -> Option<&mut Paragraph> {
2018 self.body.paragraphs.first_mut()
2019 }
2020
2021 pub fn add_paragraph(&mut self) -> &mut Paragraph {
2023 self.body.add_paragraph()
2024 }
2025
2026 pub fn add_paragraph_with_text(&mut self, text: &str) -> &mut Paragraph {
2028 self.body.add_paragraph_with_text(text)
2029 }
2030
2031 pub fn remove_paragraph(&mut self, idx: usize) -> Option<Paragraph> {
2033 self.body.remove_paragraph(idx)
2034 }
2035
2036 pub fn clear(&mut self) {
2038 self.body.clear();
2039 }
2040
2041 pub fn text_getter(&self) -> String {
2045 self.body.text()
2046 }
2047
2048 pub fn set_text(&mut self, text: &str) {
2050 self.body.set_text(text);
2051 }
2052
2053 pub fn auto_size(&self) -> MsoAutoSize {
2057 self.body.auto_size()
2058 }
2059 pub fn set_auto_size(&mut self, v: MsoAutoSize) {
2061 self.body.set_auto_size(v);
2062 }
2063
2064 pub fn vertical_anchor(&self) -> Option<MsoAnchor> {
2066 self.body.vertical_anchor()
2067 }
2068 pub fn set_vertical_anchor(&mut self, v: MsoAnchor) {
2070 self.body.set_vertical_anchor(v);
2071 }
2072
2073 pub fn word_wrap(&self) -> Option<bool> {
2075 self.body.word_wrap()
2076 }
2077 pub fn set_word_wrap(&mut self, v: bool) {
2079 self.body.set_word_wrap(v);
2080 }
2081
2082 pub fn margin_left(&self) -> Option<Emu> {
2084 self.body.margin_left()
2085 }
2086 pub fn margin_right(&self) -> Option<Emu> {
2088 self.body.margin_right()
2089 }
2090 pub fn margin_top(&self) -> Option<Emu> {
2092 self.body.margin_top()
2093 }
2094 pub fn margin_bottom(&self) -> Option<Emu> {
2096 self.body.margin_bottom()
2097 }
2098 pub fn set_margins(&mut self, l: Emu, t: Emu, r: Emu, b: Emu) {
2100 self.body.set_margins(l, t, r, b);
2101 }
2102 pub fn set_margin_left(&mut self, emu: Emu) {
2104 self.body.set_margin_left(emu);
2105 }
2106 pub fn set_margin_right(&mut self, emu: Emu) {
2108 self.body.set_margin_right(emu);
2109 }
2110 pub fn set_margin_top(&mut self, emu: Emu) {
2112 self.body.set_margin_top(emu);
2113 }
2114 pub fn set_margin_bottom(&mut self, emu: Emu) {
2116 self.body.set_margin_bottom(emu);
2117 }
2118
2119 pub fn num_cols(&self) -> Option<u32> {
2125 self.body.num_cols()
2126 }
2127
2128 pub fn set_num_cols(&mut self, count: u32) {
2130 self.body.set_num_cols(count);
2131 }
2132
2133 pub fn col_spacing(&self) -> Option<Emu> {
2135 self.body.col_spacing()
2136 }
2137
2138 pub fn set_col_spacing(&mut self, emu: Emu) {
2140 self.body.set_col_spacing(emu);
2141 }
2142
2143 pub fn set_columns(&mut self, count: u32, spacing: Option<Emu>) {
2148 self.body.set_num_cols(count);
2149 if let Some(s) = spacing {
2150 self.body.set_col_spacing(s);
2151 }
2152 }
2153}
2154
2155impl<'a> From<&'a mut TextBody> for TextFrame<'a> {
2156 fn from(b: &'a mut TextBody) -> Self {
2157 TextFrame::new(b)
2158 }
2159}
2160
2161#[cfg(test)]
2162mod tests {
2163 use super::*;
2164
2165 #[test]
2166 fn write_paragraph_simple() {
2167 let mut p = Paragraph::new();
2168 let mut r = Run::new("Hello");
2169 r.properties.size = Some(Pt(24.0));
2170 r.properties.bold = true;
2171 r.properties.color = RGBColor(0xFF, 0, 0).into();
2172 p.runs.push(r);
2173 let mut w = super::super::writer::XmlWriter::new();
2174 p.write_xml(&mut w);
2175 let s = w.into_string();
2176 assert!(s.contains("Hello"));
2177 assert!(s.contains("sz=\"2400\""));
2178 assert!(s.contains("b=\"1\""));
2179 assert!(s.contains("a:srgbClr"));
2180 }
2181
2182 #[test]
2184 fn textframe_view_mirrors_body() {
2185 let mut tb = TextBody::new();
2186 {
2187 let mut tf = TextFrame::new(&mut tb);
2188 let p = tf.add_paragraph();
2190 p.add_run_with_text("first").set_bold(true);
2191 tf.add_paragraph_with_text("second\nthird");
2192 tf.set_word_wrap(false);
2193 tf.set_margins(Emu(91440), Emu(45720), Emu(91440), Emu(45720));
2194 }
2195 assert_eq!(tb.paragraphs.len(), 3);
2197 assert_eq!(tb.paragraphs[0].runs[0].text, "first");
2198 assert!(tb.paragraphs[0].runs[0].properties.bold);
2199 assert_eq!(tb.paragraphs[1].runs[0].text, "second");
2200 assert_eq!(tb.paragraphs[2].runs[0].text, "third");
2201 assert_eq!(tb.word_wrap(), Some(false));
2203 }
2204
2205 #[test]
2207 fn paragraph_format_line_spacing_mutex() {
2208 let mut p = Paragraph::new();
2209 p.set_line_spacing(Pt(20.0));
2210 assert!(p.line_spacing().is_some());
2211 assert!(p.line_spacing_pct().is_none());
2212 p.set_line_spacing_pct(1.5);
2214 assert!(p.line_spacing().is_none());
2215 assert_eq!(p.line_spacing_pct(), Some(1.5));
2216 let mut p2 = Paragraph::new();
2218 {
2219 let mut pf = ParagraphFormat::new(&mut p2.properties);
2220 pf.set_line_spacing(Pt(15.0));
2221 }
2222 assert_eq!(p2.line_spacing(), Some(Pt(15.0)));
2223 {
2224 let mut pf = ParagraphFormat::new(&mut p2.properties);
2225 pf.set_line_spacing_pct(2.0);
2226 }
2227 assert!(p2.line_spacing().is_none());
2228 assert_eq!(p2.line_spacing_pct(), Some(2.0));
2229 }
2230
2231 #[test]
2235 fn font_strikethrough_api() {
2236 let mut r = Run::new("text");
2237 {
2238 let mut f = Font::new(&mut r.properties);
2239 f.set_strike(true);
2240 }
2241 assert!(r.properties.strike);
2242 assert!(!r.properties.strike_dbl);
2243
2244 {
2246 let mut f = Font::new(&mut r.properties);
2247 f.set_double_strike(true);
2248 }
2249 assert!(r.properties.strike_dbl);
2250
2251 let f = Font::new(&mut r.properties);
2253 assert!(f.strike());
2254 assert!(f.double_strike());
2255 }
2256
2257 #[test]
2261 fn font_highlight_api() {
2262 let mut r = Run::new("text");
2263 assert!(r.properties.highlight.is_none());
2265
2266 {
2268 let mut f = Font::new(&mut r.properties);
2269 f.set_highlight(Some(Color::RGB(RGBColor(0xFF, 0xFF, 0x00))));
2270 }
2271 assert!(r.properties.highlight.is_some());
2272
2273 let f = Font::new(&mut r.properties);
2275 let hl = f.highlight().expect("应有高亮色");
2276 assert!(matches!(hl, Color::RGB(c) if c.0 == 0xFF && c.1 == 0xFF && c.2 == 0x00));
2277
2278 {
2280 let mut f = Font::new(&mut r.properties);
2281 f.set_highlight(None);
2282 }
2283 assert!(r.properties.highlight.is_none());
2284
2285 {
2287 let mut f = Font::new(&mut r.properties);
2288 f.set_highlight(Some(Color::RGB(RGBColor(0xFF, 0x00, 0x00))));
2289 f.set_highlight(Some(Color::None));
2290 }
2291 assert!(r.properties.highlight.is_none());
2292 }
2293
2294 #[test]
2298 fn text_body_multi_column_api() {
2299 let mut tb = TextBody::new();
2300 assert!(tb.num_cols().is_none());
2302 assert!(tb.col_spacing().is_none());
2303
2304 tb.set_num_cols(3);
2306 tb.set_col_spacing(Emu(91440));
2307 assert_eq!(tb.num_cols(), Some(3));
2308 assert_eq!(tb.col_spacing(), Some(Emu(91440)));
2309
2310 let mut w = super::super::writer::XmlWriter::new();
2312 tb.body_properties
2313 .as_ref()
2314 .expect("应有 body_properties")
2315 .write_xml(&mut w);
2316 let s = w.into_string();
2317 assert!(s.contains("numCol=\"3\""), "应输出 numCol=\"3\",实际: {s}");
2318 assert!(
2319 s.contains("spcCol=\"91440\""),
2320 "应输出 spcCol=\"91440\",实际: {s}"
2321 );
2322 }
2323
2324 #[test]
2328 fn text_frame_set_columns() {
2329 let mut tb = TextBody::new();
2330 {
2331 let mut tf = TextFrame::new(&mut tb);
2332 tf.set_columns(2, Some(Emu(45720)));
2334 }
2335 assert_eq!(tb.num_cols(), Some(2));
2336 assert_eq!(tb.col_spacing(), Some(Emu(45720)));
2337
2338 {
2340 let mut tf = TextFrame::new(&mut tb);
2341 tf.set_columns(4, None);
2342 }
2343 assert_eq!(tb.num_cols(), Some(4));
2344 assert_eq!(tb.col_spacing(), Some(Emu(45720)));
2346
2347 {
2349 let tf = TextFrame::new(&mut tb);
2350 assert_eq!(tf.num_cols(), Some(4));
2351 assert_eq!(tf.col_spacing(), Some(Emu(45720)));
2352 }
2353 }
2354
2355 #[test]
2359 fn paragraph_format_bullet_char() {
2360 let mut ppr = ParagraphProperties::default();
2361 {
2362 let mut pf = ParagraphFormat::new(&mut ppr);
2363 pf.set_bullet_char("•");
2364 }
2365 assert!(ppr.bullet, "bullet 应为 true");
2366 assert!(pf_has_bullet(&ppr), "has_bullet 应为 true");
2367 let mut w = super::super::writer::XmlWriter::new();
2369 ppr.write_xml(&mut w);
2370 let s = w.into_string();
2371 assert!(s.contains("buChar"), "应输出 buChar,实际: {s}");
2372 assert!(s.contains("char=\"•\""), "应包含 char=\"•\",实际: {s}");
2373 }
2374
2375 #[test]
2379 fn paragraph_format_bullet_numbered() {
2380 let mut ppr = ParagraphProperties::default();
2381 {
2382 let mut pf = ParagraphFormat::new(&mut ppr);
2383 pf.set_bullet_numbered("arabicPeriod", Some(3));
2384 }
2385 assert!(ppr.bullet);
2386 match &ppr.bullet_style {
2387 Some(BulletStyle::AutoNum {
2388 auto_num_type,
2389 start_at,
2390 }) => {
2391 assert_eq!(auto_num_type, "arabicPeriod");
2392 assert_eq!(*start_at, Some(3));
2393 }
2394 other => panic!("期望 AutoNum,实际: {other:?}"),
2395 }
2396 let mut w = super::super::writer::XmlWriter::new();
2398 ppr.write_xml(&mut w);
2399 let s = w.into_string();
2400 assert!(s.contains("buAutoNum"), "应输出 buAutoNum,实际: {s}");
2401 assert!(
2402 s.contains("type=\"arabicPeriod\""),
2403 "应包含 type,实际: {s}"
2404 );
2405 assert!(s.contains("startAt=\"3\""), "应包含 startAt,实际: {s}");
2406 }
2407
2408 #[test]
2412 fn paragraph_format_clear_bullet() {
2413 let mut ppr = ParagraphProperties::default();
2414 {
2415 let mut pf = ParagraphFormat::new(&mut ppr);
2416 pf.set_bullet_char("•");
2417 }
2418 assert!(pf_has_bullet(&ppr));
2419 {
2420 let mut pf = ParagraphFormat::new(&mut ppr);
2421 pf.clear_bullet();
2422 }
2423 assert!(!ppr.bullet, "bullet 应为 false");
2424 assert!(!pf_has_bullet(&ppr), "has_bullet 应为 false");
2425 let mut w = super::super::writer::XmlWriter::new();
2427 ppr.write_xml(&mut w);
2428 let s = w.into_string();
2429 assert!(s.contains("buNone"), "应输出 buNone,实际: {s}");
2430 }
2431
2432 fn pf_has_bullet(ppr: &ParagraphProperties) -> bool {
2434 ppr.bullet
2435 && ppr
2436 .bullet_style
2437 .as_ref()
2438 .map(|bs| !matches!(bs, BulletStyle::None))
2439 .unwrap_or(false)
2440 }
2441
2442 #[test]
2446 fn paragraph_format_tab_stops() {
2447 let mut ppr = ParagraphProperties::default();
2448 {
2449 let mut pf = ParagraphFormat::new(&mut ppr);
2450 pf.add_tab_stop(Emu(914400), TabAlignment::Left);
2451 pf.add_tab_stop(Emu(1828800), TabAlignment::Right);
2452 pf.add_tab_stop(Emu(2743200), TabAlignment::Center);
2453 }
2454 assert_eq!(ppr.tab_stops.len(), 3);
2455 assert_eq!(ppr.tab_stops[0].pos.value(), 914400);
2456 assert_eq!(ppr.tab_stops[0].alignment, TabAlignment::Left);
2457 assert_eq!(ppr.tab_stops[1].pos.value(), 1828800);
2458 assert_eq!(ppr.tab_stops[1].alignment, TabAlignment::Right);
2459 assert_eq!(ppr.tab_stops[2].pos.value(), 2743200);
2460 assert_eq!(ppr.tab_stops[2].alignment, TabAlignment::Center);
2461
2462 let mut w = super::super::writer::XmlWriter::new();
2464 ppr.write_xml(&mut w);
2465 let s = w.into_string();
2466 assert!(s.contains("tabLst"), "应输出 tabLst,实际: {s}");
2467 assert!(s.contains("pos=\"914400\""), "应包含 pos=914400,实际: {s}");
2468 assert!(s.contains("algn=\"l\""), "应包含 algn=l,实际: {s}");
2469 assert!(s.contains("algn=\"r\""), "应包含 algn=r,实际: {s}");
2470 assert!(s.contains("algn=\"ctr\""), "应包含 algn=ctr,实际: {s}");
2471
2472 {
2474 let mut pf = ParagraphFormat::new(&mut ppr);
2475 pf.clear_tab_stops();
2476 }
2477 assert!(ppr.tab_stops.is_empty());
2478 }
2479
2480 #[test]
2484 fn paragraph_field_serialization() {
2485 let mut p = Paragraph::new();
2486 p.add_field(FieldType::SlideNumber, "1");
2487 p.add_field(FieldType::DateTime, "1/1/2024");
2488
2489 assert_eq!(p.fields.len(), 2);
2490 assert_eq!(p.fields[0].field_type, FieldType::SlideNumber);
2491 assert_eq!(p.fields[0].text, "1");
2492 assert_eq!(p.fields[1].field_type, FieldType::DateTime);
2493 assert_eq!(p.fields[1].text, "1/1/2024");
2494
2495 let mut w = super::super::writer::XmlWriter::new();
2497 p.write_xml(&mut w);
2498 let s = w.into_string();
2499 assert!(s.contains("a:fld"), "应输出 a:fld,实际: {s}");
2500 assert!(
2501 s.contains("type=\"slidenum\""),
2502 "应包含 type=slidenum,实际: {s}"
2503 );
2504 assert!(
2505 s.contains("type=\"datetime\""),
2506 "应包含 type=datetime,实际: {s}"
2507 );
2508 assert!(s.contains(">1<"), "应包含文本 1,实际: {s}");
2509 assert!(s.contains(">1/1/2024<"), "应包含文本 1/1/2024,实际: {s}");
2510 }
2511
2512 #[test]
2516 fn field_type_conversion() {
2517 assert_eq!(FieldType::SlideNumber.as_str(), "slidenum");
2519 assert_eq!(FieldType::DateTime.as_str(), "datetime");
2520 assert_eq!(FieldType::DateTime1.as_str(), "datetime1");
2521 assert_eq!(FieldType::Footer.as_str(), "footer");
2522 assert_eq!(FieldType::Custom("custom1".to_string()).as_str(), "custom1");
2523
2524 assert_eq!(
2526 FieldType::from_str_value("slidenum"),
2527 FieldType::SlideNumber
2528 );
2529 assert_eq!(FieldType::from_str_value("datetime"), FieldType::DateTime);
2530 assert_eq!(FieldType::from_str_value("datetime1"), FieldType::DateTime1);
2531 assert_eq!(FieldType::from_str_value("footer"), FieldType::Footer);
2532 assert_eq!(
2533 FieldType::from_str_value("unknown"),
2534 FieldType::Custom("unknown".to_string())
2535 );
2536 }
2537
2538 #[test]
2542 fn font_hyperlink_api() {
2543 let mut run = Run::new("链接文本");
2544 run.font().set_hyperlink("rId3", Some("点击访问"));
2546 let hl = run
2547 .properties
2548 .hlink_click
2549 .as_ref()
2550 .expect("hlink_click 应存在");
2551 assert_eq!(hl.rid.as_deref(), Some("rId3"));
2552 assert_eq!(hl.tooltip.as_deref(), Some("点击访问"));
2553
2554 run.font().set_slide_jump();
2556 let hl = run
2557 .properties
2558 .hlink_click
2559 .as_ref()
2560 .expect("hlink_click 应存在");
2561 assert_eq!(hl.action.as_deref(), Some("ppaction://hlinksldjump"));
2562
2563 run.font().clear_hlink_click();
2565 assert!(run.properties.hlink_click.is_none());
2566 }
2567
2568 #[test]
2572 fn hyperlink_serialization_roundtrip() {
2573 let mut run = Run::new("点击这里");
2574 run.font().set_hyperlink("rId7", Some("提示文字"));
2575
2576 let mut w = super::super::writer::XmlWriter::new();
2578 run.write_xml(&mut w);
2579 let s = w.into_string();
2580 assert!(s.contains("a:hlinkClick"), "应输出 a:hlinkClick,实际: {s}");
2581 assert!(s.contains("r:id=\"rId7\""), "应包含 r:id=rId7,实际: {s}");
2582 assert!(
2583 s.contains("tooltip=\"提示文字\""),
2584 "应包含 tooltip,实际: {s}"
2585 );
2586 }
2587
2588 #[test]
2594 fn end_para_rpr_set_and_serialize() {
2595 let mut p = Paragraph::new();
2596 let rpr = RunProperties {
2597 size: Some(Pt(24.0)),
2598 latin_font: Some("Calibri".to_string()),
2599 lang: Some("en-US".to_string()),
2600 ..Default::default()
2601 };
2602 p.set_end_para_rpr(rpr);
2603
2604 assert!(p.end_para_rpr().is_some());
2606 let got = p.end_para_rpr().unwrap();
2607 assert_eq!(got.size, Some(Pt(24.0)));
2608 assert_eq!(got.latin_font.as_deref(), Some("Calibri"));
2609 assert_eq!(got.lang.as_deref(), Some("en-US"));
2610
2611 let mut w = super::super::writer::XmlWriter::new();
2613 p.write_xml(&mut w);
2614 let s = w.into_string();
2615 assert!(s.contains("a:endParaRPr"), "应输出 a:endParaRPr,实际: {s}");
2616 assert!(s.contains("sz=\"2400\""), "应包含 sz=2400,实际: {s}");
2617 assert!(s.contains("lang=\"en-US\""), "应包含 lang=en-US,实际: {s}");
2618 assert!(s.contains("a:latin"), "应包含 a:latin,实际: {s}");
2619 assert!(
2620 s.contains("typeface=\"Calibri\""),
2621 "应包含 typeface=Calibri,实际: {s}"
2622 );
2623 }
2624
2625 #[test]
2627 fn end_para_rpr_clear() {
2628 let mut p = Paragraph::new();
2629 let rpr = RunProperties {
2630 size: Some(Pt(18.0)),
2631 ..Default::default()
2632 };
2633 p.set_end_para_rpr(rpr);
2634 assert!(p.end_para_rpr().is_some());
2635
2636 p.clear_end_para_rpr();
2637 assert!(p.end_para_rpr().is_none());
2638
2639 let mut w = super::super::writer::XmlWriter::new();
2641 p.write_xml(&mut w);
2642 let s = w.into_string();
2643 assert!(
2644 !s.contains("a:endParaRPr"),
2645 "不应输出 a:endParaRPr,实际: {s}"
2646 );
2647 }
2648
2649 #[test]
2651 fn end_para_rpr_mut_modify() {
2652 let mut p = Paragraph::new();
2653 let rpr = RunProperties::default();
2654 p.set_end_para_rpr(rpr);
2655
2656 if let Some(rpr) = p.end_para_rpr_mut() {
2658 rpr.size = Some(Pt(32.0));
2659 rpr.bold = true;
2660 }
2661
2662 let got = p.end_para_rpr().expect("end_para_rpr 应存在");
2663 assert_eq!(got.size, Some(Pt(32.0)));
2664 assert!(got.bold);
2665 }
2666
2667 #[test]
2672 fn end_para_rpr_roundtrip_with_children() {
2673 let mut p = Paragraph::new();
2675 p.add_run_with_text("hello");
2676 let rpr = RunProperties {
2677 size: Some(Pt(20.0)),
2678 bold: true,
2679 latin_font: Some("Arial".to_string()),
2680 lang: Some("en-US".to_string()),
2681 ..Default::default()
2682 };
2683 p.set_end_para_rpr(rpr);
2684
2685 let mut w = super::super::writer::XmlWriter::new();
2687 p.write_xml(&mut w);
2688 let xml = w.into_string();
2689
2690 let parsed = crate::oxml::parse_sld::parse_paragraph(&xml).expect("解析应成功");
2692
2693 let got = parsed.end_properties.expect("end_properties 应存在");
2695 assert_eq!(got.size, Some(Pt(20.0)), "size 应为 20pt");
2696 assert!(got.bold, "bold 应为 true");
2697 assert_eq!(
2698 got.latin_font.as_deref(),
2699 Some("Arial"),
2700 "latin_font 应为 Arial"
2701 );
2702 assert_eq!(got.lang.as_deref(), Some("en-US"), "lang 应为 en-US");
2703 }
2704
2705 #[test]
2709 fn end_para_rpr_self_closing_parse() {
2710 let xml = r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
2711<a:r><a:t>text</a:t></a:r>
2712<a:endParaRPr lang="en-US" sz="1800"/>
2713</a:p>"#;
2714 let p = crate::oxml::parse_sld::parse_paragraph(xml).expect("解析应成功");
2715 let got = p.end_properties.expect("end_properties 应存在");
2716 assert_eq!(got.size, Some(Pt(18.0)));
2717 assert_eq!(got.lang.as_deref(), Some("en-US"));
2718 }
2719
2720 #[test]
2722 fn end_para_rpr_with_solid_fill() {
2723 let xml = r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
2724<a:endParaRPr lang="en-US" sz="2400"><a:solidFill><a:srgbClr val="FF0000"/></a:solidFill></a:endParaRPr>
2725</a:p>"#;
2726 let p = crate::oxml::parse_sld::parse_paragraph(xml).expect("解析应成功");
2727 let got = p.end_properties.expect("end_properties 应存在");
2728 assert_eq!(got.size, Some(Pt(24.0)));
2729 assert_eq!(got.lang.as_deref(), Some("en-US"));
2730 assert!(
2732 matches!(got.color, crate::oxml::color::Color::RGB(c) if c.0 == 0xFF && c.1 == 0x00 && c.2 == 0x00)
2733 );
2734 }
2735
2736 #[test]
2740 fn run_eastasia_name_setter_and_getter() {
2741 let mut r = Run::new("你好");
2742 assert!(r.eastasia_name().is_none());
2743 r.set_eastasia_name("宋体");
2744 assert_eq!(r.eastasia_name(), Some("宋体"));
2745 assert_eq!(r.properties.eastasia_font.as_deref(), Some("宋体"));
2746 }
2747
2748 #[test]
2750 fn run_complex_script_name_setter_and_getter() {
2751 let mut r = Run::new("مرحبا");
2752 assert!(r.complex_script_name().is_none());
2753 r.set_complex_script_name("Traditional Arabic");
2754 assert_eq!(r.complex_script_name(), Some("Traditional Arabic"));
2755 assert_eq!(r.properties.cs_font.as_deref(), Some("Traditional Arabic"));
2756 }
2757
2758 #[test]
2760 fn run_three_fonts_independent() {
2761 let mut r = Run::new("Hello 你好 مرحبا");
2762 r.set_font_name("Arial");
2763 r.set_eastasia_name("Microsoft YaHei");
2764 r.set_complex_script_name("Tahoma");
2765 assert_eq!(r.font_name(), Some("Arial"));
2766 assert_eq!(r.eastasia_name(), Some("Microsoft YaHei"));
2767 assert_eq!(r.complex_script_name(), Some("Tahoma"));
2768 assert_eq!(r.properties.latin_font.as_deref(), Some("Arial"));
2770 assert_eq!(
2771 r.properties.eastasia_font.as_deref(),
2772 Some("Microsoft YaHei")
2773 );
2774 assert_eq!(r.properties.cs_font.as_deref(), Some("Tahoma"));
2775 }
2776
2777 #[test]
2779 fn font_clear_eastasia_name() {
2780 let mut r = Run::new("文本");
2781 r.set_eastasia_name("宋体");
2782 assert_eq!(r.eastasia_name(), Some("宋体"));
2783 {
2785 let mut f = Font::new(&mut r.properties);
2786 f.clear_eastasia_name();
2787 }
2788 assert!(r.eastasia_name().is_none());
2789 assert!(r.properties.eastasia_font.is_none());
2790 }
2791
2792 #[test]
2794 fn font_clear_complex_script_name() {
2795 let mut r = Run::new("text");
2796 r.set_complex_script_name("Arial");
2797 assert_eq!(r.complex_script_name(), Some("Arial"));
2798 {
2800 let mut f = Font::new(&mut r.properties);
2801 f.clear_complex_script_name();
2802 }
2803 assert!(r.complex_script_name().is_none());
2804 assert!(r.properties.cs_font.is_none());
2805 }
2806
2807 #[test]
2809 fn font_view_eastasia_cs_consistent_with_run() {
2810 let mut r = Run::new("混合文本");
2811 r.set_eastasia_name("黑体");
2812 r.set_complex_script_name("Tahoma");
2813 let run_ea = r.eastasia_name().map(|s| s.to_string());
2815 let run_cs = r.complex_script_name().map(|s| s.to_string());
2816 let f = Font::new(&mut r.properties);
2818 assert_eq!(f.eastasia_name(), run_ea.as_deref());
2819 assert_eq!(f.complex_script_name(), run_cs.as_deref());
2820 }
2821
2822 #[test]
2824 fn eastasia_font_serialization_order() {
2825 let mut r = Run::new("你好");
2826 r.set_font_name("Arial");
2827 r.set_eastasia_name("宋体");
2828 r.set_complex_script_name("Tahoma");
2829 let mut p = Paragraph::new();
2830 p.runs.push(r);
2831 let mut w = crate::oxml::writer::XmlWriter::new();
2832 p.write_xml(&mut w);
2833 let xml = w.into_string();
2834 assert!(
2836 xml.contains(r#"<a:latin typeface="Arial"/>"#),
2837 "xml: {}",
2838 xml
2839 );
2840 assert!(xml.contains(r#"<a:ea typeface="宋体"/>"#), "xml: {}", xml);
2841 assert!(xml.contains(r#"<a:cs typeface="Tahoma"/>"#), "xml: {}", xml);
2842 let pos_latin = xml.find("<a:latin").expect("latin should exist");
2844 let pos_ea = xml.find("<a:ea").expect("ea should exist");
2845 let pos_cs = xml.find("<a:cs").expect("cs should exist");
2846 assert!(pos_latin < pos_ea, "latin must come before ea: {}", xml);
2847 assert!(pos_ea < pos_cs, "ea must come before cs: {}", xml);
2848 }
2849}