1use crate::parsers::{FromXml, ParseError};
29use crate::types;
30use quick_xml::Reader;
31use quick_xml::events::Event;
32use std::io::Cursor;
33
34#[cfg_attr(
43 not(any(feature = "wml-styling", feature = "wml-layout")),
44 allow(dead_code)
45)]
46fn is_on(field: &Option<Box<types::OnOffElement>>) -> bool {
47 match field {
48 None => false,
49 Some(ct) => match &ct.value {
50 None => true, Some(v) => matches!(v.as_str(), "1" | "true" | "on"),
52 },
53 }
54}
55
56#[cfg_attr(not(feature = "wml-styling"), allow(dead_code))]
58fn check_toggle(field: &Option<Box<types::OnOffElement>>) -> Option<bool> {
59 field.as_ref().map(|ct| match &ct.value {
60 None => true,
61 Some(v) => matches!(v.as_str(), "1" | "true" | "on"),
62 })
63}
64
65#[cfg_attr(not(feature = "wml-styling"), allow(dead_code))]
67fn parse_half_points(s: &str) -> Option<u32> {
68 s.parse::<u32>().ok()
69}
70
71#[cfg(feature = "wml-styling")]
73fn parse_twips(s: &str) -> Option<i64> {
74 s.parse::<i64>().ok()
75}
76
77pub trait DocumentExt {
83 fn body(&self) -> Option<&types::Body>;
85}
86
87impl DocumentExt for types::Document {
88 fn body(&self) -> Option<&types::Body> {
89 self.body.as_deref()
90 }
91}
92
93#[derive(Debug, Clone, PartialEq)]
102pub struct TocEntry {
103 pub level: u8,
105 pub text: String,
107 pub page: Option<u32>,
110 pub bookmark: Option<String>,
113}
114
115#[derive(Debug, Clone, PartialEq)]
119pub struct TableOfContents {
120 pub entries: Vec<TocEntry>,
122}
123
124pub trait BodyExt {
130 fn paragraphs(&self) -> Vec<&types::Paragraph>;
132
133 fn tables(&self) -> Vec<&types::Table>;
135
136 fn text(&self) -> String;
138
139 #[cfg(feature = "wml-layout")]
141 fn section_properties(&self) -> Option<&types::SectionProperties>;
142
143 #[cfg(feature = "wml-styling")]
154 fn table_of_contents(&self) -> Vec<TableOfContents>;
155
156 #[cfg(feature = "wml-settings")]
163 fn form_fields(&self) -> Vec<FormField>;
164}
165
166impl BodyExt for types::Body {
167 fn paragraphs(&self) -> Vec<&types::Paragraph> {
168 self.block_content
169 .iter()
170 .filter_map(|elt| match elt {
171 types::BlockContent::P(p) => Some(p.as_ref()),
172 _ => None,
173 })
174 .collect()
175 }
176
177 fn tables(&self) -> Vec<&types::Table> {
178 self.block_content
179 .iter()
180 .filter_map(|elt| match elt {
181 types::BlockContent::Tbl(t) => Some(t.as_ref()),
182 _ => None,
183 })
184 .collect()
185 }
186
187 fn text(&self) -> String {
188 let texts: Vec<String> = self
189 .block_content
190 .iter()
191 .filter_map(|elt| match elt {
192 types::BlockContent::P(p) => Some(p.text()),
193 types::BlockContent::Tbl(t) => Some(t.text()),
194 _ => None,
195 })
196 .collect();
197 texts.join("\n")
198 }
199
200 #[cfg(feature = "wml-layout")]
201 fn section_properties(&self) -> Option<&types::SectionProperties> {
202 self.sect_pr.as_deref()
203 }
204
205 #[cfg(feature = "wml-styling")]
206 fn table_of_contents(&self) -> Vec<TableOfContents> {
207 collect_tocs_from_block_content(&self.block_content)
208 }
209
210 #[cfg(feature = "wml-settings")]
211 fn form_fields(&self) -> Vec<FormField> {
212 collect_form_fields_from_block_content(&self.block_content)
213 }
214}
215
216#[cfg(feature = "wml-styling")]
226fn toc_style_level(style: &str) -> Option<u8> {
227 let s = style.trim();
228
229 if let Some(rest) = s.strip_prefix("TOC ").or_else(|| s.strip_prefix("toc "))
231 && let Ok(n) = rest.trim().parse::<u8>()
232 && (1..=9).contains(&n)
233 {
234 return Some(n);
235 }
236
237 if let Some(rest) = s
239 .strip_prefix("TOC")
240 .or_else(|| s.strip_prefix("toc"))
241 .filter(|r| r.len() == 1)
242 && let Ok(n) = rest.parse::<u8>()
243 && (1..=9).contains(&n)
244 {
245 return Some(n);
246 }
247
248 None
249}
250
251#[cfg(feature = "wml-styling")]
253fn paragraph_toc_level(para: &types::Paragraph) -> Option<u8> {
254 let style = para.p_pr.as_ref()?.paragraph_style.as_ref()?.value.as_str();
255 toc_style_level(style)
256}
257
258#[cfg(feature = "wml-styling")]
264fn extract_toc_text_and_page(para: &types::Paragraph) -> (String, Option<u32>) {
265 let mut full = String::new();
267 for content in ¶.paragraph_content {
268 collect_text_from_paragraph_content(content, &mut full);
269 }
270
271 if let Some(tab_pos) = full.rfind('\t') {
273 let tail = full[tab_pos + 1..].trim();
274 if let Ok(page) = tail.parse::<u32>() {
275 let text = full[..tab_pos].trim().to_string();
276 return (text, Some(page));
277 }
278 }
279
280 (full.trim().to_string(), None)
281}
282
283#[cfg(feature = "wml-styling")]
289fn paragraph_bookmark(para: &types::Paragraph) -> Option<String> {
290 for content in ¶.paragraph_content {
291 if let types::ParagraphContent::BookmarkStart(bm) = content {
292 let name = bm.name.clone();
293 if !name.is_empty() {
294 return Some(name);
295 }
296 }
297 }
298 None
299}
300
301#[cfg(feature = "wml-styling")]
303fn paragraph_to_toc_entry(para: &types::Paragraph, level: u8) -> TocEntry {
304 let (text, page) = extract_toc_text_and_page(para);
305 let bookmark = paragraph_bookmark(para);
306 TocEntry {
307 level,
308 text,
309 page,
310 bookmark,
311 }
312}
313
314#[cfg(feature = "wml-styling")]
320fn collect_tocs_from_block_content(blocks: &[types::BlockContent]) -> Vec<TableOfContents> {
321 let mut result: Vec<TableOfContents> = Vec::new();
322 let mut current_entries: Vec<TocEntry> = Vec::new();
324
325 for block in blocks {
326 match block {
327 types::BlockContent::P(para) => {
328 if let Some(level) = paragraph_toc_level(para) {
329 current_entries.push(paragraph_to_toc_entry(para, level));
330 } else {
331 flush_toc(&mut current_entries, &mut result);
333 }
334 }
335 types::BlockContent::Sdt(sdt) => {
336 flush_toc(&mut current_entries, &mut result);
338
339 let sdt_entries = collect_toc_entries_from_sdt(sdt);
341 if !sdt_entries.is_empty() {
342 result.push(TableOfContents {
343 entries: sdt_entries,
344 });
345 }
346 }
347 _ => {
348 flush_toc(&mut current_entries, &mut result);
350 }
351 }
352 }
353
354 flush_toc(&mut current_entries, &mut result);
356
357 result
358}
359
360#[cfg(feature = "wml-styling")]
362fn flush_toc(entries: &mut Vec<TocEntry>, result: &mut Vec<TableOfContents>) {
363 if !entries.is_empty() {
364 result.push(TableOfContents {
365 entries: std::mem::take(entries),
366 });
367 }
368}
369
370#[cfg(feature = "wml-styling")]
375fn collect_toc_entries_from_sdt(sdt: &types::CTSdtBlock) -> Vec<TocEntry> {
376 let content = match &sdt.sdt_content {
377 Some(c) => c,
378 None => return Vec::new(),
379 };
380
381 content
382 .block_content
383 .iter()
384 .filter_map(|bc| match bc {
385 types::BlockContentChoice::P(para) => {
386 paragraph_toc_level(para).map(|lvl| paragraph_to_toc_entry(para, lvl))
387 }
388 _ => None,
389 })
390 .collect()
391}
392
393pub trait ParagraphExt {
399 fn runs(&self) -> Vec<&types::Run>;
401
402 fn text(&self) -> String;
404
405 fn hyperlinks(&self) -> Vec<&types::Hyperlink>;
407
408 #[cfg(feature = "wml-styling")]
410 fn properties(&self) -> Option<&types::ParagraphProperties>;
411
412 #[cfg(feature = "wml-styling")]
414 fn alignment(&self) -> Option<types::STJc>;
415
416 #[cfg(feature = "wml-styling")]
420 fn indent_left(&self) -> Option<i64>;
421
422 #[cfg(feature = "wml-styling")]
426 fn indent_right(&self) -> Option<i64>;
427
428 #[cfg(feature = "wml-styling")]
432 fn indent_first_line(&self) -> Option<i64>;
433
434 #[cfg(feature = "wml-styling")]
438 fn indent_hanging(&self) -> Option<i64>;
439
440 #[cfg(feature = "wml-styling")]
442 fn space_before(&self) -> Option<i64>;
443
444 #[cfg(feature = "wml-styling")]
446 fn space_after(&self) -> Option<i64>;
447
448 #[cfg(feature = "wml-styling")]
450 fn line_spacing(&self) -> Option<i64>;
451
452 #[cfg(feature = "wml-styling")]
454 fn line_spacing_rule(&self) -> Option<types::STLineSpacingRule>;
455
456 #[cfg(feature = "wml-numbering")]
460 fn numbering(&self) -> Option<(i64, i64)>;
461}
462
463impl ParagraphExt for types::Paragraph {
464 fn runs(&self) -> Vec<&types::Run> {
465 collect_runs_from_paragraph_content(&self.paragraph_content)
466 }
467
468 fn text(&self) -> String {
469 let mut out = String::new();
470 for content in &self.paragraph_content {
471 collect_text_from_paragraph_content(content, &mut out);
472 }
473 out
474 }
475
476 fn hyperlinks(&self) -> Vec<&types::Hyperlink> {
477 self.paragraph_content
478 .iter()
479 .filter_map(|c| match c {
480 types::ParagraphContent::Hyperlink(h) => Some(h.as_ref()),
481 _ => None,
482 })
483 .collect()
484 }
485
486 #[cfg(feature = "wml-styling")]
487 fn properties(&self) -> Option<&types::ParagraphProperties> {
488 self.p_pr.as_deref()
489 }
490
491 #[cfg(feature = "wml-styling")]
492 fn alignment(&self) -> Option<types::STJc> {
493 self.p_pr
494 .as_deref()?
495 .justification
496 .as_deref()
497 .map(|j| j.value)
498 }
499
500 #[cfg(feature = "wml-styling")]
501 fn indent_left(&self) -> Option<i64> {
502 let ind = self.p_pr.as_deref()?.indentation.as_deref()?;
503 ind.start
504 .as_deref()
505 .or(ind.left.as_deref())
506 .and_then(parse_twips)
507 }
508
509 #[cfg(feature = "wml-styling")]
510 fn indent_right(&self) -> Option<i64> {
511 let ind = self.p_pr.as_deref()?.indentation.as_deref()?;
512 ind.end
513 .as_deref()
514 .or(ind.right.as_deref())
515 .and_then(parse_twips)
516 }
517
518 #[cfg(feature = "wml-styling")]
519 fn indent_first_line(&self) -> Option<i64> {
520 let ind = self.p_pr.as_deref()?.indentation.as_deref()?;
521 ind.first_line.as_deref().and_then(parse_twips)
522 }
523
524 #[cfg(feature = "wml-styling")]
525 fn indent_hanging(&self) -> Option<i64> {
526 let ind = self.p_pr.as_deref()?.indentation.as_deref()?;
527 ind.hanging.as_deref().and_then(parse_twips)
528 }
529
530 #[cfg(feature = "wml-styling")]
531 fn space_before(&self) -> Option<i64> {
532 let spacing = self.p_pr.as_deref()?.spacing.as_deref()?;
533 spacing.before.as_deref().and_then(parse_twips)
534 }
535
536 #[cfg(feature = "wml-styling")]
537 fn space_after(&self) -> Option<i64> {
538 let spacing = self.p_pr.as_deref()?.spacing.as_deref()?;
539 spacing.after.as_deref().and_then(parse_twips)
540 }
541
542 #[cfg(feature = "wml-styling")]
543 fn line_spacing(&self) -> Option<i64> {
544 let spacing = self.p_pr.as_deref()?.spacing.as_deref()?;
545 spacing.line.as_deref().and_then(parse_twips)
546 }
547
548 #[cfg(feature = "wml-styling")]
549 fn line_spacing_rule(&self) -> Option<types::STLineSpacingRule> {
550 self.p_pr.as_deref()?.spacing.as_deref()?.line_rule
551 }
552
553 #[cfg(feature = "wml-numbering")]
554 fn numbering(&self) -> Option<(i64, i64)> {
555 let num_pr = self.p_pr.as_deref()?.num_pr.as_deref()?;
556 let num_id = num_pr.num_id.as_deref()?.value;
557 let ilvl = num_pr.ilvl.as_deref()?.value;
558 Some((num_id, ilvl))
559 }
560}
561
562fn collect_runs_from_paragraph_content(content: &[types::ParagraphContent]) -> Vec<&types::Run> {
564 let mut runs = Vec::new();
565 for item in content {
566 match item {
567 types::ParagraphContent::R(r) => runs.push(r.as_ref()),
568 types::ParagraphContent::Hyperlink(h) => {
569 runs.extend(collect_runs_from_paragraph_content(&h.paragraph_content));
570 }
571 types::ParagraphContent::FldSimple(f) => {
572 runs.extend(collect_runs_from_paragraph_content(&f.paragraph_content));
573 }
574 _ => {}
575 }
576 }
577 runs
578}
579
580fn collect_text_from_paragraph_content(content: &types::ParagraphContent, out: &mut String) {
582 match content {
583 types::ParagraphContent::R(r) => out.push_str(&r.text()),
584 types::ParagraphContent::Hyperlink(h) => {
585 for item in &h.paragraph_content {
586 collect_text_from_paragraph_content(item, out);
587 }
588 }
589 types::ParagraphContent::FldSimple(f) => {
590 for item in &f.paragraph_content {
591 collect_text_from_paragraph_content(item, out);
592 }
593 }
594 _ => {}
595 }
596}
597
598pub trait RunExt {
604 fn text(&self) -> String;
608
609 #[cfg(feature = "wml-styling")]
611 fn properties(&self) -> Option<&types::RunProperties>;
612
613 fn has_page_break(&self) -> bool;
615
616 #[cfg(feature = "wml-drawings")]
618 fn drawings(&self) -> Vec<&types::CTDrawing>;
619
620 #[cfg(feature = "wml-styling")]
622 fn is_bold(&self) -> bool;
623
624 #[cfg(feature = "wml-styling")]
626 fn is_italic(&self) -> bool;
627
628 #[cfg(feature = "wml-styling")]
630 fn is_underline(&self) -> bool;
631
632 #[cfg(feature = "wml-styling")]
634 fn is_strikethrough(&self) -> bool;
635
636 #[cfg(feature = "wml-drawings")]
638 fn has_images(&self) -> bool;
639
640 fn footnote_ref(&self) -> Option<&types::FootnoteEndnoteRef>;
642
643 fn endnote_ref(&self) -> Option<&types::FootnoteEndnoteRef>;
645}
646
647impl RunExt for types::Run {
648 fn text(&self) -> String {
649 let mut out = String::new();
650 for item in &self.run_content {
651 match item {
652 types::RunContent::T(t) => {
653 if let Some(ref text) = t.text {
654 out.push_str(text);
655 }
656 }
657 types::RunContent::Tab(_) => out.push('\t'),
658 types::RunContent::Cr(_) => out.push('\n'),
659 types::RunContent::Br(br) => {
660 if !matches!(
662 br.r#type,
663 Some(types::STBrType::Page) | Some(types::STBrType::Column)
664 ) {
665 out.push('\n');
666 }
667 }
668 _ => {}
669 }
670 }
671 out
672 }
673
674 #[cfg(feature = "wml-styling")]
675 fn properties(&self) -> Option<&types::RunProperties> {
676 self.r_pr.as_deref()
677 }
678
679 fn has_page_break(&self) -> bool {
680 self.run_content.iter().any(|item| {
681 matches!(
682 item,
683 types::RunContent::Br(br) if br.r#type == Some(types::STBrType::Page)
684 )
685 })
686 }
687
688 #[cfg(feature = "wml-drawings")]
689 fn drawings(&self) -> Vec<&types::CTDrawing> {
690 self.run_content
691 .iter()
692 .filter_map(|item| match item {
693 types::RunContent::Drawing(d) => Some(d.as_ref()),
694 _ => None,
695 })
696 .collect()
697 }
698
699 #[cfg(feature = "wml-styling")]
700 fn is_bold(&self) -> bool {
701 self.properties().is_some_and(|p| p.is_bold())
702 }
703
704 #[cfg(feature = "wml-styling")]
705 fn is_italic(&self) -> bool {
706 self.properties().is_some_and(|p| p.is_italic())
707 }
708
709 #[cfg(feature = "wml-styling")]
710 fn is_underline(&self) -> bool {
711 self.properties().is_some_and(|p| p.is_underline())
712 }
713
714 #[cfg(feature = "wml-styling")]
715 fn is_strikethrough(&self) -> bool {
716 self.properties().is_some_and(|p| p.is_strikethrough())
717 }
718
719 #[cfg(feature = "wml-drawings")]
720 fn has_images(&self) -> bool {
721 self.run_content
722 .iter()
723 .any(|item| matches!(item, types::RunContent::Drawing(_)))
724 }
725
726 fn footnote_ref(&self) -> Option<&types::FootnoteEndnoteRef> {
727 self.run_content.iter().find_map(|item| match item {
728 types::RunContent::FootnoteReference(r) => Some(r.as_ref()),
729 _ => None,
730 })
731 }
732
733 fn endnote_ref(&self) -> Option<&types::FootnoteEndnoteRef> {
734 self.run_content.iter().find_map(|item| match item {
735 types::RunContent::EndnoteReference(r) => Some(r.as_ref()),
736 _ => None,
737 })
738 }
739}
740
741#[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
750pub trait DrawingExt {
751 fn inline_image_rel_ids(&self) -> Vec<&str>;
753
754 fn anchored_image_rel_ids(&self) -> Vec<&str>;
756
757 fn all_image_rel_ids(&self) -> Vec<&str>;
759}
760
761#[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
762impl DrawingExt for types::CTDrawing {
763 fn inline_image_rel_ids(&self) -> Vec<&str> {
764 let mut ids = Vec::new();
765 for child in &self.extra_children {
766 if let ooxml_xml::RawXmlNode::Element(elem) = &child.node
767 && local_name_of(&elem.name) == "inline"
768 {
769 collect_blip_rel_ids(elem, &mut ids);
770 }
771 }
772 ids
773 }
774
775 fn anchored_image_rel_ids(&self) -> Vec<&str> {
776 let mut ids = Vec::new();
777 for child in &self.extra_children {
778 if let ooxml_xml::RawXmlNode::Element(elem) = &child.node
779 && local_name_of(&elem.name) == "anchor"
780 {
781 collect_blip_rel_ids(elem, &mut ids);
782 }
783 }
784 ids
785 }
786
787 fn all_image_rel_ids(&self) -> Vec<&str> {
788 let mut ids = self.inline_image_rel_ids();
789 ids.extend(self.anchored_image_rel_ids());
790 ids
791 }
792}
793
794#[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
797fn local_name_of(name: &str) -> &str {
798 name.rsplit(':').next().unwrap_or(name)
799}
800
801#[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
804fn collect_blip_rel_ids<'a>(elem: &'a ooxml_xml::RawXmlElement, ids: &mut Vec<&'a str>) {
805 if local_name_of(&elem.name) == "blip" {
806 for (attr_name, attr_val) in &elem.attributes {
807 if attr_name == "r:embed" || local_name_of(attr_name) == "embed" {
808 ids.push(attr_val.as_str());
809 }
810 }
811 }
812 for child in &elem.children {
813 if let ooxml_xml::RawXmlNode::Element(child_elem) = child {
814 collect_blip_rel_ids(child_elem, ids);
815 }
816 }
817}
818
819#[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
830pub trait DrawingChartExt {
831 fn inline_chart_rel_ids(&self) -> Vec<&str>;
833
834 fn anchored_chart_rel_ids(&self) -> Vec<&str>;
836
837 fn all_chart_rel_ids(&self) -> Vec<&str>;
839}
840
841#[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
842impl DrawingChartExt for types::CTDrawing {
843 fn inline_chart_rel_ids(&self) -> Vec<&str> {
844 let mut ids = Vec::new();
845 for child in &self.extra_children {
846 if let ooxml_xml::RawXmlNode::Element(elem) = &child.node
847 && local_name_of(&elem.name) == "inline"
848 {
849 collect_chart_rel_ids(elem, &mut ids);
850 }
851 }
852 ids
853 }
854
855 fn anchored_chart_rel_ids(&self) -> Vec<&str> {
856 let mut ids = Vec::new();
857 for child in &self.extra_children {
858 if let ooxml_xml::RawXmlNode::Element(elem) = &child.node
859 && local_name_of(&elem.name) == "anchor"
860 {
861 collect_chart_rel_ids(elem, &mut ids);
862 }
863 }
864 ids
865 }
866
867 fn all_chart_rel_ids(&self) -> Vec<&str> {
868 let mut ids = self.inline_chart_rel_ids();
869 ids.extend(self.anchored_chart_rel_ids());
870 ids
871 }
872}
873
874#[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
877fn collect_chart_rel_ids<'a>(elem: &'a ooxml_xml::RawXmlElement, ids: &mut Vec<&'a str>) {
878 if local_name_of(&elem.name) == "chart" {
879 for (attr_name, attr_val) in &elem.attributes {
880 if attr_name == "r:id" || local_name_of(attr_name) == "id" {
881 ids.push(attr_val.as_str());
882 }
883 }
884 }
885 for child in &elem.children {
886 if let ooxml_xml::RawXmlNode::Element(child_elem) = child {
887 collect_chart_rel_ids(child_elem, ids);
888 }
889 }
890}
891
892#[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
908pub trait DrawingTextBoxExt {
909 fn text_box_texts(&self) -> Vec<String>;
915}
916
917#[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
918impl DrawingTextBoxExt for types::CTDrawing {
919 fn text_box_texts(&self) -> Vec<String> {
920 let mut results = Vec::new();
921 for child in &self.extra_children {
922 if let ooxml_xml::RawXmlNode::Element(elem) = &child.node {
923 collect_txbx_texts_from_raw(elem, &mut results);
924 }
925 }
926 results
927 }
928}
929
930#[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
933fn collect_txbx_texts_from_raw(elem: &ooxml_xml::RawXmlElement, out: &mut Vec<String>) {
934 if local_name_of(&elem.name) == "txbxContent" {
935 match elem.parse_as::<types::CTTxbxContent>() {
937 Ok(content) => {
938 let text = txbx_content_text(&content);
939 out.push(text);
940 }
941 Err(_) => {
942 }
944 }
945 return;
947 }
948
949 for child in &elem.children {
950 if let ooxml_xml::RawXmlNode::Element(child_elem) = child {
951 collect_txbx_texts_from_raw(child_elem, out);
952 }
953 }
954}
955
956#[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
958fn txbx_content_text(content: &types::CTTxbxContent) -> String {
959 use crate::ext::{ParagraphExt, TableExt};
960 let parts: Vec<String> = content
961 .block_content
962 .iter()
963 .filter_map(|bc| match bc {
964 types::BlockContent::P(p) => Some(p.text()),
965 types::BlockContent::Tbl(t) => Some(t.text()),
966 _ => None,
967 })
968 .collect();
969 parts.join("\n")
970}
971
972#[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
986pub trait PictExt {
987 fn text_box_text(&self) -> Option<String>;
992
993 fn text_box_texts(&self) -> Vec<String>;
995}
996
997#[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
998impl PictExt for types::CTPicture {
999 fn text_box_text(&self) -> Option<String> {
1000 self.text_box_texts().into_iter().next()
1001 }
1002
1003 fn text_box_texts(&self) -> Vec<String> {
1004 let mut results = Vec::new();
1005 for child in &self.extra_children {
1006 if let ooxml_xml::RawXmlNode::Element(elem) = &child.node {
1007 collect_txbx_texts_from_raw(elem, &mut results);
1008 }
1009 }
1010 results
1011 }
1012}
1013
1014#[cfg(feature = "extra-children")]
1021#[derive(Debug, Clone)]
1022pub struct MathExpression {
1023 pub is_display: bool,
1025 #[cfg(feature = "wml-math")]
1027 pub zone: ooxml_omml::MathZone,
1028}
1029
1030#[cfg(all(feature = "extra-children", feature = "wml-math"))]
1031impl MathExpression {
1032 pub fn text(&self) -> String {
1034 self.zone.text()
1035 }
1036}
1037
1038#[cfg(feature = "extra-children")]
1047pub trait MathExt {
1048 fn math_expressions(&self) -> Vec<MathExpression>;
1050
1051 fn has_math(&self) -> bool {
1053 !self.math_expressions().is_empty()
1054 }
1055}
1056
1057#[cfg(feature = "extra-children")]
1058impl MathExt for types::Paragraph {
1059 fn math_expressions(&self) -> Vec<MathExpression> {
1060 let mut out = Vec::new();
1061 for child in &self.extra_children {
1062 if let ooxml_xml::RawXmlNode::Element(elem) = &child.node {
1063 collect_math_from_raw(elem, &mut out);
1064 }
1065 }
1066 out
1067 }
1068}
1069
1070#[cfg(feature = "extra-children")]
1071impl MathExt for types::Body {
1072 fn math_expressions(&self) -> Vec<MathExpression> {
1073 let mut out = Vec::new();
1074 for item in &self.block_content {
1075 if let types::BlockContent::P(p) = item {
1076 out.extend(p.math_expressions());
1077 }
1078 }
1079 out
1080 }
1081}
1082
1083#[cfg(all(feature = "extra-children", feature = "wml-math"))]
1090fn parse_math_zone_from_element(elem: &ooxml_xml::RawXmlElement) -> ooxml_omml::MathZone {
1091 elem.parse_as::<ooxml_omml::MathZone>().unwrap_or_default()
1092}
1093
1094#[cfg(feature = "extra-children")]
1095fn collect_math_from_raw(elem: &ooxml_xml::RawXmlElement, out: &mut Vec<MathExpression>) {
1096 let local = math_local_name(&elem.name);
1097 match local {
1098 "oMathPara" => {
1099 out.push(MathExpression {
1100 is_display: true,
1101 #[cfg(feature = "wml-math")]
1102 zone: {
1103 elem.children
1105 .iter()
1106 .filter_map(|c| match c {
1107 ooxml_xml::RawXmlNode::Element(e)
1108 if math_local_name(&e.name) == "oMath" =>
1109 {
1110 Some(parse_math_zone_from_element(e))
1111 }
1112 _ => None,
1113 })
1114 .next()
1115 .unwrap_or_default()
1116 },
1117 });
1118 }
1119 "oMath" => {
1120 out.push(MathExpression {
1121 is_display: false,
1122 #[cfg(feature = "wml-math")]
1123 zone: parse_math_zone_from_element(elem),
1124 });
1125 }
1126 _ => {
1127 for child in &elem.children {
1129 if let ooxml_xml::RawXmlNode::Element(child_elem) = child {
1130 collect_math_from_raw(child_elem, out);
1131 }
1132 }
1133 }
1134 }
1135}
1136
1137#[cfg(feature = "extra-children")]
1142#[inline]
1143fn math_local_name(name: &str) -> &str {
1144 name.rsplit(':').next().unwrap_or(name)
1145}
1146
1147#[cfg(feature = "wml-styling")]
1156pub trait RunPropertiesExt {
1157 fn is_bold(&self) -> bool;
1159
1160 fn is_italic(&self) -> bool;
1162
1163 fn is_underline(&self) -> bool;
1165
1166 fn underline_style(&self) -> Option<&types::STUnderline>;
1168
1169 fn is_strikethrough(&self) -> bool;
1171
1172 fn is_double_strikethrough(&self) -> bool;
1174
1175 fn is_all_caps(&self) -> bool;
1177
1178 fn is_small_caps(&self) -> bool;
1180
1181 fn is_hidden(&self) -> bool;
1183
1184 fn highlight_color(&self) -> Option<&types::STHighlightColor>;
1186
1187 fn vertical_alignment(&self) -> Option<&types::STVerticalAlignRun>;
1189
1190 fn is_superscript(&self) -> bool;
1192
1193 fn is_subscript(&self) -> bool;
1195
1196 fn font_size_half_points(&self) -> Option<u32>;
1198
1199 fn font_size_points(&self) -> Option<f64>;
1201
1202 fn color_hex(&self) -> Option<&str>;
1204
1205 fn style_id(&self) -> Option<&str>;
1207
1208 fn font_ascii(&self) -> Option<&str>;
1210
1211 fn is_rtl(&self) -> bool;
1213
1214 fn is_outline(&self) -> bool;
1216
1217 fn is_shadow(&self) -> bool;
1219
1220 fn is_emboss(&self) -> bool;
1222
1223 fn is_imprint(&self) -> bool;
1225
1226 fn is_no_proof(&self) -> bool;
1228
1229 fn is_snap_to_grid(&self) -> bool;
1231
1232 fn is_web_hidden(&self) -> bool;
1234
1235 fn character_spacing(&self) -> Option<i64>;
1237
1238 fn text_scale_percent(&self) -> Option<u32>;
1240
1241 fn kerning(&self) -> Option<u32>;
1243
1244 fn baseline_shift(&self) -> Option<i64>;
1246
1247 fn language(&self) -> Option<&types::LanguageElement>;
1249}
1250
1251#[cfg(feature = "wml-styling")]
1252impl RunPropertiesExt for types::RunProperties {
1253 fn is_bold(&self) -> bool {
1254 is_on(&self.bold)
1255 }
1256
1257 fn is_italic(&self) -> bool {
1258 is_on(&self.italic)
1259 }
1260
1261 fn is_underline(&self) -> bool {
1262 self.underline
1263 .as_ref()
1264 .is_some_and(|u| !matches!(u.value, Some(types::STUnderline::None)))
1265 }
1266
1267 fn underline_style(&self) -> Option<&types::STUnderline> {
1268 self.underline.as_ref().and_then(|u| u.value.as_ref())
1269 }
1270
1271 fn is_strikethrough(&self) -> bool {
1272 is_on(&self.strikethrough)
1273 }
1274
1275 fn is_double_strikethrough(&self) -> bool {
1276 is_on(&self.dstrike)
1277 }
1278
1279 fn is_all_caps(&self) -> bool {
1280 is_on(&self.caps)
1281 }
1282
1283 fn is_small_caps(&self) -> bool {
1284 is_on(&self.small_caps)
1285 }
1286
1287 fn is_hidden(&self) -> bool {
1288 is_on(&self.vanish)
1289 }
1290
1291 fn highlight_color(&self) -> Option<&types::STHighlightColor> {
1292 self.highlight.as_ref().map(|h| &h.value)
1293 }
1294
1295 fn vertical_alignment(&self) -> Option<&types::STVerticalAlignRun> {
1296 self.vert_align.as_ref().map(|va| &va.value)
1297 }
1298
1299 fn is_superscript(&self) -> bool {
1300 matches!(
1301 self.vert_align.as_ref().map(|va| &va.value),
1302 Some(types::STVerticalAlignRun::Superscript)
1303 )
1304 }
1305
1306 fn is_subscript(&self) -> bool {
1307 matches!(
1308 self.vert_align.as_ref().map(|va| &va.value),
1309 Some(types::STVerticalAlignRun::Subscript)
1310 )
1311 }
1312
1313 fn font_size_half_points(&self) -> Option<u32> {
1314 self.size
1315 .as_ref()
1316 .and_then(|sz| parse_half_points(&sz.value))
1317 }
1318
1319 fn font_size_points(&self) -> Option<f64> {
1320 self.font_size_half_points().map(|hp| hp as f64 / 2.0)
1321 }
1322
1323 fn color_hex(&self) -> Option<&str> {
1324 self.color.as_ref().map(|c| c.value.as_str())
1325 }
1326
1327 fn style_id(&self) -> Option<&str> {
1328 self.run_style.as_ref().map(|s| s.value.as_str())
1329 }
1330
1331 fn font_ascii(&self) -> Option<&str> {
1332 self.fonts.as_ref().and_then(|f| f.ascii.as_deref())
1333 }
1334
1335 fn is_rtl(&self) -> bool {
1336 is_on(&self.rtl)
1337 }
1338
1339 fn is_outline(&self) -> bool {
1340 is_on(&self.outline)
1341 }
1342
1343 fn is_shadow(&self) -> bool {
1344 is_on(&self.shadow)
1345 }
1346
1347 fn is_emboss(&self) -> bool {
1348 is_on(&self.emboss)
1349 }
1350
1351 fn is_imprint(&self) -> bool {
1352 is_on(&self.imprint)
1353 }
1354
1355 fn is_no_proof(&self) -> bool {
1356 is_on(&self.no_proof)
1357 }
1358
1359 fn is_snap_to_grid(&self) -> bool {
1360 is_on(&self.snap_to_grid)
1361 }
1362
1363 fn is_web_hidden(&self) -> bool {
1364 is_on(&self.web_hidden)
1365 }
1366
1367 fn character_spacing(&self) -> Option<i64> {
1368 self.spacing
1369 .as_ref()
1370 .and_then(|s| s.value.parse::<i64>().ok())
1371 }
1372
1373 fn text_scale_percent(&self) -> Option<u32> {
1374 self.width.as_ref()?.value.as_deref()?.parse::<u32>().ok()
1375 }
1376
1377 fn kerning(&self) -> Option<u32> {
1378 self.kern.as_ref().and_then(|k| parse_half_points(&k.value))
1379 }
1380
1381 fn baseline_shift(&self) -> Option<i64> {
1382 self.position
1383 .as_ref()
1384 .and_then(|p| p.value.parse::<i64>().ok())
1385 }
1386
1387 fn language(&self) -> Option<&types::LanguageElement> {
1388 self.lang.as_deref()
1389 }
1390}
1391
1392pub trait HyperlinkExt {
1398 fn runs(&self) -> Vec<&types::Run>;
1400
1401 fn text(&self) -> String;
1403
1404 fn anchor_str(&self) -> Option<&str>;
1406
1407 fn rel_id(&self) -> Option<&str>;
1409
1410 fn is_external(&self) -> bool;
1412}
1413
1414impl HyperlinkExt for types::Hyperlink {
1415 fn runs(&self) -> Vec<&types::Run> {
1416 collect_runs_from_paragraph_content(&self.paragraph_content)
1417 }
1418
1419 fn text(&self) -> String {
1420 let mut out = String::new();
1421 for item in &self.paragraph_content {
1422 collect_text_from_paragraph_content(item, &mut out);
1423 }
1424 out
1425 }
1426
1427 fn anchor_str(&self) -> Option<&str> {
1428 #[cfg(feature = "wml-hyperlinks")]
1429 {
1430 self.anchor.as_deref()
1431 }
1432 #[cfg(not(feature = "wml-hyperlinks"))]
1433 {
1434 None
1435 }
1436 }
1437
1438 fn rel_id(&self) -> Option<&str> {
1439 #[cfg(feature = "wml-hyperlinks")]
1440 {
1441 self.id.as_deref()
1442 }
1443 #[cfg(not(feature = "wml-hyperlinks"))]
1444 {
1445 None
1446 }
1447 }
1448
1449 fn is_external(&self) -> bool {
1450 #[cfg(feature = "wml-hyperlinks")]
1451 {
1452 self.id.is_some()
1453 }
1454 #[cfg(not(feature = "wml-hyperlinks"))]
1455 {
1456 false
1457 }
1458 }
1459}
1460
1461pub trait TableExt {
1467 fn rows(&self) -> Vec<&types::CTRow>;
1469
1470 fn row_count(&self) -> usize;
1472
1473 fn properties(&self) -> &types::TableProperties;
1475
1476 fn text(&self) -> String;
1478}
1479
1480impl TableExt for types::Table {
1481 fn rows(&self) -> Vec<&types::CTRow> {
1482 self.rows
1483 .iter()
1484 .filter_map(|c| match c {
1485 types::RowContent::Tr(row) => Some(row.as_ref()),
1486 _ => None,
1487 })
1488 .collect()
1489 }
1490
1491 fn row_count(&self) -> usize {
1492 self.rows().len()
1493 }
1494
1495 fn properties(&self) -> &types::TableProperties {
1496 &self.table_properties
1497 }
1498
1499 fn text(&self) -> String {
1500 let row_texts: Vec<String> = self.rows().iter().map(|r| r.text()).collect();
1501 row_texts.join("\n")
1502 }
1503}
1504
1505pub trait RowExt {
1511 fn cells(&self) -> Vec<&types::TableCell>;
1513
1514 #[cfg(feature = "wml-tables")]
1516 fn properties(&self) -> Option<&types::TableRowProperties>;
1517
1518 fn text(&self) -> String;
1520}
1521
1522impl RowExt for types::CTRow {
1523 fn cells(&self) -> Vec<&types::TableCell> {
1524 self.cells
1525 .iter()
1526 .filter_map(|c| match c {
1527 types::CellContent::Tc(cell) => Some(cell.as_ref()),
1528 _ => None,
1529 })
1530 .collect()
1531 }
1532
1533 #[cfg(feature = "wml-tables")]
1534 fn properties(&self) -> Option<&types::TableRowProperties> {
1535 self.row_properties.as_deref()
1536 }
1537
1538 fn text(&self) -> String {
1539 let cell_texts: Vec<String> = self.cells().iter().map(|c| c.text()).collect();
1540 cell_texts.join("\t")
1541 }
1542}
1543
1544pub trait CellExt {
1550 fn paragraphs(&self) -> Vec<&types::Paragraph>;
1552
1553 #[cfg(feature = "wml-tables")]
1555 fn properties(&self) -> Option<&types::TableCellProperties>;
1556
1557 fn text(&self) -> String;
1559}
1560
1561impl CellExt for types::TableCell {
1562 fn paragraphs(&self) -> Vec<&types::Paragraph> {
1563 self.block_content
1564 .iter()
1565 .filter_map(|elt| match elt {
1566 types::BlockContent::P(p) => Some(p.as_ref()),
1567 _ => None,
1568 })
1569 .collect()
1570 }
1571
1572 #[cfg(feature = "wml-tables")]
1573 fn properties(&self) -> Option<&types::TableCellProperties> {
1574 self.cell_properties.as_deref()
1575 }
1576
1577 fn text(&self) -> String {
1578 let texts: Vec<String> = self.paragraphs().iter().map(|p| p.text()).collect();
1579 texts.join("\n")
1580 }
1581}
1582
1583#[cfg(feature = "wml-layout")]
1589pub trait SectionPropertiesExt {
1590 fn page_size(&self) -> Option<&types::PageSize>;
1592
1593 fn page_margins(&self) -> Option<&types::PageMargins>;
1595
1596 fn page_width_twips(&self) -> Option<u64>;
1598
1599 fn page_height_twips(&self) -> Option<u64>;
1601
1602 fn page_orientation(&self) -> Option<&types::STPageOrientation>;
1604
1605 fn has_title_page(&self) -> bool;
1607
1608 #[cfg(feature = "extra-attrs")]
1610 fn header_references(&self) -> Vec<(&types::STHdrFtr, &str)>;
1611
1612 #[cfg(feature = "extra-attrs")]
1614 fn footer_references(&self) -> Vec<(&types::STHdrFtr, &str)>;
1615}
1616
1617#[cfg(feature = "wml-layout")]
1618impl SectionPropertiesExt for types::SectionProperties {
1619 fn page_size(&self) -> Option<&types::PageSize> {
1620 self.pg_sz.as_deref()
1621 }
1622
1623 fn page_margins(&self) -> Option<&types::PageMargins> {
1624 self.pg_mar.as_deref()
1625 }
1626
1627 fn page_width_twips(&self) -> Option<u64> {
1628 self.pg_sz
1629 .as_ref()
1630 .and_then(|sz| sz.width.as_ref())
1631 .and_then(|w| w.parse::<u64>().ok())
1632 }
1633
1634 fn page_height_twips(&self) -> Option<u64> {
1635 self.pg_sz
1636 .as_ref()
1637 .and_then(|sz| sz.height.as_ref())
1638 .and_then(|h| h.parse::<u64>().ok())
1639 }
1640
1641 fn page_orientation(&self) -> Option<&types::STPageOrientation> {
1642 self.pg_sz.as_ref().and_then(|sz| sz.orient.as_ref())
1643 }
1644
1645 fn has_title_page(&self) -> bool {
1646 is_on(&self.title_pg)
1647 }
1648
1649 #[cfg(feature = "extra-attrs")]
1650 fn header_references(&self) -> Vec<(&types::STHdrFtr, &str)> {
1651 self.header_footer_refs
1652 .iter()
1653 .filter_map(|r| match r {
1654 types::HeaderFooterRef::HeaderReference(h) => {
1655 h.extra_attrs.get("r:id").map(|id| (&h.r#type, id.as_str()))
1656 }
1657 _ => None,
1658 })
1659 .collect()
1660 }
1661
1662 #[cfg(feature = "extra-attrs")]
1663 fn footer_references(&self) -> Vec<(&types::STHdrFtr, &str)> {
1664 self.header_footer_refs
1665 .iter()
1666 .filter_map(|r| match r {
1667 types::HeaderFooterRef::FooterReference(f) => {
1668 f.extra_attrs.get("r:id").map(|id| (&f.r#type, id.as_str()))
1669 }
1670 _ => None,
1671 })
1672 .collect()
1673 }
1674}
1675
1676#[cfg(feature = "wml-styling")]
1688#[derive(Debug, Clone, Default)]
1689pub struct StyleContext {
1690 pub styles: std::collections::HashMap<String, types::Style>,
1692 pub default_run_properties: Option<types::RunProperties>,
1694}
1695
1696#[cfg(feature = "wml-styling")]
1697impl StyleContext {
1698 pub fn from_styles(styles_doc: &types::Styles) -> Self {
1700 let mut styles = std::collections::HashMap::new();
1701 for style in &styles_doc.style {
1702 if let Some(ref id) = style.style_id {
1703 styles.insert(id.clone(), style.clone());
1704 }
1705 }
1706
1707 let default_run_properties = styles_doc
1708 .doc_defaults
1709 .as_ref()
1710 .and_then(|dd| dd.r_pr_default.as_ref())
1711 .and_then(|rpd| rpd.r_pr.as_ref())
1712 .map(|rp| rp.as_ref().clone());
1713
1714 Self {
1715 styles,
1716 default_run_properties,
1717 }
1718 }
1719
1720 pub fn style(&self, id: &str) -> Option<&types::Style> {
1722 self.styles.get(id)
1723 }
1724
1725 fn collect_style_chain_rpr(&self, style_id: &str) -> Vec<&types::RunProperties> {
1729 let mut result = Vec::new();
1730 let mut current_id = Some(style_id.to_string());
1731 let mut depth = 0;
1732
1733 while let Some(ref id) = current_id {
1734 if depth >= 20 {
1735 break;
1736 }
1737 if let Some(style) = self.styles.get(id) {
1738 if let Some(ref rpr) = style.r_pr {
1739 result.push(rpr.as_ref());
1740 }
1741 current_id = style.based_on.as_ref().map(|b| b.value.clone());
1742 } else {
1743 break;
1744 }
1745 depth += 1;
1746 }
1747 result
1748 }
1749}
1750
1751#[cfg(feature = "wml-styling")]
1753pub trait RunResolveExt {
1754 fn resolved_is_bold(&self, ctx: &StyleContext) -> bool;
1756
1757 fn resolved_is_italic(&self, ctx: &StyleContext) -> bool;
1759
1760 fn resolved_font_size_half_points(&self, ctx: &StyleContext) -> Option<u32>;
1762
1763 fn resolved_font_ascii(&self, ctx: &StyleContext) -> Option<String>;
1765
1766 fn resolved_color_hex(&self, ctx: &StyleContext) -> Option<String>;
1768
1769 fn resolved_is_underline(&self, ctx: &StyleContext) -> bool;
1771
1772 fn resolved_is_strikethrough(&self, ctx: &StyleContext) -> bool;
1774
1775 fn resolved_is_double_strikethrough(&self, ctx: &StyleContext) -> bool;
1777
1778 fn resolved_is_all_caps(&self, ctx: &StyleContext) -> bool;
1780
1781 fn resolved_is_small_caps(&self, ctx: &StyleContext) -> bool;
1783
1784 fn resolved_is_hidden(&self, ctx: &StyleContext) -> bool;
1786
1787 fn resolved_highlight_color(&self, ctx: &StyleContext) -> Option<types::STHighlightColor>;
1789
1790 fn resolved_vertical_alignment(&self, ctx: &StyleContext) -> Option<types::STVerticalAlignRun>;
1792}
1793
1794#[cfg(feature = "wml-styling")]
1795impl RunResolveExt for types::Run {
1796 fn resolved_is_bold(&self, ctx: &StyleContext) -> bool {
1797 resolve_toggle(&self.r_pr, ctx, |rpr| &rpr.bold)
1798 }
1799
1800 fn resolved_is_italic(&self, ctx: &StyleContext) -> bool {
1801 resolve_toggle(&self.r_pr, ctx, |rpr| &rpr.italic)
1802 }
1803
1804 fn resolved_font_size_half_points(&self, ctx: &StyleContext) -> Option<u32> {
1805 resolve_option(&self.r_pr, ctx, |rpr| {
1806 rpr.size
1807 .as_ref()
1808 .and_then(|sz| parse_half_points(&sz.value))
1809 })
1810 }
1811
1812 fn resolved_font_ascii(&self, ctx: &StyleContext) -> Option<String> {
1813 resolve_option(&self.r_pr, ctx, |rpr| {
1814 rpr.fonts.as_ref().and_then(|f| f.ascii.clone())
1815 })
1816 }
1817
1818 fn resolved_color_hex(&self, ctx: &StyleContext) -> Option<String> {
1819 resolve_option(&self.r_pr, ctx, |rpr| {
1820 rpr.color.as_ref().map(|c| c.value.clone())
1821 })
1822 }
1823
1824 fn resolved_is_underline(&self, ctx: &StyleContext) -> bool {
1825 resolve_option(&self.r_pr, ctx, |rpr| {
1826 rpr.underline
1827 .as_ref()
1828 .map(|u| !matches!(u.value, Some(types::STUnderline::None)))
1829 })
1830 .unwrap_or(false)
1831 }
1832
1833 fn resolved_is_strikethrough(&self, ctx: &StyleContext) -> bool {
1834 resolve_toggle(&self.r_pr, ctx, |rpr| &rpr.strikethrough)
1835 }
1836
1837 fn resolved_is_double_strikethrough(&self, ctx: &StyleContext) -> bool {
1838 resolve_toggle(&self.r_pr, ctx, |rpr| &rpr.dstrike)
1839 }
1840
1841 fn resolved_is_all_caps(&self, ctx: &StyleContext) -> bool {
1842 resolve_toggle(&self.r_pr, ctx, |rpr| &rpr.caps)
1843 }
1844
1845 fn resolved_is_small_caps(&self, ctx: &StyleContext) -> bool {
1846 resolve_toggle(&self.r_pr, ctx, |rpr| &rpr.small_caps)
1847 }
1848
1849 fn resolved_is_hidden(&self, ctx: &StyleContext) -> bool {
1850 resolve_toggle(&self.r_pr, ctx, |rpr| &rpr.vanish)
1851 }
1852
1853 fn resolved_highlight_color(&self, ctx: &StyleContext) -> Option<types::STHighlightColor> {
1854 resolve_option(&self.r_pr, ctx, |rpr| {
1855 rpr.highlight.as_ref().map(|h| h.value)
1856 })
1857 }
1858
1859 fn resolved_vertical_alignment(&self, ctx: &StyleContext) -> Option<types::STVerticalAlignRun> {
1860 resolve_option(&self.r_pr, ctx, |rpr| {
1861 rpr.vert_align.as_ref().map(|va| va.value)
1862 })
1863 }
1864}
1865
1866#[cfg(feature = "wml-styling")]
1868fn resolve_toggle(
1869 direct_rpr: &Option<Box<types::RunProperties>>,
1870 ctx: &StyleContext,
1871 accessor: impl Fn(&types::RunProperties) -> &Option<Box<types::OnOffElement>>,
1872) -> bool {
1873 if let Some(rpr) = direct_rpr {
1875 if let Some(val) = check_toggle(accessor(rpr)) {
1876 return val;
1877 }
1878
1879 if let Some(style_ref) = &rpr.run_style {
1881 for chain_rpr in ctx.collect_style_chain_rpr(&style_ref.value) {
1882 if let Some(val) = check_toggle(accessor(chain_rpr)) {
1883 return val;
1884 }
1885 }
1886 }
1887 }
1888
1889 if let Some(defaults) = &ctx.default_run_properties
1891 && let Some(val) = check_toggle(accessor(defaults))
1892 {
1893 return val;
1894 }
1895
1896 false
1897}
1898
1899#[cfg(feature = "wml-styling")]
1901fn resolve_option<T>(
1902 direct_rpr: &Option<Box<types::RunProperties>>,
1903 ctx: &StyleContext,
1904 accessor: impl Fn(&types::RunProperties) -> Option<T>,
1905) -> Option<T> {
1906 if let Some(rpr) = direct_rpr {
1908 if let val @ Some(_) = accessor(rpr) {
1909 return val;
1910 }
1911
1912 if let Some(style_ref) = &rpr.run_style {
1914 for chain_rpr in ctx.collect_style_chain_rpr(&style_ref.value) {
1915 if let val @ Some(_) = accessor(chain_rpr) {
1916 return val;
1917 }
1918 }
1919 }
1920 }
1921
1922 if let Some(defaults) = &ctx.default_run_properties
1924 && let val @ Some(_) = accessor(defaults)
1925 {
1926 return val;
1927 }
1928
1929 None
1930}
1931
1932pub fn parse_document(xml: &[u8]) -> Result<types::Document, ParseError> {
1940 let mut reader = Reader::from_reader(Cursor::new(xml));
1941 let mut buf = Vec::new();
1942
1943 loop {
1944 match reader.read_event_into(&mut buf) {
1945 Ok(Event::Start(e)) => return types::Document::from_xml(&mut reader, &e, false),
1946 Ok(Event::Empty(e)) => return types::Document::from_xml(&mut reader, &e, true),
1947 Ok(Event::Eof) => break,
1948 Err(e) => return Err(ParseError::Xml(e)),
1949 _ => {}
1950 }
1951 buf.clear();
1952 }
1953 Err(ParseError::UnexpectedElement(
1954 "no document element found".to_string(),
1955 ))
1956}
1957
1958pub fn parse_styles(xml: &[u8]) -> Result<types::Styles, ParseError> {
1962 let mut reader = Reader::from_reader(Cursor::new(xml));
1963 let mut buf = Vec::new();
1964
1965 loop {
1966 match reader.read_event_into(&mut buf) {
1967 Ok(Event::Start(e)) => return types::Styles::from_xml(&mut reader, &e, false),
1968 Ok(Event::Empty(e)) => return types::Styles::from_xml(&mut reader, &e, true),
1969 Ok(Event::Eof) => break,
1970 Err(e) => return Err(ParseError::Xml(e)),
1971 _ => {}
1972 }
1973 buf.clear();
1974 }
1975 Err(ParseError::UnexpectedElement(
1976 "no styles element found".to_string(),
1977 ))
1978}
1979
1980pub fn parse_hdr_ftr(xml: &[u8]) -> Result<types::HeaderFooter, ParseError> {
1982 let mut reader = Reader::from_reader(Cursor::new(xml));
1983 let mut buf = Vec::new();
1984
1985 loop {
1986 match reader.read_event_into(&mut buf) {
1987 Ok(Event::Start(e)) => return types::HeaderFooter::from_xml(&mut reader, &e, false),
1988 Ok(Event::Empty(e)) => return types::HeaderFooter::from_xml(&mut reader, &e, true),
1989 Ok(Event::Eof) => break,
1990 Err(e) => return Err(ParseError::Xml(e)),
1991 _ => {}
1992 }
1993 buf.clear();
1994 }
1995 Err(ParseError::UnexpectedElement(
1996 "no header/footer element found".to_string(),
1997 ))
1998}
1999
2000pub fn parse_footnotes(xml: &[u8]) -> Result<types::Footnotes, ParseError> {
2002 let mut reader = Reader::from_reader(Cursor::new(xml));
2003 let mut buf = Vec::new();
2004
2005 loop {
2006 match reader.read_event_into(&mut buf) {
2007 Ok(Event::Start(e)) => return types::Footnotes::from_xml(&mut reader, &e, false),
2008 Ok(Event::Empty(e)) => return types::Footnotes::from_xml(&mut reader, &e, true),
2009 Ok(Event::Eof) => break,
2010 Err(e) => return Err(ParseError::Xml(e)),
2011 _ => {}
2012 }
2013 buf.clear();
2014 }
2015 Err(ParseError::UnexpectedElement(
2016 "no footnotes element found".to_string(),
2017 ))
2018}
2019
2020pub fn parse_endnotes(xml: &[u8]) -> Result<types::Endnotes, ParseError> {
2022 let mut reader = Reader::from_reader(Cursor::new(xml));
2023 let mut buf = Vec::new();
2024
2025 loop {
2026 match reader.read_event_into(&mut buf) {
2027 Ok(Event::Start(e)) => return types::Endnotes::from_xml(&mut reader, &e, false),
2028 Ok(Event::Empty(e)) => return types::Endnotes::from_xml(&mut reader, &e, true),
2029 Ok(Event::Eof) => break,
2030 Err(e) => return Err(ParseError::Xml(e)),
2031 _ => {}
2032 }
2033 buf.clear();
2034 }
2035 Err(ParseError::UnexpectedElement(
2036 "no endnotes element found".to_string(),
2037 ))
2038}
2039
2040pub fn parse_comments(xml: &[u8]) -> Result<types::Comments, ParseError> {
2042 let mut reader = Reader::from_reader(Cursor::new(xml));
2043 let mut buf = Vec::new();
2044
2045 loop {
2046 match reader.read_event_into(&mut buf) {
2047 Ok(Event::Start(e)) => return types::Comments::from_xml(&mut reader, &e, false),
2048 Ok(Event::Empty(e)) => return types::Comments::from_xml(&mut reader, &e, true),
2049 Ok(Event::Eof) => break,
2050 Err(e) => return Err(ParseError::Xml(e)),
2051 _ => {}
2052 }
2053 buf.clear();
2054 }
2055 Err(ParseError::UnexpectedElement(
2056 "no comments element found".to_string(),
2057 ))
2058}
2059
2060#[cfg(feature = "wml-charts")]
2067pub(crate) fn parse_chart(xml: &[u8]) -> Result<ooxml_dml::types::ChartSpace, ParseError> {
2068 use ooxml_dml::parsers::FromXml as DmlFromXml;
2069 let mut reader = Reader::from_reader(Cursor::new(xml));
2070 let mut buf = Vec::new();
2071
2072 loop {
2073 match reader.read_event_into(&mut buf) {
2074 Ok(Event::Start(e)) => {
2075 return ooxml_dml::types::ChartSpace::from_xml(&mut reader, &e, false)
2076 .map_err(|e| ParseError::UnexpectedElement(e.to_string()));
2077 }
2078 Ok(Event::Empty(e)) => {
2079 return ooxml_dml::types::ChartSpace::from_xml(&mut reader, &e, true)
2080 .map_err(|e| ParseError::UnexpectedElement(e.to_string()));
2081 }
2082 Ok(Event::Eof) => break,
2083 Err(e) => return Err(ParseError::Xml(e)),
2084 _ => {}
2085 }
2086 buf.clear();
2087 }
2088 Err(ParseError::UnexpectedElement(
2089 "no chartSpace element found".to_string(),
2090 ))
2091}
2092
2093#[cfg(feature = "wml-styling")]
2118pub struct ResolvedDocument {
2119 document: types::Document,
2120 context: StyleContext,
2121}
2122
2123#[cfg(feature = "wml-styling")]
2124impl ResolvedDocument {
2125 pub fn new(document: types::Document, styles: types::Styles) -> Self {
2127 let context = StyleContext::from_styles(&styles);
2128 Self { document, context }
2129 }
2130
2131 pub fn with_context(document: types::Document, context: StyleContext) -> Self {
2133 Self { document, context }
2134 }
2135
2136 pub fn document(&self) -> &types::Document {
2138 &self.document
2139 }
2140
2141 pub fn context(&self) -> &StyleContext {
2143 &self.context
2144 }
2145
2146 pub fn body(&self) -> Option<&types::Body> {
2148 self.document.body()
2149 }
2150
2151 pub fn text(&self) -> String {
2153 self.document.body().map(|b| b.text()).unwrap_or_default()
2154 }
2155
2156 pub fn is_bold(&self, run: &types::Run) -> bool {
2158 run.resolved_is_bold(&self.context)
2159 }
2160
2161 pub fn is_italic(&self, run: &types::Run) -> bool {
2163 run.resolved_is_italic(&self.context)
2164 }
2165
2166 pub fn font_size_half_points(&self, run: &types::Run) -> Option<u32> {
2168 run.resolved_font_size_half_points(&self.context)
2169 }
2170
2171 pub fn font_ascii(&self, run: &types::Run) -> Option<String> {
2173 run.resolved_font_ascii(&self.context)
2174 }
2175
2176 pub fn color_hex(&self, run: &types::Run) -> Option<String> {
2178 run.resolved_color_hex(&self.context)
2179 }
2180
2181 pub fn is_underline(&self, run: &types::Run) -> bool {
2183 run.resolved_is_underline(&self.context)
2184 }
2185
2186 pub fn is_strikethrough(&self, run: &types::Run) -> bool {
2188 run.resolved_is_strikethrough(&self.context)
2189 }
2190
2191 pub fn is_double_strikethrough(&self, run: &types::Run) -> bool {
2193 run.resolved_is_double_strikethrough(&self.context)
2194 }
2195
2196 pub fn is_all_caps(&self, run: &types::Run) -> bool {
2198 run.resolved_is_all_caps(&self.context)
2199 }
2200
2201 pub fn is_small_caps(&self, run: &types::Run) -> bool {
2203 run.resolved_is_small_caps(&self.context)
2204 }
2205
2206 pub fn is_hidden(&self, run: &types::Run) -> bool {
2208 run.resolved_is_hidden(&self.context)
2209 }
2210
2211 pub fn highlight_color(&self, run: &types::Run) -> Option<types::STHighlightColor> {
2213 run.resolved_highlight_color(&self.context)
2214 }
2215
2216 pub fn vertical_alignment(&self, run: &types::Run) -> Option<types::STVerticalAlignRun> {
2218 run.resolved_vertical_alignment(&self.context)
2219 }
2220}
2221
2222#[cfg(feature = "wml-track-changes")]
2228#[derive(Debug, Clone, PartialEq, Eq)]
2229pub enum TrackChangeType {
2230 Insertion,
2232 Deletion,
2234 MoveFrom,
2236 MoveTo,
2238}
2239
2240#[cfg(feature = "wml-track-changes")]
2242#[derive(Debug, Clone)]
2243pub struct TrackChange {
2244 pub id: i64,
2246 pub author: String,
2248 pub date: Option<String>,
2250 pub change_type: TrackChangeType,
2252 pub text: String,
2254}
2255
2256#[cfg(feature = "wml-track-changes")]
2258pub trait RevisionExt {
2259 fn track_changes(&self) -> Vec<TrackChange>;
2261
2262 fn accepted_text(&self) -> String;
2265
2266 fn rejected_text(&self) -> String;
2269
2270 fn has_track_changes(&self) -> bool;
2272}
2273
2274#[cfg(feature = "wml-track-changes")]
2276fn text_from_run_track_change(tc: &types::CTRunTrackChange) -> String {
2277 let mut out = String::new();
2278 for item in &tc.run_content {
2279 if let types::RunContentChoice::R(run) = item {
2280 for rc in &run.run_content {
2281 match rc {
2282 types::RunContent::T(t) => {
2283 if let Some(ref s) = t.text {
2284 out.push_str(s);
2285 }
2286 }
2287 types::RunContent::Tab(_) => out.push('\t'),
2288 types::RunContent::Cr(_) => out.push('\n'),
2289 types::RunContent::Br(br) => {
2290 if !matches!(
2291 br.r#type,
2292 Some(types::STBrType::Page) | Some(types::STBrType::Column)
2293 ) {
2294 out.push('\n');
2295 }
2296 }
2297 types::RunContent::DelText(t) => {
2299 if let Some(ref s) = t.text {
2300 out.push_str(s);
2301 }
2302 }
2303 _ => {}
2304 }
2305 }
2306 }
2307 }
2308 out
2309}
2310
2311#[cfg(feature = "wml-track-changes")]
2312impl RevisionExt for types::Paragraph {
2313 fn track_changes(&self) -> Vec<TrackChange> {
2314 let mut result = Vec::new();
2315 for item in &self.paragraph_content {
2316 let (tc, change_type) = match item {
2317 types::ParagraphContent::Ins(tc) => (tc.as_ref(), TrackChangeType::Insertion),
2318 types::ParagraphContent::Del(tc) => (tc.as_ref(), TrackChangeType::Deletion),
2319 types::ParagraphContent::MoveFrom(tc) => (tc.as_ref(), TrackChangeType::MoveFrom),
2320 types::ParagraphContent::MoveTo(tc) => (tc.as_ref(), TrackChangeType::MoveTo),
2321 _ => continue,
2322 };
2323 result.push(TrackChange {
2324 id: tc.id,
2325 author: tc.author.clone(),
2326 date: tc.date.clone(),
2327 change_type,
2328 text: text_from_run_track_change(tc),
2329 });
2330 }
2331 result
2332 }
2333
2334 fn accepted_text(&self) -> String {
2335 let mut out = String::new();
2336 for item in &self.paragraph_content {
2337 match item {
2338 types::ParagraphContent::R(r) => {
2340 out.push_str(&r.text());
2341 }
2342 types::ParagraphContent::Ins(tc) | types::ParagraphContent::MoveTo(tc) => {
2344 out.push_str(&text_from_run_track_change(tc));
2345 }
2346 types::ParagraphContent::Del(_) | types::ParagraphContent::MoveFrom(_) => {}
2348 types::ParagraphContent::Hyperlink(h) => {
2350 for inner in &h.paragraph_content {
2351 collect_text_from_paragraph_content(inner, &mut out);
2352 }
2353 }
2354 types::ParagraphContent::FldSimple(f) => {
2355 for inner in &f.paragraph_content {
2356 collect_text_from_paragraph_content(inner, &mut out);
2357 }
2358 }
2359 _ => {}
2360 }
2361 }
2362 out
2363 }
2364
2365 fn rejected_text(&self) -> String {
2366 let mut out = String::new();
2367 for item in &self.paragraph_content {
2368 match item {
2369 types::ParagraphContent::R(r) => {
2371 out.push_str(&r.text());
2372 }
2373 types::ParagraphContent::Ins(_) | types::ParagraphContent::MoveTo(_) => {}
2375 types::ParagraphContent::Del(tc) | types::ParagraphContent::MoveFrom(tc) => {
2377 out.push_str(&text_from_run_track_change(tc));
2378 }
2379 types::ParagraphContent::Hyperlink(h) => {
2381 for inner in &h.paragraph_content {
2382 collect_text_from_paragraph_content(inner, &mut out);
2383 }
2384 }
2385 types::ParagraphContent::FldSimple(f) => {
2386 for inner in &f.paragraph_content {
2387 collect_text_from_paragraph_content(inner, &mut out);
2388 }
2389 }
2390 _ => {}
2391 }
2392 }
2393 out
2394 }
2395
2396 fn has_track_changes(&self) -> bool {
2397 self.paragraph_content.iter().any(|item| {
2398 matches!(
2399 item,
2400 types::ParagraphContent::Ins(_)
2401 | types::ParagraphContent::Del(_)
2402 | types::ParagraphContent::MoveFrom(_)
2403 | types::ParagraphContent::MoveTo(_)
2404 )
2405 })
2406 }
2407}
2408
2409#[cfg(feature = "wml-track-changes")]
2411pub trait BodyRevisionExt {
2412 fn all_track_changes(&self) -> Vec<TrackChange>;
2414
2415 fn accepted_text(&self) -> String;
2417
2418 fn rejected_text(&self) -> String;
2420}
2421
2422#[cfg(feature = "wml-track-changes")]
2424fn paragraphs_from_block_content(blocks: &[types::BlockContent]) -> Vec<&types::Paragraph> {
2425 let mut result = Vec::new();
2426 for block in blocks {
2427 match block {
2428 types::BlockContent::P(p) => result.push(p.as_ref()),
2429 types::BlockContent::Tbl(t) => {
2430 for row in &t.rows {
2431 if let types::RowContent::Tr(tr) = row {
2432 for cell in &tr.cells {
2433 if let types::CellContent::Tc(tc) = cell {
2434 result.extend(paragraphs_from_block_content(&tc.block_content));
2435 }
2436 }
2437 }
2438 }
2439 }
2440 types::BlockContent::Sdt(sdt) => {
2441 if let Some(content) = &sdt.sdt_content {
2442 for inner in &content.block_content {
2443 if let types::BlockContentChoice::P(p) = inner {
2444 result.push(p.as_ref());
2445 }
2446 }
2447 }
2448 }
2449 _ => {}
2450 }
2451 }
2452 result
2453}
2454
2455#[cfg(feature = "wml-track-changes")]
2456impl BodyRevisionExt for types::Body {
2457 fn all_track_changes(&self) -> Vec<TrackChange> {
2458 paragraphs_from_block_content(&self.block_content)
2459 .into_iter()
2460 .flat_map(|p| p.track_changes())
2461 .collect()
2462 }
2463
2464 fn accepted_text(&self) -> String {
2465 let paras = paragraphs_from_block_content(&self.block_content);
2466 paras
2467 .iter()
2468 .map(|p| p.accepted_text())
2469 .collect::<Vec<_>>()
2470 .join("\n")
2471 }
2472
2473 fn rejected_text(&self) -> String {
2474 let paras = paragraphs_from_block_content(&self.block_content);
2475 paras
2476 .iter()
2477 .map(|p| p.rejected_text())
2478 .collect::<Vec<_>>()
2479 .join("\n")
2480 }
2481}
2482
2483#[cfg(feature = "wml-settings")]
2491#[derive(Debug, Clone, PartialEq)]
2492pub enum FormFieldType {
2493 PlainText { multi_line: bool },
2497 RichText,
2499 ComboBox { choices: Vec<String> },
2501 DropDownList { choices: Vec<String> },
2503 DatePicker { format: Option<String> },
2505 Unknown,
2507}
2508
2509#[cfg(feature = "wml-settings")]
2514#[derive(Debug, Clone)]
2515pub struct FormField {
2516 pub alias: Option<String>,
2518 pub tag: Option<String>,
2520 pub field_type: FormFieldType,
2522 pub current_value: String,
2524}
2525
2526#[cfg(feature = "wml-settings")]
2530fn sdt_pr_to_form_field(sdt_pr: &types::CTSdtPr, current_value: String) -> FormField {
2531 let alias = sdt_pr.alias.as_ref().map(|a| a.value.clone());
2532 let tag = sdt_pr.tag.as_ref().map(|t| t.value.clone());
2533
2534 let field_type = if let Some(text_elem) = &sdt_pr.text {
2535 let multi_line = text_elem
2536 .multi_line
2537 .as_deref()
2538 .map(|v| matches!(v, "1" | "true" | "on"))
2539 .unwrap_or(false);
2540 FormFieldType::PlainText { multi_line }
2541 } else if sdt_pr.rich_text.is_some() {
2542 FormFieldType::RichText
2543 } else if let Some(cb) = &sdt_pr.combo_box {
2544 let choices = cb
2545 .list_item
2546 .iter()
2547 .map(|item| {
2548 item.display_text
2549 .clone()
2550 .or_else(|| item.value.clone())
2551 .unwrap_or_default()
2552 })
2553 .collect();
2554 FormFieldType::ComboBox { choices }
2555 } else if let Some(dd) = &sdt_pr.drop_down_list {
2556 let choices = dd
2557 .list_item
2558 .iter()
2559 .map(|item| {
2560 item.display_text
2561 .clone()
2562 .or_else(|| item.value.clone())
2563 .unwrap_or_default()
2564 })
2565 .collect();
2566 FormFieldType::DropDownList { choices }
2567 } else if let Some(date) = &sdt_pr.date {
2568 let format = date.date_format.as_ref().map(|df| df.value.clone());
2569 FormFieldType::DatePicker { format }
2570 } else {
2571 FormFieldType::Unknown
2572 };
2573
2574 FormField {
2575 alias,
2576 tag,
2577 field_type,
2578 current_value,
2579 }
2580}
2581
2582#[cfg(feature = "wml-settings")]
2590pub trait FormFieldExt {
2591 fn form_field(&self) -> Option<FormField>;
2593}
2594
2595#[cfg(feature = "wml-settings")]
2596impl FormFieldExt for types::CTSdtBlock {
2597 fn form_field(&self) -> Option<FormField> {
2598 let sdt_pr = self.sdt_pr.as_deref()?;
2599 let value = extract_text_from_block_sdt_content(self.sdt_content.as_deref());
2600 Some(sdt_pr_to_form_field(sdt_pr, value))
2601 }
2602}
2603
2604#[cfg(feature = "wml-settings")]
2605impl FormFieldExt for types::CTSdtRun {
2606 fn form_field(&self) -> Option<FormField> {
2607 let sdt_pr = self.sdt_pr.as_deref()?;
2608 let value = extract_text_from_run_sdt_content(self.sdt_content.as_deref());
2609 Some(sdt_pr_to_form_field(sdt_pr, value))
2610 }
2611}
2612
2613#[cfg(feature = "wml-settings")]
2616fn extract_text_from_block_sdt_content(content: Option<&types::CTSdtContentBlock>) -> String {
2617 let content = match content {
2618 Some(c) => c,
2619 None => return String::new(),
2620 };
2621 let parts: Vec<String> = content
2622 .block_content
2623 .iter()
2624 .filter_map(|bc| match bc {
2625 types::BlockContentChoice::P(p) => Some(p.text()),
2626 types::BlockContentChoice::Tbl(t) => Some(t.text()),
2627 _ => None,
2628 })
2629 .collect();
2630 parts.join("\n")
2631}
2632
2633#[cfg(feature = "wml-settings")]
2635fn extract_text_from_run_sdt_content(content: Option<&types::CTSdtContentRun>) -> String {
2636 let content = match content {
2637 Some(c) => c,
2638 None => return String::new(),
2639 };
2640 let mut out = String::new();
2641 for item in &content.paragraph_content {
2642 collect_text_from_paragraph_content(item, &mut out);
2643 }
2644 out
2645}
2646
2647#[cfg(feature = "wml-settings")]
2650fn collect_form_fields_from_block_content(blocks: &[types::BlockContent]) -> Vec<FormField> {
2651 let mut result = Vec::new();
2652 for block in blocks {
2653 match block {
2654 types::BlockContent::Sdt(sdt) => {
2655 if let Some(field) = sdt.form_field() {
2656 result.push(field);
2657 }
2658 if let Some(content) = &sdt.sdt_content {
2660 for inner in &content.block_content {
2661 collect_form_fields_from_block_content_choice(inner, &mut result);
2662 }
2663 }
2664 }
2665 types::BlockContent::Tbl(t) => {
2666 for row in &t.rows {
2667 if let types::RowContent::Tr(tr) = row {
2668 for cell_content in &tr.cells {
2669 if let types::CellContent::Tc(tc) = cell_content {
2670 result.extend(collect_form_fields_from_block_content(
2671 &tc.block_content,
2672 ));
2673 }
2674 }
2675 }
2676 }
2677 }
2678 types::BlockContent::P(para) => {
2679 for item in ¶.paragraph_content {
2681 if let types::ParagraphContent::Sdt(sdt_run) = item
2682 && let Some(field) = sdt_run.form_field()
2683 {
2684 result.push(field);
2685 }
2686 }
2687 }
2688 _ => {}
2689 }
2690 }
2691 result
2692}
2693
2694#[cfg(feature = "wml-settings")]
2696fn collect_form_fields_from_block_content_choice(
2697 item: &types::BlockContentChoice,
2698 result: &mut Vec<FormField>,
2699) {
2700 match item {
2701 types::BlockContentChoice::Sdt(sdt) => {
2702 if let Some(field) = sdt.form_field() {
2703 result.push(field);
2704 }
2705 }
2706 types::BlockContentChoice::P(para) => {
2707 for pc in ¶.paragraph_content {
2708 if let types::ParagraphContent::Sdt(sdt_run) = pc
2709 && let Some(field) = sdt_run.form_field()
2710 {
2711 result.push(field);
2712 }
2713 }
2714 }
2715 _ => {}
2716 }
2717}
2718
2719#[cfg(test)]
2724mod tests {
2725 use super::*;
2726
2727 #[test]
2732 fn test_is_on_none() {
2733 assert!(!is_on(&None));
2734 }
2735
2736 #[test]
2737 fn test_is_on_present_no_val() {
2738 let field = Some(Box::new(types::OnOffElement {
2740 value: None,
2741 #[cfg(feature = "extra-attrs")]
2742 extra_attrs: Default::default(),
2743 }));
2744 assert!(is_on(&field));
2745 }
2746
2747 #[test]
2748 fn test_is_on_explicit_true() {
2749 for val in &["1", "true", "on"] {
2750 let field = Some(Box::new(types::OnOffElement {
2751 value: Some(val.to_string()),
2752 #[cfg(feature = "extra-attrs")]
2753 extra_attrs: Default::default(),
2754 }));
2755 assert!(is_on(&field), "expected is_on for val={val}");
2756 }
2757 }
2758
2759 #[test]
2760 fn test_is_on_explicit_false() {
2761 for val in &["0", "false", "off"] {
2762 let field = Some(Box::new(types::OnOffElement {
2763 value: Some(val.to_string()),
2764 #[cfg(feature = "extra-attrs")]
2765 extra_attrs: Default::default(),
2766 }));
2767 assert!(!is_on(&field), "expected !is_on for val={val}");
2768 }
2769 }
2770
2771 #[test]
2772 fn test_check_toggle_none() {
2773 assert_eq!(check_toggle(&None), None);
2774 }
2775
2776 #[test]
2777 fn test_check_toggle_present() {
2778 let field = Some(Box::new(types::OnOffElement {
2779 value: None,
2780 #[cfg(feature = "extra-attrs")]
2781 extra_attrs: Default::default(),
2782 }));
2783 assert_eq!(check_toggle(&field), Some(true));
2784 }
2785
2786 #[test]
2787 fn test_parse_half_points() {
2788 assert_eq!(parse_half_points("24"), Some(24));
2789 assert_eq!(parse_half_points("0"), Some(0));
2790 assert_eq!(parse_half_points("abc"), None);
2791 assert_eq!(parse_half_points(""), None);
2792 }
2793
2794 #[cfg(feature = "wml-styling")]
2799 fn make_run_properties() -> types::RunProperties {
2800 types::RunProperties {
2801 run_style: None,
2802 fonts: None,
2803 bold: None,
2804 b_cs: None,
2805 italic: None,
2806 i_cs: None,
2807 caps: None,
2808 small_caps: None,
2809 strikethrough: None,
2810 dstrike: None,
2811 outline: None,
2812 shadow: None,
2813 emboss: None,
2814 imprint: None,
2815 no_proof: None,
2816 snap_to_grid: None,
2817 vanish: None,
2818 web_hidden: None,
2819 color: None,
2820 spacing: None,
2821 width: None,
2822 kern: None,
2823 position: None,
2824 size: None,
2825 size_complex_script: None,
2826 highlight: None,
2827 underline: None,
2828 effect: None,
2829 bdr: None,
2830 shading: None,
2831 fit_text: None,
2832 vert_align: None,
2833 rtl: None,
2834 cs: None,
2835 em: None,
2836 lang: None,
2837 east_asian_layout: None,
2838 spec_vanish: None,
2839 o_math: None,
2840 r_pr_change: None,
2841 #[cfg(feature = "extra-children")]
2842 extra_children: Default::default(),
2843 }
2844 }
2845
2846 #[cfg(feature = "wml-styling")]
2847 fn on_off(val: Option<&str>) -> Option<Box<types::OnOffElement>> {
2848 Some(Box::new(types::OnOffElement {
2849 value: val.map(|v| v.to_string()),
2850 #[cfg(feature = "extra-attrs")]
2851 extra_attrs: Default::default(),
2852 }))
2853 }
2854
2855 #[test]
2856 #[cfg(feature = "wml-styling")]
2857 fn test_rpr_bold_italic() {
2858 let mut rpr = make_run_properties();
2859 assert!(!rpr.is_bold());
2860 assert!(!rpr.is_italic());
2861
2862 rpr.bold = on_off(None); rpr.italic = on_off(Some("true"));
2864 assert!(rpr.is_bold());
2865 assert!(rpr.is_italic());
2866 }
2867
2868 #[test]
2869 #[cfg(feature = "wml-styling")]
2870 fn test_rpr_underline() {
2871 let mut rpr = make_run_properties();
2872 assert!(!rpr.is_underline());
2873 assert!(rpr.underline_style().is_none());
2874
2875 rpr.underline = Some(Box::new(types::CTUnderline {
2876 value: Some(types::STUnderline::Single),
2877 color: None,
2878 theme_color: None,
2879 theme_tint: None,
2880 theme_shade: None,
2881 #[cfg(feature = "extra-attrs")]
2882 extra_attrs: Default::default(),
2883 }));
2884 assert!(rpr.is_underline());
2885 assert_eq!(rpr.underline_style(), Some(&types::STUnderline::Single));
2886
2887 rpr.underline = Some(Box::new(types::CTUnderline {
2889 value: Some(types::STUnderline::None),
2890 color: None,
2891 theme_color: None,
2892 theme_tint: None,
2893 theme_shade: None,
2894 #[cfg(feature = "extra-attrs")]
2895 extra_attrs: Default::default(),
2896 }));
2897 assert!(!rpr.is_underline());
2898 }
2899
2900 #[test]
2901 #[cfg(feature = "wml-styling")]
2902 fn test_rpr_strikethrough() {
2903 let mut rpr = make_run_properties();
2904 rpr.strikethrough = on_off(None);
2905 assert!(rpr.is_strikethrough());
2906 assert!(!rpr.is_double_strikethrough());
2907
2908 rpr.strikethrough = None;
2909 rpr.dstrike = on_off(Some("1"));
2910 assert!(!rpr.is_strikethrough());
2911 assert!(rpr.is_double_strikethrough());
2912 }
2913
2914 #[test]
2915 #[cfg(feature = "wml-styling")]
2916 fn test_rpr_caps_hidden() {
2917 let mut rpr = make_run_properties();
2918 rpr.caps = on_off(None);
2919 rpr.vanish = on_off(Some("1"));
2920 assert!(rpr.is_all_caps());
2921 assert!(!rpr.is_small_caps());
2922 assert!(rpr.is_hidden());
2923 }
2924
2925 #[test]
2926 #[cfg(feature = "wml-styling")]
2927 fn test_rpr_font_size() {
2928 let mut rpr = make_run_properties();
2929 assert!(rpr.font_size_half_points().is_none());
2930
2931 rpr.size = Some(Box::new(types::HpsMeasureElement {
2932 value: "24".to_string(),
2933 #[cfg(feature = "extra-attrs")]
2934 extra_attrs: Default::default(),
2935 }));
2936 assert_eq!(rpr.font_size_half_points(), Some(24));
2937 assert_eq!(rpr.font_size_points(), Some(12.0));
2938 }
2939
2940 #[test]
2941 #[cfg(feature = "wml-styling")]
2942 fn test_rpr_color() {
2943 let mut rpr = make_run_properties();
2944 assert!(rpr.color_hex().is_none());
2945
2946 rpr.color = Some(Box::new(types::CTColor {
2947 value: "FF0000".to_string(),
2948 theme_color: None,
2949 theme_tint: None,
2950 theme_shade: None,
2951 #[cfg(feature = "extra-attrs")]
2952 extra_attrs: Default::default(),
2953 }));
2954 assert_eq!(rpr.color_hex(), Some("FF0000"));
2955 }
2956
2957 #[test]
2958 #[cfg(feature = "wml-styling")]
2959 fn test_rpr_vertical_alignment() {
2960 let mut rpr = make_run_properties();
2961 assert!(!rpr.is_superscript());
2962 assert!(!rpr.is_subscript());
2963
2964 rpr.vert_align = Some(Box::new(types::CTVerticalAlignRun {
2965 value: types::STVerticalAlignRun::Superscript,
2966 #[cfg(feature = "extra-attrs")]
2967 extra_attrs: Default::default(),
2968 }));
2969 assert!(rpr.is_superscript());
2970 assert!(!rpr.is_subscript());
2971 }
2972
2973 #[test]
2974 #[cfg(feature = "wml-styling")]
2975 fn test_rpr_font_ascii() {
2976 let mut rpr = make_run_properties();
2977 assert!(rpr.font_ascii().is_none());
2978
2979 rpr.fonts = Some(Box::new(types::Fonts {
2980 hint: None,
2981 ascii: Some("Arial".to_string()),
2982 h_ansi: None,
2983 east_asia: None,
2984 cs: None,
2985 ascii_theme: None,
2986 h_ansi_theme: None,
2987 east_asia_theme: None,
2988 cstheme: None,
2989 #[cfg(feature = "extra-attrs")]
2990 extra_attrs: Default::default(),
2991 }));
2992 assert_eq!(rpr.font_ascii(), Some("Arial"));
2993 }
2994
2995 fn make_text(s: &str) -> types::RunContent {
3000 types::RunContent::T(Box::new(types::Text {
3001 text: Some(s.to_string()),
3002 #[cfg(feature = "extra-children")]
3003 extra_children: Default::default(),
3004 }))
3005 }
3006
3007 fn make_tab() -> types::RunContent {
3008 types::RunContent::Tab(Box::new(types::CTEmpty))
3009 }
3010
3011 fn make_br(br_type: Option<types::STBrType>) -> types::RunContent {
3012 types::RunContent::Br(Box::new(types::CTBr {
3013 r#type: br_type,
3014 clear: None,
3015 #[cfg(feature = "extra-attrs")]
3016 extra_attrs: Default::default(),
3017 }))
3018 }
3019
3020 fn make_cr() -> types::RunContent {
3021 types::RunContent::Cr(Box::new(types::CTEmpty))
3022 }
3023
3024 fn make_run(content: Vec<types::RunContent>) -> types::Run {
3025 types::Run {
3026 rsid_r_pr: None,
3027 rsid_del: None,
3028 rsid_r: None,
3029 #[cfg(feature = "wml-styling")]
3030 r_pr: None,
3031 run_content: content,
3032 #[cfg(feature = "extra-attrs")]
3033 extra_attrs: Default::default(),
3034 #[cfg(feature = "extra-children")]
3035 extra_children: Default::default(),
3036 }
3037 }
3038
3039 #[test]
3040 fn test_run_text_simple() {
3041 let run = make_run(vec![make_text("Hello"), make_text(" World")]);
3042 assert_eq!(run.text(), "Hello World");
3043 }
3044
3045 #[test]
3046 fn test_run_text_with_tab_and_break() {
3047 let run = make_run(vec![
3048 make_text("A"),
3049 make_tab(),
3050 make_text("B"),
3051 make_br(None), make_text("C"),
3053 ]);
3054 assert_eq!(run.text(), "A\tB\nC");
3055 }
3056
3057 #[test]
3058 fn test_run_text_page_break_not_text() {
3059 let run = make_run(vec![
3060 make_text("Before"),
3061 make_br(Some(types::STBrType::Page)),
3062 make_text("After"),
3063 ]);
3064 assert_eq!(run.text(), "BeforeAfter");
3066 assert!(run.has_page_break());
3067 }
3068
3069 #[test]
3070 fn test_run_text_cr() {
3071 let run = make_run(vec![make_text("A"), make_cr(), make_text("B")]);
3072 assert_eq!(run.text(), "A\nB");
3073 }
3074
3075 #[test]
3076 fn test_run_no_page_break() {
3077 let run = make_run(vec![make_text("Hello")]);
3078 assert!(!run.has_page_break());
3079 }
3080
3081 fn make_p_run(text: &str) -> types::ParagraphContent {
3086 types::ParagraphContent::R(Box::new(make_run(vec![make_text(text)])))
3087 }
3088
3089 fn make_paragraph(content: Vec<types::ParagraphContent>) -> types::Paragraph {
3090 types::Paragraph {
3091 rsid_r_pr: None,
3092 rsid_r: None,
3093 rsid_del: None,
3094 rsid_p: None,
3095 rsid_r_default: None,
3096 #[cfg(feature = "wml-styling")]
3097 p_pr: None,
3098 paragraph_content: content,
3099 #[cfg(feature = "extra-attrs")]
3100 extra_attrs: Default::default(),
3101 #[cfg(feature = "extra-children")]
3102 extra_children: Default::default(),
3103 }
3104 }
3105
3106 #[test]
3107 fn test_paragraph_runs_and_text() {
3108 let para = make_paragraph(vec![make_p_run("Hello "), make_p_run("World")]);
3109 assert_eq!(para.runs().len(), 2);
3110 assert_eq!(para.text(), "Hello World");
3111 }
3112
3113 #[test]
3114 fn test_paragraph_with_hyperlink() {
3115 let hyperlink = types::ParagraphContent::Hyperlink(Box::new(types::Hyperlink {
3116 id: None,
3117 tgt_frame: None,
3118 tooltip: None,
3119 doc_location: None,
3120 history: None,
3121 anchor: Some("bookmark1".to_string()),
3122 paragraph_content: vec![make_p_run("link text")],
3123 #[cfg(feature = "extra-attrs")]
3124 extra_attrs: Default::default(),
3125 #[cfg(feature = "extra-children")]
3126 extra_children: Default::default(),
3127 }));
3128 let para = make_paragraph(vec![make_p_run("Click "), hyperlink]);
3129 assert_eq!(para.runs().len(), 2);
3130 assert_eq!(para.text(), "Click link text");
3131 assert_eq!(para.hyperlinks().len(), 1);
3132 assert_eq!(para.hyperlinks()[0].anchor_str(), Some("bookmark1"));
3133 }
3134
3135 #[test]
3136 fn test_paragraph_with_fld_simple() {
3137 let fld = types::ParagraphContent::FldSimple(Box::new(types::CTSimpleField {
3138 instr: "PAGE".to_string(),
3139 fld_lock: None,
3140 dirty: None,
3141 fld_data: None,
3142 paragraph_content: vec![make_p_run("1")],
3143 #[cfg(feature = "extra-attrs")]
3144 extra_attrs: Default::default(),
3145 #[cfg(feature = "extra-children")]
3146 extra_children: Default::default(),
3147 }));
3148 let para = make_paragraph(vec![make_p_run("Page "), fld]);
3149 assert_eq!(para.runs().len(), 2);
3150 assert_eq!(para.text(), "Page 1");
3151 }
3152
3153 fn make_body(content: Vec<types::BlockContent>) -> types::Body {
3158 types::Body {
3159 block_content: content,
3160 #[cfg(feature = "wml-layout")]
3161 sect_pr: None,
3162 #[cfg(feature = "extra-children")]
3163 extra_children: Default::default(),
3164 }
3165 }
3166
3167 #[test]
3168 fn test_body_paragraphs() {
3169 let p1 = types::BlockContent::P(Box::new(make_paragraph(vec![make_p_run("First")])));
3170 let p2 = types::BlockContent::P(Box::new(make_paragraph(vec![make_p_run("Second")])));
3171 let body = make_body(vec![p1, p2]);
3172 assert_eq!(body.paragraphs().len(), 2);
3173 assert_eq!(body.text(), "First\nSecond");
3174 }
3175
3176 #[test]
3177 fn test_body_tables() {
3178 let tbl = types::BlockContent::Tbl(Box::new(types::Table {
3179 range_markup: vec![],
3180 table_properties: Box::default(),
3181 tbl_grid: Box::default(),
3182 rows: vec![],
3183 #[cfg(feature = "extra-children")]
3184 extra_children: Default::default(),
3185 }));
3186 let body = make_body(vec![tbl]);
3187 assert_eq!(body.tables().len(), 1);
3188 assert_eq!(body.paragraphs().len(), 0);
3189 }
3190
3191 #[test]
3196 fn test_document_ext_body() {
3197 let doc = types::Document {
3198 background: None,
3199 body: Some(Box::new(make_body(vec![]))),
3200 conformance: None,
3201 #[cfg(feature = "extra-attrs")]
3202 extra_attrs: Default::default(),
3203 #[cfg(feature = "extra-children")]
3204 extra_children: Default::default(),
3205 };
3206 assert!(doc.body().is_some());
3207
3208 let doc_no_body = types::Document {
3209 background: None,
3210 body: None,
3211 conformance: None,
3212 #[cfg(feature = "extra-attrs")]
3213 extra_attrs: Default::default(),
3214 #[cfg(feature = "extra-children")]
3215 extra_children: Default::default(),
3216 };
3217 assert!(doc_no_body.body().is_none());
3218 }
3219
3220 #[test]
3225 fn test_hyperlink_ext() {
3226 let h = types::Hyperlink {
3227 id: None,
3228 tgt_frame: None,
3229 tooltip: None,
3230 doc_location: None,
3231 history: None,
3232 anchor: Some("top".to_string()),
3233 paragraph_content: vec![make_p_run("click"), make_p_run(" here")],
3234 #[cfg(feature = "extra-attrs")]
3235 extra_attrs: Default::default(),
3236 #[cfg(feature = "extra-children")]
3237 extra_children: Default::default(),
3238 };
3239 assert_eq!(h.runs().len(), 2);
3240 assert_eq!(h.text(), "click here");
3241 assert_eq!(h.anchor_str(), Some("top"));
3242 }
3243
3244 fn make_table_cell(text: &str) -> types::CellContent {
3249 types::CellContent::Tc(Box::new(types::TableCell {
3250 id: None,
3251 cell_properties: None,
3252 block_content: vec![types::BlockContent::P(Box::new(make_paragraph(vec![
3253 make_p_run(text),
3254 ])))],
3255 #[cfg(feature = "extra-attrs")]
3256 extra_attrs: Default::default(),
3257 #[cfg(feature = "extra-children")]
3258 extra_children: Default::default(),
3259 }))
3260 }
3261
3262 fn make_table_row(cells: Vec<types::CellContent>) -> types::RowContent {
3263 types::RowContent::Tr(Box::new(types::CTRow {
3264 rsid_r_pr: None,
3265 rsid_r: None,
3266 rsid_del: None,
3267 rsid_tr: None,
3268 tbl_pr_ex: None,
3269 row_properties: None,
3270 cells,
3271 #[cfg(feature = "extra-attrs")]
3272 extra_attrs: Default::default(),
3273 #[cfg(feature = "extra-children")]
3274 extra_children: Default::default(),
3275 }))
3276 }
3277
3278 fn make_table(rows: Vec<types::RowContent>) -> types::Table {
3279 types::Table {
3280 range_markup: vec![],
3281 table_properties: Box::default(),
3282 tbl_grid: Box::default(),
3283 rows,
3284 #[cfg(feature = "extra-children")]
3285 extra_children: Default::default(),
3286 }
3287 }
3288
3289 #[test]
3290 fn test_table_rows_and_text() {
3291 let tbl = make_table(vec![
3292 make_table_row(vec![make_table_cell("A1"), make_table_cell("B1")]),
3293 make_table_row(vec![make_table_cell("A2"), make_table_cell("B2")]),
3294 ]);
3295 assert_eq!(tbl.row_count(), 2);
3296 assert_eq!(tbl.rows().len(), 2);
3297 assert_eq!(tbl.text(), "A1\tB1\nA2\tB2");
3298 }
3299
3300 #[test]
3301 fn test_row_cells_and_text() {
3302 let row = types::CTRow {
3303 rsid_r_pr: None,
3304 rsid_r: None,
3305 rsid_del: None,
3306 rsid_tr: None,
3307 tbl_pr_ex: None,
3308 row_properties: None,
3309 cells: vec![make_table_cell("X"), make_table_cell("Y")],
3310 #[cfg(feature = "extra-attrs")]
3311 extra_attrs: Default::default(),
3312 #[cfg(feature = "extra-children")]
3313 extra_children: Default::default(),
3314 };
3315 assert_eq!(row.cells().len(), 2);
3316 assert_eq!(row.text(), "X\tY");
3317 }
3318
3319 #[test]
3320 fn test_cell_paragraphs_and_text() {
3321 let cell = types::TableCell {
3322 id: None,
3323 cell_properties: None,
3324 block_content: vec![
3325 types::BlockContent::P(Box::new(make_paragraph(vec![make_p_run("Line 1")]))),
3326 types::BlockContent::P(Box::new(make_paragraph(vec![make_p_run("Line 2")]))),
3327 ],
3328 #[cfg(feature = "extra-attrs")]
3329 extra_attrs: Default::default(),
3330 #[cfg(feature = "extra-children")]
3331 extra_children: Default::default(),
3332 };
3333 assert_eq!(cell.paragraphs().len(), 2);
3334 assert_eq!(cell.text(), "Line 1\nLine 2");
3335 }
3336
3337 #[test]
3342 #[cfg(feature = "wml-layout")]
3343 fn test_section_properties_ext() {
3344 let sect_pr = types::SectionProperties {
3345 rsid_r_pr: None,
3346 rsid_del: None,
3347 rsid_r: None,
3348 rsid_sect: None,
3349 header_footer_refs: vec![],
3350 footnote_pr: None,
3351 endnote_pr: None,
3352 r#type: None,
3353 pg_sz: Some(Box::new(types::PageSize {
3354 width: Some("12240".to_string()),
3355 height: Some("15840".to_string()),
3356 orient: Some(types::STPageOrientation::Portrait),
3357 code: None,
3358 #[cfg(feature = "extra-attrs")]
3359 extra_attrs: Default::default(),
3360 })),
3361 pg_mar: Some(Box::new(types::PageMargins {
3362 top: "1440".to_string(),
3363 right: "1440".to_string(),
3364 bottom: "1440".to_string(),
3365 left: "1440".to_string(),
3366 header: "720".to_string(),
3367 footer: "720".to_string(),
3368 gutter: "0".to_string(),
3369 #[cfg(feature = "extra-attrs")]
3370 extra_attrs: Default::default(),
3371 })),
3372 paper_src: None,
3373 pg_borders: None,
3374 ln_num_type: None,
3375 pg_num_type: None,
3376 cols: None,
3377 form_prot: None,
3378 v_align: None,
3379 no_endnote: None,
3380 title_pg: on_off(None),
3381 text_direction: None,
3382 bidi: None,
3383 rtl_gutter: None,
3384 doc_grid: None,
3385 printer_settings: None,
3386 sect_pr_change: None,
3387 #[cfg(feature = "extra-attrs")]
3388 extra_attrs: Default::default(),
3389 #[cfg(feature = "extra-children")]
3390 extra_children: Default::default(),
3391 };
3392
3393 assert_eq!(sect_pr.page_width_twips(), Some(12240));
3394 assert_eq!(sect_pr.page_height_twips(), Some(15840));
3395 assert_eq!(
3396 sect_pr.page_orientation(),
3397 Some(&types::STPageOrientation::Portrait)
3398 );
3399 assert!(sect_pr.has_title_page());
3400 assert!(sect_pr.page_size().is_some());
3401 assert!(sect_pr.page_margins().is_some());
3402 }
3403
3404 #[test]
3409 fn test_parse_document_simple() {
3410 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
3412 <document xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3413 <body>
3414 <p>
3415 <r>
3416 <t>Hello World</t>
3417 </r>
3418 </p>
3419 </body>
3420 </document>"#;
3421
3422 let doc = parse_document(xml).expect("parse_document failed");
3423 let body = doc.body().expect("body should exist");
3424 let paragraphs = body.paragraphs();
3425 assert_eq!(paragraphs.len(), 1);
3426 assert_eq!(paragraphs[0].text(), "Hello World");
3427 }
3428
3429 #[test]
3430 fn test_parse_document_multiple_paragraphs() {
3431 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
3432 <document xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3433 <body>
3434 <p>
3435 <r><t>First</t></r>
3436 </p>
3437 <p>
3438 <r><t>Second</t></r>
3439 </p>
3440 </body>
3441 </document>"#;
3442
3443 let doc = parse_document(xml).expect("parse failed");
3444 let body = doc.body().expect("body");
3445 assert_eq!(body.paragraphs().len(), 2);
3446 assert_eq!(body.text(), "First\nSecond");
3447 }
3448
3449 #[test]
3450 fn test_parse_styles_basic() {
3451 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
3452 <styles xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3453 <style type="character" styleId="BoldStyle">
3454 <name val="Bold Style"/>
3455 <rPr>
3456 <b/>
3457 </rPr>
3458 </style>
3459 </styles>"#;
3460
3461 let styles = parse_styles(xml).expect("parse_styles failed");
3462 assert_eq!(styles.style.len(), 1);
3463 assert_eq!(styles.style[0].style_id.as_deref(), Some("BoldStyle"));
3464 }
3465
3466 #[test]
3467 fn test_parse_document_no_element() {
3468 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>"#;
3469 assert!(parse_document(xml).is_err());
3470 }
3471
3472 #[test]
3477 #[cfg(feature = "wml-styling")]
3478 fn test_style_context_from_styles() {
3479 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
3480 <styles xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3481 <docDefaults>
3482 <rPrDefault>
3483 <rPr>
3484 <sz val="24"/>
3485 </rPr>
3486 </rPrDefault>
3487 </docDefaults>
3488 <style type="character" styleId="Strong">
3489 <name val="Strong"/>
3490 <rPr>
3491 <b/>
3492 </rPr>
3493 </style>
3494 </styles>"#;
3495
3496 let styles = parse_styles(xml).expect("parse");
3497 let ctx = StyleContext::from_styles(&styles);
3498
3499 assert!(ctx.style("Strong").is_some());
3500 assert!(ctx.style("Nonexistent").is_none());
3501 assert!(ctx.default_run_properties.is_some());
3502 assert_eq!(
3503 ctx.default_run_properties
3504 .as_ref()
3505 .unwrap()
3506 .font_size_half_points(),
3507 Some(24)
3508 );
3509 }
3510
3511 #[test]
3512 #[cfg(feature = "wml-styling")]
3513 fn test_resolve_bold_from_direct() {
3514 let run = types::Run {
3515 rsid_r_pr: None,
3516 rsid_del: None,
3517 rsid_r: None,
3518 r_pr: Some(Box::new({
3519 let mut rpr = make_run_properties();
3520 rpr.bold = on_off(None);
3521 rpr
3522 })),
3523 run_content: vec![make_text("bold")],
3524 #[cfg(feature = "extra-attrs")]
3525 extra_attrs: Default::default(),
3526 #[cfg(feature = "extra-children")]
3527 extra_children: Default::default(),
3528 };
3529
3530 let ctx = StyleContext::default();
3531 assert!(run.resolved_is_bold(&ctx));
3532 assert!(!run.resolved_is_italic(&ctx));
3533 }
3534
3535 #[test]
3536 #[cfg(feature = "wml-styling")]
3537 fn test_resolve_bold_from_style_chain() {
3538 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
3540 <styles xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3541 <style type="character" styleId="Strong">
3542 <name val="Strong"/>
3543 <rPr>
3544 <b/>
3545 <sz val="28"/>
3546 </rPr>
3547 </style>
3548 <style type="character" styleId="Emphasis">
3549 <name val="Emphasis"/>
3550 <basedOn val="Strong"/>
3551 <rPr>
3552 <i/>
3553 </rPr>
3554 </style>
3555 </styles>"#;
3556
3557 let styles = parse_styles(xml).expect("parse");
3558 let ctx = StyleContext::from_styles(&styles);
3559
3560 let run = types::Run {
3562 rsid_r_pr: None,
3563 rsid_del: None,
3564 rsid_r: None,
3565 r_pr: Some(Box::new({
3566 let mut rpr = make_run_properties();
3567 rpr.run_style = Some(Box::new(types::CTString {
3568 value: "Emphasis".to_string(),
3569 #[cfg(feature = "extra-attrs")]
3570 extra_attrs: Default::default(),
3571 }));
3572 rpr
3573 })),
3574 run_content: vec![make_text("styled")],
3575 #[cfg(feature = "extra-attrs")]
3576 extra_attrs: Default::default(),
3577 #[cfg(feature = "extra-children")]
3578 extra_children: Default::default(),
3579 };
3580
3581 assert!(run.resolved_is_bold(&ctx));
3582 assert!(run.resolved_is_italic(&ctx));
3583 assert_eq!(run.resolved_font_size_half_points(&ctx), Some(28));
3584 }
3585
3586 #[test]
3587 #[cfg(feature = "wml-styling")]
3588 fn test_resolve_from_doc_defaults() {
3589 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
3590 <styles xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3591 <docDefaults>
3592 <rPrDefault>
3593 <rPr>
3594 <sz val="22"/>
3595 <rFonts ascii="Calibri"/>
3596 </rPr>
3597 </rPrDefault>
3598 </docDefaults>
3599 </styles>"#;
3600
3601 let styles = parse_styles(xml).expect("parse");
3602 let ctx = StyleContext::from_styles(&styles);
3603
3604 let run = types::Run {
3606 rsid_r_pr: None,
3607 rsid_del: None,
3608 rsid_r: None,
3609 r_pr: None,
3610 run_content: vec![make_text("default")],
3611 #[cfg(feature = "extra-attrs")]
3612 extra_attrs: Default::default(),
3613 #[cfg(feature = "extra-children")]
3614 extra_children: Default::default(),
3615 };
3616
3617 assert!(!run.resolved_is_bold(&ctx));
3618 assert_eq!(run.resolved_font_size_half_points(&ctx), Some(22));
3619 assert_eq!(run.resolved_font_ascii(&ctx), Some("Calibri".to_string()));
3620 }
3621
3622 #[test]
3623 #[cfg(feature = "wml-styling")]
3624 fn test_resolved_document() {
3625 let doc_xml = br#"<?xml version="1.0" encoding="UTF-8"?>
3626 <document xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3627 <body>
3628 <p>
3629 <r>
3630 <rPr><b/></rPr>
3631 <t>Bold text</t>
3632 </r>
3633 </p>
3634 </body>
3635 </document>"#;
3636
3637 let styles_xml = br#"<?xml version="1.0" encoding="UTF-8"?>
3638 <styles xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3639 </styles>"#;
3640
3641 let doc = parse_document(doc_xml).expect("parse doc");
3642 let styles = parse_styles(styles_xml).expect("parse styles");
3643 let resolved = ResolvedDocument::new(doc, styles);
3644
3645 assert_eq!(resolved.text(), "Bold text");
3646
3647 let body = resolved.body().expect("body");
3648 let paras = body.paragraphs();
3649 let runs = paras[0].runs();
3650 assert!(resolved.is_bold(runs[0]));
3651 assert!(!resolved.is_italic(runs[0]));
3652 }
3653
3654 #[test]
3659 #[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
3660 fn test_drawing_chart_rel_ids() {
3661 use super::DrawingChartExt;
3662 use ooxml_xml::{PositionedNode, RawXmlElement, RawXmlNode};
3663
3664 let chart = RawXmlElement {
3666 name: "c:chart".to_string(),
3667 attributes: vec![("r:id".to_string(), "rId5".to_string())],
3668 children: vec![],
3669 self_closing: true,
3670 };
3671 let graphic_data = RawXmlElement {
3672 name: "a:graphicData".to_string(),
3673 attributes: vec![(
3674 "uri".to_string(),
3675 "http://schemas.openxmlformats.org/drawingml/2006/chart".to_string(),
3676 )],
3677 children: vec![RawXmlNode::Element(chart)],
3678 self_closing: false,
3679 };
3680 let graphic = RawXmlElement {
3681 name: "a:graphic".to_string(),
3682 attributes: vec![],
3683 children: vec![RawXmlNode::Element(graphic_data)],
3684 self_closing: false,
3685 };
3686 let anchor = RawXmlElement {
3687 name: "wp:anchor".to_string(),
3688 attributes: vec![],
3689 children: vec![RawXmlNode::Element(graphic)],
3690 self_closing: false,
3691 };
3692
3693 let drawing = types::CTDrawing {
3694 extra_children: vec![PositionedNode::new(0, RawXmlNode::Element(anchor))],
3695 };
3696
3697 let ids = drawing.all_chart_rel_ids();
3698 assert_eq!(ids, vec!["rId5"]);
3699
3700 assert_eq!(drawing.anchored_chart_rel_ids(), vec!["rId5"]);
3702 assert!(drawing.inline_chart_rel_ids().is_empty());
3704 }
3705
3706 #[test]
3707 #[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
3708 fn test_drawing_no_charts() {
3709 use super::DrawingChartExt;
3710 use ooxml_xml::{PositionedNode, RawXmlElement, RawXmlNode};
3711
3712 let blip = RawXmlElement {
3714 name: "a:blip".to_string(),
3715 attributes: vec![("r:embed".to_string(), "rId1".to_string())],
3716 children: vec![],
3717 self_closing: true,
3718 };
3719 let anchor = RawXmlElement {
3720 name: "wp:anchor".to_string(),
3721 attributes: vec![],
3722 children: vec![RawXmlNode::Element(blip)],
3723 self_closing: false,
3724 };
3725
3726 let drawing = types::CTDrawing {
3727 extra_children: vec![PositionedNode::new(0, RawXmlNode::Element(anchor))],
3728 };
3729
3730 assert!(drawing.all_chart_rel_ids().is_empty());
3731 }
3732
3733 #[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
3740 fn make_drawing_with_textbox(text: &str) -> types::CTDrawing {
3741 use ooxml_xml::{PositionedNode, RawXmlElement, RawXmlNode};
3742
3743 let t = RawXmlElement {
3759 name: "w:t".to_string(),
3760 attributes: vec![],
3761 children: vec![RawXmlNode::Text(text.to_string())],
3762 self_closing: false,
3763 };
3764 let r = RawXmlElement {
3765 name: "w:r".to_string(),
3766 attributes: vec![],
3767 children: vec![RawXmlNode::Element(t)],
3768 self_closing: false,
3769 };
3770 let p = RawXmlElement {
3771 name: "w:p".to_string(),
3772 attributes: vec![],
3773 children: vec![RawXmlNode::Element(r)],
3774 self_closing: false,
3775 };
3776 let txbx_content = RawXmlElement {
3777 name: "w:txbxContent".to_string(),
3778 attributes: vec![],
3779 children: vec![RawXmlNode::Element(p)],
3780 self_closing: false,
3781 };
3782 let txbx = RawXmlElement {
3783 name: "wps:txbx".to_string(),
3784 attributes: vec![],
3785 children: vec![RawXmlNode::Element(txbx_content)],
3786 self_closing: false,
3787 };
3788 let wsp = RawXmlElement {
3789 name: "wps:wsp".to_string(),
3790 attributes: vec![],
3791 children: vec![RawXmlNode::Element(txbx)],
3792 self_closing: false,
3793 };
3794 let anchor = RawXmlElement {
3795 name: "wp:anchor".to_string(),
3796 attributes: vec![],
3797 children: vec![RawXmlNode::Element(wsp)],
3798 self_closing: false,
3799 };
3800
3801 types::CTDrawing {
3802 extra_children: vec![PositionedNode::new(0, RawXmlNode::Element(anchor))],
3803 }
3804 }
3805
3806 #[test]
3807 #[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
3808 fn test_drawing_text_box_texts_single() {
3809 use super::DrawingTextBoxExt;
3810 let drawing = make_drawing_with_textbox("Hello from text box");
3811 let texts = drawing.text_box_texts();
3812 assert_eq!(texts.len(), 1);
3813 assert_eq!(texts[0], "Hello from text box");
3814 }
3815
3816 #[test]
3817 #[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
3818 fn test_drawing_text_box_texts_empty() {
3819 use super::DrawingTextBoxExt;
3820 let drawing = types::CTDrawing {
3821 extra_children: vec![],
3822 };
3823 let texts = drawing.text_box_texts();
3824 assert!(texts.is_empty());
3825 }
3826
3827 #[test]
3828 #[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
3829 fn test_drawing_text_box_texts_multiple() {
3830 use super::DrawingTextBoxExt;
3831 use ooxml_xml::{PositionedNode, RawXmlElement, RawXmlNode};
3832
3833 fn make_anchor(text: &str) -> RawXmlElement {
3835 let t = RawXmlElement {
3836 name: "w:t".to_string(),
3837 attributes: vec![],
3838 children: vec![RawXmlNode::Text(text.to_string())],
3839 self_closing: false,
3840 };
3841 let r = RawXmlElement {
3842 name: "w:r".to_string(),
3843 attributes: vec![],
3844 children: vec![RawXmlNode::Element(t)],
3845 self_closing: false,
3846 };
3847 let p = RawXmlElement {
3848 name: "w:p".to_string(),
3849 attributes: vec![],
3850 children: vec![RawXmlNode::Element(r)],
3851 self_closing: false,
3852 };
3853 let txbx_content = RawXmlElement {
3854 name: "w:txbxContent".to_string(),
3855 attributes: vec![],
3856 children: vec![RawXmlNode::Element(p)],
3857 self_closing: false,
3858 };
3859 RawXmlElement {
3860 name: "wp:anchor".to_string(),
3861 attributes: vec![],
3862 children: vec![RawXmlNode::Element(txbx_content)],
3863 self_closing: false,
3864 }
3865 }
3866
3867 let drawing = types::CTDrawing {
3868 extra_children: vec![
3869 PositionedNode::new(0, RawXmlNode::Element(make_anchor("First box"))),
3870 PositionedNode::new(1, RawXmlNode::Element(make_anchor("Second box"))),
3871 ],
3872 };
3873
3874 let texts = drawing.text_box_texts();
3875 assert_eq!(texts.len(), 2);
3876 assert_eq!(texts[0], "First box");
3877 assert_eq!(texts[1], "Second box");
3878 }
3879
3880 #[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
3887 fn make_pict_with_textbox(text: &str) -> types::CTPicture {
3888 use ooxml_xml::{PositionedNode, RawXmlElement, RawXmlNode};
3889
3890 let t = RawXmlElement {
3891 name: "w:t".to_string(),
3892 attributes: vec![],
3893 children: vec![RawXmlNode::Text(text.to_string())],
3894 self_closing: false,
3895 };
3896 let r = RawXmlElement {
3897 name: "w:r".to_string(),
3898 attributes: vec![],
3899 children: vec![RawXmlNode::Element(t)],
3900 self_closing: false,
3901 };
3902 let p = RawXmlElement {
3903 name: "w:p".to_string(),
3904 attributes: vec![],
3905 children: vec![RawXmlNode::Element(r)],
3906 self_closing: false,
3907 };
3908 let txbx_content = RawXmlElement {
3909 name: "w:txbxContent".to_string(),
3910 attributes: vec![],
3911 children: vec![RawXmlNode::Element(p)],
3912 self_closing: false,
3913 };
3914 let textbox = RawXmlElement {
3915 name: "v:textbox".to_string(),
3916 attributes: vec![],
3917 children: vec![RawXmlNode::Element(txbx_content)],
3918 self_closing: false,
3919 };
3920 let shape = RawXmlElement {
3921 name: "v:shape".to_string(),
3922 attributes: vec![("id".to_string(), "TextBox1".to_string())],
3923 children: vec![RawXmlNode::Element(textbox)],
3924 self_closing: false,
3925 };
3926
3927 types::CTPicture {
3928 #[cfg(feature = "wml-drawings")]
3929 movie: None,
3930 #[cfg(feature = "wml-drawings")]
3931 control: None,
3932 extra_children: vec![PositionedNode::new(0, RawXmlNode::Element(shape))],
3933 }
3934 }
3935
3936 #[test]
3937 #[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
3938 fn test_pict_text_box_text() {
3939 use super::PictExt;
3940 let pict = make_pict_with_textbox("VML text box content");
3941 assert_eq!(
3942 pict.text_box_text(),
3943 Some("VML text box content".to_string())
3944 );
3945 }
3946
3947 #[test]
3948 #[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
3949 fn test_pict_text_box_text_none_when_empty() {
3950 use super::PictExt;
3951 let pict = types::CTPicture {
3952 #[cfg(feature = "wml-drawings")]
3953 movie: None,
3954 #[cfg(feature = "wml-drawings")]
3955 control: None,
3956 extra_children: vec![],
3957 };
3958 assert_eq!(pict.text_box_text(), None);
3959 }
3960
3961 #[test]
3962 #[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
3963 fn test_pict_text_box_texts() {
3964 use super::PictExt;
3965 let pict = make_pict_with_textbox("Hello");
3966 let texts = pict.text_box_texts();
3967 assert_eq!(texts.len(), 1);
3968 assert_eq!(texts[0], "Hello");
3969 }
3970
3971 #[test]
3972 #[cfg(all(feature = "wml-drawings", feature = "extra-children"))]
3973 fn test_drawing_text_box_via_xml_parse() {
3974 use super::DrawingTextBoxExt;
3976
3977 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
3978<document xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
3979 xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
3980 xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
3981 <body>
3982 <p>
3983 <r>
3984 <drawing>
3985 <wp:anchor>
3986 <wps:wsp>
3987 <wps:txbx>
3988 <txbxContent>
3989 <p><r><t>Anchored box text</t></r></p>
3990 </txbxContent>
3991 </wps:txbx>
3992 </wps:wsp>
3993 </wp:anchor>
3994 </drawing>
3995 </r>
3996 </p>
3997 </body>
3998</document>"#;
3999
4000 let doc = parse_document(xml.as_bytes()).expect("parse");
4001 let body = doc.body().expect("body");
4002 let paras = body.paragraphs();
4003 assert!(!paras.is_empty());
4004
4005 let run = ¶s[0].runs()[0];
4006 let drawings = run.drawings();
4007 assert_eq!(drawings.len(), 1);
4008
4009 let texts = drawings[0].text_box_texts();
4010 assert_eq!(texts.len(), 1);
4011 assert_eq!(texts[0], "Anchored box text");
4012 }
4013
4014 #[cfg(feature = "wml-styling")]
4020 fn toc_para_xml(style: &str, text: &str) -> String {
4021 format!(
4022 r#"<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4023 <w:pPr><w:pStyle w:val="{style}"/></w:pPr>
4024 <w:r><w:t>{text}</w:t></w:r>
4025</w:p>"#,
4026 )
4027 }
4028
4029 #[cfg(feature = "wml-styling")]
4031 fn doc_with_body(body_inner: &str) -> String {
4032 format!(
4033 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4034 <w:body>
4035 {body_inner}
4036 </w:body>
4037</w:document>"#,
4038 )
4039 }
4040
4041 #[test]
4042 #[cfg(feature = "wml-styling")]
4043 fn test_toc_no_entries() {
4044 let xml = doc_with_body(
4046 r#"<w:p><w:pPr><w:pStyle w:val="Normal"/></w:pPr><w:r><w:t>Hello</w:t></w:r></w:p>"#,
4047 );
4048 let doc = parse_document(xml.as_bytes()).expect("parse");
4049 let body = doc.body().expect("body");
4050 let tocs = body.table_of_contents();
4051 assert!(tocs.is_empty(), "expected no TOCs, got: {tocs:?}");
4052 }
4053
4054 #[test]
4055 #[cfg(feature = "wml-styling")]
4056 fn test_toc_levels() {
4057 let p1 = toc_para_xml("TOC 1", "Chapter One");
4059 let p2 = toc_para_xml("TOC 2", "Section 1.1");
4060 let p3 = toc_para_xml("TOC 3", "Subsection 1.1.1");
4061 let xml = doc_with_body(&format!("{p1}{p2}{p3}"));
4062 let doc = parse_document(xml.as_bytes()).expect("parse");
4063 let body = doc.body().expect("body");
4064 let tocs = body.table_of_contents();
4065 assert_eq!(tocs.len(), 1);
4066 let toc = &tocs[0];
4067 assert_eq!(toc.entries.len(), 3);
4068 assert_eq!(toc.entries[0].level, 1);
4069 assert_eq!(toc.entries[0].text, "Chapter One");
4070 assert_eq!(toc.entries[1].level, 2);
4071 assert_eq!(toc.entries[1].text, "Section 1.1");
4072 assert_eq!(toc.entries[2].level, 3);
4073 assert_eq!(toc.entries[2].text, "Subsection 1.1.1");
4074 }
4075
4076 #[test]
4077 #[cfg(feature = "wml-styling")]
4078 fn test_toc_style_id_form() {
4079 let p1 = toc_para_xml("toc1", "First");
4081 let p2 = toc_para_xml("toc2", "Second");
4082 let xml = doc_with_body(&format!("{p1}{p2}"));
4083 let doc = parse_document(xml.as_bytes()).expect("parse");
4084 let body = doc.body().expect("body");
4085 let tocs = body.table_of_contents();
4086 assert_eq!(tocs.len(), 1);
4087 assert_eq!(tocs[0].entries[0].level, 1);
4088 assert_eq!(tocs[0].entries[1].level, 2);
4089 }
4090
4091 #[test]
4092 #[cfg(feature = "wml-styling")]
4093 fn test_toc_entry_from_sdt() {
4094 let xml = doc_with_body(
4096 r#"<w:sdt xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4097 <w:sdtContent>
4098 <w:p><w:pPr><w:pStyle w:val="TOC 1"/></w:pPr><w:r><w:t>Alpha</w:t></w:r></w:p>
4099 <w:p><w:pPr><w:pStyle w:val="TOC 2"/></w:pPr><w:r><w:t>Beta</w:t></w:r></w:p>
4100 </w:sdtContent>
4101</w:sdt>"#,
4102 );
4103 let doc = parse_document(xml.as_bytes()).expect("parse");
4104 let body = doc.body().expect("body");
4105 let tocs = body.table_of_contents();
4106 assert_eq!(tocs.len(), 1, "expected 1 TOC from SDT, got: {tocs:?}");
4107 assert_eq!(tocs[0].entries.len(), 2);
4108 assert_eq!(tocs[0].entries[0].level, 1);
4109 assert_eq!(tocs[0].entries[0].text, "Alpha");
4110 assert_eq!(tocs[0].entries[1].level, 2);
4111 assert_eq!(tocs[0].entries[1].text, "Beta");
4112 }
4113
4114 #[test]
4115 #[cfg(feature = "wml-styling")]
4116 fn test_toc_page_number_extraction() {
4117 let xml = doc_with_body(
4119 r#"<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4120 <w:pPr><w:pStyle w:val="TOC 1"/></w:pPr>
4121 <w:r><w:t>My Chapter</w:t></w:r>
4122 <w:r><w:tab/></w:r>
4123 <w:r><w:t>42</w:t></w:r>
4124</w:p>"#,
4125 );
4126 let doc = parse_document(xml.as_bytes()).expect("parse");
4127 let body = doc.body().expect("body");
4128 let tocs = body.table_of_contents();
4129 assert_eq!(tocs.len(), 1);
4130 let entry = &tocs[0].entries[0];
4131 assert_eq!(entry.text, "My Chapter");
4132 assert_eq!(entry.page, Some(42));
4133 }
4134
4135 #[test]
4136 #[cfg(feature = "wml-styling")]
4137 fn test_toc_non_toc_para_splits_groups() {
4138 let p1 = toc_para_xml("TOC 1", "First TOC entry");
4140 let normal = r#"<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4141 <w:pPr><w:pStyle w:val="Normal"/></w:pPr>
4142 <w:r><w:t>Regular text</w:t></w:r>
4143</w:p>"#;
4144 let p2 = toc_para_xml("TOC 1", "Second TOC entry");
4145 let xml = doc_with_body(&format!("{p1}{normal}{p2}"));
4146 let doc = parse_document(xml.as_bytes()).expect("parse");
4147 let body = doc.body().expect("body");
4148 let tocs = body.table_of_contents();
4149 assert_eq!(tocs.len(), 2, "expected 2 separate TOCs");
4150 assert_eq!(tocs[0].entries[0].text, "First TOC entry");
4151 assert_eq!(tocs[1].entries[0].text, "Second TOC entry");
4152 }
4153
4154 #[cfg(feature = "wml-track-changes")]
4161 fn make_para_with_ins(ins_text: &str, suffix: &str) -> types::Paragraph {
4162 use crate::convenience::ins_run;
4163 let mut para = types::Paragraph::default();
4164 para.paragraph_content
4165 .push(ins_run(1, "Alice", Some("2026-01-01T00:00:00Z"), ins_text));
4166 let t = types::Text {
4168 text: Some(suffix.to_string()),
4169 #[cfg(feature = "extra-children")]
4170 extra_children: Vec::new(),
4171 };
4172 let run = types::Run {
4173 #[cfg(feature = "wml-track-changes")]
4174 rsid_r_pr: None,
4175 #[cfg(feature = "wml-track-changes")]
4176 rsid_del: None,
4177 #[cfg(feature = "wml-track-changes")]
4178 rsid_r: None,
4179 #[cfg(feature = "wml-styling")]
4180 r_pr: None,
4181 run_content: vec![types::RunContent::T(Box::new(t))],
4182 #[cfg(feature = "extra-attrs")]
4183 extra_attrs: Default::default(),
4184 #[cfg(feature = "extra-children")]
4185 extra_children: Vec::new(),
4186 };
4187 para.paragraph_content
4188 .push(types::ParagraphContent::R(Box::new(run)));
4189 para
4190 }
4191
4192 #[cfg(feature = "wml-track-changes")]
4195 fn make_para_with_del(del_text: &str, suffix: &str) -> types::Paragraph {
4196 use crate::convenience::del_run;
4197 let mut para = types::Paragraph::default();
4198 para.paragraph_content
4199 .push(del_run(2, "Bob", None, del_text));
4200 let t = types::Text {
4201 text: Some(suffix.to_string()),
4202 #[cfg(feature = "extra-children")]
4203 extra_children: Vec::new(),
4204 };
4205 let run = types::Run {
4206 #[cfg(feature = "wml-track-changes")]
4207 rsid_r_pr: None,
4208 #[cfg(feature = "wml-track-changes")]
4209 rsid_del: None,
4210 #[cfg(feature = "wml-track-changes")]
4211 rsid_r: None,
4212 #[cfg(feature = "wml-styling")]
4213 r_pr: None,
4214 run_content: vec![types::RunContent::T(Box::new(t))],
4215 #[cfg(feature = "extra-attrs")]
4216 extra_attrs: Default::default(),
4217 #[cfg(feature = "extra-children")]
4218 extra_children: Vec::new(),
4219 };
4220 para.paragraph_content
4221 .push(types::ParagraphContent::R(Box::new(run)));
4222 para
4223 }
4224
4225 #[test]
4226 #[cfg(feature = "wml-track-changes")]
4227 fn test_track_changes_accepted_text() {
4228 use super::RevisionExt;
4229 let para = make_para_with_ins("hello", " world");
4231 assert_eq!(para.accepted_text(), "hello world");
4232 }
4233
4234 #[test]
4235 #[cfg(feature = "wml-track-changes")]
4236 fn test_track_changes_rejected_text() {
4237 use super::RevisionExt;
4238 let para = make_para_with_del("old", " word");
4240 assert_eq!(para.rejected_text(), "old word");
4241 }
4242
4243 #[test]
4244 #[cfg(feature = "wml-track-changes")]
4245 fn test_track_changes_accepted_text_excludes_deletions() {
4246 use super::RevisionExt;
4247 let para = make_para_with_del("old", " word");
4249 assert_eq!(para.accepted_text(), " word");
4250 }
4251
4252 #[test]
4253 #[cfg(feature = "wml-track-changes")]
4254 fn test_track_changes_rejected_text_excludes_insertions() {
4255 use super::RevisionExt;
4256 let para = make_para_with_ins("hello", " world");
4258 assert_eq!(para.rejected_text(), " world");
4259 }
4260
4261 #[test]
4262 #[cfg(feature = "wml-track-changes")]
4263 fn test_has_track_changes() {
4264 use super::RevisionExt;
4265 let para_with = make_para_with_ins("text", "");
4266 assert!(para_with.has_track_changes());
4267
4268 let plain = types::Paragraph::default();
4270 assert!(!plain.has_track_changes());
4271 }
4272
4273 #[test]
4274 #[cfg(feature = "wml-track-changes")]
4275 fn test_track_changes_list() {
4276 use super::{RevisionExt, TrackChangeType};
4277 let para = make_para_with_ins("hello", " world");
4278 let changes = para.track_changes();
4279 assert_eq!(changes.len(), 1);
4280 let tc = &changes[0];
4281 assert_eq!(tc.id, 1);
4282 assert_eq!(tc.author, "Alice");
4283 assert_eq!(tc.date.as_deref(), Some("2026-01-01T00:00:00Z"));
4284 assert_eq!(tc.change_type, TrackChangeType::Insertion);
4285 assert_eq!(tc.text, "hello");
4286 }
4287
4288 #[test]
4289 #[cfg(feature = "wml-track-changes")]
4290 fn test_track_changes_deletion_list() {
4291 use super::{RevisionExt, TrackChangeType};
4292 let para = make_para_with_del("old", " text");
4293 let changes = para.track_changes();
4294 assert_eq!(changes.len(), 1);
4295 let tc = &changes[0];
4296 assert_eq!(tc.id, 2);
4297 assert_eq!(tc.author, "Bob");
4298 assert_eq!(tc.date, None);
4299 assert_eq!(tc.change_type, TrackChangeType::Deletion);
4300 assert_eq!(tc.text, "old");
4301 }
4302
4303 #[test]
4304 #[cfg(feature = "wml-track-changes")]
4305 fn test_body_revision_ext_all_track_changes() {
4306 use super::{BodyRevisionExt, TrackChangeType};
4307 let para1 = make_para_with_ins("inserted", "");
4308 let para2 = make_para_with_del("deleted", "");
4309
4310 let body = types::Body {
4311 block_content: vec![
4312 types::BlockContent::P(Box::new(para1)),
4313 types::BlockContent::P(Box::new(para2)),
4314 ],
4315 #[cfg(feature = "wml-layout")]
4316 sect_pr: None,
4317 #[cfg(feature = "extra-children")]
4318 extra_children: Vec::new(),
4319 };
4320 let all = body.all_track_changes();
4321 assert_eq!(all.len(), 2);
4322 assert_eq!(all[0].change_type, TrackChangeType::Insertion);
4323 assert_eq!(all[0].text, "inserted");
4324 assert_eq!(all[1].change_type, TrackChangeType::Deletion);
4325 assert_eq!(all[1].text, "deleted");
4326 }
4327
4328 #[test]
4329 #[cfg(feature = "wml-track-changes")]
4330 fn test_body_revision_ext_accepted_text() {
4331 use super::BodyRevisionExt;
4332 let para1 = make_para_with_ins("hello", " world");
4336 let para2 = make_para_with_del("old", " text");
4337
4338 let body = types::Body {
4339 block_content: vec![
4340 types::BlockContent::P(Box::new(para1)),
4341 types::BlockContent::P(Box::new(para2)),
4342 ],
4343 #[cfg(feature = "wml-layout")]
4344 sect_pr: None,
4345 #[cfg(feature = "extra-children")]
4346 extra_children: Vec::new(),
4347 };
4348 assert_eq!(body.accepted_text(), "hello world\n text");
4349 }
4350
4351 #[cfg(feature = "wml-settings")]
4357 fn make_sdt_pr_base(alias: Option<&str>, tag: Option<&str>) -> types::CTSdtPr {
4358 types::CTSdtPr {
4359 r_pr: None,
4360 alias: alias.map(|s| {
4361 Box::new(types::CTString {
4362 value: s.to_string(),
4363 #[cfg(feature = "extra-attrs")]
4364 extra_attrs: Default::default(),
4365 })
4366 }),
4367 tag: tag.map(|s| {
4368 Box::new(types::CTString {
4369 value: s.to_string(),
4370 #[cfg(feature = "extra-attrs")]
4371 extra_attrs: Default::default(),
4372 })
4373 }),
4374 id: None,
4375 lock: None,
4376 placeholder: None,
4377 temporary: None,
4378 showing_plc_hdr: None,
4379 data_binding: None,
4380 label: None,
4381 tab_index: None,
4382 equation: None,
4383 combo_box: None,
4384 date: None,
4385 doc_part_obj: None,
4386 doc_part_list: None,
4387 drop_down_list: None,
4388 picture: None,
4389 rich_text: None,
4390 text: None,
4391 citation: None,
4392 group: None,
4393 bibliography: None,
4394 #[cfg(feature = "extra-children")]
4395 extra_children: Default::default(),
4396 }
4397 }
4398
4399 #[cfg(feature = "wml-settings")]
4401 fn make_sdt_run(sdt_pr: types::CTSdtPr, value_text: &str) -> types::CTSdtRun {
4402 let content = types::CTSdtContentRun {
4403 paragraph_content: vec![types::ParagraphContent::R(Box::new(make_run(vec![
4404 make_text(value_text),
4405 ])))],
4406 #[cfg(feature = "extra-children")]
4407 extra_children: Default::default(),
4408 };
4409 types::CTSdtRun {
4410 sdt_pr: Some(Box::new(sdt_pr)),
4411 sdt_end_pr: None,
4412 sdt_content: Some(Box::new(content)),
4413 #[cfg(feature = "extra-children")]
4414 extra_children: Default::default(),
4415 }
4416 }
4417
4418 #[cfg(feature = "wml-settings")]
4420 fn make_sdt_block(sdt_pr: types::CTSdtPr, value_text: &str) -> types::CTSdtBlock {
4421 let para = make_paragraph(vec![make_p_run(value_text)]);
4422 let content = types::CTSdtContentBlock {
4423 block_content: vec![types::BlockContentChoice::P(Box::new(para))],
4424 #[cfg(feature = "extra-children")]
4425 extra_children: Default::default(),
4426 };
4427 types::CTSdtBlock {
4428 sdt_pr: Some(Box::new(sdt_pr)),
4429 sdt_end_pr: None,
4430 sdt_content: Some(Box::new(content)),
4431 #[cfg(feature = "extra-children")]
4432 extra_children: Default::default(),
4433 }
4434 }
4435
4436 #[test]
4437 #[cfg(feature = "wml-settings")]
4438 fn test_form_field_plain_text() {
4439 use super::{FormFieldExt, FormFieldType};
4440 let mut sdt_pr = make_sdt_pr_base(None, None);
4441 sdt_pr.text = Some(Box::new(types::CTSdtText {
4442 multi_line: None,
4443 #[cfg(feature = "extra-attrs")]
4444 extra_attrs: Default::default(),
4445 }));
4446 let sdt_run = make_sdt_run(sdt_pr, "my value");
4447 let field = sdt_run.form_field().expect("should have form field");
4448 assert_eq!(
4449 field.field_type,
4450 FormFieldType::PlainText { multi_line: false }
4451 );
4452 assert_eq!(field.current_value, "my value");
4453 }
4454
4455 #[test]
4456 #[cfg(feature = "wml-settings")]
4457 fn test_form_field_plain_text_multiline() {
4458 use super::{FormFieldExt, FormFieldType};
4459 let mut sdt_pr = make_sdt_pr_base(None, None);
4460 sdt_pr.text = Some(Box::new(types::CTSdtText {
4461 multi_line: Some("1".to_string()),
4462 #[cfg(feature = "extra-attrs")]
4463 extra_attrs: Default::default(),
4464 }));
4465 let sdt_run = make_sdt_run(sdt_pr, "line1");
4466 let field = sdt_run.form_field().expect("should have form field");
4467 assert_eq!(
4468 field.field_type,
4469 FormFieldType::PlainText { multi_line: true }
4470 );
4471 }
4472
4473 #[test]
4474 #[cfg(feature = "wml-settings")]
4475 fn test_form_field_combo_box() {
4476 use super::{FormFieldExt, FormFieldType};
4477 let mut sdt_pr = make_sdt_pr_base(None, None);
4478 sdt_pr.combo_box = Some(Box::new(types::CTSdtComboBox {
4479 last_value: Some("Option A".to_string()),
4480 list_item: vec![
4481 types::CTSdtListItem {
4482 display_text: Some("Option A".to_string()),
4483 value: Some("a".to_string()),
4484 #[cfg(feature = "extra-attrs")]
4485 extra_attrs: Default::default(),
4486 },
4487 types::CTSdtListItem {
4488 display_text: Some("Option B".to_string()),
4489 value: Some("b".to_string()),
4490 #[cfg(feature = "extra-attrs")]
4491 extra_attrs: Default::default(),
4492 },
4493 ],
4494 #[cfg(feature = "extra-attrs")]
4495 extra_attrs: Default::default(),
4496 #[cfg(feature = "extra-children")]
4497 extra_children: Default::default(),
4498 }));
4499 let sdt_block = make_sdt_block(sdt_pr, "Option A");
4500 let field = sdt_block.form_field().expect("should have form field");
4501 match &field.field_type {
4502 FormFieldType::ComboBox { choices } => {
4503 assert_eq!(
4504 choices,
4505 &vec!["Option A".to_string(), "Option B".to_string()]
4506 );
4507 }
4508 other => panic!("expected ComboBox, got {other:?}"),
4509 }
4510 assert_eq!(field.current_value, "Option A");
4511 }
4512
4513 #[test]
4514 #[cfg(feature = "wml-settings")]
4515 fn test_form_field_dropdown() {
4516 use super::{FormFieldExt, FormFieldType};
4517 let mut sdt_pr = make_sdt_pr_base(None, None);
4518 sdt_pr.drop_down_list = Some(Box::new(types::CTSdtDropDownList {
4519 last_value: None,
4520 list_item: vec![
4521 types::CTSdtListItem {
4522 display_text: Some("Red".to_string()),
4523 value: Some("red".to_string()),
4524 #[cfg(feature = "extra-attrs")]
4525 extra_attrs: Default::default(),
4526 },
4527 types::CTSdtListItem {
4528 display_text: Some("Blue".to_string()),
4529 value: Some("blue".to_string()),
4530 #[cfg(feature = "extra-attrs")]
4531 extra_attrs: Default::default(),
4532 },
4533 ],
4534 #[cfg(feature = "extra-attrs")]
4535 extra_attrs: Default::default(),
4536 #[cfg(feature = "extra-children")]
4537 extra_children: Default::default(),
4538 }));
4539 let sdt_block = make_sdt_block(sdt_pr, "Red");
4540 let field = sdt_block.form_field().expect("should have form field");
4541 match &field.field_type {
4542 FormFieldType::DropDownList { choices } => {
4543 assert_eq!(choices, &vec!["Red".to_string(), "Blue".to_string()]);
4544 }
4545 other => panic!("expected DropDownList, got {other:?}"),
4546 }
4547 assert_eq!(field.current_value, "Red");
4548 }
4549
4550 #[test]
4551 #[cfg(feature = "wml-settings")]
4552 fn test_form_field_alias_and_tag() {
4553 use super::{FormFieldExt, FormFieldType};
4554 let mut sdt_pr = make_sdt_pr_base(Some("Full Name"), Some("fullName"));
4555 sdt_pr.text = Some(Box::new(types::CTSdtText {
4556 multi_line: None,
4557 #[cfg(feature = "extra-attrs")]
4558 extra_attrs: Default::default(),
4559 }));
4560 let sdt_run = make_sdt_run(sdt_pr, "Jane Doe");
4561 let field = sdt_run.form_field().expect("should have form field");
4562 assert_eq!(field.alias.as_deref(), Some("Full Name"));
4563 assert_eq!(field.tag.as_deref(), Some("fullName"));
4564 assert_eq!(
4565 field.field_type,
4566 FormFieldType::PlainText { multi_line: false }
4567 );
4568 assert_eq!(field.current_value, "Jane Doe");
4569 }
4570
4571 #[test]
4572 #[cfg(feature = "wml-settings")]
4573 fn test_form_field_rich_text() {
4574 use super::{FormFieldExt, FormFieldType};
4575 let mut sdt_pr = make_sdt_pr_base(None, None);
4576 sdt_pr.rich_text = Some(Box::new(types::CTEmpty));
4577 let sdt_block = make_sdt_block(sdt_pr, "rich content here");
4578 let field = sdt_block.form_field().expect("should have form field");
4579 assert_eq!(field.field_type, FormFieldType::RichText);
4580 assert_eq!(field.current_value, "rich content here");
4581 }
4582
4583 #[test]
4584 #[cfg(feature = "wml-settings")]
4585 fn test_form_field_date_picker() {
4586 use super::{FormFieldExt, FormFieldType};
4587 let mut sdt_pr = make_sdt_pr_base(None, None);
4588 sdt_pr.date = Some(Box::new(types::CTSdtDate {
4589 full_date: Some("2026-02-24T00:00:00Z".to_string()),
4590 date_format: Some(Box::new(types::CTString {
4591 value: "yyyy-MM-dd".to_string(),
4592 #[cfg(feature = "extra-attrs")]
4593 extra_attrs: Default::default(),
4594 })),
4595 lid: None,
4596 store_mapped_data_as: None,
4597 calendar: None,
4598 #[cfg(feature = "extra-attrs")]
4599 extra_attrs: Default::default(),
4600 #[cfg(feature = "extra-children")]
4601 extra_children: Default::default(),
4602 }));
4603 let sdt_block = make_sdt_block(sdt_pr, "2026-02-24");
4604 let field = sdt_block.form_field().expect("should have form field");
4605 match &field.field_type {
4606 FormFieldType::DatePicker { format } => {
4607 assert_eq!(format.as_deref(), Some("yyyy-MM-dd"));
4608 }
4609 other => panic!("expected DatePicker, got {other:?}"),
4610 }
4611 assert_eq!(field.current_value, "2026-02-24");
4612 }
4613
4614 #[test]
4615 #[cfg(feature = "wml-settings")]
4616 fn test_form_fields_from_body() {
4617 use super::{BodyExt, FormFieldType};
4618
4619 let sdt_pr1 = {
4621 let mut pr = make_sdt_pr_base(Some("First Name"), Some("firstName"));
4622 pr.text = Some(Box::new(types::CTSdtText {
4623 multi_line: None,
4624 #[cfg(feature = "extra-attrs")]
4625 extra_attrs: Default::default(),
4626 }));
4627 pr
4628 };
4629 let block_sdt = make_sdt_block(sdt_pr1, "John");
4630
4631 let sdt_pr2 = {
4633 let mut pr = make_sdt_pr_base(Some("Color"), None);
4634 pr.combo_box = Some(Box::new(types::CTSdtComboBox {
4635 last_value: Some("Red".to_string()),
4636 list_item: vec![types::CTSdtListItem {
4637 display_text: Some("Red".to_string()),
4638 value: Some("red".to_string()),
4639 #[cfg(feature = "extra-attrs")]
4640 extra_attrs: Default::default(),
4641 }],
4642 #[cfg(feature = "extra-attrs")]
4643 extra_attrs: Default::default(),
4644 #[cfg(feature = "extra-children")]
4645 extra_children: Default::default(),
4646 }));
4647 pr
4648 };
4649 let inline_sdt = make_sdt_run(sdt_pr2, "Red");
4650 let para_with_sdt =
4651 make_paragraph(vec![types::ParagraphContent::Sdt(Box::new(inline_sdt))]);
4652
4653 let body = make_body(vec![
4654 types::BlockContent::Sdt(Box::new(block_sdt)),
4655 types::BlockContent::P(Box::new(para_with_sdt)),
4656 ]);
4657
4658 let fields = body.form_fields();
4659 assert_eq!(fields.len(), 2);
4660
4661 assert_eq!(fields[0].alias.as_deref(), Some("First Name"));
4662 assert_eq!(fields[0].tag.as_deref(), Some("firstName"));
4663 assert_eq!(
4664 fields[0].field_type,
4665 FormFieldType::PlainText { multi_line: false }
4666 );
4667 assert_eq!(fields[0].current_value, "John");
4668
4669 assert_eq!(fields[1].alias.as_deref(), Some("Color"));
4670 assert!(
4671 matches!(&fields[1].field_type, FormFieldType::ComboBox { choices } if choices == &["Red"])
4672 );
4673 assert_eq!(fields[1].current_value, "Red");
4674 }
4675
4676 #[test]
4677 #[cfg(feature = "wml-settings")]
4678 fn test_form_field_no_sdt_pr_returns_none() {
4679 use super::FormFieldExt;
4680 let sdt_run = types::CTSdtRun {
4681 sdt_pr: None,
4682 sdt_end_pr: None,
4683 sdt_content: None,
4684 #[cfg(feature = "extra-children")]
4685 extra_children: Default::default(),
4686 };
4687 assert!(sdt_run.form_field().is_none());
4688 }
4689
4690 #[cfg(feature = "extra-children")]
4706 fn make_paragraph_with_inline_math(math_text: &str) -> types::Paragraph {
4707 use ooxml_xml::{PositionedNode, RawXmlElement, RawXmlNode};
4708
4709 let t = RawXmlElement {
4710 name: "m:t".to_string(),
4711 attributes: vec![],
4712 children: vec![RawXmlNode::Text(math_text.to_string())],
4713 self_closing: false,
4714 };
4715 let r = RawXmlElement {
4716 name: "m:r".to_string(),
4717 attributes: vec![],
4718 children: vec![RawXmlNode::Element(t)],
4719 self_closing: false,
4720 };
4721 let o_math = RawXmlElement {
4722 name: "m:oMath".to_string(),
4723 attributes: vec![],
4724 children: vec![RawXmlNode::Element(r)],
4725 self_closing: false,
4726 };
4727
4728 types::Paragraph {
4729 #[cfg(feature = "wml-track-changes")]
4730 rsid_r_pr: None,
4731 #[cfg(feature = "wml-track-changes")]
4732 rsid_r: None,
4733 #[cfg(feature = "wml-track-changes")]
4734 rsid_del: None,
4735 #[cfg(feature = "wml-track-changes")]
4736 rsid_p: None,
4737 #[cfg(feature = "wml-track-changes")]
4738 rsid_r_default: None,
4739 #[cfg(feature = "wml-styling")]
4740 p_pr: None,
4741 paragraph_content: vec![],
4742 #[cfg(feature = "extra-attrs")]
4743 extra_attrs: Default::default(),
4744 extra_children: vec![PositionedNode::new(0, RawXmlNode::Element(o_math))],
4745 }
4746 }
4747
4748 #[cfg(feature = "extra-children")]
4760 fn make_paragraph_with_display_math(math_text: &str) -> types::Paragraph {
4761 use ooxml_xml::{PositionedNode, RawXmlElement, RawXmlNode};
4762
4763 let t = RawXmlElement {
4764 name: "m:t".to_string(),
4765 attributes: vec![],
4766 children: vec![RawXmlNode::Text(math_text.to_string())],
4767 self_closing: false,
4768 };
4769 let r = RawXmlElement {
4770 name: "m:r".to_string(),
4771 attributes: vec![],
4772 children: vec![RawXmlNode::Element(t)],
4773 self_closing: false,
4774 };
4775 let o_math = RawXmlElement {
4776 name: "m:oMath".to_string(),
4777 attributes: vec![],
4778 children: vec![RawXmlNode::Element(r)],
4779 self_closing: false,
4780 };
4781 let o_math_para = RawXmlElement {
4782 name: "m:oMathPara".to_string(),
4783 attributes: vec![],
4784 children: vec![RawXmlNode::Element(o_math)],
4785 self_closing: false,
4786 };
4787
4788 types::Paragraph {
4789 #[cfg(feature = "wml-track-changes")]
4790 rsid_r_pr: None,
4791 #[cfg(feature = "wml-track-changes")]
4792 rsid_r: None,
4793 #[cfg(feature = "wml-track-changes")]
4794 rsid_del: None,
4795 #[cfg(feature = "wml-track-changes")]
4796 rsid_p: None,
4797 #[cfg(feature = "wml-track-changes")]
4798 rsid_r_default: None,
4799 #[cfg(feature = "wml-styling")]
4800 p_pr: None,
4801 paragraph_content: vec![],
4802 #[cfg(feature = "extra-attrs")]
4803 extra_attrs: Default::default(),
4804 extra_children: vec![PositionedNode::new(0, RawXmlNode::Element(o_math_para))],
4805 }
4806 }
4807
4808 #[test]
4809 #[cfg(feature = "extra-children")]
4810 fn test_math_expression_inline() {
4811 use super::MathExt;
4812 let para = make_paragraph_with_inline_math("x+y");
4813 let exprs = para.math_expressions();
4814 assert_eq!(exprs.len(), 1);
4815 assert!(!exprs[0].is_display);
4816 #[cfg(feature = "wml-math")]
4817 assert_eq!(exprs[0].text(), "x+y");
4818 }
4819
4820 #[test]
4821 #[cfg(feature = "extra-children")]
4822 fn test_math_expression_display() {
4823 use super::MathExt;
4824 let para = make_paragraph_with_display_math("E=mc²");
4825 let exprs = para.math_expressions();
4826 assert_eq!(exprs.len(), 1);
4827 assert!(exprs[0].is_display);
4828 #[cfg(feature = "wml-math")]
4829 assert_eq!(exprs[0].text(), "E=mc²");
4830 }
4831
4832 #[test]
4833 #[cfg(feature = "extra-children")]
4834 fn test_has_math_true() {
4835 use super::MathExt;
4836 let para = make_paragraph_with_inline_math("a²+b²=c²");
4837 assert!(para.has_math());
4838 }
4839
4840 #[test]
4841 #[cfg(feature = "extra-children")]
4842 fn test_has_math_false() {
4843 use super::MathExt;
4844 let para = types::Paragraph {
4846 #[cfg(feature = "wml-track-changes")]
4847 rsid_r_pr: None,
4848 #[cfg(feature = "wml-track-changes")]
4849 rsid_r: None,
4850 #[cfg(feature = "wml-track-changes")]
4851 rsid_del: None,
4852 #[cfg(feature = "wml-track-changes")]
4853 rsid_p: None,
4854 #[cfg(feature = "wml-track-changes")]
4855 rsid_r_default: None,
4856 #[cfg(feature = "wml-styling")]
4857 p_pr: None,
4858 paragraph_content: vec![],
4859 #[cfg(feature = "extra-attrs")]
4860 extra_attrs: Default::default(),
4861 extra_children: vec![],
4862 };
4863 assert!(!para.has_math());
4864 }
4865
4866 #[test]
4867 #[cfg(feature = "extra-children")]
4868 fn test_body_math_expressions() {
4869 use super::MathExt;
4870
4871 let para1 = make_paragraph_with_inline_math("x+y");
4872 let para2 = make_paragraph_with_display_math("∫f(x)dx");
4873 let para3 = types::Paragraph {
4875 #[cfg(feature = "wml-track-changes")]
4876 rsid_r_pr: None,
4877 #[cfg(feature = "wml-track-changes")]
4878 rsid_r: None,
4879 #[cfg(feature = "wml-track-changes")]
4880 rsid_del: None,
4881 #[cfg(feature = "wml-track-changes")]
4882 rsid_p: None,
4883 #[cfg(feature = "wml-track-changes")]
4884 rsid_r_default: None,
4885 #[cfg(feature = "wml-styling")]
4886 p_pr: None,
4887 paragraph_content: vec![],
4888 #[cfg(feature = "extra-attrs")]
4889 extra_attrs: Default::default(),
4890 extra_children: vec![],
4891 };
4892
4893 let body = types::Body {
4894 block_content: vec![
4895 types::BlockContent::P(Box::new(para1)),
4896 types::BlockContent::P(Box::new(para3)),
4897 types::BlockContent::P(Box::new(para2)),
4898 ],
4899 #[cfg(feature = "wml-layout")]
4900 sect_pr: None,
4901 extra_children: vec![],
4902 };
4903
4904 let exprs = body.math_expressions();
4905 assert_eq!(exprs.len(), 2);
4906 assert!(!exprs[0].is_display);
4907 assert!(exprs[1].is_display);
4908 }
4909}