1use crate::document::{
7 AppProperties, CoreProperties, serialize_app_properties, serialize_core_properties,
8};
9use crate::error::Result;
10use crate::generated_serializers::ToXml;
11use crate::types;
12use ooxml_opc::{PackageWriter, Relationship, Relationships, content_type, rel_type};
13use ooxml_xml::{PositionedNode, RawXmlElement, RawXmlNode};
14use std::collections::HashMap;
15use std::fs::File;
16use std::io::{BufWriter, Seek, Write};
17use std::path::Path;
18
19pub const NS_W: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
21pub const NS_R: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
23pub const NS_WP: &str = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
25pub const NS_A: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
27pub const NS_PIC: &str = "http://schemas.openxmlformats.org/drawingml/2006/picture";
29pub const NS_WPS: &str = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
31pub const NS_MC: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
33
34const NS_DECLS: &[(&str, &str)] = &[
36 ("xmlns:w", NS_W),
37 ("xmlns:r", NS_R),
38 ("xmlns:wp", NS_WP),
39 ("xmlns:a", NS_A),
40 ("xmlns:pic", NS_PIC),
41];
42
43#[derive(Clone)]
45pub struct PendingImage {
46 pub data: Vec<u8>,
48 pub content_type: String,
50 pub rel_id: String,
52 pub filename: String,
54}
55
56#[derive(Clone)]
58pub struct PendingHyperlink {
59 pub rel_id: String,
61 pub url: String,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ListType {
68 Bullet,
70 Decimal,
72 LowerLetter,
74 UpperLetter,
76 LowerRoman,
78 UpperRoman,
80}
81
82#[derive(Clone)]
84pub struct PendingNumbering {
85 pub abstract_num_id: u32,
87 pub num_id: u32,
89 pub list_type: Option<ListType>,
91 pub custom_levels: Option<Vec<NumberingLevel>>,
93}
94
95#[derive(Debug, Clone)]
102pub struct NumberingLevel {
103 pub ilvl: u32,
105 pub format: ListType,
107 pub start: u32,
109 pub text: String,
112 pub indent_left: Option<u32>,
114 pub hanging: Option<u32>,
116}
117
118impl NumberingLevel {
119 pub fn bullet(ilvl: u32) -> Self {
121 Self {
122 ilvl,
123 format: ListType::Bullet,
124 start: 1,
125 text: "\u{2022}".to_string(),
126 indent_left: Some(720 * (ilvl + 1)),
127 hanging: Some(360),
128 }
129 }
130
131 pub fn decimal(ilvl: u32) -> Self {
133 Self {
134 ilvl,
135 format: ListType::Decimal,
136 start: 1,
137 text: format!("%{}.", ilvl + 1),
138 indent_left: Some(720 * (ilvl + 1)),
139 hanging: Some(360),
140 }
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
148pub enum HeaderFooterType {
149 #[default]
151 Default,
152 First,
154 Even,
156}
157
158impl HeaderFooterType {
159 pub fn parse(s: &str) -> Self {
161 match s {
162 "first" => Self::First,
163 "even" => Self::Even,
164 _ => Self::Default,
165 }
166 }
167
168 pub fn as_str(&self) -> &'static str {
170 match self {
171 Self::Default => "default",
172 Self::First => "first",
173 Self::Even => "even",
174 }
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
182pub enum WrapType {
183 #[default]
185 None,
186 Square,
188 Tight,
190 Through,
192 TopAndBottom,
194}
195
196#[derive(Clone)]
198pub struct PendingHeader {
199 pub body: types::HeaderFooter,
201 pub rel_id: String,
203 pub header_type: HeaderFooterType,
205 pub filename: String,
207}
208
209#[derive(Clone)]
211pub struct PendingFooter {
212 pub body: types::HeaderFooter,
214 pub rel_id: String,
216 pub footer_type: HeaderFooterType,
218 pub filename: String,
220}
221
222#[derive(Clone)]
224pub struct PendingFootnote {
225 pub id: i32,
227 pub body: types::FootnoteEndnote,
229}
230
231#[derive(Clone)]
233pub struct PendingEndnote {
234 pub id: i32,
236 pub body: types::FootnoteEndnote,
238}
239
240#[cfg(feature = "wml-settings")]
248#[derive(Debug, Clone, Default)]
249pub struct DocumentSettingsOptions {
250 pub default_tab_stop: Option<u32>,
252 pub even_and_odd_headers: bool,
255 pub track_changes: bool,
257 pub rsid_root: Option<String>,
259 pub compat_mode: bool,
262}
263
264#[cfg(feature = "wml-charts")]
270#[derive(Clone)]
271pub struct PendingChart {
272 pub data: Vec<u8>,
274 pub rel_id: String,
276 pub filename: String,
278}
279
280#[derive(Clone)]
282pub struct PendingComment {
283 pub id: i32,
285 pub author: Option<String>,
287 pub date: Option<String>,
289 pub initials: Option<String>,
291 pub body: types::Comment,
293}
294
295pub struct HeaderBuilder<'a> {
299 builder: &'a mut DocumentBuilder,
300 rel_id: String,
301}
302
303impl<'a> HeaderBuilder<'a> {
304 pub fn body_mut(&mut self) -> &mut types::HeaderFooter {
306 &mut self
307 .builder
308 .headers
309 .get_mut(&self.rel_id)
310 .expect("header should exist")
311 .body
312 }
313
314 pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
316 self.body_mut().add_paragraph().add_run().set_text(text);
317 self
318 }
319
320 pub fn rel_id(&self) -> &str {
322 &self.rel_id
323 }
324}
325
326pub struct FooterBuilder<'a> {
330 builder: &'a mut DocumentBuilder,
331 rel_id: String,
332}
333
334impl<'a> FooterBuilder<'a> {
335 pub fn body_mut(&mut self) -> &mut types::HeaderFooter {
337 &mut self
338 .builder
339 .footers
340 .get_mut(&self.rel_id)
341 .expect("footer should exist")
342 .body
343 }
344
345 pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
347 self.body_mut().add_paragraph().add_run().set_text(text);
348 self
349 }
350
351 pub fn rel_id(&self) -> &str {
353 &self.rel_id
354 }
355}
356
357pub struct FootnoteBuilder<'a> {
359 builder: &'a mut DocumentBuilder,
360 id: i32,
361}
362
363impl<'a> FootnoteBuilder<'a> {
364 pub fn body_mut(&mut self) -> &mut types::FootnoteEndnote {
366 &mut self
367 .builder
368 .footnotes
369 .get_mut(&self.id)
370 .expect("footnote should exist")
371 .body
372 }
373
374 pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
376 self.body_mut().add_paragraph().add_run().set_text(text);
377 self
378 }
379
380 pub fn id(&self) -> u32 {
384 self.id as u32
385 }
386}
387
388pub struct EndnoteBuilder<'a> {
390 builder: &'a mut DocumentBuilder,
391 id: i32,
392}
393
394impl<'a> EndnoteBuilder<'a> {
395 pub fn body_mut(&mut self) -> &mut types::FootnoteEndnote {
397 &mut self
398 .builder
399 .endnotes
400 .get_mut(&self.id)
401 .expect("endnote should exist")
402 .body
403 }
404
405 pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
407 self.body_mut().add_paragraph().add_run().set_text(text);
408 self
409 }
410
411 pub fn id(&self) -> u32 {
415 self.id as u32
416 }
417}
418
419pub struct CommentBuilder<'a> {
421 builder: &'a mut DocumentBuilder,
422 id: i32,
423}
424
425impl<'a> CommentBuilder<'a> {
426 pub fn body_mut(&mut self) -> &mut types::Comment {
428 &mut self
429 .builder
430 .comments
431 .get_mut(&self.id)
432 .expect("comment should exist")
433 .body
434 }
435
436 pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
438 self.body_mut().add_paragraph().add_run().set_text(text);
439 self
440 }
441
442 pub fn set_author(&mut self, author: &str) -> &mut Self {
444 self.builder
445 .comments
446 .get_mut(&self.id)
447 .expect("comment should exist")
448 .author = Some(author.to_string());
449 self
450 }
451
452 pub fn set_date(&mut self, date: &str) -> &mut Self {
454 self.builder
455 .comments
456 .get_mut(&self.id)
457 .expect("comment should exist")
458 .date = Some(date.to_string());
459 self
460 }
461
462 pub fn set_initials(&mut self, initials: &str) -> &mut Self {
464 self.builder
465 .comments
466 .get_mut(&self.id)
467 .expect("comment should exist")
468 .initials = Some(initials.to_string());
469 self
470 }
471
472 pub fn id(&self) -> u32 {
476 self.id as u32
477 }
478}
479
480#[derive(Debug, Clone)]
489pub struct TextBox {
490 pub text: String,
492 pub width_emu: i64,
494 pub height_emu: i64,
496}
497
498impl TextBox {
499 pub fn new(text: impl Into<String>) -> Self {
501 Self {
502 text: text.into(),
503 width_emu: 914400, height_emu: 457200, }
506 }
507
508 pub fn set_width_emu(&mut self, emu: i64) -> &mut Self {
510 self.width_emu = emu;
511 self
512 }
513
514 pub fn set_height_emu(&mut self, emu: i64) -> &mut Self {
516 self.height_emu = emu;
517 self
518 }
519
520 pub fn set_width_inches(&mut self, inches: f64) -> &mut Self {
522 self.width_emu = (inches * 914400.0) as i64;
523 self
524 }
525
526 pub fn set_height_inches(&mut self, inches: f64) -> &mut Self {
528 self.height_emu = (inches * 914400.0) as i64;
529 self
530 }
531}
532
533#[derive(Debug, Clone, Default)]
539pub struct Drawing {
540 images: Vec<InlineImage>,
542 anchored_images: Vec<AnchoredImage>,
544 text_boxes: Vec<TextBox>,
546}
547
548impl Drawing {
549 pub fn new() -> Self {
551 Self::default()
552 }
553
554 pub fn images(&self) -> &[InlineImage] {
556 &self.images
557 }
558
559 pub fn images_mut(&mut self) -> &mut Vec<InlineImage> {
561 &mut self.images
562 }
563
564 pub fn add_image(&mut self, rel_id: impl Into<String>) -> &mut InlineImage {
566 self.images.push(InlineImage::new(rel_id));
567 self.images.last_mut().unwrap()
568 }
569
570 pub fn anchored_images(&self) -> &[AnchoredImage] {
572 &self.anchored_images
573 }
574
575 pub fn anchored_images_mut(&mut self) -> &mut Vec<AnchoredImage> {
577 &mut self.anchored_images
578 }
579
580 pub fn add_anchored_image(&mut self, rel_id: impl Into<String>) -> &mut AnchoredImage {
582 self.anchored_images.push(AnchoredImage::new(rel_id));
583 self.anchored_images.last_mut().unwrap()
584 }
585
586 pub fn add_text_box(&mut self, text: impl Into<String>) -> &mut TextBox {
602 self.text_boxes.push(TextBox::new(text));
603 self.text_boxes.last_mut().unwrap()
604 }
605
606 pub fn text_boxes(&self) -> &[TextBox] {
608 &self.text_boxes
609 }
610
611 pub fn build(self, doc_id: &mut usize) -> types::CTDrawing {
615 let mut children = Vec::new();
616 let mut child_idx = 0usize;
617
618 for image in &self.images {
619 let elem = build_inline_image_element(image, *doc_id);
620 children.push(PositionedNode::new(child_idx, RawXmlNode::Element(elem)));
621 child_idx += 1;
622 *doc_id += 1;
623 }
624
625 for image in &self.anchored_images {
626 let elem = build_anchored_image_element(image, *doc_id);
627 children.push(PositionedNode::new(child_idx, RawXmlNode::Element(elem)));
628 child_idx += 1;
629 *doc_id += 1;
630 }
631
632 for text_box in &self.text_boxes {
633 let elem = build_text_box_element(text_box, *doc_id);
634 children.push(PositionedNode::new(child_idx, RawXmlNode::Element(elem)));
635 child_idx += 1;
636 *doc_id += 1;
637 }
638
639 types::CTDrawing {
640 #[cfg(feature = "extra-children")]
641 extra_children: children,
642 }
643 }
644}
645
646#[derive(Debug, Clone)]
651pub struct InlineImage {
652 rel_id: String,
654 width_emu: Option<i64>,
656 height_emu: Option<i64>,
658 description: Option<String>,
660}
661
662impl InlineImage {
663 pub fn new(rel_id: impl Into<String>) -> Self {
665 Self {
666 rel_id: rel_id.into(),
667 width_emu: None,
668 height_emu: None,
669 description: None,
670 }
671 }
672
673 pub fn rel_id(&self) -> &str {
675 &self.rel_id
676 }
677
678 pub fn width_emu(&self) -> Option<i64> {
680 self.width_emu
681 }
682
683 pub fn height_emu(&self) -> Option<i64> {
685 self.height_emu
686 }
687
688 pub fn width_inches(&self) -> Option<f64> {
690 self.width_emu.map(|e| e as f64 / 914400.0)
691 }
692
693 pub fn height_inches(&self) -> Option<f64> {
695 self.height_emu.map(|e| e as f64 / 914400.0)
696 }
697
698 pub fn set_width_emu(&mut self, emu: i64) -> &mut Self {
700 self.width_emu = Some(emu);
701 self
702 }
703
704 pub fn set_height_emu(&mut self, emu: i64) -> &mut Self {
706 self.height_emu = Some(emu);
707 self
708 }
709
710 pub fn set_width_inches(&mut self, inches: f64) -> &mut Self {
712 self.width_emu = Some((inches * 914400.0) as i64);
713 self
714 }
715
716 pub fn set_height_inches(&mut self, inches: f64) -> &mut Self {
718 self.height_emu = Some((inches * 914400.0) as i64);
719 self
720 }
721
722 pub fn description(&self) -> Option<&str> {
724 self.description.as_deref()
725 }
726
727 pub fn set_description(&mut self, desc: impl Into<String>) -> &mut Self {
729 self.description = Some(desc.into());
730 self
731 }
732}
733
734#[derive(Debug, Clone)]
740pub struct AnchoredImage {
741 rel_id: String,
743 width_emu: Option<i64>,
745 height_emu: Option<i64>,
747 description: Option<String>,
749 behind_doc: bool,
751 pos_x: i64,
753 pos_y: i64,
755 wrap_type: WrapType,
757}
758
759impl AnchoredImage {
760 pub fn new(rel_id: impl Into<String>) -> Self {
762 Self {
763 rel_id: rel_id.into(),
764 width_emu: None,
765 height_emu: None,
766 description: None,
767 behind_doc: false,
768 pos_x: 0,
769 pos_y: 0,
770 wrap_type: WrapType::None,
771 }
772 }
773
774 pub fn rel_id(&self) -> &str {
776 &self.rel_id
777 }
778
779 pub fn width_emu(&self) -> Option<i64> {
781 self.width_emu
782 }
783
784 pub fn height_emu(&self) -> Option<i64> {
786 self.height_emu
787 }
788
789 pub fn width_inches(&self) -> Option<f64> {
791 self.width_emu.map(|e| e as f64 / 914400.0)
792 }
793
794 pub fn height_inches(&self) -> Option<f64> {
796 self.height_emu.map(|e| e as f64 / 914400.0)
797 }
798
799 pub fn set_width_emu(&mut self, emu: i64) -> &mut Self {
801 self.width_emu = Some(emu);
802 self
803 }
804
805 pub fn set_height_emu(&mut self, emu: i64) -> &mut Self {
807 self.height_emu = Some(emu);
808 self
809 }
810
811 pub fn set_width_inches(&mut self, inches: f64) -> &mut Self {
813 self.width_emu = Some((inches * 914400.0) as i64);
814 self
815 }
816
817 pub fn set_height_inches(&mut self, inches: f64) -> &mut Self {
819 self.height_emu = Some((inches * 914400.0) as i64);
820 self
821 }
822
823 pub fn description(&self) -> Option<&str> {
825 self.description.as_deref()
826 }
827
828 pub fn set_description(&mut self, desc: impl Into<String>) -> &mut Self {
830 self.description = Some(desc.into());
831 self
832 }
833
834 pub fn is_behind_doc(&self) -> bool {
836 self.behind_doc
837 }
838
839 pub fn set_behind_doc(&mut self, behind: bool) -> &mut Self {
841 self.behind_doc = behind;
842 self
843 }
844
845 pub fn pos_x(&self) -> i64 {
847 self.pos_x
848 }
849
850 pub fn pos_y(&self) -> i64 {
852 self.pos_y
853 }
854
855 pub fn set_pos_x(&mut self, emu: i64) -> &mut Self {
857 self.pos_x = emu;
858 self
859 }
860
861 pub fn set_pos_y(&mut self, emu: i64) -> &mut Self {
863 self.pos_y = emu;
864 self
865 }
866
867 pub fn wrap_type(&self) -> WrapType {
869 self.wrap_type
870 }
871
872 pub fn set_wrap_type(&mut self, wrap: WrapType) -> &mut Self {
874 self.wrap_type = wrap;
875 self
876 }
877}
878
879pub struct DocumentBuilder {
885 document: types::Document,
886 images: HashMap<String, PendingImage>,
888 hyperlinks: HashMap<String, PendingHyperlink>,
890 numberings: HashMap<u32, PendingNumbering>,
892 styles: Option<types::Styles>,
894 headers: HashMap<String, PendingHeader>,
896 footers: HashMap<String, PendingFooter>,
898 footnotes: HashMap<i32, PendingFootnote>,
900 endnotes: HashMap<i32, PendingEndnote>,
902 comments: HashMap<i32, PendingComment>,
904 #[cfg(feature = "wml-settings")]
906 settings: Option<DocumentSettingsOptions>,
907 #[cfg(feature = "wml-charts")]
909 charts: HashMap<String, PendingChart>,
910 #[cfg(feature = "wml-charts")]
912 next_chart_id: u32,
913 core_properties: Option<CoreProperties>,
915 app_properties: Option<AppProperties>,
917 next_rel_id: u32,
919 next_num_id: u32,
921 next_header_id: u32,
923 next_footer_id: u32,
925 next_footnote_id: i32,
928 next_endnote_id: i32,
931 next_comment_id: i32,
933 next_drawing_id: usize,
935}
936
937impl Default for DocumentBuilder {
938 fn default() -> Self {
939 Self::new()
940 }
941}
942
943impl DocumentBuilder {
944 pub fn new() -> Self {
946 let document = types::Document {
947 #[cfg(feature = "wml-styling")]
948 background: None,
949 body: Some(Box::new(types::Body::default())),
950 conformance: None,
951 #[cfg(feature = "extra-attrs")]
952 extra_attrs: std::collections::HashMap::new(),
953 #[cfg(feature = "extra-children")]
954 extra_children: Vec::new(),
955 };
956
957 Self {
958 document,
959 images: HashMap::new(),
960 hyperlinks: HashMap::new(),
961 numberings: HashMap::new(),
962 styles: None,
963 headers: HashMap::new(),
964 footers: HashMap::new(),
965 footnotes: HashMap::new(),
966 endnotes: HashMap::new(),
967 comments: HashMap::new(),
968 #[cfg(feature = "wml-settings")]
969 settings: None,
970 #[cfg(feature = "wml-charts")]
971 charts: HashMap::new(),
972 #[cfg(feature = "wml-charts")]
973 next_chart_id: 1,
974 core_properties: None,
975 app_properties: None,
976 next_rel_id: 1,
977 next_num_id: 1,
978 next_header_id: 1,
979 next_footer_id: 1,
980 next_footnote_id: 1,
981 next_endnote_id: 1,
982 next_comment_id: 0,
983 next_drawing_id: 1,
984 }
985 }
986
987 pub fn add_image(&mut self, data: Vec<u8>, content_type: &str) -> String {
992 let id = self.next_rel_id;
993 self.next_rel_id += 1;
994
995 let rel_id = format!("rId{}", id);
996 let ext = extension_from_content_type(content_type);
997 let filename = format!("image{}.{}", id, ext);
998
999 self.images.insert(
1000 rel_id.clone(),
1001 PendingImage {
1002 data,
1003 content_type: content_type.to_string(),
1004 rel_id: rel_id.clone(),
1005 filename,
1006 },
1007 );
1008
1009 rel_id
1010 }
1011
1012 pub fn add_hyperlink(&mut self, url: &str) -> String {
1016 let id = self.next_rel_id;
1017 self.next_rel_id += 1;
1018
1019 let rel_id = format!("rId{}", id);
1020
1021 self.hyperlinks.insert(
1022 rel_id.clone(),
1023 PendingHyperlink {
1024 rel_id: rel_id.clone(),
1025 url: url.to_string(),
1026 },
1027 );
1028
1029 rel_id
1030 }
1031
1032 pub fn add_list(&mut self, list_type: ListType) -> u32 {
1036 let num_id = self.next_num_id;
1037 self.next_num_id += 1;
1038
1039 self.numberings.insert(
1040 num_id,
1041 PendingNumbering {
1042 abstract_num_id: num_id, num_id,
1044 list_type: Some(list_type),
1045 custom_levels: None,
1046 },
1047 );
1048
1049 num_id
1050 }
1051
1052 pub fn add_custom_list(&mut self, levels: Vec<NumberingLevel>) -> u32 {
1085 let num_id = self.next_num_id;
1086 self.next_num_id += 1;
1087
1088 self.numberings.insert(
1089 num_id,
1090 PendingNumbering {
1091 abstract_num_id: num_id,
1092 num_id,
1093 list_type: None,
1094 custom_levels: Some(levels),
1095 },
1096 );
1097
1098 num_id
1099 }
1100
1101 pub fn set_styles(&mut self, styles: types::Styles) -> &mut Self {
1108 self.styles = Some(styles);
1109 self
1110 }
1111
1112 pub fn add_style(&mut self, style: types::Style) -> &mut Self {
1119 self.styles
1120 .get_or_insert_with(types::Styles::default)
1121 .style
1122 .push(style);
1123 self
1124 }
1125
1126 pub fn add_header(&mut self, header_type: HeaderFooterType) -> HeaderBuilder<'_> {
1130 let id = self.next_rel_id;
1131 self.next_rel_id += 1;
1132 let header_num = self.next_header_id;
1133 self.next_header_id += 1;
1134
1135 let rel_id = format!("rId{}", id);
1136 let filename = format!("header{}.xml", header_num);
1137
1138 self.headers.insert(
1139 rel_id.clone(),
1140 PendingHeader {
1141 body: types::HeaderFooter::default(),
1142 rel_id: rel_id.clone(),
1143 header_type,
1144 filename,
1145 },
1146 );
1147
1148 HeaderBuilder {
1149 builder: self,
1150 rel_id,
1151 }
1152 }
1153
1154 pub fn add_footer(&mut self, footer_type: HeaderFooterType) -> FooterBuilder<'_> {
1158 let id = self.next_rel_id;
1159 self.next_rel_id += 1;
1160 let footer_num = self.next_footer_id;
1161 self.next_footer_id += 1;
1162
1163 let rel_id = format!("rId{}", id);
1164 let filename = format!("footer{}.xml", footer_num);
1165
1166 self.footers.insert(
1167 rel_id.clone(),
1168 PendingFooter {
1169 body: types::HeaderFooter::default(),
1170 rel_id: rel_id.clone(),
1171 footer_type,
1172 filename,
1173 },
1174 );
1175
1176 FooterBuilder {
1177 builder: self,
1178 rel_id,
1179 }
1180 }
1181
1182 pub fn add_footnote(&mut self) -> FootnoteBuilder<'_> {
1186 let id = self.next_footnote_id;
1187 self.next_footnote_id += 1;
1188
1189 self.footnotes.insert(
1190 id,
1191 PendingFootnote {
1192 id,
1193 body: types::FootnoteEndnote {
1194 #[cfg(feature = "wml-comments")]
1195 r#type: None,
1196 id: id as i64,
1197 block_content: Vec::new(),
1198 #[cfg(feature = "extra-attrs")]
1199 extra_attrs: std::collections::HashMap::new(),
1200 #[cfg(feature = "extra-children")]
1201 extra_children: Vec::new(),
1202 },
1203 },
1204 );
1205
1206 FootnoteBuilder { builder: self, id }
1207 }
1208
1209 pub fn add_endnote(&mut self) -> EndnoteBuilder<'_> {
1213 let id = self.next_endnote_id;
1214 self.next_endnote_id += 1;
1215
1216 self.endnotes.insert(
1217 id,
1218 PendingEndnote {
1219 id,
1220 body: types::FootnoteEndnote {
1221 #[cfg(feature = "wml-comments")]
1222 r#type: None,
1223 id: id as i64,
1224 block_content: Vec::new(),
1225 #[cfg(feature = "extra-attrs")]
1226 extra_attrs: std::collections::HashMap::new(),
1227 #[cfg(feature = "extra-children")]
1228 extra_children: Vec::new(),
1229 },
1230 },
1231 );
1232
1233 EndnoteBuilder { builder: self, id }
1234 }
1235
1236 pub fn add_comment(&mut self) -> CommentBuilder<'_> {
1240 let id = self.next_comment_id;
1241 self.next_comment_id += 1;
1242
1243 self.comments.insert(
1244 id,
1245 PendingComment {
1246 id,
1247 author: None,
1248 date: None,
1249 initials: None,
1250 body: types::Comment {
1251 id: 0, author: String::new(), #[cfg(feature = "wml-comments")]
1254 date: None,
1255 block_content: Vec::new(),
1256 #[cfg(feature = "wml-comments")]
1257 initials: None,
1258 #[cfg(feature = "extra-attrs")]
1259 extra_attrs: Default::default(),
1260 #[cfg(feature = "extra-children")]
1261 extra_children: Vec::new(),
1262 },
1263 },
1264 );
1265
1266 CommentBuilder { builder: self, id }
1267 }
1268
1269 pub fn set_core_properties(&mut self, props: CoreProperties) -> &mut Self {
1275 self.core_properties = Some(props);
1276 self
1277 }
1278
1279 pub fn set_app_properties(&mut self, props: AppProperties) -> &mut Self {
1285 self.app_properties = Some(props);
1286 self
1287 }
1288
1289 #[cfg(feature = "wml-settings")]
1293 pub fn set_settings(&mut self, opts: DocumentSettingsOptions) -> &mut Self {
1294 self.settings = Some(opts);
1295 self
1296 }
1297
1298 #[cfg(feature = "wml-charts")]
1305 pub fn embed_chart(&mut self, chart_xml: &[u8]) -> crate::error::Result<String> {
1306 let id = self.next_rel_id;
1307 self.next_rel_id += 1;
1308 let chart_num = self.next_chart_id;
1309 self.next_chart_id += 1;
1310
1311 let rel_id = format!("rId{}", id);
1312 let filename = format!("chart{}.xml", chart_num);
1313
1314 self.charts.insert(
1315 rel_id.clone(),
1316 PendingChart {
1317 data: chart_xml.to_vec(),
1318 rel_id: rel_id.clone(),
1319 filename,
1320 },
1321 );
1322
1323 Ok(rel_id)
1324 }
1325
1326 pub fn body_mut(&mut self) -> &mut types::Body {
1328 self.document
1329 .body
1330 .as_deref_mut()
1331 .expect("document body should exist")
1332 }
1333
1334 pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
1336 let para = self.body_mut().add_paragraph();
1337 para.add_run().set_text(text);
1338 self
1339 }
1340
1341 pub fn build_drawing(&mut self, drawing: Drawing) -> types::CTDrawing {
1346 drawing.build(&mut self.next_drawing_id)
1347 }
1348
1349 pub fn save<P: AsRef<Path>>(self, path: P) -> Result<()> {
1351 let file = File::create(path)?;
1352 let writer = BufWriter::new(file);
1353 self.write(writer)
1354 }
1355
1356 pub fn write<W: Write + Seek>(mut self, writer: W) -> Result<()> {
1358 let mut pkg = PackageWriter::new(writer);
1359
1360 pkg.add_default_content_type("rels", content_type::RELATIONSHIPS);
1362 pkg.add_default_content_type("xml", content_type::XML);
1363
1364 pkg.add_default_content_type("png", "image/png");
1366 pkg.add_default_content_type("jpg", "image/jpeg");
1367 pkg.add_default_content_type("jpeg", "image/jpeg");
1368 pkg.add_default_content_type("gif", "image/gif");
1369
1370 let mut doc_rels = Relationships::new();
1372
1373 if !self.headers.is_empty() || !self.footers.is_empty() {
1375 #[cfg(feature = "wml-layout")]
1377 {
1378 let body = self.document.body.as_deref_mut().expect("document body");
1379 if body.sect_pr.is_none() {
1380 body.sect_pr = Some(Box::new(types::SectionProperties::default()));
1381 }
1382 let sect_pr = body.sect_pr.as_deref_mut().unwrap();
1383
1384 for header in self.headers.values() {
1386 let hdr_ref = types::HeaderFooterReference {
1387 id: header.rel_id.clone(),
1388 r#type: match header.header_type {
1389 HeaderFooterType::Default => types::STHdrFtr::Default,
1390 HeaderFooterType::First => types::STHdrFtr::First,
1391 HeaderFooterType::Even => types::STHdrFtr::Even,
1392 },
1393 #[cfg(feature = "extra-attrs")]
1394 extra_attrs: std::collections::HashMap::new(),
1395 };
1396 sect_pr
1397 .header_footer_refs
1398 .push(types::HeaderFooterRef::HeaderReference(Box::new(hdr_ref)));
1399 }
1400
1401 for footer in self.footers.values() {
1403 let ftr_ref = types::HeaderFooterReference {
1404 id: footer.rel_id.clone(),
1405 r#type: match footer.footer_type {
1406 HeaderFooterType::Default => types::STHdrFtr::Default,
1407 HeaderFooterType::First => types::STHdrFtr::First,
1408 HeaderFooterType::Even => types::STHdrFtr::Even,
1409 },
1410 #[cfg(feature = "extra-attrs")]
1411 extra_attrs: std::collections::HashMap::new(),
1412 };
1413 sect_pr
1414 .header_footer_refs
1415 .push(types::HeaderFooterRef::FooterReference(Box::new(ftr_ref)));
1416 }
1417 }
1418 }
1419
1420 #[cfg(feature = "extra-attrs")]
1422 {
1423 for &(key, value) in NS_DECLS {
1424 self.document
1425 .extra_attrs
1426 .insert(key.to_string(), value.to_string());
1427 }
1428 }
1429
1430 let doc_xml = serialize_to_xml_bytes(&self.document, "w:document")?;
1432 pkg.add_part(
1433 "word/document.xml",
1434 content_type::WORDPROCESSING_DOCUMENT,
1435 &doc_xml,
1436 )?;
1437
1438 let mut pkg_rels = Relationships::new();
1440 pkg_rels.add(Relationship::new(
1441 "rId1",
1442 rel_type::OFFICE_DOCUMENT,
1443 "word/document.xml",
1444 ));
1445
1446 if let Some(ref core_props) = self.core_properties {
1448 let core_xml = serialize_core_properties(core_props)?;
1449 pkg.add_part(
1450 "docProps/core.xml",
1451 content_type::CORE_PROPERTIES,
1452 &core_xml,
1453 )?;
1454 pkg_rels.add(Relationship::new(
1455 "rId2",
1456 rel_type::CORE_PROPERTIES,
1457 "docProps/core.xml",
1458 ));
1459 }
1460
1461 if let Some(ref app_props) = self.app_properties {
1463 let app_xml = serialize_app_properties(app_props)?;
1464 pkg.add_part(
1465 "docProps/app.xml",
1466 content_type::EXTENDED_PROPERTIES,
1467 &app_xml,
1468 )?;
1469 pkg_rels.add(Relationship::new(
1470 "rId3",
1471 rel_type::EXTENDED_PROPERTIES,
1472 "docProps/app.xml",
1473 ));
1474 }
1475
1476 pkg.add_part(
1477 "_rels/.rels",
1478 content_type::RELATIONSHIPS,
1479 pkg_rels.serialize().as_bytes(),
1480 )?;
1481
1482 for image in self.images.values() {
1484 doc_rels.add(Relationship::new(
1485 &image.rel_id,
1486 rel_type::IMAGE,
1487 format!("media/{}", image.filename),
1488 ));
1489
1490 let image_path = format!("word/media/{}", image.filename);
1491 pkg.add_part(&image_path, &image.content_type, &image.data)?;
1492 }
1493
1494 for hyperlink in self.hyperlinks.values() {
1496 doc_rels.add(Relationship::external(
1497 &hyperlink.rel_id,
1498 rel_type::HYPERLINK,
1499 &hyperlink.url,
1500 ));
1501 }
1502
1503 for header in self.headers.values() {
1505 let header_xml = serialize_with_namespaces(&header.body, "w:hdr")?;
1506 let header_path = format!("word/{}", header.filename);
1507 pkg.add_part(
1508 &header_path,
1509 content_type::WORDPROCESSING_HEADER,
1510 &header_xml,
1511 )?;
1512
1513 doc_rels.add(Relationship::new(
1514 &header.rel_id,
1515 rel_type::HEADER,
1516 &header.filename,
1517 ));
1518 }
1519
1520 for footer in self.footers.values() {
1522 let footer_xml = serialize_with_namespaces(&footer.body, "w:ftr")?;
1523 let footer_path = format!("word/{}", footer.filename);
1524 pkg.add_part(
1525 &footer_path,
1526 content_type::WORDPROCESSING_FOOTER,
1527 &footer_xml,
1528 )?;
1529
1530 doc_rels.add(Relationship::new(
1531 &footer.rel_id,
1532 rel_type::FOOTER,
1533 &footer.filename,
1534 ));
1535 }
1536
1537 if !self.footnotes.is_empty() {
1539 let fns = build_footnotes(&self.footnotes);
1540 let footnotes_xml = serialize_with_namespaces(&fns, "w:footnotes")?;
1541 pkg.add_part(
1542 "word/footnotes.xml",
1543 content_type::WORDPROCESSING_FOOTNOTES,
1544 &footnotes_xml,
1545 )?;
1546
1547 let footnotes_rel_id = format!("rId{}", self.next_rel_id);
1548 self.next_rel_id += 1;
1549 doc_rels.add(Relationship::new(
1550 &footnotes_rel_id,
1551 rel_type::FOOTNOTES,
1552 "footnotes.xml",
1553 ));
1554 }
1555
1556 if !self.endnotes.is_empty() {
1558 let ens = build_endnotes(&self.endnotes);
1559 let endnotes_xml = serialize_with_namespaces(&ens, "w:endnotes")?;
1560 pkg.add_part(
1561 "word/endnotes.xml",
1562 content_type::WORDPROCESSING_ENDNOTES,
1563 &endnotes_xml,
1564 )?;
1565
1566 let endnotes_rel_id = format!("rId{}", self.next_rel_id);
1567 self.next_rel_id += 1;
1568 doc_rels.add(Relationship::new(
1569 &endnotes_rel_id,
1570 rel_type::ENDNOTES,
1571 "endnotes.xml",
1572 ));
1573 }
1574
1575 if !self.comments.is_empty() {
1577 let comments = build_comments(&self.comments);
1578 let comments_xml = serialize_with_namespaces(&comments, "w:comments")?;
1579 pkg.add_part(
1580 "word/comments.xml",
1581 content_type::WORDPROCESSING_COMMENTS,
1582 &comments_xml,
1583 )?;
1584
1585 let comments_rel_id = format!("rId{}", self.next_rel_id);
1586 self.next_rel_id += 1;
1587 doc_rels.add(Relationship::new(
1588 &comments_rel_id,
1589 rel_type::COMMENTS,
1590 "comments.xml",
1591 ));
1592 }
1593
1594 if let Some(ref styles) = self.styles {
1596 let styles_xml = serialize_with_namespaces(styles, "w:styles")?;
1597 pkg.add_part(
1598 "word/styles.xml",
1599 content_type::WORDPROCESSING_STYLES,
1600 &styles_xml,
1601 )?;
1602
1603 let styles_rel_id = format!("rId{}", self.next_rel_id);
1604 self.next_rel_id += 1;
1605 doc_rels.add(Relationship::new(
1606 &styles_rel_id,
1607 rel_type::STYLES,
1608 "styles.xml",
1609 ));
1610 }
1611
1612 if !self.numberings.is_empty() {
1614 let numbering = build_numbering(&self.numberings);
1615 let num_xml = serialize_with_namespaces(&numbering, "w:numbering")?;
1616 pkg.add_part(
1617 "word/numbering.xml",
1618 content_type::WORDPROCESSING_NUMBERING,
1619 &num_xml,
1620 )?;
1621
1622 let num_rel_id = format!("rId{}", self.next_rel_id);
1623 self.next_rel_id += 1;
1624 doc_rels.add(Relationship::new(
1625 &num_rel_id,
1626 rel_type::NUMBERING,
1627 "numbering.xml",
1628 ));
1629 }
1630
1631 #[cfg(feature = "wml-settings")]
1633 if let Some(ref settings_opts) = self.settings {
1634 let settings_xml = build_settings_xml(settings_opts);
1635 pkg.add_part(
1636 "word/settings.xml",
1637 "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
1638 settings_xml.as_bytes(),
1639 )?;
1640
1641 let settings_rel_id = format!("rId{}", self.next_rel_id);
1642 self.next_rel_id += 1;
1643 doc_rels.add(Relationship::new(
1644 &settings_rel_id,
1645 rel_type::SETTINGS,
1646 "settings.xml",
1647 ));
1648 }
1649
1650 #[cfg(feature = "wml-charts")]
1652 for chart in self.charts.values() {
1653 let chart_path = format!("word/charts/{}", chart.filename);
1654 pkg.add_part(
1655 &chart_path,
1656 "application/vnd.openxmlformats-officedocument.drawingml.chart+xml",
1657 &chart.data,
1658 )?;
1659
1660 doc_rels.add(Relationship::new(
1661 &chart.rel_id,
1662 rel_type::CHART,
1663 format!("charts/{}", chart.filename),
1664 ));
1665 }
1666
1667 pkg.add_part(
1668 "word/_rels/document.xml.rels",
1669 content_type::RELATIONSHIPS,
1670 doc_rels.serialize().as_bytes(),
1671 )?;
1672
1673 pkg.finish()?;
1674 Ok(())
1675 }
1676}
1677
1678fn serialize_to_xml_bytes(value: &impl ToXml, tag: &str) -> Result<Vec<u8>> {
1684 let inner = Vec::new();
1685 let mut writer = quick_xml::Writer::new(inner);
1686 value.write_element(tag, &mut writer)?;
1687 let inner = writer.into_inner();
1688 let mut buf = Vec::with_capacity(
1689 b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".len() + inner.len(),
1690 );
1691 buf.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n");
1692 buf.extend_from_slice(&inner);
1693 Ok(buf)
1694}
1695
1696fn serialize_with_namespaces(value: &impl ToXml, tag: &str) -> Result<Vec<u8>> {
1700 use quick_xml::events::{BytesEnd, BytesStart, Event};
1701
1702 let inner = Vec::new();
1703 let mut writer = quick_xml::Writer::new(inner);
1704
1705 let start = BytesStart::new(tag);
1707 let start = value.write_attrs(start);
1708 let mut start = start;
1709 for &(key, val) in NS_DECLS {
1710 start.push_attribute((key, val));
1711 }
1712
1713 if value.is_empty_element() {
1714 writer.write_event(Event::Empty(start))?;
1715 } else {
1716 writer.write_event(Event::Start(start))?;
1717 value.write_children(&mut writer)?;
1718 writer.write_event(Event::End(BytesEnd::new(tag)))?;
1719 }
1720
1721 let inner = writer.into_inner();
1722 let mut buf = Vec::with_capacity(
1723 b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".len() + inner.len(),
1724 );
1725 buf.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n");
1726 buf.extend_from_slice(&inner);
1727 Ok(buf)
1728}
1729
1730fn build_separator_ftn_edn(id: i64, ftn_type: types::STFtnEdn) -> types::FootnoteEndnote {
1739 let separator_content = match ftn_type {
1740 types::STFtnEdn::Separator => types::RunContent::Separator(Box::new(types::CTEmpty)),
1741 types::STFtnEdn::ContinuationSeparator => {
1742 types::RunContent::ContinuationSeparator(Box::new(types::CTEmpty))
1743 }
1744 _ => unreachable!("only Separator and ContinuationSeparator expected"),
1745 };
1746
1747 let run = types::Run {
1748 #[cfg(feature = "wml-track-changes")]
1749 rsid_r_pr: None,
1750 #[cfg(feature = "wml-track-changes")]
1751 rsid_del: None,
1752 #[cfg(feature = "wml-track-changes")]
1753 rsid_r: None,
1754 #[cfg(feature = "wml-styling")]
1755 r_pr: None,
1756 run_content: vec![separator_content],
1757 #[cfg(feature = "extra-attrs")]
1758 extra_attrs: std::collections::HashMap::new(),
1759 #[cfg(feature = "extra-children")]
1760 extra_children: Vec::new(),
1761 };
1762
1763 let para = types::Paragraph {
1764 #[cfg(feature = "wml-track-changes")]
1765 rsid_r_pr: None,
1766 #[cfg(feature = "wml-track-changes")]
1767 rsid_r: None,
1768 #[cfg(feature = "wml-track-changes")]
1769 rsid_del: None,
1770 #[cfg(feature = "wml-track-changes")]
1771 rsid_p: None,
1772 #[cfg(feature = "wml-track-changes")]
1773 rsid_r_default: None,
1774 #[cfg(feature = "wml-styling")]
1775 p_pr: None,
1776 paragraph_content: vec![types::ParagraphContent::R(Box::new(run))],
1777 #[cfg(feature = "extra-attrs")]
1778 extra_attrs: std::collections::HashMap::new(),
1779 #[cfg(feature = "extra-children")]
1780 extra_children: Vec::new(),
1781 };
1782
1783 types::FootnoteEndnote {
1784 #[cfg(feature = "wml-comments")]
1785 r#type: Some(ftn_type),
1786 id,
1787 block_content: vec![types::BlockContent::P(Box::new(para))],
1788 #[cfg(feature = "extra-attrs")]
1789 extra_attrs: std::collections::HashMap::new(),
1790 #[cfg(feature = "extra-children")]
1791 extra_children: Vec::new(),
1792 }
1793}
1794
1795fn build_footnotes(footnotes: &HashMap<i32, PendingFootnote>) -> types::Footnotes {
1797 let mut fns = types::Footnotes {
1798 footnote: Vec::new(),
1799 #[cfg(feature = "extra-children")]
1800 extra_children: Vec::new(),
1801 };
1802
1803 fns.footnote
1805 .push(build_separator_ftn_edn(-1, types::STFtnEdn::Separator));
1806 fns.footnote.push(build_separator_ftn_edn(
1807 0,
1808 types::STFtnEdn::ContinuationSeparator,
1809 ));
1810
1811 let mut sorted: Vec<_> = footnotes.values().collect();
1813 sorted.sort_by_key(|f| f.id);
1814 for footnote in sorted {
1815 fns.footnote.push(footnote.body.clone());
1816 }
1817
1818 fns
1819}
1820
1821fn build_endnotes(endnotes: &HashMap<i32, PendingEndnote>) -> types::Endnotes {
1823 let mut ens = types::Endnotes {
1824 endnote: Vec::new(),
1825 #[cfg(feature = "extra-children")]
1826 extra_children: Vec::new(),
1827 };
1828
1829 ens.endnote
1831 .push(build_separator_ftn_edn(-1, types::STFtnEdn::Separator));
1832 ens.endnote.push(build_separator_ftn_edn(
1833 0,
1834 types::STFtnEdn::ContinuationSeparator,
1835 ));
1836
1837 let mut sorted: Vec<_> = endnotes.values().collect();
1839 sorted.sort_by_key(|e| e.id);
1840 for endnote in sorted {
1841 ens.endnote.push(endnote.body.clone());
1842 }
1843
1844 ens
1845}
1846
1847fn build_comments(comments: &HashMap<i32, PendingComment>) -> types::Comments {
1849 let mut result = types::Comments {
1850 comment: Vec::new(),
1851 #[cfg(feature = "extra-children")]
1852 extra_children: Vec::new(),
1853 };
1854
1855 let mut sorted: Vec<_> = comments.values().collect();
1857 sorted.sort_by_key(|c| c.id);
1858
1859 for pc in sorted {
1860 let mut comment = pc.body.clone();
1861 comment.id = pc.id as i64;
1862 if let Some(ref author) = pc.author {
1863 comment.author = author.clone();
1864 }
1865 #[cfg(feature = "wml-comments")]
1866 if let Some(ref date) = pc.date {
1867 comment.date = Some(date.clone());
1868 }
1869 #[cfg(feature = "wml-comments")]
1870 if let Some(ref initials) = pc.initials {
1871 comment.initials = Some(initials.clone());
1872 }
1873 result.comment.push(comment);
1874 }
1875
1876 result
1877}
1878
1879fn list_type_to_num_fmt_and_text(list_type: ListType) -> (types::STNumberFormat, &'static str) {
1881 match list_type {
1882 ListType::Bullet => (types::STNumberFormat::Bullet, "\u{2022}"),
1883 ListType::Decimal => (types::STNumberFormat::Decimal, "%1."),
1884 ListType::LowerLetter => (types::STNumberFormat::LowerLetter, "%1."),
1885 ListType::UpperLetter => (types::STNumberFormat::UpperLetter, "%1."),
1886 ListType::LowerRoman => (types::STNumberFormat::LowerRoman, "%1."),
1887 ListType::UpperRoman => (types::STNumberFormat::UpperRoman, "%1."),
1888 }
1889}
1890
1891#[cfg(feature = "wml-numbering")]
1893fn list_type_to_num_fmt(list_type: ListType) -> types::STNumberFormat {
1894 match list_type {
1895 ListType::Bullet => types::STNumberFormat::Bullet,
1896 ListType::Decimal => types::STNumberFormat::Decimal,
1897 ListType::LowerLetter => types::STNumberFormat::LowerLetter,
1898 ListType::UpperLetter => types::STNumberFormat::UpperLetter,
1899 ListType::LowerRoman => types::STNumberFormat::LowerRoman,
1900 ListType::UpperRoman => types::STNumberFormat::UpperRoman,
1901 }
1902}
1903
1904fn build_level_from_spec(spec: &NumberingLevel) -> types::Level {
1906 #[cfg(feature = "wml-numbering")]
1907 let is_bullet = spec.format == ListType::Bullet;
1908 #[cfg(feature = "wml-numbering")]
1909 let indent_left = spec.indent_left.unwrap_or(720 * (spec.ilvl + 1));
1910 #[cfg(feature = "wml-numbering")]
1911 let hanging = spec.hanging.unwrap_or(360);
1912
1913 types::Level {
1914 ilvl: spec.ilvl as i64,
1915 #[cfg(feature = "wml-numbering")]
1916 tplc: None,
1917 #[cfg(feature = "wml-numbering")]
1918 tentative: None,
1919 #[cfg(feature = "wml-numbering")]
1920 start: Some(Box::new(types::CTDecimalNumber {
1921 value: spec.start as i64,
1922 #[cfg(feature = "extra-attrs")]
1923 extra_attrs: std::collections::HashMap::new(),
1924 })),
1925 #[cfg(feature = "wml-numbering")]
1926 num_fmt: Some(Box::new(types::CTNumFmt {
1927 value: list_type_to_num_fmt(spec.format),
1928 format: None,
1929 #[cfg(feature = "extra-attrs")]
1930 extra_attrs: std::collections::HashMap::new(),
1931 })),
1932 #[cfg(feature = "wml-numbering")]
1933 lvl_restart: None,
1934 #[cfg(feature = "wml-numbering")]
1935 paragraph_style: None,
1936 #[cfg(feature = "wml-numbering")]
1937 is_lgl: None,
1938 #[cfg(feature = "wml-numbering")]
1939 suff: None,
1940 #[cfg(feature = "wml-numbering")]
1941 lvl_text: Some(Box::new(types::CTLevelText {
1942 value: Some(spec.text.clone()),
1943 null: None,
1944 #[cfg(feature = "extra-attrs")]
1945 extra_attrs: std::collections::HashMap::new(),
1946 })),
1947 #[cfg(feature = "wml-numbering")]
1948 lvl_pic_bullet_id: None,
1949 #[cfg(feature = "wml-numbering")]
1950 legacy: None,
1951 #[cfg(feature = "wml-numbering")]
1952 lvl_jc: Some(Box::new(types::CTJc {
1953 value: types::STJc::Left,
1954 #[cfg(feature = "extra-attrs")]
1955 extra_attrs: std::collections::HashMap::new(),
1956 })),
1957 #[cfg(feature = "wml-numbering")]
1958 p_pr: Some(Box::new(build_level_paragraph_properties(
1959 indent_left,
1960 hanging,
1961 ))),
1962 #[cfg(feature = "wml-numbering")]
1963 r_pr: if is_bullet {
1964 Some(Box::new(build_bullet_run_properties()))
1965 } else {
1966 None
1967 },
1968 #[cfg(feature = "extra-attrs")]
1969 extra_attrs: std::collections::HashMap::new(),
1970 #[cfg(feature = "extra-children")]
1971 extra_children: Vec::new(),
1972 }
1973}
1974
1975#[cfg(feature = "wml-numbering")]
1977fn build_level_paragraph_properties(indent_left: u32, hanging: u32) -> types::CTPPrGeneral {
1978 let ind = types::CTInd {
1979 #[cfg(feature = "wml-styling")]
1980 left: Some(indent_left.to_string()),
1981 #[cfg(feature = "wml-styling")]
1982 hanging: Some(hanging.to_string()),
1983 ..Default::default()
1984 };
1985 let _ = indent_left;
1987 let _ = hanging;
1988 types::CTPPrGeneral {
1989 indentation: Some(Box::new(ind)),
1990 ..Default::default()
1991 }
1992}
1993
1994fn build_numbering(numberings: &HashMap<u32, PendingNumbering>) -> types::Numbering {
1996 let mut numbering = types::Numbering {
1997 #[cfg(feature = "wml-numbering")]
1998 num_pic_bullet: Vec::new(),
1999 abstract_num: Vec::new(),
2000 num: Vec::new(),
2001 #[cfg(feature = "wml-numbering")]
2002 num_id_mac_at_cleanup: None,
2003 #[cfg(feature = "extra-children")]
2004 extra_children: Vec::new(),
2005 };
2006
2007 let mut sorted: Vec<_> = numberings.values().collect();
2009 sorted.sort_by_key(|n| n.num_id);
2010
2011 for pn in &sorted {
2012 let levels: Vec<types::Level> = if let Some(ref custom_levels) = pn.custom_levels {
2013 custom_levels.iter().map(build_level_from_spec).collect()
2015 } else if let Some(list_type) = pn.list_type {
2016 let (_num_fmt, _lvl_text) = list_type_to_num_fmt_and_text(list_type);
2018 let spec = NumberingLevel {
2019 ilvl: 0,
2020 format: list_type,
2021 start: 1,
2022 text: _lvl_text.to_string(),
2023 indent_left: Some(720),
2024 hanging: Some(360),
2025 };
2026 vec![build_level_from_spec(&spec)]
2027 } else {
2028 Vec::new()
2029 };
2030
2031 let abs = types::AbstractNumbering {
2032 abstract_num_id: pn.abstract_num_id as i64,
2033 #[cfg(feature = "wml-numbering")]
2034 nsid: None,
2035 #[cfg(feature = "wml-numbering")]
2036 multi_level_type: None,
2037 #[cfg(feature = "wml-numbering")]
2038 tmpl: None,
2039 #[cfg(feature = "wml-numbering")]
2040 name: None,
2041 #[cfg(feature = "wml-numbering")]
2042 style_link: None,
2043 #[cfg(feature = "wml-numbering")]
2044 num_style_link: None,
2045 lvl: levels,
2046 #[cfg(feature = "extra-attrs")]
2047 extra_attrs: std::collections::HashMap::new(),
2048 #[cfg(feature = "extra-children")]
2049 extra_children: Vec::new(),
2050 };
2051 numbering.abstract_num.push(abs);
2052
2053 let inst = types::NumberingInstance {
2054 num_id: pn.num_id as i64,
2055 abstract_num_id: Box::new(types::CTDecimalNumber {
2056 value: pn.abstract_num_id as i64,
2057 #[cfg(feature = "extra-attrs")]
2058 extra_attrs: std::collections::HashMap::new(),
2059 }),
2060 #[cfg(feature = "wml-numbering")]
2061 lvl_override: Vec::new(),
2062 #[cfg(feature = "extra-attrs")]
2063 extra_attrs: std::collections::HashMap::new(),
2064 #[cfg(feature = "extra-children")]
2065 extra_children: Vec::new(),
2066 };
2067 numbering.num.push(inst);
2068 }
2069
2070 numbering
2071}
2072
2073#[cfg(feature = "wml-styling")]
2075fn build_bullet_run_properties() -> types::RunProperties {
2076 types::RunProperties {
2077 fonts: Some(Box::new(types::Fonts {
2078 ascii: Some("Symbol".to_string()),
2079 h_ansi: Some("Symbol".to_string()),
2080 hint: Some(types::STHint::Default),
2081 ..Default::default()
2082 })),
2083 ..Default::default()
2084 }
2085}
2086
2087#[cfg(not(feature = "wml-styling"))]
2089#[allow(dead_code)]
2090fn build_bullet_run_properties() -> types::RunProperties {
2091 types::RunProperties::default()
2092}
2093
2094#[cfg(feature = "wml-settings")]
2102fn build_settings_xml(opts: &DocumentSettingsOptions) -> String {
2103 let mut xml = String::from(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
2104 xml.push_str("\r\n");
2105 xml.push_str(
2106 r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">"#,
2107 );
2108
2109 if let Some(tab_stop) = opts.default_tab_stop {
2110 xml.push_str(&format!(r#"<w:defaultTabStop w:val="{}"/>"#, tab_stop));
2111 }
2112
2113 if opts.even_and_odd_headers {
2114 xml.push_str("<w:evenAndOddHeaders/>");
2115 }
2116
2117 if opts.track_changes {
2118 xml.push_str("<w:trackChanges/>");
2119 }
2120
2121 if let Some(ref rsid) = opts.rsid_root {
2122 xml.push_str(&format!(r#"<w:rsidRoot w:val="{}"/>"#, rsid));
2123 }
2124
2125 if opts.compat_mode {
2126 xml.push_str(concat!(
2127 "<w:compat>",
2128 r#"<w:compatSetting w:name="compatibilityMode" "#,
2129 r#"w:uri="http://schemas.microsoft.com/office/word" "#,
2130 r#"w:val="15"/>"#,
2131 "</w:compat>",
2132 ));
2133 }
2134
2135 xml.push_str("</w:settings>");
2136 xml
2137}
2138
2139fn build_graphic_element(rel_id: &str, width: i64, height: i64, doc_id: usize) -> RawXmlElement {
2145 let blip = RawXmlElement {
2146 name: "a:blip".to_string(),
2147 attributes: vec![("r:embed".to_string(), rel_id.to_string())],
2148 children: vec![],
2149 self_closing: true,
2150 };
2151
2152 let fill_rect = RawXmlElement {
2153 name: "a:fillRect".to_string(),
2154 attributes: vec![],
2155 children: vec![],
2156 self_closing: true,
2157 };
2158
2159 let stretch = RawXmlElement {
2160 name: "a:stretch".to_string(),
2161 attributes: vec![],
2162 children: vec![RawXmlNode::Element(fill_rect)],
2163 self_closing: false,
2164 };
2165
2166 let blip_fill = RawXmlElement {
2167 name: "pic:blipFill".to_string(),
2168 attributes: vec![],
2169 children: vec![RawXmlNode::Element(blip), RawXmlNode::Element(stretch)],
2170 self_closing: false,
2171 };
2172
2173 let cnv_pr = RawXmlElement {
2174 name: "pic:cNvPr".to_string(),
2175 attributes: vec![
2176 ("id".to_string(), doc_id.to_string()),
2177 ("name".to_string(), format!("Picture {}", doc_id)),
2178 ],
2179 children: vec![],
2180 self_closing: true,
2181 };
2182
2183 let cnv_pic_pr = RawXmlElement {
2184 name: "pic:cNvPicPr".to_string(),
2185 attributes: vec![],
2186 children: vec![],
2187 self_closing: true,
2188 };
2189
2190 let nv_pic_pr = RawXmlElement {
2191 name: "pic:nvPicPr".to_string(),
2192 attributes: vec![],
2193 children: vec![RawXmlNode::Element(cnv_pr), RawXmlNode::Element(cnv_pic_pr)],
2194 self_closing: false,
2195 };
2196
2197 let off = RawXmlElement {
2198 name: "a:off".to_string(),
2199 attributes: vec![
2200 ("x".to_string(), "0".to_string()),
2201 ("y".to_string(), "0".to_string()),
2202 ],
2203 children: vec![],
2204 self_closing: true,
2205 };
2206
2207 let ext = RawXmlElement {
2208 name: "a:ext".to_string(),
2209 attributes: vec![
2210 ("cx".to_string(), width.to_string()),
2211 ("cy".to_string(), height.to_string()),
2212 ],
2213 children: vec![],
2214 self_closing: true,
2215 };
2216
2217 let xfrm = RawXmlElement {
2218 name: "a:xfrm".to_string(),
2219 attributes: vec![],
2220 children: vec![RawXmlNode::Element(off), RawXmlNode::Element(ext)],
2221 self_closing: false,
2222 };
2223
2224 let av_lst = RawXmlElement {
2225 name: "a:avLst".to_string(),
2226 attributes: vec![],
2227 children: vec![],
2228 self_closing: true,
2229 };
2230
2231 let prst_geom = RawXmlElement {
2232 name: "a:prstGeom".to_string(),
2233 attributes: vec![("prst".to_string(), "rect".to_string())],
2234 children: vec![RawXmlNode::Element(av_lst)],
2235 self_closing: false,
2236 };
2237
2238 let sp_pr = RawXmlElement {
2239 name: "pic:spPr".to_string(),
2240 attributes: vec![],
2241 children: vec![RawXmlNode::Element(xfrm), RawXmlNode::Element(prst_geom)],
2242 self_closing: false,
2243 };
2244
2245 let pic = RawXmlElement {
2246 name: "pic:pic".to_string(),
2247 attributes: vec![],
2248 children: vec![
2249 RawXmlNode::Element(nv_pic_pr),
2250 RawXmlNode::Element(blip_fill),
2251 RawXmlNode::Element(sp_pr),
2252 ],
2253 self_closing: false,
2254 };
2255
2256 let graphic_data = RawXmlElement {
2257 name: "a:graphicData".to_string(),
2258 attributes: vec![(
2259 "uri".to_string(),
2260 "http://schemas.openxmlformats.org/drawingml/2006/picture".to_string(),
2261 )],
2262 children: vec![RawXmlNode::Element(pic)],
2263 self_closing: false,
2264 };
2265
2266 RawXmlElement {
2267 name: "a:graphic".to_string(),
2268 attributes: vec![],
2269 children: vec![RawXmlNode::Element(graphic_data)],
2270 self_closing: false,
2271 }
2272}
2273
2274fn build_inline_image_element(image: &InlineImage, doc_id: usize) -> RawXmlElement {
2276 let width_emu = image.width_emu.unwrap_or(914400);
2277 let height_emu = image.height_emu.unwrap_or(914400);
2278 let desc = image.description.as_deref().unwrap_or("Image");
2279
2280 let extent = RawXmlElement {
2281 name: "wp:extent".to_string(),
2282 attributes: vec![
2283 ("cx".to_string(), width_emu.to_string()),
2284 ("cy".to_string(), height_emu.to_string()),
2285 ],
2286 children: vec![],
2287 self_closing: true,
2288 };
2289
2290 let doc_pr = RawXmlElement {
2291 name: "wp:docPr".to_string(),
2292 attributes: vec![
2293 ("id".to_string(), doc_id.to_string()),
2294 ("name".to_string(), format!("Picture {}", doc_id)),
2295 ("descr".to_string(), desc.to_string()),
2296 ],
2297 children: vec![],
2298 self_closing: true,
2299 };
2300
2301 let graphic_frame_locks = RawXmlElement {
2302 name: "a:graphicFrameLocks".to_string(),
2303 attributes: vec![("noChangeAspect".to_string(), "1".to_string())],
2304 children: vec![],
2305 self_closing: true,
2306 };
2307
2308 let cnv_graphic_frame_pr = RawXmlElement {
2309 name: "wp:cNvGraphicFramePr".to_string(),
2310 attributes: vec![],
2311 children: vec![RawXmlNode::Element(graphic_frame_locks)],
2312 self_closing: false,
2313 };
2314
2315 let graphic = build_graphic_element(&image.rel_id, width_emu, height_emu, doc_id);
2316
2317 RawXmlElement {
2318 name: "wp:inline".to_string(),
2319 attributes: vec![
2320 ("distT".to_string(), "0".to_string()),
2321 ("distB".to_string(), "0".to_string()),
2322 ("distL".to_string(), "0".to_string()),
2323 ("distR".to_string(), "0".to_string()),
2324 ],
2325 children: vec![
2326 RawXmlNode::Element(extent),
2327 RawXmlNode::Element(doc_pr),
2328 RawXmlNode::Element(cnv_graphic_frame_pr),
2329 RawXmlNode::Element(graphic),
2330 ],
2331 self_closing: false,
2332 }
2333}
2334
2335fn build_wrap_element(wrap_type: WrapType) -> RawXmlElement {
2337 match wrap_type {
2338 WrapType::None => RawXmlElement {
2339 name: "wp:wrapNone".to_string(),
2340 attributes: vec![],
2341 children: vec![],
2342 self_closing: true,
2343 },
2344 WrapType::Square => RawXmlElement {
2345 name: "wp:wrapSquare".to_string(),
2346 attributes: vec![("wrapText".to_string(), "bothSides".to_string())],
2347 children: vec![],
2348 self_closing: true,
2349 },
2350 WrapType::Tight => {
2351 let polygon = build_default_wrap_polygon();
2352 RawXmlElement {
2353 name: "wp:wrapTight".to_string(),
2354 attributes: vec![("wrapText".to_string(), "bothSides".to_string())],
2355 children: vec![RawXmlNode::Element(polygon)],
2356 self_closing: false,
2357 }
2358 }
2359 WrapType::Through => {
2360 let polygon = build_default_wrap_polygon();
2361 RawXmlElement {
2362 name: "wp:wrapThrough".to_string(),
2363 attributes: vec![("wrapText".to_string(), "bothSides".to_string())],
2364 children: vec![RawXmlNode::Element(polygon)],
2365 self_closing: false,
2366 }
2367 }
2368 WrapType::TopAndBottom => RawXmlElement {
2369 name: "wp:wrapTopAndBottom".to_string(),
2370 attributes: vec![],
2371 children: vec![],
2372 self_closing: true,
2373 },
2374 }
2375}
2376
2377fn build_default_wrap_polygon() -> RawXmlElement {
2379 let start = RawXmlElement {
2380 name: "wp:start".to_string(),
2381 attributes: vec![
2382 ("x".to_string(), "0".to_string()),
2383 ("y".to_string(), "0".to_string()),
2384 ],
2385 children: vec![],
2386 self_closing: true,
2387 };
2388
2389 let line_to_1 = RawXmlElement {
2390 name: "wp:lineTo".to_string(),
2391 attributes: vec![
2392 ("x".to_string(), "0".to_string()),
2393 ("y".to_string(), "21600".to_string()),
2394 ],
2395 children: vec![],
2396 self_closing: true,
2397 };
2398
2399 let line_to_2 = RawXmlElement {
2400 name: "wp:lineTo".to_string(),
2401 attributes: vec![
2402 ("x".to_string(), "21600".to_string()),
2403 ("y".to_string(), "21600".to_string()),
2404 ],
2405 children: vec![],
2406 self_closing: true,
2407 };
2408
2409 let line_to_3 = RawXmlElement {
2410 name: "wp:lineTo".to_string(),
2411 attributes: vec![
2412 ("x".to_string(), "21600".to_string()),
2413 ("y".to_string(), "0".to_string()),
2414 ],
2415 children: vec![],
2416 self_closing: true,
2417 };
2418
2419 let line_to_4 = RawXmlElement {
2420 name: "wp:lineTo".to_string(),
2421 attributes: vec![
2422 ("x".to_string(), "0".to_string()),
2423 ("y".to_string(), "0".to_string()),
2424 ],
2425 children: vec![],
2426 self_closing: true,
2427 };
2428
2429 RawXmlElement {
2430 name: "wp:wrapPolygon".to_string(),
2431 attributes: vec![("edited".to_string(), "0".to_string())],
2432 children: vec![
2433 RawXmlNode::Element(start),
2434 RawXmlNode::Element(line_to_1),
2435 RawXmlNode::Element(line_to_2),
2436 RawXmlNode::Element(line_to_3),
2437 RawXmlNode::Element(line_to_4),
2438 ],
2439 self_closing: false,
2440 }
2441}
2442
2443fn build_anchored_image_element(image: &AnchoredImage, doc_id: usize) -> RawXmlElement {
2445 let width_emu = image.width_emu.unwrap_or(914400);
2446 let height_emu = image.height_emu.unwrap_or(914400);
2447 let desc = image.description.as_deref().unwrap_or("Image");
2448 let behind_doc = if image.behind_doc { "1" } else { "0" };
2449
2450 let simple_pos = RawXmlElement {
2451 name: "wp:simplePos".to_string(),
2452 attributes: vec![
2453 ("x".to_string(), "0".to_string()),
2454 ("y".to_string(), "0".to_string()),
2455 ],
2456 children: vec![],
2457 self_closing: true,
2458 };
2459
2460 let pos_offset_h = RawXmlElement {
2461 name: "wp:posOffset".to_string(),
2462 attributes: vec![],
2463 children: vec![RawXmlNode::Text(image.pos_x.to_string())],
2464 self_closing: false,
2465 };
2466
2467 let position_h = RawXmlElement {
2468 name: "wp:positionH".to_string(),
2469 attributes: vec![("relativeFrom".to_string(), "column".to_string())],
2470 children: vec![RawXmlNode::Element(pos_offset_h)],
2471 self_closing: false,
2472 };
2473
2474 let pos_offset_v = RawXmlElement {
2475 name: "wp:posOffset".to_string(),
2476 attributes: vec![],
2477 children: vec![RawXmlNode::Text(image.pos_y.to_string())],
2478 self_closing: false,
2479 };
2480
2481 let position_v = RawXmlElement {
2482 name: "wp:positionV".to_string(),
2483 attributes: vec![("relativeFrom".to_string(), "paragraph".to_string())],
2484 children: vec![RawXmlNode::Element(pos_offset_v)],
2485 self_closing: false,
2486 };
2487
2488 let extent = RawXmlElement {
2489 name: "wp:extent".to_string(),
2490 attributes: vec![
2491 ("cx".to_string(), width_emu.to_string()),
2492 ("cy".to_string(), height_emu.to_string()),
2493 ],
2494 children: vec![],
2495 self_closing: true,
2496 };
2497
2498 let effect_extent = RawXmlElement {
2499 name: "wp:effectExtent".to_string(),
2500 attributes: vec![
2501 ("l".to_string(), "0".to_string()),
2502 ("t".to_string(), "0".to_string()),
2503 ("r".to_string(), "0".to_string()),
2504 ("b".to_string(), "0".to_string()),
2505 ],
2506 children: vec![],
2507 self_closing: true,
2508 };
2509
2510 let wrap = build_wrap_element(image.wrap_type);
2511
2512 let doc_pr = RawXmlElement {
2513 name: "wp:docPr".to_string(),
2514 attributes: vec![
2515 ("id".to_string(), doc_id.to_string()),
2516 ("name".to_string(), format!("Picture {}", doc_id)),
2517 ("descr".to_string(), desc.to_string()),
2518 ],
2519 children: vec![],
2520 self_closing: true,
2521 };
2522
2523 let graphic_frame_locks = RawXmlElement {
2524 name: "a:graphicFrameLocks".to_string(),
2525 attributes: vec![("noChangeAspect".to_string(), "1".to_string())],
2526 children: vec![],
2527 self_closing: true,
2528 };
2529
2530 let cnv_graphic_frame_pr = RawXmlElement {
2531 name: "wp:cNvGraphicFramePr".to_string(),
2532 attributes: vec![],
2533 children: vec![RawXmlNode::Element(graphic_frame_locks)],
2534 self_closing: false,
2535 };
2536
2537 let graphic = build_graphic_element(&image.rel_id, width_emu, height_emu, doc_id);
2538
2539 RawXmlElement {
2540 name: "wp:anchor".to_string(),
2541 attributes: vec![
2542 ("distT".to_string(), "0".to_string()),
2543 ("distB".to_string(), "0".to_string()),
2544 ("distL".to_string(), "114300".to_string()),
2545 ("distR".to_string(), "114300".to_string()),
2546 ("simplePos".to_string(), "0".to_string()),
2547 ("relativeHeight".to_string(), "251658240".to_string()),
2548 ("behindDoc".to_string(), behind_doc.to_string()),
2549 ("locked".to_string(), "0".to_string()),
2550 ("layoutInCell".to_string(), "1".to_string()),
2551 ("allowOverlap".to_string(), "1".to_string()),
2552 ],
2553 children: vec![
2554 RawXmlNode::Element(simple_pos),
2555 RawXmlNode::Element(position_h),
2556 RawXmlNode::Element(position_v),
2557 RawXmlNode::Element(extent),
2558 RawXmlNode::Element(effect_extent),
2559 RawXmlNode::Element(wrap),
2560 RawXmlNode::Element(doc_pr),
2561 RawXmlNode::Element(cnv_graphic_frame_pr),
2562 RawXmlNode::Element(graphic),
2563 ],
2564 self_closing: false,
2565 }
2566}
2567
2568fn build_text_box_element(text_box: &TextBox, doc_id: usize) -> RawXmlElement {
2599 let w = text_box.width_emu;
2600 let h = text_box.height_emu;
2601
2602 let off = RawXmlElement {
2604 name: "a:off".to_string(),
2605 attributes: vec![
2606 ("x".to_string(), "0".to_string()),
2607 ("y".to_string(), "0".to_string()),
2608 ],
2609 children: vec![],
2610 self_closing: true,
2611 };
2612 let ext = RawXmlElement {
2613 name: "a:ext".to_string(),
2614 attributes: vec![
2615 ("cx".to_string(), w.to_string()),
2616 ("cy".to_string(), h.to_string()),
2617 ],
2618 children: vec![],
2619 self_closing: true,
2620 };
2621 let xfrm = RawXmlElement {
2622 name: "a:xfrm".to_string(),
2623 attributes: vec![],
2624 children: vec![RawXmlNode::Element(off), RawXmlNode::Element(ext)],
2625 self_closing: false,
2626 };
2627 let av_lst = RawXmlElement {
2628 name: "a:avLst".to_string(),
2629 attributes: vec![],
2630 children: vec![],
2631 self_closing: true,
2632 };
2633 let prst_geom = RawXmlElement {
2634 name: "a:prstGeom".to_string(),
2635 attributes: vec![("prst".to_string(), "rect".to_string())],
2636 children: vec![RawXmlNode::Element(av_lst)],
2637 self_closing: false,
2638 };
2639 let sp_pr = RawXmlElement {
2640 name: "wps:spPr".to_string(),
2641 attributes: vec![],
2642 children: vec![RawXmlNode::Element(xfrm), RawXmlNode::Element(prst_geom)],
2643 self_closing: false,
2644 };
2645
2646 let t_node = RawXmlElement {
2648 name: "w:t".to_string(),
2649 attributes: vec![("xml:space".to_string(), "preserve".to_string())],
2650 children: vec![RawXmlNode::Text(text_box.text.clone())],
2651 self_closing: false,
2652 };
2653 let r_node = RawXmlElement {
2654 name: "w:r".to_string(),
2655 attributes: vec![],
2656 children: vec![RawXmlNode::Element(t_node)],
2657 self_closing: false,
2658 };
2659 let p_node = RawXmlElement {
2660 name: "w:p".to_string(),
2661 attributes: vec![],
2662 children: vec![RawXmlNode::Element(r_node)],
2663 self_closing: false,
2664 };
2665 let txbx_content = RawXmlElement {
2666 name: "w:txbxContent".to_string(),
2667 attributes: vec![],
2668 children: vec![RawXmlNode::Element(p_node)],
2669 self_closing: false,
2670 };
2671 let txbx = RawXmlElement {
2672 name: "wps:txbx".to_string(),
2673 attributes: vec![],
2674 children: vec![RawXmlNode::Element(txbx_content)],
2675 self_closing: false,
2676 };
2677
2678 let wsp = RawXmlElement {
2680 name: "wps:wsp".to_string(),
2681 attributes: vec![],
2682 children: vec![RawXmlNode::Element(sp_pr), RawXmlNode::Element(txbx)],
2683 self_closing: false,
2684 };
2685
2686 let graphic_data = RawXmlElement {
2688 name: "a:graphicData".to_string(),
2689 attributes: vec![("uri".to_string(), NS_WPS.to_string())],
2690 children: vec![RawXmlNode::Element(wsp)],
2691 self_closing: false,
2692 };
2693
2694 let graphic = RawXmlElement {
2696 name: "a:graphic".to_string(),
2697 attributes: vec![],
2698 children: vec![RawXmlNode::Element(graphic_data)],
2699 self_closing: false,
2700 };
2701
2702 let doc_pr = RawXmlElement {
2704 name: "wp:docPr".to_string(),
2705 attributes: vec![
2706 ("id".to_string(), doc_id.to_string()),
2707 ("name".to_string(), format!("Text Box {}", doc_id)),
2708 ],
2709 children: vec![],
2710 self_closing: true,
2711 };
2712
2713 let extent = RawXmlElement {
2715 name: "wp:extent".to_string(),
2716 attributes: vec![
2717 ("cx".to_string(), w.to_string()),
2718 ("cy".to_string(), h.to_string()),
2719 ],
2720 children: vec![],
2721 self_closing: true,
2722 };
2723
2724 RawXmlElement {
2726 name: "wp:inline".to_string(),
2727 attributes: vec![
2728 ("distT".to_string(), "0".to_string()),
2729 ("distB".to_string(), "0".to_string()),
2730 ("distL".to_string(), "0".to_string()),
2731 ("distR".to_string(), "0".to_string()),
2732 ],
2733 children: vec![
2734 RawXmlNode::Element(extent),
2735 RawXmlNode::Element(doc_pr),
2736 RawXmlNode::Element(graphic),
2737 ],
2738 self_closing: false,
2739 }
2740}
2741
2742fn extension_from_content_type(content_type: &str) -> &'static str {
2748 match content_type {
2749 "image/png" => "png",
2750 "image/jpeg" => "jpg",
2751 "image/gif" => "gif",
2752 "image/bmp" => "bmp",
2753 "image/tiff" => "tiff",
2754 "image/webp" => "webp",
2755 "image/svg+xml" => "svg",
2756 "image/x-emf" | "image/emf" => "emf",
2757 "image/x-wmf" | "image/wmf" => "wmf",
2758 _ => "bin",
2759 }
2760}
2761
2762#[cfg(test)]
2763mod tests {
2764 use super::*;
2765
2766 #[test]
2767 fn test_document_builder_simple() {
2768 let mut builder = DocumentBuilder::new();
2769 builder.add_paragraph("Hello, World!");
2770 builder.add_paragraph("Second paragraph");
2771
2772 let body = builder.document.body.as_ref().unwrap();
2773 assert_eq!(body.block_content.len(), 2);
2774 }
2775
2776 #[test]
2777 fn test_serialize_to_xml_bytes() {
2778 let doc = types::Document {
2779 background: None,
2780 body: Some(Box::new(types::Body::default())),
2781 conformance: None,
2782 #[cfg(feature = "extra-attrs")]
2783 extra_attrs: std::collections::HashMap::new(),
2784 #[cfg(feature = "extra-children")]
2785 extra_children: Vec::new(),
2786 };
2787
2788 let bytes = serialize_to_xml_bytes(&doc, "w:document").unwrap();
2789 let xml = String::from_utf8(bytes).unwrap();
2790 assert!(xml.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"));
2791 assert!(xml.contains("w:document"));
2792 }
2793
2794 #[test]
2795 fn test_list_type_mapping() {
2796 let (fmt, text) = list_type_to_num_fmt_and_text(ListType::Bullet);
2797 assert!(matches!(fmt, types::STNumberFormat::Bullet));
2798 assert_eq!(text, "\u{2022}");
2799
2800 let (fmt, text) = list_type_to_num_fmt_and_text(ListType::Decimal);
2801 assert!(matches!(fmt, types::STNumberFormat::Decimal));
2802 assert_eq!(text, "%1.");
2803 }
2804
2805 #[test]
2806 fn test_extension_from_content_type() {
2807 assert_eq!(extension_from_content_type("image/png"), "png");
2808 assert_eq!(extension_from_content_type("image/jpeg"), "jpg");
2809 assert_eq!(extension_from_content_type("image/gif"), "gif");
2810 assert_eq!(extension_from_content_type("unknown/type"), "bin");
2811 }
2812
2813 #[test]
2814 fn test_drawing_build() {
2815 let mut drawing = Drawing::new();
2816 drawing
2817 .add_image("rId1")
2818 .set_width_inches(1.0)
2819 .set_height_inches(1.0);
2820
2821 let mut doc_id = 1;
2822 let ct_drawing = drawing.build(&mut doc_id);
2823 assert_eq!(doc_id, 2);
2824
2825 #[cfg(feature = "extra-children")]
2826 assert_eq!(ct_drawing.extra_children.len(), 1);
2827 let _ = ct_drawing;
2828 }
2829
2830 #[test]
2831 fn test_text_box_build() {
2832 let mut drawing = Drawing::new();
2833 drawing
2834 .add_text_box("Hello, text box!")
2835 .set_width_inches(2.0)
2836 .set_height_inches(1.0);
2837
2838 assert_eq!(drawing.text_boxes().len(), 1);
2839 assert_eq!(drawing.text_boxes()[0].text, "Hello, text box!");
2840 assert_eq!(drawing.text_boxes()[0].width_emu, (2.0 * 914400.0) as i64);
2841
2842 let mut doc_id = 1;
2843 let ct_drawing = drawing.build(&mut doc_id);
2844 assert_eq!(doc_id, 2);
2845 #[cfg(feature = "extra-children")]
2846 assert_eq!(ct_drawing.extra_children.len(), 1);
2847 let _ = ct_drawing;
2848 }
2849
2850 #[test]
2851 #[cfg(all(feature = "extra-attrs", feature = "extra-children"))]
2852 fn test_roundtrip_with_text_box() {
2853 use crate::Document;
2854 use std::io::Cursor;
2855
2856 let mut builder = DocumentBuilder::new();
2857 {
2858 let body = builder.body_mut();
2859 let para = body.add_paragraph();
2860 let run = para.add_run();
2861 let mut drawing = Drawing::new();
2863 drawing.add_text_box("My text box content");
2864 let ct = drawing.build(&mut 1usize.clone());
2865 run.add_drawing(ct);
2866 }
2867
2868 let mut buf = Cursor::new(Vec::new());
2869 builder.write(&mut buf).unwrap();
2870
2871 buf.set_position(0);
2873 let doc = Document::from_reader(buf).unwrap();
2874 let body = doc.body();
2876 assert_eq!(body.block_content.len(), 1);
2877 }
2878
2879 #[test]
2880 fn test_add_custom_list() {
2881 let mut builder = DocumentBuilder::new();
2882 let num_id = builder.add_custom_list(vec![
2883 NumberingLevel {
2884 ilvl: 0,
2885 format: ListType::Decimal,
2886 start: 1,
2887 text: "%1.".to_string(),
2888 indent_left: Some(720),
2889 hanging: Some(360),
2890 },
2891 NumberingLevel {
2892 ilvl: 1,
2893 format: ListType::LowerLetter,
2894 start: 1,
2895 text: "%2.".to_string(),
2896 indent_left: Some(1440),
2897 hanging: Some(360),
2898 },
2899 ]);
2900 assert_eq!(num_id, 1);
2901 assert!(builder.numberings.contains_key(&1));
2902 let pn = &builder.numberings[&1];
2903 assert!(pn.custom_levels.is_some());
2904 assert_eq!(pn.custom_levels.as_ref().unwrap().len(), 2);
2905 }
2906
2907 #[test]
2908 fn test_numbering_level_helpers() {
2909 let bullet = NumberingLevel::bullet(0);
2910 assert_eq!(bullet.ilvl, 0);
2911 assert_eq!(bullet.format, ListType::Bullet);
2912 assert_eq!(bullet.start, 1);
2913 assert_eq!(bullet.indent_left, Some(720));
2914
2915 let decimal = NumberingLevel::decimal(2);
2916 assert_eq!(decimal.ilvl, 2);
2917 assert_eq!(decimal.format, ListType::Decimal);
2918 assert_eq!(decimal.indent_left, Some(2160));
2919 }
2920
2921 #[test]
2922 #[cfg(all(
2923 feature = "wml-numbering",
2924 feature = "extra-attrs",
2925 feature = "extra-children"
2926 ))]
2927 fn test_roundtrip_custom_list() {
2928 use crate::Document;
2929 use crate::ext::BodyExt;
2930 use std::io::Cursor;
2931
2932 let mut builder = DocumentBuilder::new();
2933 let num_id = builder.add_custom_list(vec![NumberingLevel {
2934 ilvl: 0,
2935 format: ListType::Decimal,
2936 start: 1,
2937 text: "%1.".to_string(),
2938 indent_left: Some(720),
2939 hanging: Some(360),
2940 }]);
2941
2942 {
2943 let body = builder.body_mut();
2944 let para = body.add_paragraph();
2945 #[cfg(feature = "wml-styling")]
2946 para.set_numbering(num_id, 0);
2947 para.add_run().set_text("Item one");
2948 }
2949
2950 let mut buf = Cursor::new(Vec::new());
2951 builder.write(&mut buf).unwrap();
2952
2953 buf.set_position(0);
2954 let doc = Document::from_reader(buf).unwrap();
2955 assert_eq!(doc.body().paragraphs().len(), 1);
2956 let _ = num_id;
2958 }
2959
2960 #[test]
2961 fn test_core_and_app_properties_roundtrip() {
2962 use crate::Document;
2963 use crate::document::{AppProperties, CoreProperties};
2964 use std::io::Cursor;
2965
2966 let mut builder = DocumentBuilder::new();
2967 builder.add_paragraph("Hello");
2968 builder.set_core_properties(CoreProperties {
2969 title: Some("Test Doc".to_string()),
2970 creator: Some("Test Author".to_string()),
2971 created: Some("2024-01-01T00:00:00Z".to_string()),
2972 ..Default::default()
2973 });
2974 builder.set_app_properties(AppProperties {
2975 application: Some("ooxml-wml".to_string()),
2976 pages: Some(1),
2977 ..Default::default()
2978 });
2979
2980 let mut buffer = Cursor::new(Vec::new());
2981 builder.write(&mut buffer).unwrap();
2982
2983 buffer.set_position(0);
2984 let doc = Document::from_reader(buffer).unwrap();
2985
2986 let core = doc
2987 .core_properties()
2988 .expect("core properties should be present");
2989 assert_eq!(core.title, Some("Test Doc".to_string()));
2990 assert_eq!(core.creator, Some("Test Author".to_string()));
2991 assert_eq!(core.created, Some("2024-01-01T00:00:00Z".to_string()));
2992
2993 let app = doc
2994 .app_properties()
2995 .expect("app properties should be present");
2996 assert_eq!(app.application, Some("ooxml-wml".to_string()));
2997 assert_eq!(app.pages, Some(1));
2998 }
2999
3000 #[test]
3001 fn test_roundtrip_create_and_read() {
3002 use crate::Document;
3003 use crate::ext::BodyExt;
3004 use std::io::Cursor;
3005
3006 let mut builder = DocumentBuilder::new();
3008 builder.add_paragraph("Test content");
3009
3010 let mut buffer = Cursor::new(Vec::new());
3012 builder.write(&mut buffer).unwrap();
3013
3014 buffer.set_position(0);
3016 let doc = Document::from_reader(buffer).unwrap();
3017
3018 assert_eq!(doc.body().paragraphs().len(), 1);
3019 assert_eq!(doc.text(), "Test content");
3020 }
3021
3022 #[test]
3023 fn test_styles_written_and_readable() {
3024 use crate::Document;
3025 use std::io::Cursor;
3026
3027 let mut builder = DocumentBuilder::new();
3029 builder.add_paragraph("Styled content");
3030
3031 let style = types::Style {
3033 r#type: Some(types::STStyleType::Paragraph),
3034 style_id: Some("MyHeading".to_string()),
3035 name: Some(Box::new(types::CTString {
3036 value: "My Heading".to_string(),
3037 #[cfg(feature = "extra-attrs")]
3038 extra_attrs: std::collections::HashMap::new(),
3039 })),
3040 ..Default::default()
3041 };
3042 builder.add_style(style);
3043
3044 let mut buffer = Cursor::new(Vec::new());
3046 builder.write(&mut buffer).unwrap();
3047
3048 buffer.set_position(0);
3050 let doc = Document::from_reader(buffer).unwrap();
3051
3052 let styles = doc.styles();
3053 assert_eq!(styles.style.len(), 1);
3054 assert_eq!(styles.style[0].style_id.as_deref(), Some("MyHeading"));
3055 }
3056}