1use std::io::{Seek, Write};
4
5use opc::{Package, Part, Relationship, Relationships, TargetMode};
6use xml_core::{BytesDecl, BytesEnd, BytesStart, BytesText, Event, Writer};
7
8use crate::error::Result;
9use crate::model::{
10 Alignment, Block, Bookmark, BreakKind, ColorScheme, Comment, CustomPropertyValue, Document,
11 DocumentProperties, EmbeddedChart, ExtendedProperties, Field, FontScheme, HeaderFooter,
12 Highlight, Hyperlink, Image, Note, NoteKind, NoteReference, NumberingDefinition, Orientation,
13 PageSetup, Paragraph, ProtectionKind, Run, RunContent, StructuredDocumentTag, Style, StyleKind,
14 TabStop, Table, TableCell, TableRow, Theme, UnderlineStyle, VerticalAlign,
15};
16
17const WORDPROCESSINGML_NAMESPACE: &str =
18 "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19const RELATIONSHIPS_NAMESPACE: &str =
24 "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
25const WORDPROCESSING_DRAWING_NAMESPACE: &str =
26 "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
27const DRAWINGML_NAMESPACE: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
28const DRAWINGML_PICTURE_NAMESPACE: &str =
29 "http://schemas.openxmlformats.org/drawingml/2006/picture";
30const IMAGE_RELATIONSHIP_TYPE: &str =
31 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
32
33const DRAWINGML_CHART_NAMESPACE: &str = "http://schemas.openxmlformats.org/drawingml/2006/chart";
35const CHART_CONTENT_TYPE: &str =
36 "application/vnd.openxmlformats-officedocument.drawingml.chart+xml";
37const CHART_RELATIONSHIP_TYPE: &str =
38 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
39
40const MAIN_DOCUMENT_CONTENT_TYPE: &str =
41 "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml";
42const MAIN_DOCUMENT_RELATIONSHIP_TYPE: &str =
43 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
44const MAIN_DOCUMENT_PART_NAME: &str = "/word/document.xml";
45const MAIN_DOCUMENT_ENTRY_NAME: &str = "word/document.xml";
46
47const CORE_PROPERTIES_CONTENT_TYPE: &str =
59 "application/vnd.openxmlformats-package.core-properties+xml";
60const CORE_PROPERTIES_RELATIONSHIP_TYPE: &str =
61 "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
62const CORE_PROPERTIES_PART_NAME: &str = "/docProps/core.xml";
63const CORE_PROPERTIES_ENTRY_NAME: &str = "docProps/core.xml";
64const CORE_PROPERTIES_NAMESPACE: &str =
65 "http://schemas.openxmlformats.org/package/2006/metadata/core-properties";
66const DC_NAMESPACE: &str = "http://purl.org/dc/elements/1.1/";
67const DCTERMS_NAMESPACE: &str = "http://purl.org/dc/terms/";
68const XSI_NAMESPACE: &str = "http://www.w3.org/2001/XMLSchema-instance";
69
70const EXTENDED_PROPERTIES_CONTENT_TYPE: &str =
71 "application/vnd.openxmlformats-officedocument.extended-properties+xml";
72const EXTENDED_PROPERTIES_RELATIONSHIP_TYPE: &str =
73 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties";
74const EXTENDED_PROPERTIES_PART_NAME: &str = "/docProps/app.xml";
75const EXTENDED_PROPERTIES_ENTRY_NAME: &str = "docProps/app.xml";
76const EXTENDED_PROPERTIES_NAMESPACE: &str =
77 "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties";
78
79const CUSTOM_PROPERTIES_CONTENT_TYPE: &str =
85 "application/vnd.openxmlformats-officedocument.custom-properties+xml";
86const CUSTOM_PROPERTIES_RELATIONSHIP_TYPE: &str =
87 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
88const CUSTOM_PROPERTIES_PART_NAME: &str = "/docProps/custom.xml";
89const CUSTOM_PROPERTIES_ENTRY_NAME: &str = "docProps/custom.xml";
90const CUSTOM_PROPERTIES_NAMESPACE: &str =
91 "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties";
92const CUSTOM_PROPERTIES_VT_NAMESPACE: &str =
93 "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes";
94const CUSTOM_PROPERTY_FMTID: &str = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";
97const FIRST_CUSTOM_PROPERTY_PID: i32 = 2;
101
102const SETTINGS_CONTENT_TYPE: &str =
103 "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml";
104const SETTINGS_RELATIONSHIP_TYPE: &str =
105 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings";
106const SETTINGS_PART_NAME: &str = "/word/settings.xml";
107const SETTINGS_ENTRY_NAME: &str = "settings.xml";
108
109const HEADER_CONTENT_TYPE: &str =
114 "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml";
115const HEADER_RELATIONSHIP_TYPE: &str =
116 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
117const HEADER_PART_NAME: &str = "/word/header1.xml";
118const HEADER_ENTRY_NAME: &str = "header1.xml";
119const HEADER_FIRST_PART_NAME: &str = "/word/header2.xml";
120const HEADER_FIRST_ENTRY_NAME: &str = "header2.xml";
121const HEADER_EVEN_PART_NAME: &str = "/word/header3.xml";
122const HEADER_EVEN_ENTRY_NAME: &str = "header3.xml";
123
124const FOOTER_CONTENT_TYPE: &str =
125 "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml";
126const FOOTER_RELATIONSHIP_TYPE: &str =
127 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
128const FOOTER_PART_NAME: &str = "/word/footer1.xml";
129const FOOTER_ENTRY_NAME: &str = "footer1.xml";
130const FOOTER_FIRST_PART_NAME: &str = "/word/footer2.xml";
131const FOOTER_FIRST_ENTRY_NAME: &str = "footer2.xml";
132const FOOTER_EVEN_PART_NAME: &str = "/word/footer3.xml";
133const FOOTER_EVEN_ENTRY_NAME: &str = "footer3.xml";
134
135const STYLES_CONTENT_TYPE: &str =
139 "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml";
140const STYLES_RELATIONSHIP_TYPE: &str =
141 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
142const STYLES_PART_NAME: &str = "/word/styles.xml";
143const STYLES_ENTRY_NAME: &str = "styles.xml";
144
145const HYPERLINK_RELATIONSHIP_TYPE: &str =
150 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
151
152const NUMBERING_CONTENT_TYPE: &str =
156 "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
157const NUMBERING_RELATIONSHIP_TYPE: &str =
158 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
159const NUMBERING_PART_NAME: &str = "/word/numbering.xml";
160const NUMBERING_ENTRY_NAME: &str = "numbering.xml";
161
162const FOOTNOTES_CONTENT_TYPE: &str =
166 "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml";
167const FOOTNOTES_RELATIONSHIP_TYPE: &str =
168 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";
169const FOOTNOTES_PART_NAME: &str = "/word/footnotes.xml";
170const FOOTNOTES_ENTRY_NAME: &str = "footnotes.xml";
171
172const ENDNOTES_CONTENT_TYPE: &str =
173 "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml";
174const ENDNOTES_RELATIONSHIP_TYPE: &str =
175 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes";
176const ENDNOTES_PART_NAME: &str = "/word/endnotes.xml";
177const ENDNOTES_ENTRY_NAME: &str = "endnotes.xml";
178
179const COMMENTS_CONTENT_TYPE: &str =
183 "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
184const COMMENTS_RELATIONSHIP_TYPE: &str =
185 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
186const COMMENTS_PART_NAME: &str = "/word/comments.xml";
187const COMMENTS_ENTRY_NAME: &str = "comments.xml";
188
189const COMMENT_REFERENCE_STYLE_ID: &str = "CommentReference";
193
194const THEME_CONTENT_TYPE: &str = "application/vnd.openxmlformats-officedocument.theme+xml";
198const THEME_RELATIONSHIP_TYPE: &str =
199 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
200const THEME_PART_NAME: &str = "/word/theme/theme1.xml";
201const THEME_ENTRY_NAME: &str = "theme/theme1.xml";
202
203impl Document {
204 pub fn write_to<W: Write + Seek>(&self, writer: W) -> Result<W> {
217 let mut package = Package::new();
218
219 package.add_relationship(Relationship {
220 id: "rId1".to_string(),
221 rel_type: MAIN_DOCUMENT_RELATIONSHIP_TYPE.to_string(),
222 target: MAIN_DOCUMENT_ENTRY_NAME.to_string(),
223 target_mode: TargetMode::Internal,
224 });
225 package.add_relationship(Relationship {
226 id: "rId2".to_string(),
227 rel_type: CORE_PROPERTIES_RELATIONSHIP_TYPE.to_string(),
228 target: CORE_PROPERTIES_ENTRY_NAME.to_string(),
229 target_mode: TargetMode::Internal,
230 });
231 package.add_relationship(Relationship {
232 id: "rId3".to_string(),
233 rel_type: EXTENDED_PROPERTIES_RELATIONSHIP_TYPE.to_string(),
234 target: EXTENDED_PROPERTIES_ENTRY_NAME.to_string(),
235 target_mode: TargetMode::Internal,
236 });
237 if !self.custom_properties.is_empty() {
241 package.add_relationship(Relationship {
242 id: "rId4".to_string(),
243 rel_type: CUSTOM_PROPERTIES_RELATIONSHIP_TYPE.to_string(),
244 target: CUSTOM_PROPERTIES_ENTRY_NAME.to_string(),
245 target_mode: TargetMode::Internal,
246 });
247 }
248
249 let mut state = PackageState::new();
256 let mut document_relationships = PartRelationships::new();
257 document_relationships.add(SETTINGS_RELATIONSHIP_TYPE, SETTINGS_ENTRY_NAME); if !self.styles.is_empty() {
264 document_relationships.add(STYLES_RELATIONSHIP_TYPE, STYLES_ENTRY_NAME);
265 package.add_part(Part::new(
266 STYLES_PART_NAME,
267 STYLES_CONTENT_TYPE,
268 to_styles_xml(&self.styles)?,
269 ));
270 }
271
272 if !self.numbering_definitions.is_empty() {
276 document_relationships.add(NUMBERING_RELATIONSHIP_TYPE, NUMBERING_ENTRY_NAME);
277 package.add_part(Part::new(
278 NUMBERING_PART_NAME,
279 NUMBERING_CONTENT_TYPE,
280 to_numbering_xml(&self.numbering_definitions)?,
281 ));
282 }
283
284 if !self.footnotes.is_empty() {
289 document_relationships.add(FOOTNOTES_RELATIONSHIP_TYPE, FOOTNOTES_ENTRY_NAME);
290 let mut footnotes_relationships = PartRelationships::new();
291 let xml = to_notes_xml(
292 "w:footnotes",
293 "w:footnote",
294 &self.footnotes,
295 &mut state,
296 &mut footnotes_relationships,
297 )?;
298 let mut part = Part::new(FOOTNOTES_PART_NAME, FOOTNOTES_CONTENT_TYPE, xml);
299 part.relationships = footnotes_relationships.relationships;
300 package.add_part(part);
301 }
302 if !self.endnotes.is_empty() {
303 document_relationships.add(ENDNOTES_RELATIONSHIP_TYPE, ENDNOTES_ENTRY_NAME);
304 let mut endnotes_relationships = PartRelationships::new();
305 let xml = to_notes_xml(
306 "w:endnotes",
307 "w:endnote",
308 &self.endnotes,
309 &mut state,
310 &mut endnotes_relationships,
311 )?;
312 let mut part = Part::new(ENDNOTES_PART_NAME, ENDNOTES_CONTENT_TYPE, xml);
313 part.relationships = endnotes_relationships.relationships;
314 package.add_part(part);
315 }
316
317 if !self.comments.is_empty() {
319 document_relationships.add(COMMENTS_RELATIONSHIP_TYPE, COMMENTS_ENTRY_NAME);
320 let mut comments_relationships = PartRelationships::new();
321 let xml = to_comments_xml(&self.comments, &mut state, &mut comments_relationships)?;
322 let mut part = Part::new(COMMENTS_PART_NAME, COMMENTS_CONTENT_TYPE, xml);
323 part.relationships = comments_relationships.relationships;
324 package.add_part(part);
325 }
326
327 if let Some(theme) = &self.theme {
331 document_relationships.add(THEME_RELATIONSHIP_TYPE, THEME_ENTRY_NAME);
332 package.add_part(Part::new(
333 THEME_PART_NAME,
334 THEME_CONTENT_TYPE,
335 to_theme_xml(theme)?,
336 ));
337 }
338
339 let header_relationship_id = match &self.header {
345 Some(header) => Some(write_header_footer_part(
346 &mut package,
347 &mut state,
348 &mut document_relationships,
349 header,
350 "w:hdr",
351 HEADER_PART_NAME,
352 HEADER_CONTENT_TYPE,
353 HEADER_RELATIONSHIP_TYPE,
354 HEADER_ENTRY_NAME,
355 )?),
356 None => None,
357 };
358 let footer_relationship_id = match &self.footer {
359 Some(footer) => Some(write_header_footer_part(
360 &mut package,
361 &mut state,
362 &mut document_relationships,
363 footer,
364 "w:ftr",
365 FOOTER_PART_NAME,
366 FOOTER_CONTENT_TYPE,
367 FOOTER_RELATIONSHIP_TYPE,
368 FOOTER_ENTRY_NAME,
369 )?),
370 None => None,
371 };
372 let header_first_relationship_id = match &self.header_first {
379 Some(header) => Some(write_header_footer_part(
380 &mut package,
381 &mut state,
382 &mut document_relationships,
383 header,
384 "w:hdr",
385 HEADER_FIRST_PART_NAME,
386 HEADER_CONTENT_TYPE,
387 HEADER_RELATIONSHIP_TYPE,
388 HEADER_FIRST_ENTRY_NAME,
389 )?),
390 None => None,
391 };
392 let footer_first_relationship_id = match &self.footer_first {
393 Some(footer) => Some(write_header_footer_part(
394 &mut package,
395 &mut state,
396 &mut document_relationships,
397 footer,
398 "w:ftr",
399 FOOTER_FIRST_PART_NAME,
400 FOOTER_CONTENT_TYPE,
401 FOOTER_RELATIONSHIP_TYPE,
402 FOOTER_FIRST_ENTRY_NAME,
403 )?),
404 None => None,
405 };
406 let header_even_relationship_id = match &self.header_even {
407 Some(header) => Some(write_header_footer_part(
408 &mut package,
409 &mut state,
410 &mut document_relationships,
411 header,
412 "w:hdr",
413 HEADER_EVEN_PART_NAME,
414 HEADER_CONTENT_TYPE,
415 HEADER_RELATIONSHIP_TYPE,
416 HEADER_EVEN_ENTRY_NAME,
417 )?),
418 None => None,
419 };
420 let footer_even_relationship_id = match &self.footer_even {
421 Some(footer) => Some(write_header_footer_part(
422 &mut package,
423 &mut state,
424 &mut document_relationships,
425 footer,
426 "w:ftr",
427 FOOTER_EVEN_PART_NAME,
428 FOOTER_CONTENT_TYPE,
429 FOOTER_RELATIONSHIP_TYPE,
430 FOOTER_EVEN_ENTRY_NAME,
431 )?),
432 None => None,
433 };
434 let title_pg = self.header_first.is_some() || self.footer_first.is_some();
435 let even_and_odd_headers = self.header_even.is_some() || self.footer_even.is_some();
436
437 let document_xml = self.to_document_xml(
438 &mut state,
439 &mut document_relationships,
440 SectionReferenceIds {
441 header: header_relationship_id.as_deref(),
442 footer: footer_relationship_id.as_deref(),
443 header_first: header_first_relationship_id.as_deref(),
444 footer_first: footer_first_relationship_id.as_deref(),
445 header_even: header_even_relationship_id.as_deref(),
446 footer_even: footer_even_relationship_id.as_deref(),
447 title_pg,
448 },
449 )?;
450 let mut document_part = Part::new(
451 MAIN_DOCUMENT_PART_NAME,
452 MAIN_DOCUMENT_CONTENT_TYPE,
453 document_xml,
454 );
455 document_part.relationships = document_relationships.relationships;
456 package.add_part(document_part);
457
458 for media_part in state.images.media_parts {
459 package.add_part(Part::new(
460 format!("/word/{}", media_part.entry_name),
461 media_part.content_type,
462 media_part.data,
463 ));
464 }
465
466 for chart_part in state.charts.chart_parts {
467 package.add_part(Part::new(
468 format!("/word/{}", chart_part.entry_name),
469 CHART_CONTENT_TYPE,
470 chart_part.data,
471 ));
472 }
473
474 package.add_part(Part::new(
475 SETTINGS_PART_NAME,
476 SETTINGS_CONTENT_TYPE,
477 to_settings_xml(self.track_changes, self.protection, even_and_odd_headers)?,
478 ));
479 package.add_part(Part::new(
480 CORE_PROPERTIES_PART_NAME,
481 CORE_PROPERTIES_CONTENT_TYPE,
482 to_core_properties_xml(&self.properties)?,
483 ));
484 package.add_part(Part::new(
485 EXTENDED_PROPERTIES_PART_NAME,
486 EXTENDED_PROPERTIES_CONTENT_TYPE,
487 to_extended_properties_xml(&self.extended_properties)?,
488 ));
489 if !self.custom_properties.is_empty() {
490 package.add_part(Part::new(
491 CUSTOM_PROPERTIES_PART_NAME,
492 CUSTOM_PROPERTIES_CONTENT_TYPE,
493 to_custom_properties_xml(&self.custom_properties)?,
494 ));
495 }
496
497 Ok(package.write_to(writer)?)
498 }
499
500 fn to_document_xml(
506 &self,
507 state: &mut PackageState,
508 relationships: &mut PartRelationships,
509 section_references: SectionReferenceIds<'_>,
510 ) -> Result<Vec<u8>> {
511 let mut writer = Writer::new(Vec::new());
512
513 writer.write_event(Event::Decl(BytesDecl::new(
514 "1.0",
515 Some("UTF-8"),
516 Some("yes"),
517 )))?;
518
519 let mut root = BytesStart::new("w:document");
520 root.push_attribute(("xmlns:w", WORDPROCESSINGML_NAMESPACE));
521 root.push_attribute(("xmlns:r", RELATIONSHIPS_NAMESPACE));
522 root.push_attribute(("xmlns:wp", WORDPROCESSING_DRAWING_NAMESPACE));
523 root.push_attribute(("xmlns:a", DRAWINGML_NAMESPACE));
524 root.push_attribute(("xmlns:pic", DRAWINGML_PICTURE_NAMESPACE));
525 writer.write_event(Event::Start(root))?;
526 writer.write_event(Event::Start(BytesStart::new("w:body")))?;
527
528 for block in &self.blocks {
529 write_block(&mut writer, block, state, relationships)?;
530 }
531
532 write_section_properties(&mut writer, section_references, &self.page_setup)?;
533
534 writer.write_event(Event::End(BytesEnd::new("w:body")))?;
535 writer.write_event(Event::End(BytesEnd::new("w:document")))?;
536
537 Ok(writer.into_inner())
538 }
539}
540
541#[derive(Default)]
548struct SectionReferenceIds<'a> {
549 header: Option<&'a str>,
550 footer: Option<&'a str>,
551 header_first: Option<&'a str>,
552 footer_first: Option<&'a str>,
553 header_even: Option<&'a str>,
554 footer_even: Option<&'a str>,
555 title_pg: bool,
556}
557
558#[allow(clippy::too_many_arguments)]
563fn write_header_footer_part(
564 package: &mut Package,
565 state: &mut PackageState,
566 document_relationships: &mut PartRelationships,
567 header_footer: &HeaderFooter,
568 root_name: &str,
569 part_name: &str,
570 content_type: &str,
571 relationship_type: &str,
572 entry_name: &str,
573) -> Result<String> {
574 let mut part_relationships = PartRelationships::new();
575 let xml = to_header_footer_xml(
576 root_name,
577 &header_footer.blocks,
578 state,
579 &mut part_relationships,
580 )?;
581
582 let mut part = Part::new(part_name, content_type, xml);
583 part.relationships = part_relationships.relationships;
584 package.add_part(part);
585
586 Ok(document_relationships.add(relationship_type, entry_name))
587}
588
589fn to_header_footer_xml(
595 root_name: &str,
596 blocks: &[Block],
597 state: &mut PackageState,
598 relationships: &mut PartRelationships,
599) -> Result<Vec<u8>> {
600 let mut writer = Writer::new(Vec::new());
601
602 writer.write_event(Event::Decl(BytesDecl::new(
603 "1.0",
604 Some("UTF-8"),
605 Some("yes"),
606 )))?;
607
608 let mut root = BytesStart::new(root_name);
609 root.push_attribute(("xmlns:w", WORDPROCESSINGML_NAMESPACE));
610 root.push_attribute(("xmlns:r", RELATIONSHIPS_NAMESPACE));
611 root.push_attribute(("xmlns:wp", WORDPROCESSING_DRAWING_NAMESPACE));
612 root.push_attribute(("xmlns:a", DRAWINGML_NAMESPACE));
613 root.push_attribute(("xmlns:pic", DRAWINGML_PICTURE_NAMESPACE));
614 writer.write_event(Event::Start(root))?;
615
616 for block in blocks {
617 write_block(&mut writer, block, state, relationships)?;
618 }
619
620 writer.write_event(Event::End(BytesEnd::new(root_name)))?;
621
622 Ok(writer.into_inner())
623}
624
625fn write_block<W: Write>(
631 writer: &mut Writer<W>,
632 block: &Block,
633 state: &mut PackageState,
634 relationships: &mut PartRelationships,
635) -> Result<()> {
636 match block {
637 Block::Paragraph(paragraph) => write_paragraph(writer, paragraph, state, relationships),
638 Block::Table(table) => write_table(writer, table, state, relationships),
639 Block::StructuredDocumentTag(sdt) => {
640 write_structured_document_tag(writer, sdt, state, relationships)
641 }
642 }
643}
644
645fn write_paragraph<W: Write>(
651 writer: &mut Writer<W>,
652 paragraph: &Paragraph,
653 state: &mut PackageState,
654 relationships: &mut PartRelationships,
655) -> Result<()> {
656 if paragraph.runs.is_empty()
657 && paragraph.alignment.is_none()
658 && paragraph.style_id.is_none()
659 && paragraph.numbering_id.is_none()
660 && paragraph.line_spacing.is_none()
661 && paragraph.space_before.is_none()
662 && paragraph.space_after.is_none()
663 && paragraph.shading_color.is_none()
664 && !paragraph.border
665 && !paragraph.keep_with_next
666 && !paragraph.keep_lines_together
667 && !paragraph.page_break_before
668 && paragraph.tabs.is_empty()
669 && !paragraph.contextual_spacing
670 {
671 writer.write_event(Event::Empty(BytesStart::new("w:p")))?;
672 return Ok(());
673 }
674
675 writer.write_event(Event::Start(BytesStart::new("w:p")))?;
676
677 if paragraph.style_id.is_some()
684 || paragraph.numbering_id.is_some()
685 || paragraph.shading_color.is_some()
686 || paragraph.border
687 || paragraph.line_spacing.is_some()
688 || paragraph.space_before.is_some()
689 || paragraph.space_after.is_some()
690 || paragraph.alignment.is_some()
691 || paragraph.keep_with_next
692 || paragraph.keep_lines_together
693 || paragraph.page_break_before
694 || !paragraph.tabs.is_empty()
695 || paragraph.contextual_spacing
696 {
697 writer.write_event(Event::Start(BytesStart::new("w:pPr")))?;
698 if let Some(style_id) = ¶graph.style_id {
699 let mut p_style = BytesStart::new("w:pStyle");
700 p_style.push_attribute(("w:val", style_id.as_str()));
701 writer.write_event(Event::Empty(p_style))?;
702 }
703 if paragraph.keep_with_next {
704 writer.write_event(Event::Empty(BytesStart::new("w:keepNext")))?;
705 }
706 if paragraph.keep_lines_together {
707 writer.write_event(Event::Empty(BytesStart::new("w:keepLines")))?;
708 }
709 if paragraph.page_break_before {
710 writer.write_event(Event::Empty(BytesStart::new("w:pageBreakBefore")))?;
711 }
712 if let Some(numbering_id) = paragraph.numbering_id {
713 writer.write_event(Event::Start(BytesStart::new("w:numPr")))?;
715 let mut ilvl = BytesStart::new("w:ilvl");
716 let level = paragraph.numbering_level.to_string();
717 ilvl.push_attribute(("w:val", level.as_str()));
718 writer.write_event(Event::Empty(ilvl))?;
719 let mut num_id = BytesStart::new("w:numId");
720 let id = numbering_id.to_string();
721 num_id.push_attribute(("w:val", id.as_str()));
722 writer.write_event(Event::Empty(num_id))?;
723 writer.write_event(Event::End(BytesEnd::new("w:numPr")))?;
724 }
725 if paragraph.border {
726 write_paragraph_border(writer)?;
727 }
728 if let Some(shading_color) = ¶graph.shading_color {
729 write_shading(writer, "w:shd", shading_color)?;
730 }
731 if !paragraph.tabs.is_empty() {
732 write_tabs(writer, ¶graph.tabs)?;
733 }
734 if paragraph.line_spacing.is_some()
735 || paragraph.space_before.is_some()
736 || paragraph.space_after.is_some()
737 {
738 write_paragraph_spacing(
739 writer,
740 paragraph.line_spacing,
741 paragraph.space_before,
742 paragraph.space_after,
743 )?;
744 }
745 if paragraph.contextual_spacing {
746 writer.write_event(Event::Empty(BytesStart::new("w:contextualSpacing")))?;
747 }
748 if let Some(alignment) = paragraph.alignment {
749 let mut jc = BytesStart::new("w:jc");
750 jc.push_attribute(("w:val", alignment_value(alignment)));
751 writer.write_event(Event::Empty(jc))?;
752 }
753 writer.write_event(Event::End(BytesEnd::new("w:pPr")))?;
754 }
755
756 let mut active_comment_ids: Vec<u32> = Vec::new();
764
765 let mut active_bookmarks: Vec<Bookmark> = Vec::new();
771
772 for run in ¶graph.runs {
773 let to_close: Vec<u32> = active_comment_ids
774 .iter()
775 .copied()
776 .filter(|id| !run.comment_ids.contains(id))
777 .collect();
778 for id in &to_close {
779 write_comment_range_end_and_reference(writer, *id)?;
780 }
781 active_comment_ids.retain(|id| !to_close.contains(id));
782
783 let to_open: Vec<u32> = run
784 .comment_ids
785 .iter()
786 .copied()
787 .filter(|id| !active_comment_ids.contains(id))
788 .collect();
789 for id in &to_open {
790 let id_string = id.to_string();
791 let mut start = BytesStart::new("w:commentRangeStart");
792 start.push_attribute(("w:id", id_string.as_str()));
793 writer.write_event(Event::Empty(start))?;
794 }
795 active_comment_ids.extend(to_open);
796
797 let bookmarks_to_close: Vec<u32> = active_bookmarks
798 .iter()
799 .map(|bookmark| bookmark.id)
800 .filter(|id| !run.bookmarks.iter().any(|bookmark| bookmark.id == *id))
801 .collect();
802 for id in &bookmarks_to_close {
803 write_bookmark_end(writer, *id)?;
804 }
805 active_bookmarks.retain(|bookmark| !bookmarks_to_close.contains(&bookmark.id));
806
807 let bookmarks_to_open: Vec<Bookmark> = run
808 .bookmarks
809 .iter()
810 .filter(|bookmark| {
811 !active_bookmarks
812 .iter()
813 .any(|active| active.id == bookmark.id)
814 })
815 .cloned()
816 .collect();
817 for bookmark in &bookmarks_to_open {
818 write_bookmark_start(writer, bookmark)?;
819 }
820 active_bookmarks.extend(bookmarks_to_open);
821
822 match &run.hyperlink {
823 Some(Hyperlink::External(url)) => {
829 let relationship_id =
830 relationships.add_external(HYPERLINK_RELATIONSHIP_TYPE, url.as_str());
831 let mut hyperlink = BytesStart::new("w:hyperlink");
832 hyperlink.push_attribute(("r:id", relationship_id.as_str()));
833 writer.write_event(Event::Start(hyperlink))?;
834 write_run(writer, run, state, relationships)?;
835 writer.write_event(Event::End(BytesEnd::new("w:hyperlink")))?;
836 }
837 Some(Hyperlink::Internal(anchor)) => {
838 let mut hyperlink = BytesStart::new("w:hyperlink");
839 hyperlink.push_attribute(("w:anchor", anchor.as_str()));
840 writer.write_event(Event::Start(hyperlink))?;
841 write_run(writer, run, state, relationships)?;
842 writer.write_event(Event::End(BytesEnd::new("w:hyperlink")))?;
843 }
844 None => write_run(writer, run, state, relationships)?,
845 }
846 }
847
848 for id in active_comment_ids {
853 write_comment_range_end_and_reference(writer, id)?;
854 }
855
856 for bookmark in active_bookmarks {
859 write_bookmark_end(writer, bookmark.id)?;
860 }
861
862 writer.write_event(Event::End(BytesEnd::new("w:p")))?;
863 Ok(())
864}
865
866fn write_bookmark_start<W: Write>(writer: &mut Writer<W>, bookmark: &Bookmark) -> Result<()> {
868 let id_string = bookmark.id.to_string();
869 let mut start = BytesStart::new("w:bookmarkStart");
870 start.push_attribute(("w:id", id_string.as_str()));
871 start.push_attribute(("w:name", bookmark.name.as_str()));
872 writer.write_event(Event::Empty(start))?;
873 Ok(())
874}
875
876fn write_bookmark_end<W: Write>(writer: &mut Writer<W>, id: u32) -> Result<()> {
880 let id_string = id.to_string();
881 let mut end = BytesStart::new("w:bookmarkEnd");
882 end.push_attribute(("w:id", id_string.as_str()));
883 writer.write_event(Event::Empty(end))?;
884 Ok(())
885}
886
887fn write_comment_range_end_and_reference<W: Write>(writer: &mut Writer<W>, id: u32) -> Result<()> {
893 let id_string = id.to_string();
894
895 let mut end = BytesStart::new("w:commentRangeEnd");
896 end.push_attribute(("w:id", id_string.as_str()));
897 writer.write_event(Event::Empty(end))?;
898
899 writer.write_event(Event::Start(BytesStart::new("w:r")))?;
900 writer.write_event(Event::Start(BytesStart::new("w:rPr")))?;
901 let mut r_style = BytesStart::new("w:rStyle");
902 r_style.push_attribute(("w:val", COMMENT_REFERENCE_STYLE_ID));
903 writer.write_event(Event::Empty(r_style))?;
904 writer.write_event(Event::End(BytesEnd::new("w:rPr")))?;
905 let mut reference = BytesStart::new("w:commentReference");
906 reference.push_attribute(("w:id", id_string.as_str()));
907 writer.write_event(Event::Empty(reference))?;
908 writer.write_event(Event::End(BytesEnd::new("w:r")))?;
909
910 Ok(())
911}
912
913fn write_run<W: Write>(
918 writer: &mut Writer<W>,
919 run: &Run,
920 state: &mut PackageState,
921 relationships: &mut PartRelationships,
922) -> Result<()> {
923 match &run.content {
924 RunContent::Field(field) => write_field(writer, run, *field)?,
928 RunContent::Image(image) => {
929 writer.write_event(Event::Start(BytesStart::new("w:r")))?;
930 write_drawing(writer, image, state, relationships)?;
931 writer.write_event(Event::End(BytesEnd::new("w:r")))?;
932 }
933 RunContent::Chart(chart) => {
934 writer.write_event(Event::Start(BytesStart::new("w:r")))?;
935 write_embedded_chart(writer, chart, state, relationships)?;
936 writer.write_event(Event::End(BytesEnd::new("w:r")))?;
937 }
938 RunContent::Text(text) => {
939 writer.write_event(Event::Start(BytesStart::new("w:r")))?;
940 write_run_properties(writer, run)?;
941
942 let mut text_element = BytesStart::new("w:t");
943 if starts_or_ends_with_whitespace(text) {
944 text_element.push_attribute(("xml:space", "preserve"));
945 }
946 writer.write_event(Event::Start(text_element))?;
947 writer.write_event(Event::Text(BytesText::new(text)))?;
948 writer.write_event(Event::End(BytesEnd::new("w:t")))?;
949 writer.write_event(Event::End(BytesEnd::new("w:r")))?;
950 }
951 RunContent::NoteReference(reference) => {
958 writer.write_event(Event::Start(BytesStart::new("w:r")))?;
959 write_run_properties(writer, run)?;
960 let (element_name, id) = match reference {
961 NoteReference::Footnote(id) => ("w:footnoteReference", *id),
962 NoteReference::Endnote(id) => ("w:endnoteReference", *id),
963 };
964 let id = id.to_string();
965 let mut element = BytesStart::new(element_name);
966 element.push_attribute(("w:id", id.as_str()));
967 writer.write_event(Event::Empty(element))?;
968 writer.write_event(Event::End(BytesEnd::new("w:r")))?;
969 }
970 RunContent::NoteMarker(kind) => {
971 writer.write_event(Event::Start(BytesStart::new("w:r")))?;
972 write_run_properties(writer, run)?;
973 let element_name = match kind {
974 NoteKind::Footnote => "w:footnoteRef",
975 NoteKind::Endnote => "w:endnoteRef",
976 };
977 writer.write_event(Event::Empty(BytesStart::new(element_name)))?;
978 writer.write_event(Event::End(BytesEnd::new("w:r")))?;
979 }
980 RunContent::Break(kind) => {
987 writer.write_event(Event::Start(BytesStart::new("w:r")))?;
988 write_run_properties(writer, run)?;
989 let mut element = BytesStart::new("w:br");
990 if *kind != BreakKind::TextWrapping {
991 element.push_attribute(("w:type", kind.attribute_value()));
992 }
993 writer.write_event(Event::Empty(element))?;
994 writer.write_event(Event::End(BytesEnd::new("w:r")))?;
995 }
996 RunContent::CarriageReturn => {
997 writer.write_event(Event::Start(BytesStart::new("w:r")))?;
998 write_run_properties(writer, run)?;
999 writer.write_event(Event::Empty(BytesStart::new("w:cr")))?;
1000 writer.write_event(Event::End(BytesEnd::new("w:r")))?;
1001 }
1002 }
1003 Ok(())
1004}
1005
1006fn write_run_properties<W: Write>(writer: &mut Writer<W>, run: &Run) -> Result<()> {
1012 if run.style_id.is_none()
1013 && run.font_family.is_none()
1014 && run.color.is_none()
1015 && run.font_size.is_none()
1016 && run.highlight.is_none()
1017 && run.vertical_align.is_none()
1018 && run.underline.is_none()
1019 && run.shading_color.is_none()
1020 && run.character_spacing.is_none()
1021 && run.vertical_position.is_none()
1022 && !(run.bold
1023 || run.italic
1024 || run.strike
1025 || run.small_caps
1026 || run.all_caps
1027 || run.text_border
1028 || run.hidden)
1029 {
1030 return Ok(());
1031 }
1032
1033 writer.write_event(Event::Start(BytesStart::new("w:rPr")))?;
1034 if let Some(style_id) = &run.style_id {
1035 let mut r_style = BytesStart::new("w:rStyle");
1036 r_style.push_attribute(("w:val", style_id.as_str()));
1037 writer.write_event(Event::Empty(r_style))?;
1038 }
1039 write_character_formatting(
1040 writer,
1041 run.font_family.as_deref(),
1042 run.color.as_deref(),
1043 run.font_size,
1044 run.highlight,
1045 run.bold,
1046 run.italic,
1047 run.underline,
1048 run.underline_color.as_deref(),
1049 run.strike,
1050 run.vertical_align,
1051 run.small_caps,
1052 run.all_caps,
1053 run.shading_color.as_deref(),
1054 run.text_border,
1055 run.character_spacing,
1056 run.vertical_position,
1057 run.hidden,
1058 )?;
1059 writer.write_event(Event::End(BytesEnd::new("w:rPr")))?;
1060 Ok(())
1061}
1062
1063#[allow(clippy::too_many_arguments)]
1084fn write_character_formatting<W: Write>(
1085 writer: &mut Writer<W>,
1086 font_family: Option<&str>,
1087 color: Option<&str>,
1088 size_points: Option<u16>,
1089 highlight: Option<Highlight>,
1090 bold: bool,
1091 italic: bool,
1092 underline: Option<UnderlineStyle>,
1093 underline_color: Option<&str>,
1094 strike: bool,
1095 vertical_align: Option<VerticalAlign>,
1096 small_caps: bool,
1097 all_caps: bool,
1098 shading_color: Option<&str>,
1099 text_border: bool,
1100 character_spacing: Option<i16>,
1101 vertical_position: Option<i16>,
1102 hidden: bool,
1103) -> Result<()> {
1104 if let Some(font_family) = font_family {
1105 let mut r_fonts = BytesStart::new("w:rFonts");
1106 r_fonts.push_attribute(("w:ascii", font_family));
1107 r_fonts.push_attribute(("w:hAnsi", font_family));
1108 writer.write_event(Event::Empty(r_fonts))?;
1109 }
1110 if let Some(color) = color {
1111 let mut color_element = BytesStart::new("w:color");
1112 color_element.push_attribute(("w:val", color));
1113 writer.write_event(Event::Empty(color_element))?;
1114 }
1115 if let Some(size_points) = size_points {
1116 let half_points = (u32::from(size_points) * 2).to_string();
1117 let mut sz = BytesStart::new("w:sz");
1118 sz.push_attribute(("w:val", half_points.as_str()));
1119 writer.write_event(Event::Empty(sz))?;
1120 let mut sz_cs = BytesStart::new("w:szCs");
1121 sz_cs.push_attribute(("w:val", half_points.as_str()));
1122 writer.write_event(Event::Empty(sz_cs))?;
1123 }
1124 if let Some(highlight) = highlight {
1125 let mut highlight_element = BytesStart::new("w:highlight");
1126 highlight_element.push_attribute(("w:val", highlight.attribute_value()));
1127 writer.write_event(Event::Empty(highlight_element))?;
1128 }
1129 if bold {
1130 writer.write_event(Event::Empty(BytesStart::new("w:b")))?;
1131 }
1132 if italic {
1133 writer.write_event(Event::Empty(BytesStart::new("w:i")))?;
1134 }
1135 if let Some(underline) = underline {
1136 let mut underline_element = BytesStart::new("w:u");
1137 underline_element.push_attribute(("w:val", underline.attribute_value()));
1138 if let Some(underline_color) = underline_color {
1139 underline_element.push_attribute(("w:color", underline_color));
1140 }
1141 writer.write_event(Event::Empty(underline_element))?;
1142 }
1143 if strike {
1144 writer.write_event(Event::Empty(BytesStart::new("w:strike")))?;
1145 }
1146 if let Some(vertical_align) = vertical_align {
1147 let mut vert_align_element = BytesStart::new("w:vertAlign");
1148 vert_align_element.push_attribute(("w:val", vertical_align.attribute_value()));
1149 writer.write_event(Event::Empty(vert_align_element))?;
1150 }
1151 if small_caps {
1152 writer.write_event(Event::Empty(BytesStart::new("w:smallCaps")))?;
1153 }
1154 if all_caps {
1155 writer.write_event(Event::Empty(BytesStart::new("w:caps")))?;
1156 }
1157 if let Some(shading_color) = shading_color {
1158 write_shading(writer, "w:shd", shading_color)?;
1159 }
1160 if text_border {
1161 let mut bdr = BytesStart::new("w:bdr");
1162 bdr.push_attribute(("w:val", "single"));
1163 bdr.push_attribute(("w:sz", "4"));
1164 bdr.push_attribute(("w:space", "0"));
1165 bdr.push_attribute(("w:color", "auto"));
1166 writer.write_event(Event::Empty(bdr))?;
1167 }
1168 if let Some(character_spacing) = character_spacing {
1169 let mut spacing = BytesStart::new("w:spacing");
1170 spacing.push_attribute(("w:val", character_spacing.to_string().as_str()));
1171 writer.write_event(Event::Empty(spacing))?;
1172 }
1173 if let Some(vertical_position) = vertical_position {
1174 let mut position = BytesStart::new("w:position");
1175 position.push_attribute(("w:val", vertical_position.to_string().as_str()));
1176 writer.write_event(Event::Empty(position))?;
1177 }
1178 if hidden {
1179 writer.write_event(Event::Empty(BytesStart::new("w:vanish")))?;
1180 }
1181 Ok(())
1182}
1183
1184fn write_shading<W: Write>(writer: &mut Writer<W>, tag_name: &str, fill: &str) -> Result<()> {
1190 let mut shd = BytesStart::new(tag_name);
1191 shd.push_attribute(("w:val", "clear"));
1192 shd.push_attribute(("w:color", "auto"));
1193 shd.push_attribute(("w:fill", fill));
1194 writer.write_event(Event::Empty(shd))?;
1195 Ok(())
1196}
1197
1198fn write_border_side<W: Write>(writer: &mut Writer<W>, tag_name: &str) -> Result<()> {
1203 let mut side = BytesStart::new(tag_name);
1204 side.push_attribute(("w:val", "single"));
1205 side.push_attribute(("w:sz", "4"));
1206 side.push_attribute(("w:space", "0"));
1207 side.push_attribute(("w:color", "auto"));
1208 writer.write_event(Event::Empty(side))?;
1209 Ok(())
1210}
1211
1212fn write_paragraph_border<W: Write>(writer: &mut Writer<W>) -> Result<()> {
1217 writer.write_event(Event::Start(BytesStart::new("w:pBdr")))?;
1218 write_border_side(writer, "w:top")?;
1219 write_border_side(writer, "w:left")?;
1220 write_border_side(writer, "w:bottom")?;
1221 write_border_side(writer, "w:right")?;
1222 writer.write_event(Event::End(BytesEnd::new("w:pBdr")))?;
1223 Ok(())
1224}
1225
1226fn write_tabs<W: Write>(writer: &mut Writer<W>, tabs: &[TabStop]) -> Result<()> {
1231 writer.write_event(Event::Start(BytesStart::new("w:tabs")))?;
1232 for tab in tabs {
1233 write_tab_stop(writer, tab)?;
1234 }
1235 writer.write_event(Event::End(BytesEnd::new("w:tabs")))?;
1236 Ok(())
1237}
1238
1239fn write_tab_stop<W: Write>(writer: &mut Writer<W>, tab: &TabStop) -> Result<()> {
1243 let mut element = BytesStart::new("w:tab");
1244 let position = tab.position_twips.to_string();
1245 element.push_attribute(("w:val", tab.alignment.attribute_value()));
1246 if let Some(leader) = tab.leader {
1247 element.push_attribute(("w:leader", leader.attribute_value()));
1248 }
1249 element.push_attribute(("w:pos", position.as_str()));
1250 writer.write_event(Event::Empty(element))?;
1251 Ok(())
1252}
1253
1254fn write_paragraph_spacing<W: Write>(
1262 writer: &mut Writer<W>,
1263 line: Option<u32>,
1264 before: Option<u32>,
1265 after: Option<u32>,
1266) -> Result<()> {
1267 let mut spacing = BytesStart::new("w:spacing");
1268 let before_string = before.map(|value| value.to_string());
1269 let after_string = after.map(|value| value.to_string());
1270 let line_string = line.map(|value| value.to_string());
1271 if let Some(before_string) = &before_string {
1272 spacing.push_attribute(("w:before", before_string.as_str()));
1273 }
1274 if let Some(after_string) = &after_string {
1275 spacing.push_attribute(("w:after", after_string.as_str()));
1276 }
1277 if let Some(line_string) = &line_string {
1278 spacing.push_attribute(("w:line", line_string.as_str()));
1279 spacing.push_attribute(("w:lineRule", "auto"));
1280 }
1281 writer.write_event(Event::Empty(spacing))?;
1282 Ok(())
1283}
1284
1285fn write_field<W: Write>(writer: &mut Writer<W>, run: &Run, field: Field) -> Result<()> {
1300 let mut fld_simple = BytesStart::new("w:fldSimple");
1301 fld_simple.push_attribute(("w:instr", field.instruction()));
1302 writer.write_event(Event::Start(fld_simple))?;
1303
1304 writer.write_event(Event::Start(BytesStart::new("w:r")))?;
1305 write_run_properties(writer, run)?;
1306 writer.write_event(Event::Start(BytesStart::new("w:t")))?;
1307 writer.write_event(Event::Text(BytesText::new("1")))?;
1308 writer.write_event(Event::End(BytesEnd::new("w:t")))?;
1309 writer.write_event(Event::End(BytesEnd::new("w:r")))?;
1310
1311 writer.write_event(Event::End(BytesEnd::new("w:fldSimple")))?;
1312 Ok(())
1313}
1314
1315struct ImageRegistry {
1321 next_media_index: usize,
1322 media_parts: Vec<MediaPart>,
1323}
1324
1325struct MediaPart {
1326 entry_name: String,
1328 content_type: &'static str,
1329 data: Vec<u8>,
1330}
1331
1332impl ImageRegistry {
1333 fn new() -> Self {
1334 Self {
1335 next_media_index: 1,
1336 media_parts: Vec::new(),
1337 }
1338 }
1339
1340 fn register(&mut self, image: &Image) -> String {
1345 let index = self.next_media_index;
1346 self.next_media_index += 1;
1347 let entry_name = format!("media/image{index}.{}", image.format.extension());
1348 self.media_parts.push(MediaPart {
1349 entry_name: entry_name.clone(),
1350 content_type: image.format.content_type(),
1351 data: image.data.clone(),
1352 });
1353 entry_name
1354 }
1355}
1356
1357struct ChartRegistry {
1360 next_chart_index: usize,
1361 chart_parts: Vec<ChartPart>,
1362}
1363
1364struct ChartPart {
1365 entry_name: String,
1367 data: Vec<u8>,
1368}
1369
1370impl ChartRegistry {
1371 fn new() -> Self {
1372 Self {
1373 next_chart_index: 1,
1374 chart_parts: Vec::new(),
1375 }
1376 }
1377
1378 fn register(&mut self, chart_space: &chart::ChartSpace) -> Result<String> {
1382 let index = self.next_chart_index;
1383 self.next_chart_index += 1;
1384 let entry_name = format!("charts/chart{index}.xml");
1385 let mut chart_writer = Writer::new(Vec::new());
1386 chart::write_chart_space(&mut chart_writer, chart_space)?;
1387 self.chart_parts.push(ChartPart {
1388 entry_name: entry_name.clone(),
1389 data: chart_writer.into_inner(),
1390 });
1391 Ok(entry_name)
1392 }
1393}
1394
1395struct PackageState {
1407 images: ImageRegistry,
1408 charts: ChartRegistry,
1409 next_drawing_id: usize,
1416}
1417
1418impl PackageState {
1419 fn new() -> Self {
1420 Self {
1421 images: ImageRegistry::new(),
1422 charts: ChartRegistry::new(),
1423 next_drawing_id: 1,
1424 }
1425 }
1426
1427 fn next_drawing_id(&mut self) -> usize {
1429 let id = self.next_drawing_id;
1430 self.next_drawing_id += 1;
1431 id
1432 }
1433}
1434
1435struct PartRelationships {
1440 next_id: usize,
1441 relationships: Relationships,
1442}
1443
1444impl PartRelationships {
1445 fn new() -> Self {
1446 Self {
1447 next_id: 1,
1448 relationships: Relationships::new(),
1449 }
1450 }
1451
1452 fn add(&mut self, rel_type: &str, target: impl Into<String>) -> String {
1455 self.add_with_mode(rel_type, target, TargetMode::Internal)
1456 }
1457
1458 fn add_external(&mut self, rel_type: &str, target: impl Into<String>) -> String {
1461 self.add_with_mode(rel_type, target, TargetMode::External)
1462 }
1463
1464 fn add_with_mode(
1465 &mut self,
1466 rel_type: &str,
1467 target: impl Into<String>,
1468 target_mode: TargetMode,
1469 ) -> String {
1470 let id = format!("rId{}", self.next_id);
1471 self.next_id += 1;
1472 self.relationships.add(Relationship {
1473 id: id.clone(),
1474 rel_type: rel_type.to_string(),
1475 target: target.into(),
1476 target_mode,
1477 });
1478 id
1479 }
1480}
1481
1482fn write_drawing<W: Write>(
1489 writer: &mut Writer<W>,
1490 image: &Image,
1491 state: &mut PackageState,
1492 relationships: &mut PartRelationships,
1493) -> Result<()> {
1494 let entry_name = state.images.register(image);
1495 let relationship_id = relationships.add(IMAGE_RELATIONSHIP_TYPE, entry_name);
1496 let drawing_id = state.next_drawing_id().to_string();
1500 let width = image.width_emu.to_string();
1501 let height = image.height_emu.to_string();
1502
1503 writer.write_event(Event::Start(BytesStart::new("w:drawing")))?;
1504
1505 let mut inline = BytesStart::new("wp:inline");
1506 inline.push_attribute(("distT", "0"));
1507 inline.push_attribute(("distB", "0"));
1508 inline.push_attribute(("distL", "0"));
1509 inline.push_attribute(("distR", "0"));
1510 writer.write_event(Event::Start(inline))?;
1511
1512 let mut extent = BytesStart::new("wp:extent");
1513 extent.push_attribute(("cx", width.as_str()));
1514 extent.push_attribute(("cy", height.as_str()));
1515 writer.write_event(Event::Empty(extent))?;
1516
1517 let mut doc_pr = BytesStart::new("wp:docPr");
1518 doc_pr.push_attribute(("id", drawing_id.as_str()));
1519 doc_pr.push_attribute(("name", relationship_id.as_str()));
1520 doc_pr.push_attribute(("descr", image.description.as_str()));
1521 writer.write_event(Event::Empty(doc_pr))?;
1522
1523 writer.write_event(Event::Start(BytesStart::new("a:graphic")))?;
1524 let mut graphic_data = BytesStart::new("a:graphicData");
1525 graphic_data.push_attribute(("uri", DRAWINGML_PICTURE_NAMESPACE));
1526 writer.write_event(Event::Start(graphic_data))?;
1527
1528 writer.write_event(Event::Start(BytesStart::new("pic:pic")))?;
1529
1530 writer.write_event(Event::Start(BytesStart::new("pic:nvPicPr")))?;
1531 let mut c_nv_pr = BytesStart::new("pic:cNvPr");
1532 c_nv_pr.push_attribute(("id", "0"));
1533 c_nv_pr.push_attribute(("name", relationship_id.as_str()));
1534 c_nv_pr.push_attribute(("descr", image.description.as_str()));
1535 writer.write_event(Event::Empty(c_nv_pr))?;
1536 writer.write_event(Event::Start(BytesStart::new("pic:cNvPicPr")))?;
1537 let mut pic_locks = BytesStart::new("a:picLocks");
1538 pic_locks.push_attribute(("noChangeAspect", "1"));
1539 writer.write_event(Event::Empty(pic_locks))?;
1540 writer.write_event(Event::End(BytesEnd::new("pic:cNvPicPr")))?;
1541 writer.write_event(Event::End(BytesEnd::new("pic:nvPicPr")))?;
1542
1543 writer.write_event(Event::Start(BytesStart::new("pic:blipFill")))?;
1544 let mut blip = BytesStart::new("a:blip");
1545 blip.push_attribute(("r:embed", relationship_id.as_str()));
1546 writer.write_event(Event::Empty(blip))?;
1547 writer.write_event(Event::Start(BytesStart::new("a:stretch")))?;
1548 writer.write_event(Event::Empty(BytesStart::new("a:fillRect")))?;
1549 writer.write_event(Event::End(BytesEnd::new("a:stretch")))?;
1550 writer.write_event(Event::End(BytesEnd::new("pic:blipFill")))?;
1551
1552 writer.write_event(Event::Start(BytesStart::new("pic:spPr")))?;
1553 writer.write_event(Event::Start(BytesStart::new("a:xfrm")))?;
1554 let mut off = BytesStart::new("a:off");
1555 off.push_attribute(("x", "0"));
1556 off.push_attribute(("y", "0"));
1557 writer.write_event(Event::Empty(off))?;
1558 let mut ext = BytesStart::new("a:ext");
1559 ext.push_attribute(("cx", width.as_str()));
1560 ext.push_attribute(("cy", height.as_str()));
1561 writer.write_event(Event::Empty(ext))?;
1562 writer.write_event(Event::End(BytesEnd::new("a:xfrm")))?;
1563
1564 let empty_properties = drawing::ShapeProperties::default();
1574 let shape_properties = image.shape_properties.as_ref().unwrap_or(&empty_properties);
1575 drawing::write_geometry_fill_line_effects(writer, shape_properties, "rect")?;
1576
1577 writer.write_event(Event::End(BytesEnd::new("pic:spPr")))?;
1578
1579 writer.write_event(Event::End(BytesEnd::new("pic:pic")))?;
1580 writer.write_event(Event::End(BytesEnd::new("a:graphicData")))?;
1581 writer.write_event(Event::End(BytesEnd::new("a:graphic")))?;
1582 writer.write_event(Event::End(BytesEnd::new("wp:inline")))?;
1583 writer.write_event(Event::End(BytesEnd::new("w:drawing")))?;
1584
1585 Ok(())
1586}
1587
1588fn write_embedded_chart<W: Write>(
1598 writer: &mut Writer<W>,
1599 chart: &EmbeddedChart,
1600 state: &mut PackageState,
1601 relationships: &mut PartRelationships,
1602) -> Result<()> {
1603 let entry_name = state.charts.register(&chart.chart_space)?;
1604 let relationship_id = relationships.add(CHART_RELATIONSHIP_TYPE, entry_name);
1605 let drawing_id = state.next_drawing_id().to_string();
1606 let width = chart.width_emu.to_string();
1607 let height = chart.height_emu.to_string();
1608
1609 writer.write_event(Event::Start(BytesStart::new("w:drawing")))?;
1610
1611 let mut inline = BytesStart::new("wp:inline");
1612 inline.push_attribute(("distT", "0"));
1613 inline.push_attribute(("distB", "0"));
1614 inline.push_attribute(("distL", "0"));
1615 inline.push_attribute(("distR", "0"));
1616 writer.write_event(Event::Start(inline))?;
1617
1618 let mut extent = BytesStart::new("wp:extent");
1619 extent.push_attribute(("cx", width.as_str()));
1620 extent.push_attribute(("cy", height.as_str()));
1621 writer.write_event(Event::Empty(extent))?;
1622
1623 let mut doc_pr = BytesStart::new("wp:docPr");
1624 doc_pr.push_attribute(("id", drawing_id.as_str()));
1625 doc_pr.push_attribute(("name", chart.name.as_str()));
1626 doc_pr.push_attribute(("descr", chart.name.as_str()));
1630 writer.write_event(Event::Empty(doc_pr))?;
1631
1632 writer.write_event(Event::Start(BytesStart::new("a:graphic")))?;
1633 let mut graphic_data = BytesStart::new("a:graphicData");
1634 graphic_data.push_attribute(("uri", DRAWINGML_CHART_NAMESPACE));
1635 writer.write_event(Event::Start(graphic_data))?;
1636
1637 let mut c_chart = BytesStart::new("c:chart");
1642 c_chart.push_attribute(("xmlns:c", DRAWINGML_CHART_NAMESPACE));
1643 c_chart.push_attribute(("r:id", relationship_id.as_str()));
1644 writer.write_event(Event::Empty(c_chart))?;
1645
1646 writer.write_event(Event::End(BytesEnd::new("a:graphicData")))?;
1647 writer.write_event(Event::End(BytesEnd::new("a:graphic")))?;
1648 writer.write_event(Event::End(BytesEnd::new("wp:inline")))?;
1649 writer.write_event(Event::End(BytesEnd::new("w:drawing")))?;
1650
1651 Ok(())
1652}
1653
1654const DEFAULT_COLUMN_WIDTH_TWIPS: &str = "2000";
1659
1660fn write_table<W: Write>(
1668 writer: &mut Writer<W>,
1669 table: &Table,
1670 state: &mut PackageState,
1671 relationships: &mut PartRelationships,
1672) -> Result<()> {
1673 writer.write_event(Event::Start(BytesStart::new("w:tbl")))?;
1674
1675 let column_count = table
1686 .rows
1687 .iter()
1688 .map(|row| {
1689 row.cells
1690 .iter()
1691 .map(|cell| cell.horizontal_span.unwrap_or(1).max(1) as usize)
1692 .sum::<usize>()
1693 })
1694 .max()
1695 .unwrap_or(0);
1696
1697 let has_column_widths = !table.column_widths.is_empty();
1704 writer.write_event(Event::Start(BytesStart::new("w:tblPr")))?;
1705 if let Some(style_id) = &table.style_id {
1708 let mut tbl_style = BytesStart::new("w:tblStyle");
1709 tbl_style.push_attribute(("w:val", style_id.as_str()));
1710 writer.write_event(Event::Empty(tbl_style))?;
1711 }
1712 let mut table_width = BytesStart::new("w:tblW");
1713 if has_column_widths {
1714 let total_width: u32 = table.column_widths.iter().sum();
1715 table_width.push_attribute(("w:w", total_width.to_string().as_str()));
1716 table_width.push_attribute(("w:type", "dxa"));
1717 } else {
1718 table_width.push_attribute(("w:w", "0"));
1719 table_width.push_attribute(("w:type", "auto"));
1720 }
1721 writer.write_event(Event::Empty(table_width))?;
1722 if let Some(alignment) = table.alignment {
1725 let mut jc = BytesStart::new("w:jc");
1726 jc.push_attribute(("w:val", alignment_value(alignment)));
1727 writer.write_event(Event::Empty(jc))?;
1728 }
1729 if let Some(indent_twips) = table.indent_twips {
1730 let mut tbl_ind = BytesStart::new("w:tblInd");
1731 tbl_ind.push_attribute(("w:w", indent_twips.to_string().as_str()));
1732 tbl_ind.push_attribute(("w:type", "dxa"));
1733 writer.write_event(Event::Empty(tbl_ind))?;
1734 }
1735 let border_color = table.border_color.as_deref().unwrap_or("auto");
1736 writer.write_event(Event::Start(BytesStart::new("w:tblBorders")))?;
1737 for side_element_name in [
1738 "w:top",
1739 "w:left",
1740 "w:bottom",
1741 "w:right",
1742 "w:insideH",
1743 "w:insideV",
1744 ] {
1745 let mut border = BytesStart::new(side_element_name);
1746 border.push_attribute(("w:val", "single"));
1747 border.push_attribute(("w:sz", "4"));
1748 border.push_attribute(("w:space", "0"));
1749 border.push_attribute(("w:color", border_color));
1750 writer.write_event(Event::Empty(border))?;
1751 }
1752 writer.write_event(Event::End(BytesEnd::new("w:tblBorders")))?;
1753 if has_column_widths {
1754 let mut tbl_layout = BytesStart::new("w:tblLayout");
1757 tbl_layout.push_attribute(("w:type", "fixed"));
1758 writer.write_event(Event::Empty(tbl_layout))?;
1759 }
1760 writer.write_event(Event::End(BytesEnd::new("w:tblPr")))?;
1761
1762 writer.write_event(Event::Start(BytesStart::new("w:tblGrid")))?;
1763 for column_index in 0..column_count {
1764 let width = table
1765 .column_widths
1766 .get(column_index)
1767 .map(u32::to_string)
1768 .unwrap_or_else(|| DEFAULT_COLUMN_WIDTH_TWIPS.to_string());
1769 let mut grid_col = BytesStart::new("w:gridCol");
1770 grid_col.push_attribute(("w:w", width.as_str()));
1771 writer.write_event(Event::Empty(grid_col))?;
1772 }
1773 writer.write_event(Event::End(BytesEnd::new("w:tblGrid")))?;
1774
1775 for row in &table.rows {
1776 writer.write_event(Event::Start(BytesStart::new("w:tr")))?;
1777 write_table_row_properties(writer, row)?;
1778 for cell in &row.cells {
1779 writer.write_event(Event::Start(BytesStart::new("w:tc")))?;
1780 write_table_cell_properties(writer, cell)?;
1781 for block in &cell.blocks {
1782 write_block(writer, block, state, relationships)?;
1783 }
1784 let needs_trailing_paragraph = !matches!(cell.blocks.last(), Some(Block::Paragraph(_)));
1791 if needs_trailing_paragraph {
1792 writer.write_event(Event::Empty(BytesStart::new("w:p")))?;
1793 }
1794 writer.write_event(Event::End(BytesEnd::new("w:tc")))?;
1795 }
1796 writer.write_event(Event::End(BytesEnd::new("w:tr")))?;
1797 }
1798
1799 writer.write_event(Event::End(BytesEnd::new("w:tbl")))?;
1800 Ok(())
1801}
1802
1803fn write_table_row_properties<W: Write>(writer: &mut Writer<W>, row: &TableRow) -> Result<()> {
1812 if !row.cant_split && row.height_twips.is_none() && !row.repeat_as_header_row {
1813 return Ok(());
1814 }
1815 writer.write_event(Event::Start(BytesStart::new("w:trPr")))?;
1816 if row.cant_split {
1817 writer.write_event(Event::Empty(BytesStart::new("w:cantSplit")))?;
1818 }
1819 if let Some(height_twips) = row.height_twips {
1820 let mut tr_height = BytesStart::new("w:trHeight");
1821 tr_height.push_attribute(("w:val", height_twips.to_string().as_str()));
1822 tr_height.push_attribute(("w:hRule", "atLeast"));
1823 writer.write_event(Event::Empty(tr_height))?;
1824 }
1825 if row.repeat_as_header_row {
1826 writer.write_event(Event::Empty(BytesStart::new("w:tblHeader")))?;
1827 }
1828 writer.write_event(Event::End(BytesEnd::new("w:trPr")))?;
1829 Ok(())
1830}
1831
1832fn write_table_cell_properties<W: Write>(writer: &mut Writer<W>, cell: &TableCell) -> Result<()> {
1845 if cell.horizontal_span.is_none()
1846 && cell.vertical_merge.is_none()
1847 && !cell.border
1848 && cell.shading_color.is_none()
1849 && cell.margin_top_twips.is_none()
1850 && cell.margin_left_twips.is_none()
1851 && cell.margin_bottom_twips.is_none()
1852 && cell.margin_right_twips.is_none()
1853 && cell.text_direction.is_none()
1854 && cell.vertical_alignment.is_none()
1855 {
1856 return Ok(());
1857 }
1858 writer.write_event(Event::Start(BytesStart::new("w:tcPr")))?;
1859 if let Some(horizontal_span) = cell.horizontal_span {
1860 let mut grid_span = BytesStart::new("w:gridSpan");
1861 grid_span.push_attribute(("w:val", horizontal_span.to_string().as_str()));
1862 writer.write_event(Event::Empty(grid_span))?;
1863 }
1864 if let Some(vertical_merge) = cell.vertical_merge {
1865 let mut v_merge = BytesStart::new("w:vMerge");
1866 v_merge.push_attribute(("w:val", vertical_merge.attribute_value()));
1867 writer.write_event(Event::Empty(v_merge))?;
1868 }
1869 if cell.border {
1870 writer.write_event(Event::Start(BytesStart::new("w:tcBorders")))?;
1871 write_border_side(writer, "w:top")?;
1872 write_border_side(writer, "w:left")?;
1873 write_border_side(writer, "w:bottom")?;
1874 write_border_side(writer, "w:right")?;
1875 writer.write_event(Event::End(BytesEnd::new("w:tcBorders")))?;
1876 }
1877 if let Some(shading_color) = &cell.shading_color {
1878 write_shading(writer, "w:shd", shading_color)?;
1879 }
1880 if cell.margin_top_twips.is_some()
1881 || cell.margin_left_twips.is_some()
1882 || cell.margin_bottom_twips.is_some()
1883 || cell.margin_right_twips.is_some()
1884 {
1885 writer.write_event(Event::Start(BytesStart::new("w:tcMar")))?;
1887 write_cell_margin_side(writer, "w:top", cell.margin_top_twips)?;
1888 write_cell_margin_side(writer, "w:left", cell.margin_left_twips)?;
1889 write_cell_margin_side(writer, "w:bottom", cell.margin_bottom_twips)?;
1890 write_cell_margin_side(writer, "w:right", cell.margin_right_twips)?;
1891 writer.write_event(Event::End(BytesEnd::new("w:tcMar")))?;
1892 }
1893 if let Some(text_direction) = cell.text_direction {
1894 let mut element = BytesStart::new("w:textDirection");
1895 element.push_attribute(("w:val", text_direction.attribute_value()));
1896 writer.write_event(Event::Empty(element))?;
1897 }
1898 if let Some(vertical_alignment) = cell.vertical_alignment {
1899 let mut v_align = BytesStart::new("w:vAlign");
1900 v_align.push_attribute(("w:val", vertical_alignment.attribute_value()));
1901 writer.write_event(Event::Empty(v_align))?;
1902 }
1903 writer.write_event(Event::End(BytesEnd::new("w:tcPr")))?;
1904 Ok(())
1905}
1906
1907fn write_cell_margin_side<W: Write>(
1912 writer: &mut Writer<W>,
1913 tag_name: &str,
1914 value: Option<u32>,
1915) -> Result<()> {
1916 if let Some(value) = value {
1917 let mut element = BytesStart::new(tag_name);
1918 element.push_attribute(("w:w", value.to_string().as_str()));
1919 element.push_attribute(("w:type", "dxa"));
1920 writer.write_event(Event::Empty(element))?;
1921 }
1922 Ok(())
1923}
1924
1925fn write_structured_document_tag<W: Write>(
1934 writer: &mut Writer<W>,
1935 sdt: &StructuredDocumentTag,
1936 state: &mut PackageState,
1937 relationships: &mut PartRelationships,
1938) -> Result<()> {
1939 writer.write_event(Event::Start(BytesStart::new("w:sdt")))?;
1940
1941 if sdt.alias.is_some() || sdt.tag.is_some() || sdt.id.is_some() {
1942 writer.write_event(Event::Start(BytesStart::new("w:sdtPr")))?;
1943 if let Some(alias) = &sdt.alias {
1944 let mut element = BytesStart::new("w:alias");
1945 element.push_attribute(("w:val", alias.as_str()));
1946 writer.write_event(Event::Empty(element))?;
1947 }
1948 if let Some(tag) = &sdt.tag {
1949 let mut element = BytesStart::new("w:tag");
1950 element.push_attribute(("w:val", tag.as_str()));
1951 writer.write_event(Event::Empty(element))?;
1952 }
1953 if let Some(id) = sdt.id {
1954 let id_string = id.to_string();
1955 let mut element = BytesStart::new("w:id");
1956 element.push_attribute(("w:val", id_string.as_str()));
1957 writer.write_event(Event::Empty(element))?;
1958 }
1959 writer.write_event(Event::End(BytesEnd::new("w:sdtPr")))?;
1960 }
1961
1962 writer.write_event(Event::Start(BytesStart::new("w:sdtContent")))?;
1963 for block in &sdt.blocks {
1964 write_block(writer, block, state, relationships)?;
1965 }
1966 writer.write_event(Event::End(BytesEnd::new("w:sdtContent")))?;
1967
1968 writer.write_event(Event::End(BytesEnd::new("w:sdt")))?;
1969 Ok(())
1970}
1971
1972const DEFAULT_COLUMN_SPACING_TWIPS: u32 = 720;
1978
1979fn write_section_properties<W: Write>(
1996 writer: &mut Writer<W>,
1997 section_references: SectionReferenceIds<'_>,
1998 page_setup: &PageSetup,
1999) -> Result<()> {
2000 writer.write_event(Event::Start(BytesStart::new("w:sectPr")))?;
2001
2002 write_header_footer_reference(
2003 writer,
2004 "w:headerReference",
2005 "default",
2006 section_references.header,
2007 )?;
2008 write_header_footer_reference(
2009 writer,
2010 "w:headerReference",
2011 "first",
2012 section_references.header_first,
2013 )?;
2014 write_header_footer_reference(
2015 writer,
2016 "w:headerReference",
2017 "even",
2018 section_references.header_even,
2019 )?;
2020 write_header_footer_reference(
2021 writer,
2022 "w:footerReference",
2023 "default",
2024 section_references.footer,
2025 )?;
2026 write_header_footer_reference(
2027 writer,
2028 "w:footerReference",
2029 "first",
2030 section_references.footer_first,
2031 )?;
2032 write_header_footer_reference(
2033 writer,
2034 "w:footerReference",
2035 "even",
2036 section_references.footer_even,
2037 )?;
2038
2039 let mut page_size = BytesStart::new("w:pgSz");
2040 let width = page_setup.width_twips.to_string();
2041 let height = page_setup.height_twips.to_string();
2042 page_size.push_attribute(("w:w", width.as_str()));
2043 page_size.push_attribute(("w:h", height.as_str()));
2044 if page_setup.orientation == Orientation::Landscape {
2045 page_size.push_attribute(("w:orient", page_setup.orientation.attribute_value()));
2046 }
2047 writer.write_event(Event::Empty(page_size))?;
2048
2049 let mut page_margin = BytesStart::new("w:pgMar");
2050 let top = page_setup.margin_top_twips.to_string();
2051 let right = page_setup.margin_right_twips.to_string();
2052 let bottom = page_setup.margin_bottom_twips.to_string();
2053 let left = page_setup.margin_left_twips.to_string();
2054 let header = page_setup.margin_header_twips.to_string();
2055 let footer = page_setup.margin_footer_twips.to_string();
2056 let gutter = page_setup.margin_gutter_twips.to_string();
2057 page_margin.push_attribute(("w:top", top.as_str()));
2058 page_margin.push_attribute(("w:right", right.as_str()));
2059 page_margin.push_attribute(("w:bottom", bottom.as_str()));
2060 page_margin.push_attribute(("w:left", left.as_str()));
2061 page_margin.push_attribute(("w:header", header.as_str()));
2062 page_margin.push_attribute(("w:footer", footer.as_str()));
2063 page_margin.push_attribute(("w:gutter", gutter.as_str()));
2064 writer.write_event(Event::Empty(page_margin))?;
2065
2066 if let Some(columns) = page_setup.columns {
2067 let mut cols = BytesStart::new("w:cols");
2068 let num = columns.to_string();
2069 let space = DEFAULT_COLUMN_SPACING_TWIPS.to_string();
2070 cols.push_attribute(("w:num", num.as_str()));
2071 cols.push_attribute(("w:space", space.as_str()));
2072 cols.push_attribute(("w:equalWidth", "true"));
2073 writer.write_event(Event::Empty(cols))?;
2074 }
2075
2076 if section_references.title_pg {
2077 writer.write_event(Event::Empty(BytesStart::new("w:titlePg")))?;
2078 }
2079
2080 writer.write_event(Event::End(BytesEnd::new("w:sectPr")))?;
2081 Ok(())
2082}
2083
2084fn write_header_footer_reference<W: Write>(
2088 writer: &mut Writer<W>,
2089 element_name: &str,
2090 type_value: &str,
2091 relationship_id: Option<&str>,
2092) -> Result<()> {
2093 if let Some(id) = relationship_id {
2094 let mut reference = BytesStart::new(element_name);
2095 reference.push_attribute(("w:type", type_value));
2096 reference.push_attribute(("r:id", id));
2097 writer.write_event(Event::Empty(reference))?;
2098 }
2099 Ok(())
2100}
2101
2102fn starts_or_ends_with_whitespace(text: &str) -> bool {
2103 text.starts_with(char::is_whitespace) || text.ends_with(char::is_whitespace)
2104}
2105
2106fn alignment_value(alignment: Alignment) -> &'static str {
2112 match alignment {
2113 Alignment::Left => "left",
2114 Alignment::Center => "center",
2115 Alignment::Right => "right",
2116 Alignment::Justify => "both",
2117 }
2118}
2119
2120fn to_styles_xml(styles: &[Style]) -> Result<Vec<u8>> {
2129 let mut writer = Writer::new(Vec::new());
2130
2131 writer.write_event(Event::Decl(BytesDecl::new(
2132 "1.0",
2133 Some("UTF-8"),
2134 Some("yes"),
2135 )))?;
2136
2137 let mut root = BytesStart::new("w:styles");
2138 root.push_attribute(("xmlns:w", WORDPROCESSINGML_NAMESPACE));
2139 writer.write_event(Event::Start(root))?;
2140
2141 for style in styles {
2142 let mut style_element = BytesStart::new("w:style");
2143 style_element.push_attribute(("w:type", style.kind.attribute_value()));
2144 style_element.push_attribute(("w:styleId", style.id.as_str()));
2145 writer.write_event(Event::Start(style_element))?;
2146
2147 let mut name = BytesStart::new("w:name");
2148 name.push_attribute(("w:val", style.name.as_str()));
2149 writer.write_event(Event::Empty(name))?;
2150
2151 if let Some(based_on) = &style.based_on {
2152 let mut based_on_element = BytesStart::new("w:basedOn");
2153 based_on_element.push_attribute(("w:val", based_on.as_str()));
2154 writer.write_event(Event::Empty(based_on_element))?;
2155 }
2156
2157 if style.kind == StyleKind::Paragraph
2165 && (style.alignment.is_some()
2166 || style.paragraph_shading_color.is_some()
2167 || style.paragraph_border
2168 || style.line_spacing.is_some()
2169 || style.space_before.is_some()
2170 || style.space_after.is_some()
2171 || style.keep_with_next
2172 || style.keep_lines_together
2173 || style.page_break_before
2174 || !style.tabs.is_empty()
2175 || style.contextual_spacing)
2176 {
2177 writer.write_event(Event::Start(BytesStart::new("w:pPr")))?;
2178 if style.keep_with_next {
2179 writer.write_event(Event::Empty(BytesStart::new("w:keepNext")))?;
2180 }
2181 if style.keep_lines_together {
2182 writer.write_event(Event::Empty(BytesStart::new("w:keepLines")))?;
2183 }
2184 if style.page_break_before {
2185 writer.write_event(Event::Empty(BytesStart::new("w:pageBreakBefore")))?;
2186 }
2187 if style.paragraph_border {
2188 write_paragraph_border(&mut writer)?;
2189 }
2190 if let Some(paragraph_shading_color) = &style.paragraph_shading_color {
2191 write_shading(&mut writer, "w:shd", paragraph_shading_color)?;
2192 }
2193 if !style.tabs.is_empty() {
2194 write_tabs(&mut writer, &style.tabs)?;
2195 }
2196 if style.line_spacing.is_some()
2197 || style.space_before.is_some()
2198 || style.space_after.is_some()
2199 {
2200 write_paragraph_spacing(
2201 &mut writer,
2202 style.line_spacing,
2203 style.space_before,
2204 style.space_after,
2205 )?;
2206 }
2207 if style.contextual_spacing {
2208 writer.write_event(Event::Empty(BytesStart::new("w:contextualSpacing")))?;
2209 }
2210 if let Some(alignment) = style.alignment {
2211 let mut jc = BytesStart::new("w:jc");
2212 jc.push_attribute(("w:val", alignment_value(alignment)));
2213 writer.write_event(Event::Empty(jc))?;
2214 }
2215 writer.write_event(Event::End(BytesEnd::new("w:pPr")))?;
2216 }
2217
2218 if style.font_family.is_some()
2219 || style.color.is_some()
2220 || style.font_size.is_some()
2221 || style.highlight.is_some()
2222 || style.vertical_align.is_some()
2223 || style.underline.is_some()
2224 || style.shading_color.is_some()
2225 || style.character_spacing.is_some()
2226 || style.vertical_position.is_some()
2227 || style.bold
2228 || style.italic
2229 || style.strike
2230 || style.small_caps
2231 || style.all_caps
2232 || style.text_border
2233 || style.hidden
2234 {
2235 writer.write_event(Event::Start(BytesStart::new("w:rPr")))?;
2236 write_character_formatting(
2237 &mut writer,
2238 style.font_family.as_deref(),
2239 style.color.as_deref(),
2240 style.font_size,
2241 style.highlight,
2242 style.bold,
2243 style.italic,
2244 style.underline,
2245 style.underline_color.as_deref(),
2246 style.strike,
2247 style.vertical_align,
2248 style.small_caps,
2249 style.all_caps,
2250 style.shading_color.as_deref(),
2251 style.text_border,
2252 style.character_spacing,
2253 style.vertical_position,
2254 style.hidden,
2255 )?;
2256 writer.write_event(Event::End(BytesEnd::new("w:rPr")))?;
2257 }
2258
2259 writer.write_event(Event::End(BytesEnd::new("w:style")))?;
2260 }
2261
2262 writer.write_event(Event::End(BytesEnd::new("w:styles")))?;
2263
2264 Ok(writer.into_inner())
2265}
2266
2267fn to_numbering_xml(definitions: &[NumberingDefinition]) -> Result<Vec<u8>> {
2275 let mut writer = Writer::new(Vec::new());
2276
2277 writer.write_event(Event::Decl(BytesDecl::new(
2278 "1.0",
2279 Some("UTF-8"),
2280 Some("yes"),
2281 )))?;
2282
2283 let mut root = BytesStart::new("w:numbering");
2284 root.push_attribute(("xmlns:w", WORDPROCESSINGML_NAMESPACE));
2285 writer.write_event(Event::Start(root))?;
2286
2287 for definition in definitions {
2288 let abstract_num_id = definition.id.to_string();
2289 let mut abstract_num = BytesStart::new("w:abstractNum");
2290 abstract_num.push_attribute(("w:abstractNumId", abstract_num_id.as_str()));
2291 writer.write_event(Event::Start(abstract_num))?;
2292
2293 for (level_index, level) in definition.levels.iter().enumerate() {
2294 let ilvl = level_index.to_string();
2295 let mut lvl = BytesStart::new("w:lvl");
2296 lvl.push_attribute(("w:ilvl", ilvl.as_str()));
2297 writer.write_event(Event::Start(lvl))?;
2298
2299 let mut start = BytesStart::new("w:start");
2302 start.push_attribute(("w:val", "1"));
2303 writer.write_event(Event::Empty(start))?;
2304
2305 let mut num_fmt = BytesStart::new("w:numFmt");
2306 num_fmt.push_attribute(("w:val", level.format.attribute_value()));
2307 writer.write_event(Event::Empty(num_fmt))?;
2308
2309 let mut lvl_text = BytesStart::new("w:lvlText");
2310 lvl_text.push_attribute(("w:val", level.text.as_str()));
2311 writer.write_event(Event::Empty(lvl_text))?;
2312
2313 let mut lvl_jc = BytesStart::new("w:lvlJc");
2314 lvl_jc.push_attribute(("w:val", "left"));
2315 writer.write_event(Event::Empty(lvl_jc))?;
2316
2317 writer.write_event(Event::Start(BytesStart::new("w:pPr")))?;
2318 let mut ind = BytesStart::new("w:ind");
2319 ind.push_attribute(("w:left", level.indent_twips.to_string().as_str()));
2320 ind.push_attribute(("w:hanging", level.hanging_twips.to_string().as_str()));
2321 writer.write_event(Event::Empty(ind))?;
2322 writer.write_event(Event::End(BytesEnd::new("w:pPr")))?;
2323
2324 writer.write_event(Event::End(BytesEnd::new("w:lvl")))?;
2325 }
2326
2327 writer.write_event(Event::End(BytesEnd::new("w:abstractNum")))?;
2328 }
2329
2330 for definition in definitions {
2331 let num_id = definition.id.to_string();
2332 let mut num = BytesStart::new("w:num");
2333 num.push_attribute(("w:numId", num_id.as_str()));
2334 writer.write_event(Event::Start(num))?;
2335
2336 let mut abstract_num_id_ref = BytesStart::new("w:abstractNumId");
2337 abstract_num_id_ref.push_attribute(("w:val", num_id.as_str()));
2338 writer.write_event(Event::Empty(abstract_num_id_ref))?;
2339
2340 writer.write_event(Event::End(BytesEnd::new("w:num")))?;
2341 }
2342
2343 writer.write_event(Event::End(BytesEnd::new("w:numbering")))?;
2344
2345 Ok(writer.into_inner())
2346}
2347
2348fn to_notes_xml(
2358 root_name: &str,
2359 note_element_name: &str,
2360 notes: &[Note],
2361 state: &mut PackageState,
2362 relationships: &mut PartRelationships,
2363) -> Result<Vec<u8>> {
2364 let mut writer = Writer::new(Vec::new());
2365
2366 writer.write_event(Event::Decl(BytesDecl::new(
2367 "1.0",
2368 Some("UTF-8"),
2369 Some("yes"),
2370 )))?;
2371
2372 let mut root = BytesStart::new(root_name);
2373 root.push_attribute(("xmlns:w", WORDPROCESSINGML_NAMESPACE));
2374 root.push_attribute(("xmlns:r", RELATIONSHIPS_NAMESPACE));
2375 root.push_attribute(("xmlns:wp", WORDPROCESSING_DRAWING_NAMESPACE));
2376 root.push_attribute(("xmlns:a", DRAWINGML_NAMESPACE));
2377 root.push_attribute(("xmlns:pic", DRAWINGML_PICTURE_NAMESPACE));
2378 writer.write_event(Event::Start(root))?;
2379
2380 for note in notes {
2381 let id = note.id.to_string();
2382 let mut note_element = BytesStart::new(note_element_name);
2383 note_element.push_attribute(("w:id", id.as_str()));
2384 writer.write_event(Event::Start(note_element))?;
2385
2386 for block in ¬e.blocks {
2387 write_block(&mut writer, block, state, relationships)?;
2388 }
2389
2390 writer.write_event(Event::End(BytesEnd::new(note_element_name)))?;
2391 }
2392
2393 writer.write_event(Event::End(BytesEnd::new(root_name)))?;
2394
2395 Ok(writer.into_inner())
2396}
2397
2398fn to_comments_xml(
2404 comments: &[Comment],
2405 state: &mut PackageState,
2406 relationships: &mut PartRelationships,
2407) -> Result<Vec<u8>> {
2408 let mut writer = Writer::new(Vec::new());
2409
2410 writer.write_event(Event::Decl(BytesDecl::new(
2411 "1.0",
2412 Some("UTF-8"),
2413 Some("yes"),
2414 )))?;
2415
2416 let mut root = BytesStart::new("w:comments");
2417 root.push_attribute(("xmlns:w", WORDPROCESSINGML_NAMESPACE));
2418 root.push_attribute(("xmlns:r", RELATIONSHIPS_NAMESPACE));
2419 root.push_attribute(("xmlns:wp", WORDPROCESSING_DRAWING_NAMESPACE));
2420 root.push_attribute(("xmlns:a", DRAWINGML_NAMESPACE));
2421 root.push_attribute(("xmlns:pic", DRAWINGML_PICTURE_NAMESPACE));
2422 writer.write_event(Event::Start(root))?;
2423
2424 for comment in comments {
2425 let id = comment.id.to_string();
2426 let mut comment_element = BytesStart::new("w:comment");
2427 comment_element.push_attribute(("w:id", id.as_str()));
2428 if let Some(author) = &comment.author {
2429 comment_element.push_attribute(("w:author", author.as_str()));
2430 }
2431 if let Some(date) = &comment.date {
2432 comment_element.push_attribute(("w:date", date.as_str()));
2433 }
2434 if let Some(initials) = &comment.initials {
2435 comment_element.push_attribute(("w:initials", initials.as_str()));
2436 }
2437 writer.write_event(Event::Start(comment_element))?;
2438
2439 for block in &comment.blocks {
2440 write_block(&mut writer, block, state, relationships)?;
2441 }
2442
2443 writer.write_event(Event::End(BytesEnd::new("w:comment")))?;
2444 }
2445
2446 writer.write_event(Event::End(BytesEnd::new("w:comments")))?;
2447
2448 Ok(writer.into_inner())
2449}
2450
2451fn to_theme_xml(theme: &Theme) -> Result<Vec<u8>> {
2456 let mut writer = Writer::new(Vec::new());
2457
2458 writer.write_event(Event::Decl(BytesDecl::new(
2459 "1.0",
2460 Some("UTF-8"),
2461 Some("yes"),
2462 )))?;
2463
2464 let mut root = BytesStart::new("a:theme");
2465 root.push_attribute(("xmlns:a", DRAWINGML_NAMESPACE));
2466 root.push_attribute(("name", theme.name.as_str()));
2467 writer.write_event(Event::Start(root))?;
2468
2469 writer.write_event(Event::Start(BytesStart::new("a:themeElements")))?;
2470 write_color_scheme(&mut writer, &theme.name, &theme.colors)?;
2471 write_font_scheme(&mut writer, &theme.name, &theme.fonts)?;
2472 writer.write_raw(FIXED_FORMAT_SCHEME_XML)?;
2473 writer.write_event(Event::End(BytesEnd::new("a:themeElements")))?;
2474
2475 writer.write_event(Event::End(BytesEnd::new("a:theme")))?;
2476
2477 Ok(writer.into_inner())
2478}
2479
2480fn write_color_scheme<W: Write>(
2484 writer: &mut Writer<W>,
2485 name: &str,
2486 colors: &ColorScheme,
2487) -> Result<()> {
2488 let mut clr_scheme = BytesStart::new("a:clrScheme");
2489 clr_scheme.push_attribute(("name", name));
2490 writer.write_event(Event::Start(clr_scheme))?;
2491
2492 for (slot, value) in [
2493 ("dk1", &colors.dark1),
2494 ("lt1", &colors.light1),
2495 ("dk2", &colors.dark2),
2496 ("lt2", &colors.light2),
2497 ("accent1", &colors.accent1),
2498 ("accent2", &colors.accent2),
2499 ("accent3", &colors.accent3),
2500 ("accent4", &colors.accent4),
2501 ("accent5", &colors.accent5),
2502 ("accent6", &colors.accent6),
2503 ("hlink", &colors.hyperlink),
2504 ("folHlink", &colors.followed_hyperlink),
2505 ] {
2506 writer.write_event(Event::Start(BytesStart::new(format!("a:{slot}"))))?;
2507 let mut srgb_clr = BytesStart::new("a:srgbClr");
2508 srgb_clr.push_attribute(("val", value.as_str()));
2509 writer.write_event(Event::Empty(srgb_clr))?;
2510 writer.write_event(Event::End(BytesEnd::new(format!("a:{slot}"))))?;
2511 }
2512
2513 writer.write_event(Event::End(BytesEnd::new("a:clrScheme")))?;
2514 Ok(())
2515}
2516
2517fn write_font_scheme<W: Write>(
2521 writer: &mut Writer<W>,
2522 name: &str,
2523 fonts: &FontScheme,
2524) -> Result<()> {
2525 let mut font_scheme = BytesStart::new("a:fontScheme");
2526 font_scheme.push_attribute(("name", name));
2527 writer.write_event(Event::Start(font_scheme))?;
2528
2529 for (wrapper, typeface) in [
2530 ("a:majorFont", &fonts.major_latin),
2531 ("a:minorFont", &fonts.minor_latin),
2532 ] {
2533 writer.write_event(Event::Start(BytesStart::new(wrapper)))?;
2534
2535 let mut latin = BytesStart::new("a:latin");
2536 latin.push_attribute(("typeface", typeface.as_str()));
2537 writer.write_event(Event::Empty(latin))?;
2538
2539 let mut ea = BytesStart::new("a:ea");
2540 ea.push_attribute(("typeface", ""));
2541 writer.write_event(Event::Empty(ea))?;
2542
2543 let mut cs = BytesStart::new("a:cs");
2544 cs.push_attribute(("typeface", ""));
2545 writer.write_event(Event::Empty(cs))?;
2546
2547 writer.write_event(Event::End(BytesEnd::new(wrapper)))?;
2548 }
2549
2550 writer.write_event(Event::End(BytesEnd::new("a:fontScheme")))?;
2551 Ok(())
2552}
2553
2554const FIXED_FORMAT_SCHEME_XML: &str = concat!(
2559 "<a:fmtScheme name=\"Office\">",
2560 "<a:fillStyleLst>",
2561 "<a:solidFill><a:schemeClr val=\"phClr\"/></a:solidFill>",
2562 "<a:gradFill rotWithShape=\"1\"><a:gsLst>",
2563 "<a:gs pos=\"0\"><a:schemeClr val=\"phClr\"><a:tint val=\"50000\"/><a:satMod val=\"300000\"/></a:schemeClr></a:gs>",
2564 "<a:gs pos=\"35000\"><a:schemeClr val=\"phClr\"><a:tint val=\"37000\"/><a:satMod val=\"300000\"/></a:schemeClr></a:gs>",
2565 "<a:gs pos=\"100000\"><a:schemeClr val=\"phClr\"><a:tint val=\"15000\"/><a:satMod val=\"350000\"/></a:schemeClr></a:gs>",
2566 "</a:gsLst><a:lin ang=\"16200000\" scaled=\"1\"/></a:gradFill>",
2567 "<a:gradFill rotWithShape=\"1\"><a:gsLst>",
2568 "<a:gs pos=\"0\"><a:schemeClr val=\"phClr\"><a:tint val=\"100000\"/><a:shade val=\"100000\"/><a:satMod val=\"130000\"/></a:schemeClr></a:gs>",
2569 "<a:gs pos=\"100000\"><a:schemeClr val=\"phClr\"><a:tint val=\"50000\"/><a:shade val=\"100000\"/><a:satMod val=\"350000\"/></a:schemeClr></a:gs>",
2570 "</a:gsLst><a:lin ang=\"16200000\" scaled=\"0\"/></a:gradFill>",
2571 "</a:fillStyleLst>",
2572 "<a:lnStyleLst>",
2573 "<a:ln w=\"9525\" cap=\"flat\" cmpd=\"sng\" algn=\"ctr\"><a:solidFill><a:schemeClr val=\"phClr\"><a:shade val=\"95000\"/><a:satMod val=\"105000\"/></a:schemeClr></a:solidFill><a:prstDash val=\"solid\"/></a:ln>",
2574 "<a:ln w=\"25400\" cap=\"flat\" cmpd=\"sng\" algn=\"ctr\"><a:solidFill><a:schemeClr val=\"phClr\"/></a:solidFill><a:prstDash val=\"solid\"/></a:ln>",
2575 "<a:ln w=\"38100\" cap=\"flat\" cmpd=\"sng\" algn=\"ctr\"><a:solidFill><a:schemeClr val=\"phClr\"/></a:solidFill><a:prstDash val=\"solid\"/></a:ln>",
2576 "</a:lnStyleLst>",
2577 "<a:effectStyleLst>",
2578 "<a:effectStyle><a:effectLst><a:outerShdw blurRad=\"40000\" dist=\"20000\" dir=\"5400000\" rotWithShape=\"0\"><a:srgbClr val=\"000000\"><a:alpha val=\"38000\"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle>",
2579 "<a:effectStyle><a:effectLst><a:outerShdw blurRad=\"40000\" dist=\"23000\" dir=\"5400000\" rotWithShape=\"0\"><a:srgbClr val=\"000000\"><a:alpha val=\"35000\"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle>",
2580 "<a:effectStyle>",
2581 "<a:effectLst><a:outerShdw blurRad=\"40000\" dist=\"23000\" dir=\"5400000\" rotWithShape=\"0\"><a:srgbClr val=\"000000\"><a:alpha val=\"35000\"/></a:srgbClr></a:outerShdw></a:effectLst>",
2582 "<a:scene3d><a:camera prst=\"orthographicFront\"><a:rot lat=\"0\" lon=\"0\" rev=\"0\"/></a:camera><a:lightRig rig=\"threePt\" dir=\"t\"><a:rot lat=\"0\" lon=\"0\" rev=\"1200000\"/></a:lightRig></a:scene3d>",
2583 "<a:sp3d><a:bevelT w=\"63500\" h=\"25400\"/></a:sp3d>",
2584 "</a:effectStyle>",
2585 "</a:effectStyleLst>",
2586 "<a:bgFillStyleLst>",
2587 "<a:solidFill><a:schemeClr val=\"phClr\"/></a:solidFill>",
2588 "<a:gradFill rotWithShape=\"1\"><a:gsLst>",
2589 "<a:gs pos=\"0\"><a:schemeClr val=\"phClr\"><a:tint val=\"40000\"/><a:satMod val=\"350000\"/></a:schemeClr></a:gs>",
2590 "<a:gs pos=\"40000\"><a:schemeClr val=\"phClr\"><a:tint val=\"45000\"/><a:shade val=\"99000\"/><a:satMod val=\"350000\"/></a:schemeClr></a:gs>",
2591 "<a:gs pos=\"100000\"><a:schemeClr val=\"phClr\"><a:shade val=\"20000\"/><a:satMod val=\"255000\"/></a:schemeClr></a:gs>",
2592 "</a:gsLst><a:path path=\"circle\"><a:fillToRect l=\"50000\" t=\"-80000\" r=\"50000\" b=\"180000\"/></a:path></a:gradFill>",
2593 "<a:gradFill rotWithShape=\"1\"><a:gsLst>",
2594 "<a:gs pos=\"0\"><a:schemeClr val=\"phClr\"><a:tint val=\"80000\"/><a:satMod val=\"300000\"/></a:schemeClr></a:gs>",
2595 "<a:gs pos=\"100000\"><a:schemeClr val=\"phClr\"><a:shade val=\"30000\"/><a:satMod val=\"200000\"/></a:schemeClr></a:gs>",
2596 "</a:gsLst><a:path path=\"circle\"><a:fillToRect l=\"50000\" t=\"50000\" r=\"50000\" b=\"50000\"/></a:path></a:gradFill>",
2597 "</a:bgFillStyleLst>",
2598 "</a:fmtScheme>",
2599);
2600
2601fn to_settings_xml(
2627 track_changes: bool,
2628 protection: Option<ProtectionKind>,
2629 even_and_odd_headers: bool,
2630) -> Result<Vec<u8>> {
2631 let mut writer = Writer::new(Vec::new());
2632
2633 writer.write_event(Event::Decl(BytesDecl::new(
2634 "1.0",
2635 Some("UTF-8"),
2636 Some("yes"),
2637 )))?;
2638
2639 let mut root = BytesStart::new("w:settings");
2640 root.push_attribute(("xmlns:w", WORDPROCESSINGML_NAMESPACE));
2641
2642 if track_changes || protection.is_some() || even_and_odd_headers {
2643 writer.write_event(Event::Start(root))?;
2644 if track_changes {
2645 writer.write_event(Event::Empty(BytesStart::new("w:trackRevisions")))?;
2646 }
2647 if let Some(protection) = protection {
2648 let mut document_protection = BytesStart::new("w:documentProtection");
2649 document_protection.push_attribute(("w:edit", protection.attribute_value()));
2650 document_protection.push_attribute(("w:enforcement", "1"));
2651 writer.write_event(Event::Empty(document_protection))?;
2652 }
2653 if even_and_odd_headers {
2654 writer.write_event(Event::Empty(BytesStart::new("w:evenAndOddHeaders")))?;
2655 }
2656 writer.write_event(Event::End(BytesEnd::new("w:settings")))?;
2657 } else {
2658 writer.write_event(Event::Empty(root))?;
2659 }
2660
2661 Ok(writer.into_inner())
2662}
2663
2664fn to_core_properties_xml(properties: &DocumentProperties) -> Result<Vec<u8>> {
2676 let mut writer = Writer::new(Vec::new());
2677
2678 writer.write_event(Event::Decl(BytesDecl::new(
2679 "1.0",
2680 Some("UTF-8"),
2681 Some("yes"),
2682 )))?;
2683
2684 let mut root = BytesStart::new("cp:coreProperties");
2685 root.push_attribute(("xmlns:cp", CORE_PROPERTIES_NAMESPACE));
2686 root.push_attribute(("xmlns:dc", DC_NAMESPACE));
2687 root.push_attribute(("xmlns:dcterms", DCTERMS_NAMESPACE));
2688 root.push_attribute(("xmlns:xsi", XSI_NAMESPACE));
2689
2690 let has_any_property = properties.title.is_some()
2691 || properties.subject.is_some()
2692 || properties.creator.is_some()
2693 || properties.keywords.is_some()
2694 || properties.description.is_some()
2695 || properties.last_modified_by.is_some()
2696 || properties.revision.is_some()
2697 || properties.created.is_some()
2698 || properties.modified.is_some()
2699 || properties.category.is_some()
2700 || properties.content_status.is_some();
2701
2702 if !has_any_property {
2703 writer.write_event(Event::Empty(root))?;
2704 return Ok(writer.into_inner());
2705 }
2706
2707 writer.write_event(Event::Start(root))?;
2708 write_core_property_text(&mut writer, "dc:title", properties.title.as_deref())?;
2709 write_core_property_text(&mut writer, "dc:subject", properties.subject.as_deref())?;
2710 write_core_property_text(&mut writer, "dc:creator", properties.creator.as_deref())?;
2711 write_core_property_text(&mut writer, "cp:keywords", properties.keywords.as_deref())?;
2712 write_core_property_text(
2713 &mut writer,
2714 "dc:description",
2715 properties.description.as_deref(),
2716 )?;
2717 write_core_property_text(
2718 &mut writer,
2719 "cp:lastModifiedBy",
2720 properties.last_modified_by.as_deref(),
2721 )?;
2722 write_core_property_text(&mut writer, "cp:revision", properties.revision.as_deref())?;
2723 write_core_property_datetime(
2724 &mut writer,
2725 "dcterms:created",
2726 properties.created.as_deref(),
2727 )?;
2728 write_core_property_datetime(
2729 &mut writer,
2730 "dcterms:modified",
2731 properties.modified.as_deref(),
2732 )?;
2733 write_core_property_text(&mut writer, "cp:category", properties.category.as_deref())?;
2734 write_core_property_text(
2735 &mut writer,
2736 "cp:contentStatus",
2737 properties.content_status.as_deref(),
2738 )?;
2739 writer.write_event(Event::End(BytesEnd::new("cp:coreProperties")))?;
2740
2741 Ok(writer.into_inner())
2742}
2743
2744fn write_core_property_text<W: Write>(
2747 writer: &mut Writer<W>,
2748 element_name: &str,
2749 value: Option<&str>,
2750) -> Result<()> {
2751 if let Some(value) = value {
2752 writer.write_event(Event::Start(BytesStart::new(element_name)))?;
2753 writer.write_event(Event::Text(BytesText::new(value)))?;
2754 writer.write_event(Event::End(BytesEnd::new(element_name)))?;
2755 }
2756 Ok(())
2757}
2758
2759fn write_core_property_datetime<W: Write>(
2764 writer: &mut Writer<W>,
2765 element_name: &str,
2766 value: Option<&str>,
2767) -> Result<()> {
2768 if let Some(value) = value {
2769 let mut start = BytesStart::new(element_name);
2770 start.push_attribute(("xsi:type", "dcterms:W3CDTF"));
2771 writer.write_event(Event::Start(start))?;
2772 writer.write_event(Event::Text(BytesText::new(value)))?;
2773 writer.write_event(Event::End(BytesEnd::new(element_name)))?;
2774 }
2775 Ok(())
2776}
2777
2778fn to_extended_properties_xml(extended_properties: &ExtendedProperties) -> Result<Vec<u8>> {
2786 let mut writer = Writer::new(Vec::new());
2787
2788 writer.write_event(Event::Decl(BytesDecl::new(
2789 "1.0",
2790 Some("UTF-8"),
2791 Some("yes"),
2792 )))?;
2793
2794 let mut root = BytesStart::new("Properties");
2795 root.push_attribute(("xmlns", EXTENDED_PROPERTIES_NAMESPACE));
2796 writer.write_event(Event::Start(root))?;
2797
2798 writer.write_event(Event::Start(BytesStart::new("Application")))?;
2799 writer.write_event(Event::Text(BytesText::new("office-toolkit")))?;
2800 writer.write_event(Event::End(BytesEnd::new("Application")))?;
2801
2802 write_core_property_text(
2803 &mut writer,
2804 "Manager",
2805 extended_properties.manager.as_deref(),
2806 )?;
2807 write_core_property_text(
2808 &mut writer,
2809 "Company",
2810 extended_properties.company.as_deref(),
2811 )?;
2812
2813 writer.write_event(Event::End(BytesEnd::new("Properties")))?;
2814
2815 Ok(writer.into_inner())
2816}
2817
2818fn to_custom_properties_xml(
2826 custom_properties: &[(String, CustomPropertyValue)],
2827) -> Result<Vec<u8>> {
2828 let mut writer = Writer::new(Vec::new());
2829
2830 writer.write_event(Event::Decl(BytesDecl::new(
2831 "1.0",
2832 Some("UTF-8"),
2833 Some("yes"),
2834 )))?;
2835
2836 let mut root = BytesStart::new("Properties");
2837 root.push_attribute(("xmlns", CUSTOM_PROPERTIES_NAMESPACE));
2838 root.push_attribute(("xmlns:vt", CUSTOM_PROPERTIES_VT_NAMESPACE));
2839 writer.write_event(Event::Start(root))?;
2840
2841 for (index, (name, value)) in custom_properties.iter().enumerate() {
2842 let pid = FIRST_CUSTOM_PROPERTY_PID + index as i32;
2843
2844 let mut property = BytesStart::new("property");
2845 property.push_attribute(("fmtid", CUSTOM_PROPERTY_FMTID));
2846 property.push_attribute(("pid", pid.to_string().as_str()));
2847 property.push_attribute(("name", name.as_str()));
2848 writer.write_event(Event::Start(property))?;
2849
2850 let (variant_element, text) = match value {
2851 CustomPropertyValue::Text(text) => ("vt:lpwstr", text.clone()),
2852 CustomPropertyValue::Bool(value) => (
2853 "vt:bool",
2854 if *value {
2855 "true".to_string()
2856 } else {
2857 "false".to_string()
2858 },
2859 ),
2860 CustomPropertyValue::Int(value) => ("vt:i4", value.to_string()),
2861 CustomPropertyValue::Number(value) => ("vt:r8", value.to_string()),
2862 };
2863 writer.write_event(Event::Start(BytesStart::new(variant_element)))?;
2864 writer.write_event(Event::Text(BytesText::new(&text)))?;
2865 writer.write_event(Event::End(BytesEnd::new(variant_element)))?;
2866
2867 writer.write_event(Event::End(BytesEnd::new("property")))?;
2868 }
2869
2870 writer.write_event(Event::End(BytesEnd::new("Properties")))?;
2871
2872 Ok(writer.into_inner())
2873}
2874
2875#[cfg(test)]
2876mod tests {
2877 use super::*;
2878 use crate::model::Paragraph;
2879
2880 fn document_xml(document: &Document) -> String {
2884 let xml = document
2885 .to_document_xml(
2886 &mut PackageState::new(),
2887 &mut PartRelationships::new(),
2888 SectionReferenceIds::default(),
2889 )
2890 .unwrap();
2891 String::from_utf8(xml).unwrap()
2892 }
2893
2894 #[test]
2895 fn writes_paragraphs_and_runs() {
2896 let document = Document::new()
2897 .with_paragraph(Paragraph::with_text("Hello"))
2898 .with_paragraph(Paragraph::with_text("World"));
2899
2900 let xml = document_xml(&document);
2901
2902 assert!(xml.contains("<w:t>Hello</w:t>"), "{xml}");
2903 assert!(xml.contains("<w:t>World</w:t>"), "{xml}");
2904 assert!(xml.contains("<w:sectPr>"), "{xml}");
2905 }
2906
2907 #[test]
2908 fn marks_runs_with_significant_whitespace_as_preserved() {
2909 let document = Document::new().with_paragraph(Paragraph::with_text(" padded "));
2910
2911 let xml = document_xml(&document);
2912
2913 assert!(
2914 xml.contains(r#"<w:t xml:space="preserve"> padded </w:t>"#),
2915 "{xml}"
2916 );
2917 }
2918
2919 #[test]
2920 fn escapes_xml_special_characters_in_written_text() {
2921 let text = r#"A & B < C > D ' E " F"#;
2926 let document = Document::new().with_paragraph(Paragraph::with_text(text));
2927
2928 let xml = document_xml(&document);
2929
2930 assert!(
2931 xml.contains("<w:t>A & B < C > D ' E " F</w:t>"),
2932 "expected escaped entities in the raw XML, got: {xml}"
2933 );
2934 }
2935
2936 #[test]
2937 fn writes_bold_italic_and_underline_run_properties() {
2938 use crate::model::{Run, UnderlineStyle};
2939
2940 let document = Document::new().with_paragraph(
2941 Paragraph::new().with_run(
2942 Run::new("Styled")
2943 .with_bold(true)
2944 .with_italic(true)
2945 .with_underline(UnderlineStyle::Single),
2946 ),
2947 );
2948
2949 let xml = document_xml(&document);
2950
2951 assert!(
2952 xml.contains(r#"<w:rPr><w:b/><w:i/><w:u w:val="single"/></w:rPr><w:t>Styled</w:t>"#),
2953 "{xml}"
2954 );
2955 }
2956
2957 #[test]
2958 fn writes_color_size_and_font_family_run_properties() {
2959 use crate::model::Run;
2960
2961 let document = Document::new().with_paragraph(
2962 Paragraph::new().with_run(
2963 Run::new("Colorful")
2964 .with_color("FF0000")
2965 .with_font_size(14)
2966 .with_font_family("Georgia"),
2967 ),
2968 );
2969
2970 let xml = document_xml(&document);
2971
2972 assert!(
2973 xml.contains(
2974 r#"<w:rPr><w:rFonts w:ascii="Georgia" w:hAnsi="Georgia"/><w:color w:val="FF0000"/><w:sz w:val="28"/><w:szCs w:val="28"/></w:rPr><w:t>Colorful</w:t>"#
2975 ),
2976 "{xml}"
2977 );
2978 }
2979
2980 #[test]
2981 fn combines_color_size_font_and_bold_italic_underline_in_one_run() {
2982 use crate::model::{Run, UnderlineStyle};
2983
2984 let document = Document::new().with_paragraph(
2985 Paragraph::new().with_run(
2986 Run::new("All of it")
2987 .with_color("0000FF")
2988 .with_font_size(10)
2989 .with_font_family("Verdana")
2990 .with_bold(true)
2991 .with_italic(true)
2992 .with_underline(UnderlineStyle::Single),
2993 ),
2994 );
2995
2996 let xml = document_xml(&document);
2997
2998 assert!(
2999 xml.contains(
3000 r#"<w:rPr><w:rFonts w:ascii="Verdana" w:hAnsi="Verdana"/><w:color w:val="0000FF"/><w:sz w:val="20"/><w:szCs w:val="20"/><w:b/><w:i/><w:u w:val="single"/></w:rPr>"#
3001 ),
3002 "{xml}"
3003 );
3004 }
3005
3006 #[test]
3007 fn writes_a_highlighted_run() {
3008 use crate::model::{Highlight, Run};
3009
3010 let document = Document::new().with_paragraph(
3011 Paragraph::new().with_run(Run::new("Highlighted").with_highlight(Highlight::Yellow)),
3012 );
3013
3014 let xml = document_xml(&document);
3015
3016 assert!(
3017 xml.contains(r#"<w:rPr><w:highlight w:val="yellow"/></w:rPr><w:t>Highlighted</w:t>"#),
3018 "{xml}"
3019 );
3020 }
3021
3022 #[test]
3023 fn combines_highlight_with_color_size_font_and_bold_italic_underline() {
3024 use crate::model::{Highlight, Run, UnderlineStyle};
3025
3026 let document = Document::new().with_paragraph(
3027 Paragraph::new().with_run(
3028 Run::new("All of it")
3029 .with_color("0000FF")
3030 .with_font_size(10)
3031 .with_font_family("Verdana")
3032 .with_highlight(Highlight::Cyan)
3033 .with_bold(true)
3034 .with_italic(true)
3035 .with_underline(UnderlineStyle::Single),
3036 ),
3037 );
3038
3039 let xml = document_xml(&document);
3040
3041 assert!(
3042 xml.contains(
3043 r#"<w:rPr><w:rFonts w:ascii="Verdana" w:hAnsi="Verdana"/><w:color w:val="0000FF"/><w:sz w:val="20"/><w:szCs w:val="20"/><w:highlight w:val="cyan"/><w:b/><w:i/><w:u w:val="single"/></w:rPr>"#
3044 ),
3045 "{xml}"
3046 );
3047 }
3048
3049 #[test]
3050 fn writes_a_struck_through_run() {
3051 use crate::model::Run;
3052
3053 let document = Document::new()
3054 .with_paragraph(Paragraph::new().with_run(Run::new("Struck").with_strike(true)));
3055
3056 let xml = document_xml(&document);
3057
3058 assert!(
3059 xml.contains(r#"<w:rPr><w:strike/></w:rPr><w:t>Struck</w:t>"#),
3060 "{xml}"
3061 );
3062 }
3063
3064 #[test]
3065 fn writes_a_superscript_run() {
3066 use crate::model::{Run, VerticalAlign};
3067
3068 let document = Document::new().with_paragraph(
3069 Paragraph::new()
3070 .with_run(Run::new("Superscript").with_vertical_align(VerticalAlign::Superscript)),
3071 );
3072
3073 let xml = document_xml(&document);
3074
3075 assert!(
3076 xml.contains(
3077 r#"<w:rPr><w:vertAlign w:val="superscript"/></w:rPr><w:t>Superscript</w:t>"#
3078 ),
3079 "{xml}"
3080 );
3081 }
3082
3083 #[test]
3084 fn writes_a_subscript_run() {
3085 use crate::model::{Run, VerticalAlign};
3086
3087 let document = Document::new().with_paragraph(
3088 Paragraph::new()
3089 .with_run(Run::new("Subscript").with_vertical_align(VerticalAlign::Subscript)),
3090 );
3091
3092 let xml = document_xml(&document);
3093
3094 assert!(
3095 xml.contains(r#"<w:rPr><w:vertAlign w:val="subscript"/></w:rPr><w:t>Subscript</w:t>"#),
3096 "{xml}"
3097 );
3098 }
3099
3100 #[test]
3101 fn combines_strike_and_vertical_align_with_highlight_color_size_font_and_bold_italic_underline()
3102 {
3103 use crate::model::{Highlight, Run, UnderlineStyle, VerticalAlign};
3104
3105 let document = Document::new().with_paragraph(
3106 Paragraph::new().with_run(
3107 Run::new("Everything")
3108 .with_color("0000FF")
3109 .with_font_size(10)
3110 .with_font_family("Verdana")
3111 .with_highlight(Highlight::Cyan)
3112 .with_bold(true)
3113 .with_italic(true)
3114 .with_underline(UnderlineStyle::Single)
3115 .with_strike(true)
3116 .with_vertical_align(VerticalAlign::Superscript),
3117 ),
3118 );
3119
3120 let xml = document_xml(&document);
3121
3122 assert!(
3123 xml.contains(
3124 r#"<w:rPr><w:rFonts w:ascii="Verdana" w:hAnsi="Verdana"/><w:color w:val="0000FF"/><w:sz w:val="20"/><w:szCs w:val="20"/><w:highlight w:val="cyan"/><w:b/><w:i/><w:u w:val="single"/><w:strike/><w:vertAlign w:val="superscript"/></w:rPr>"#
3125 ),
3126 "{xml}"
3127 );
3128 }
3129
3130 #[test]
3131 fn writes_a_wave_underline() {
3132 use crate::model::{Run, UnderlineStyle};
3133
3134 let document = Document::new().with_paragraph(
3135 Paragraph::new().with_run(Run::new("Wavy").with_underline(UnderlineStyle::Wave)),
3136 );
3137
3138 let xml = document_xml(&document);
3139
3140 assert!(
3141 xml.contains(r#"<w:rPr><w:u w:val="wave"/></w:rPr><w:t>Wavy</w:t>"#),
3142 "{xml}"
3143 );
3144 }
3145
3146 #[test]
3147 fn writes_an_underline_color() {
3148 use crate::model::{Run, UnderlineStyle};
3149
3150 let document = Document::new().with_paragraph(
3151 Paragraph::new().with_run(
3152 Run::new("Red wave")
3153 .with_underline(UnderlineStyle::Wave)
3154 .with_underline_color("FF0000"),
3155 ),
3156 );
3157
3158 let xml = document_xml(&document);
3159
3160 assert!(
3161 xml.contains(
3162 r#"<w:rPr><w:u w:val="wave" w:color="FF0000"/></w:rPr><w:t>Red wave</w:t>"#
3163 ),
3164 "{xml}"
3165 );
3166 }
3167
3168 #[test]
3169 fn writes_small_caps_and_all_caps() {
3170 use crate::model::Run;
3171
3172 let document = Document::new()
3173 .with_paragraph(Paragraph::new().with_run(Run::new("Small caps").with_small_caps(true)))
3174 .with_paragraph(Paragraph::new().with_run(Run::new("All caps").with_all_caps(true)));
3175
3176 let xml = document_xml(&document);
3177
3178 assert!(
3179 xml.contains(r#"<w:rPr><w:smallCaps/></w:rPr><w:t>Small caps</w:t>"#),
3180 "{xml}"
3181 );
3182 assert!(
3183 xml.contains(r#"<w:rPr><w:caps/></w:rPr><w:t>All caps</w:t>"#),
3184 "{xml}"
3185 );
3186 }
3187
3188 #[test]
3189 fn writes_a_shading_color() {
3190 use crate::model::Run;
3191
3192 let document = Document::new().with_paragraph(
3193 Paragraph::new().with_run(Run::new("Shaded").with_shading_color("FFFF00")),
3194 );
3195
3196 let xml = document_xml(&document);
3197
3198 assert!(
3199 xml.contains(r#"<w:rPr><w:shd w:val="clear" w:color="auto" w:fill="FFFF00"/></w:rPr><w:t>Shaded</w:t>"#),
3200 "{xml}"
3201 );
3202 }
3203
3204 #[test]
3205 fn writes_a_text_border() {
3206 use crate::model::Run;
3207
3208 let document = Document::new()
3209 .with_paragraph(Paragraph::new().with_run(Run::new("Bordered").with_text_border(true)));
3210
3211 let xml = document_xml(&document);
3212
3213 assert!(
3214 xml.contains(
3215 r#"<w:rPr><w:bdr w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:rPr><w:t>Bordered</w:t>"#
3216 ),
3217 "{xml}"
3218 );
3219 }
3220
3221 #[test]
3222 fn writes_character_spacing_and_vertical_position() {
3223 use crate::model::Run;
3224
3225 let document = Document::new()
3226 .with_paragraph(
3227 Paragraph::new().with_run(Run::new("Expanded").with_character_spacing(40)),
3228 )
3229 .with_paragraph(
3230 Paragraph::new().with_run(Run::new("Condensed").with_character_spacing(-20)),
3231 )
3232 .with_paragraph(Paragraph::new().with_run(Run::new("Raised").with_vertical_position(6)))
3233 .with_paragraph(
3234 Paragraph::new().with_run(Run::new("Lowered").with_vertical_position(-6)),
3235 );
3236
3237 let xml = document_xml(&document);
3238
3239 assert!(
3240 xml.contains(r#"<w:rPr><w:spacing w:val="40"/></w:rPr><w:t>Expanded</w:t>"#),
3241 "{xml}"
3242 );
3243 assert!(
3244 xml.contains(r#"<w:rPr><w:spacing w:val="-20"/></w:rPr><w:t>Condensed</w:t>"#),
3245 "{xml}"
3246 );
3247 assert!(
3248 xml.contains(r#"<w:rPr><w:position w:val="6"/></w:rPr><w:t>Raised</w:t>"#),
3249 "{xml}"
3250 );
3251 assert!(
3252 xml.contains(r#"<w:rPr><w:position w:val="-6"/></w:rPr><w:t>Lowered</w:t>"#),
3253 "{xml}"
3254 );
3255 }
3256
3257 #[test]
3258 fn writes_hidden_text() {
3259 use crate::model::Run;
3260
3261 let document = Document::new()
3262 .with_paragraph(Paragraph::new().with_run(Run::new("Hidden").with_hidden(true)));
3263
3264 let xml = document_xml(&document);
3265
3266 assert!(
3267 xml.contains(r#"<w:rPr><w:vanish/></w:rPr><w:t>Hidden</w:t>"#),
3268 "{xml}"
3269 );
3270 }
3271
3272 #[test]
3273 fn plain_runs_have_no_run_properties_element() {
3274 let document = Document::new().with_paragraph(Paragraph::with_text("Plain"));
3275
3276 let xml = document_xml(&document);
3277
3278 assert!(!xml.contains("w:rPr"), "{xml}");
3279 }
3280
3281 #[test]
3282 fn writes_paragraph_alignment_before_its_runs() {
3283 let document = Document::new()
3284 .with_paragraph(Paragraph::with_text("Centered").with_alignment(Alignment::Center));
3285
3286 let xml = document_xml(&document);
3287
3288 assert!(
3289 xml.contains(r#"<w:p><w:pPr><w:jc w:val="center"/></w:pPr><w:r>"#),
3290 "{xml}"
3291 );
3292 }
3293
3294 #[test]
3295 fn writes_paragraph_line_spacing_and_before_after() {
3296 let document = Document::new().with_paragraph(
3297 Paragraph::with_text("Spaced out")
3298 .with_line_spacing(480)
3299 .with_space_before(240)
3300 .with_space_after(120),
3301 );
3302
3303 let xml = document_xml(&document);
3304
3305 assert!(
3306 xml.contains(r#"<w:pPr><w:spacing w:before="240" w:after="120" w:line="480" w:lineRule="auto"/></w:pPr>"#),
3307 "{xml}"
3308 );
3309 }
3310
3311 #[test]
3312 fn writes_a_paragraph_shading_color() {
3313 let document = Document::new()
3314 .with_paragraph(Paragraph::with_text("Shaded paragraph").with_shading_color("D9D9D9"));
3315
3316 let xml = document_xml(&document);
3317
3318 assert!(
3319 xml.contains(r#"<w:pPr><w:shd w:val="clear" w:color="auto" w:fill="D9D9D9"/></w:pPr>"#),
3320 "{xml}"
3321 );
3322 }
3323
3324 #[test]
3325 fn writes_a_paragraph_border_on_all_four_sides() {
3326 let document = Document::new()
3327 .with_paragraph(Paragraph::with_text("Boxed paragraph").with_border(true));
3328
3329 let xml = document_xml(&document);
3330
3331 assert!(
3332 xml.contains(
3333 r#"<w:pPr><w:pBdr><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:pBdr></w:pPr>"#
3334 ),
3335 "{xml}"
3336 );
3337 }
3338
3339 #[test]
3340 fn writes_pbdr_before_shd_before_spacing_before_jc() {
3341 let document = Document::new().with_paragraph(
3342 Paragraph::with_text("All paragraph properties at once")
3343 .with_border(true)
3344 .with_shading_color("FFFF00")
3345 .with_line_spacing(360)
3346 .with_alignment(Alignment::Center),
3347 );
3348
3349 let xml = document_xml(&document);
3350
3351 let pbdr = xml.find("<w:pBdr>").expect("missing w:pBdr");
3352 let shd = xml.find("<w:shd").expect("missing w:shd");
3353 let spacing = xml.find("<w:spacing").expect("missing w:spacing");
3354 let jc = xml.find("<w:jc").expect("missing w:jc");
3355 assert!(pbdr < shd && shd < spacing && spacing < jc, "{xml}");
3356 }
3357
3358 #[test]
3359 fn writes_a_table_with_tblpr_before_tblgrid_before_rows() {
3360 use crate::model::{Table, TableCell, TableRow};
3361
3362 let document = Document::new().with_table(
3363 Table::new().with_row(
3364 TableRow::new()
3365 .with_cell(TableCell::with_text("A1"))
3366 .with_cell(TableCell::with_text("B1")),
3367 ),
3368 );
3369
3370 let xml = document_xml(&document);
3371
3372 let tbl_pr = xml.find("<w:tblPr>").expect("missing w:tblPr");
3374 let tbl_grid = xml.find("<w:tblGrid>").expect("missing w:tblGrid");
3375 let first_row = xml.find("<w:tr>").expect("missing w:tr");
3376 assert!(tbl_pr < tbl_grid && tbl_grid < first_row, "{xml}");
3377
3378 assert!(
3379 xml.contains("<w:gridCol w:w=\"2000\"/><w:gridCol w:w=\"2000\"/>"),
3380 "{xml}"
3381 );
3382 assert!(xml.contains("<w:t>A1</w:t>"), "{xml}");
3383 assert!(xml.contains("<w:t>B1</w:t>"), "{xml}");
3384 }
3385
3386 #[test]
3387 fn writes_explicit_column_widths_and_a_fixed_layout() {
3388 use crate::model::{Table, TableCell, TableRow};
3389
3390 let document = Document::new().with_table(
3391 Table::new().with_column_widths(vec![1000, 3000]).with_row(
3392 TableRow::new()
3393 .with_cell(TableCell::with_text("A1"))
3394 .with_cell(TableCell::with_text("B1")),
3395 ),
3396 );
3397
3398 let xml = document_xml(&document);
3399
3400 assert!(
3401 xml.contains(r#"<w:tblW w:w="4000" w:type="dxa"/>"#),
3402 "{xml}"
3403 );
3404 assert!(xml.contains(r#"<w:tblLayout w:type="fixed"/>"#), "{xml}");
3405 assert!(
3406 xml.contains(r#"<w:gridCol w:w="1000"/><w:gridCol w:w="3000"/>"#),
3407 "{xml}"
3408 );
3409 }
3410
3411 #[test]
3412 fn a_table_with_no_column_widths_keeps_auto_layout() {
3413 use crate::model::{Table, TableCell, TableRow};
3414
3415 let document = Document::new().with_table(
3416 Table::new().with_row(TableRow::new().with_cell(TableCell::with_text("A1"))),
3417 );
3418
3419 let xml = document_xml(&document);
3420
3421 assert!(xml.contains(r#"<w:tblW w:w="0" w:type="auto"/>"#), "{xml}");
3422 assert!(!xml.contains("w:tblLayout"), "{xml}");
3423 }
3424
3425 #[test]
3426 fn column_widths_shorter_than_the_real_column_count_fall_back_to_the_default_width() {
3427 use crate::model::{Table, TableCell, TableRow};
3428
3429 let document = Document::new().with_table(
3430 Table::new().with_column_widths(vec![1000]).with_row(
3431 TableRow::new()
3432 .with_cell(TableCell::with_text("A1"))
3433 .with_cell(TableCell::with_text("B1")),
3434 ),
3435 );
3436
3437 let xml = document_xml(&document);
3438
3439 assert!(
3440 xml.contains(r#"<w:gridCol w:w="1000"/><w:gridCol w:w="2000"/>"#),
3441 "{xml}"
3442 );
3443 }
3444
3445 #[test]
3446 fn writes_an_empty_paragraph_for_an_empty_table_cell() {
3447 use crate::model::{Table, TableCell, TableRow};
3448
3449 let document = Document::new()
3450 .with_table(Table::new().with_row(TableRow::new().with_cell(TableCell::new())));
3451
3452 let xml = document_xml(&document);
3453
3454 assert!(xml.contains("<w:tc><w:p/></w:tc>"), "{xml}");
3455 }
3456
3457 #[test]
3458 fn writes_a_nested_table_inside_a_cell_followed_by_a_trailing_paragraph() {
3459 use crate::model::{Table, TableCell, TableRow};
3460
3461 let inner_table =
3462 Table::new().with_row(TableRow::new().with_cell(TableCell::with_text("Inner")));
3463 let document = Document::new().with_table(
3464 Table::new()
3465 .with_row(TableRow::new().with_cell(TableCell::new().with_table(inner_table))),
3466 );
3467
3468 let xml = document_xml(&document);
3469
3470 assert!(xml.contains("<w:tc><w:tbl>"), "{xml}");
3474 assert!(xml.contains("</w:tbl><w:p/></w:tc>"), "{xml}");
3475 assert!(xml.contains(">Inner<"), "{xml}");
3476 }
3477
3478 #[test]
3479 fn a_cell_ending_in_a_paragraph_after_a_nested_table_gets_no_extra_trailing_paragraph() {
3480 use crate::model::{Table, TableCell, TableRow};
3481
3482 let inner_table =
3483 Table::new().with_row(TableRow::new().with_cell(TableCell::with_text("Inner")));
3484 let document = Document::new().with_table(
3485 Table::new().with_row(
3486 TableRow::new().with_cell(
3487 TableCell::new()
3488 .with_table(inner_table)
3489 .with_paragraph(Paragraph::with_text("Caption")),
3490 ),
3491 ),
3492 );
3493
3494 let xml = document_xml(&document);
3495
3496 assert!(xml.contains("</w:tbl><w:p>"), "{xml}");
3497 assert!(xml.contains(">Caption<"), "{xml}");
3498 assert!(!xml.contains("</w:tbl><w:p/><w:p>"), "{xml}");
3501 }
3502
3503 #[test]
3504 fn writes_a_horizontal_cell_merge() {
3505 use crate::model::{Table, TableCell, TableRow};
3506
3507 let document = Document::new().with_table(
3508 Table::new().with_row(
3509 TableRow::new()
3510 .with_cell(TableCell::with_text("Spans two columns").with_horizontal_span(2)),
3511 ),
3512 );
3513
3514 let xml = document_xml(&document);
3515
3516 assert!(
3517 xml.contains(r#"<w:tc><w:tcPr><w:gridSpan w:val="2"/></w:tcPr><w:p>"#),
3518 "{xml}"
3519 );
3520 }
3521
3522 #[test]
3523 fn writes_a_vertical_cell_merge() {
3524 use crate::model::{Table, TableCell, TableRow, VerticalMerge};
3525
3526 let document = Document::new().with_table(
3527 Table::new()
3528 .with_row(TableRow::new().with_cell(
3529 TableCell::with_text("Merged").with_vertical_merge(VerticalMerge::Restart),
3530 ))
3531 .with_row(
3532 TableRow::new()
3533 .with_cell(TableCell::new().with_vertical_merge(VerticalMerge::Continue)),
3534 ),
3535 );
3536
3537 let xml = document_xml(&document);
3538
3539 assert!(
3540 xml.contains(r#"<w:tc><w:tcPr><w:vMerge w:val="restart"/></w:tcPr><w:p>"#),
3541 "{xml}"
3542 );
3543 assert!(
3544 xml.contains(r#"<w:tc><w:tcPr><w:vMerge w:val="continue"/></w:tcPr><w:p/></w:tc>"#),
3545 "{xml}"
3546 );
3547 }
3548
3549 #[test]
3550 fn writes_gridspan_before_vmerge_in_tcpr() {
3551 use crate::model::{Table, TableCell, TableRow, VerticalMerge};
3552
3553 let document = Document::new().with_table(
3554 Table::new().with_row(
3555 TableRow::new().with_cell(
3556 TableCell::with_text("Both at once")
3557 .with_horizontal_span(2)
3558 .with_vertical_merge(VerticalMerge::Restart),
3559 ),
3560 ),
3561 );
3562
3563 let xml = document_xml(&document);
3564
3565 assert!(
3566 xml.contains(r#"<w:tcPr><w:gridSpan w:val="2"/><w:vMerge w:val="restart"/></w:tcPr>"#),
3567 "{xml}"
3568 );
3569 }
3570
3571 #[test]
3572 fn tblgrid_column_count_accounts_for_horizontal_span() {
3573 use crate::model::{Table, TableCell, TableRow};
3574
3575 let document = Document::new().with_table(
3581 Table::new()
3582 .with_row(
3583 TableRow::new()
3584 .with_cell(TableCell::with_text("A1").with_horizontal_span(2))
3585 .with_cell(TableCell::with_text("B1").with_horizontal_span(2)),
3586 )
3587 .with_row(
3588 TableRow::new()
3589 .with_cell(TableCell::with_text("A2").with_horizontal_span(2))
3590 .with_cell(TableCell::with_text("B2").with_horizontal_span(2)),
3591 ),
3592 );
3593
3594 let xml = document_xml(&document);
3595
3596 let grid_col_count = xml.matches("<w:gridCol ").count();
3597 assert_eq!(grid_col_count, 4, "{xml}");
3598 }
3599
3600 #[test]
3601 fn writes_a_custom_table_border_color() {
3602 use crate::model::{Table, TableCell, TableRow};
3603
3604 let document = Document::new().with_table(
3605 Table::new()
3606 .with_border_color("FF0000")
3607 .with_row(TableRow::new().with_cell(TableCell::with_text("A1"))),
3608 );
3609
3610 let xml = document_xml(&document);
3611
3612 assert!(
3613 xml.contains(r#"<w:top w:val="single" w:sz="4" w:space="0" w:color="FF0000"/>"#),
3614 "{xml}"
3615 );
3616 assert!(!xml.contains(r#"w:color="auto""#), "{xml}");
3617 }
3618
3619 #[test]
3620 fn a_table_with_no_border_color_keeps_the_fixed_auto_color() {
3621 use crate::model::{Table, TableCell, TableRow};
3622
3623 let document = Document::new().with_table(
3624 Table::new().with_row(TableRow::new().with_cell(TableCell::with_text("A1"))),
3625 );
3626
3627 let xml = document_xml(&document);
3628
3629 assert!(
3630 xml.contains(r#"<w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/>"#),
3631 "{xml}"
3632 );
3633 }
3634
3635 #[test]
3636 fn writes_tblstyle_before_tblw_and_jc_and_tblind_after_it() {
3637 use crate::model::{Alignment, Table, TableCell, TableRow};
3638
3639 let document = Document::new().with_table(
3640 Table::new()
3641 .with_style_id("TableGrid")
3642 .with_alignment(Alignment::Center)
3643 .with_indent_twips(200)
3644 .with_row(TableRow::new().with_cell(TableCell::with_text("A1"))),
3645 );
3646
3647 let xml = document_xml(&document);
3648
3649 let tbl_style = xml
3650 .find(r#"<w:tblStyle w:val="TableGrid"/>"#)
3651 .expect("missing w:tblStyle");
3652 let tbl_w = xml.find("<w:tblW ").expect("missing w:tblW");
3653 let jc = xml.find(r#"<w:jc w:val="center"/>"#).expect("missing w:jc");
3654 let tbl_ind = xml
3655 .find(r#"<w:tblInd w:w="200" w:type="dxa"/>"#)
3656 .expect("missing w:tblInd");
3657 let tbl_borders = xml.find("<w:tblBorders>").expect("missing w:tblBorders");
3658 assert!(
3659 tbl_style < tbl_w && tbl_w < jc && jc < tbl_ind && tbl_ind < tbl_borders,
3660 "{xml}"
3661 );
3662 }
3663
3664 #[test]
3665 fn a_table_with_no_alignment_indent_or_style_omits_them() {
3666 use crate::model::{Table, TableCell, TableRow};
3667
3668 let document = Document::new().with_table(
3669 Table::new().with_row(TableRow::new().with_cell(TableCell::with_text("A1"))),
3670 );
3671
3672 let xml = document_xml(&document);
3673
3674 assert!(!xml.contains("w:tblStyle"), "{xml}");
3675 assert!(!xml.contains("w:tblInd"), "{xml}");
3676 assert!(!xml.contains("<w:jc "), "{xml}");
3680 }
3681
3682 #[test]
3683 fn writes_a_cell_border() {
3684 use crate::model::{Table, TableCell, TableRow};
3685
3686 let document =
3687 Document::new().with_table(Table::new().with_row(
3688 TableRow::new().with_cell(TableCell::with_text("Boxed").with_border(true)),
3689 ));
3690
3691 let xml = document_xml(&document);
3692
3693 assert!(
3694 xml.contains(
3695 r#"<w:tcPr><w:tcBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:tcBorders></w:tcPr>"#
3696 ),
3697 "{xml}"
3698 );
3699 }
3700
3701 #[test]
3702 fn writes_a_cell_shading_color() {
3703 use crate::model::{Table, TableCell, TableRow};
3704
3705 let document = Document::new().with_table(Table::new().with_row(
3706 TableRow::new().with_cell(TableCell::with_text("Shaded").with_shading_color("FFFF00")),
3707 ));
3708
3709 let xml = document_xml(&document);
3710
3711 assert!(
3712 xml.contains(
3713 r#"<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="FFFF00"/></w:tcPr>"#
3714 ),
3715 "{xml}"
3716 );
3717 }
3718
3719 #[test]
3720 fn writes_a_cell_vertical_alignment() {
3721 use crate::model::{CellVerticalAlign, Table, TableCell, TableRow};
3722
3723 let document =
3724 Document::new().with_table(Table::new().with_row(TableRow::new().with_cell(
3725 TableCell::with_text("Centered").with_vertical_alignment(CellVerticalAlign::Center),
3726 )));
3727
3728 let xml = document_xml(&document);
3729
3730 assert!(
3731 xml.contains(r#"<w:tcPr><w:vAlign w:val="center"/></w:tcPr>"#),
3732 "{xml}"
3733 );
3734 }
3735
3736 #[test]
3737 fn writes_cell_margins_in_tcmar_order() {
3738 use crate::model::{Table, TableCell, TableRow};
3739
3740 let document =
3741 Document::new().with_table(Table::new().with_row(
3742 TableRow::new().with_cell(
3743 TableCell::with_text("Padded").with_margins_twips(100, 200, 300, 400),
3744 ),
3745 ));
3746
3747 let xml = document_xml(&document);
3748
3749 assert!(
3750 xml.contains(
3751 r#"<w:tcMar><w:top w:w="100" w:type="dxa"/><w:left w:w="200" w:type="dxa"/><w:bottom w:w="300" w:type="dxa"/><w:right w:w="400" w:type="dxa"/></w:tcMar>"#
3752 ),
3753 "{xml}"
3754 );
3755 }
3756
3757 #[test]
3758 fn writes_a_cell_text_direction() {
3759 use crate::model::{Table, TableCell, TableRow, TextDirection};
3760
3761 let document =
3762 Document::new().with_table(Table::new().with_row(TableRow::new().with_cell(
3763 TableCell::with_text("Rotated").with_text_direction(TextDirection::TopToBottom),
3764 )));
3765
3766 let xml = document_xml(&document);
3767
3768 assert!(
3769 xml.contains(r#"<w:tcPr><w:textDirection w:val="tbRl"/></w:tcPr>"#),
3770 "{xml}"
3771 );
3772 }
3773
3774 #[test]
3775 fn writes_tcpr_children_in_ct_tcpr_sequence_order() {
3776 use crate::model::{
3777 CellVerticalAlign, Table, TableCell, TableRow, TextDirection, VerticalMerge,
3778 };
3779
3780 let document = Document::new().with_table(
3784 Table::new().with_row(
3785 TableRow::new().with_cell(
3786 TableCell::with_text("Everything")
3787 .with_horizontal_span(2)
3788 .with_vertical_merge(VerticalMerge::Restart)
3789 .with_border(true)
3790 .with_shading_color("00FF00")
3791 .with_margins_twips(10, 20, 30, 40)
3792 .with_text_direction(TextDirection::BottomToTop)
3793 .with_vertical_alignment(CellVerticalAlign::Bottom),
3794 ),
3795 ),
3796 );
3797
3798 let xml = document_xml(&document);
3799
3800 let grid_span = xml.find("<w:gridSpan").expect("missing w:gridSpan");
3801 let v_merge = xml.find("<w:vMerge").expect("missing w:vMerge");
3802 let tc_borders = xml.find("<w:tcBorders>").expect("missing w:tcBorders");
3803 let shd = xml.find("<w:shd").expect("missing w:shd");
3804 let tc_mar = xml.find("<w:tcMar>").expect("missing w:tcMar");
3805 let text_direction = xml
3806 .find("<w:textDirection")
3807 .expect("missing w:textDirection");
3808 let v_align = xml.find("<w:vAlign").expect("missing w:vAlign");
3809 assert!(
3810 grid_span < v_merge
3811 && v_merge < tc_borders
3812 && tc_borders < shd
3813 && shd < tc_mar
3814 && tc_mar < text_direction
3815 && text_direction < v_align,
3816 "{xml}"
3817 );
3818 }
3819
3820 #[test]
3821 fn writes_a_repeating_header_row() {
3822 use crate::model::{Table, TableCell, TableRow};
3823
3824 let document = Document::new().with_table(
3825 Table::new().with_row(
3826 TableRow::new()
3827 .with_cell(TableCell::with_text("Header"))
3828 .with_repeat_as_header_row(true),
3829 ),
3830 );
3831
3832 let xml = document_xml(&document);
3833
3834 assert!(
3835 xml.contains("<w:tr><w:trPr><w:tblHeader/></w:trPr>"),
3836 "{xml}"
3837 );
3838 }
3839
3840 #[test]
3841 fn writes_a_row_height_with_at_least_rule() {
3842 use crate::model::{Table, TableCell, TableRow};
3843
3844 let document = Document::new().with_table(
3845 Table::new().with_row(
3846 TableRow::new()
3847 .with_cell(TableCell::with_text("Tall"))
3848 .with_height_twips(800),
3849 ),
3850 );
3851
3852 let xml = document_xml(&document);
3853
3854 assert!(
3855 xml.contains(r#"<w:trHeight w:val="800" w:hRule="atLeast"/>"#),
3856 "{xml}"
3857 );
3858 }
3859
3860 #[test]
3861 fn writes_cant_split_before_tr_height_before_tbl_header() {
3862 use crate::model::{Table, TableCell, TableRow};
3863
3864 let document = Document::new().with_table(
3865 Table::new().with_row(
3866 TableRow::new()
3867 .with_cell(TableCell::with_text("Row"))
3868 .with_cant_split(true)
3869 .with_height_twips(500)
3870 .with_repeat_as_header_row(true),
3871 ),
3872 );
3873
3874 let xml = document_xml(&document);
3875
3876 assert!(
3877 xml.contains(r#"<w:trPr><w:cantSplit/><w:trHeight w:val="500" w:hRule="atLeast"/><w:tblHeader/></w:trPr>"#),
3878 "{xml}"
3879 );
3880 }
3881
3882 #[test]
3883 fn a_row_with_no_special_properties_omits_trpr() {
3884 use crate::model::{Table, TableCell, TableRow};
3885
3886 let document = Document::new().with_table(
3887 Table::new().with_row(TableRow::new().with_cell(TableCell::with_text("Row"))),
3888 );
3889
3890 let xml = document_xml(&document);
3891
3892 assert!(!xml.contains("w:trPr"), "{xml}");
3893 }
3894
3895 #[test]
3896 fn writes_an_inline_image_and_registers_it_starting_after_settings() {
3897 use crate::model::{Image, ImageFormat, Run};
3898
3899 let document = Document::new().with_paragraph(
3900 Paragraph::new().with_run(Run::with_image(
3901 Image::new(vec![0u8, 1, 2, 3], ImageFormat::Png, 914_400, 457_200)
3902 .with_description("a test image"),
3903 )),
3904 );
3905
3906 let mut state = PackageState::new();
3907 let mut relationships = PartRelationships::new();
3908 relationships.add(SETTINGS_RELATIONSHIP_TYPE, SETTINGS_ENTRY_NAME);
3911
3912 let xml = document
3913 .to_document_xml(
3914 &mut state,
3915 &mut relationships,
3916 SectionReferenceIds::default(),
3917 )
3918 .unwrap();
3919 let xml = String::from_utf8(xml).unwrap();
3920
3921 assert_eq!(state.images.media_parts.len(), 1);
3922 assert_eq!(state.images.media_parts[0].entry_name, "media/image1.png");
3923 assert_eq!(state.images.media_parts[0].content_type, "image/png");
3924 assert_eq!(state.images.media_parts[0].data, vec![0u8, 1, 2, 3]);
3925
3926 let relationship = relationships
3927 .relationships
3928 .by_id("rId2")
3929 .expect("missing image relationship");
3930 assert_eq!(relationship.target, "media/image1.png");
3931
3932 assert!(
3933 xml.contains(r#"<wp:extent cx="914400" cy="457200"/>"#),
3934 "{xml}"
3935 );
3936 assert!(xml.contains(r#"descr="a test image""#), "{xml}");
3937 assert!(xml.contains(r#"<a:blip r:embed="rId2"/>"#), "{xml}");
3938 assert!(!xml.contains("<w:t>"), "{xml}");
3940 }
3941
3942 #[test]
3943 fn writes_header_and_footer_references_before_pgsz() {
3944 let document = Document::new()
3945 .with_paragraph(Paragraph::with_text("Body"))
3946 .with_header(HeaderFooter::new().with_paragraph(Paragraph::with_text("Header text")))
3947 .with_footer(HeaderFooter::new().with_paragraph(Paragraph::with_text("Footer text")));
3948
3949 let mut state = PackageState::new();
3950 let mut relationships = PartRelationships::new();
3951 relationships.add(SETTINGS_RELATIONSHIP_TYPE, SETTINGS_ENTRY_NAME); let header_id = relationships.add(HEADER_RELATIONSHIP_TYPE, HEADER_ENTRY_NAME); let footer_id = relationships.add(FOOTER_RELATIONSHIP_TYPE, FOOTER_ENTRY_NAME); let xml = document
3956 .to_document_xml(
3957 &mut state,
3958 &mut relationships,
3959 SectionReferenceIds {
3960 header: Some(&header_id),
3961 footer: Some(&footer_id),
3962 ..SectionReferenceIds::default()
3963 },
3964 )
3965 .unwrap();
3966 let xml = String::from_utf8(xml).unwrap();
3967
3968 let header_reference = xml.find(r#"<w:headerReference w:type="default" r:id="rId2"/>"#);
3969 let footer_reference = xml.find(r#"<w:footerReference w:type="default" r:id="rId3"/>"#);
3970 let sect_pr = xml.find("<w:sectPr>").expect("missing w:sectPr");
3971 let pg_sz = xml.find("<w:pgSz").expect("missing w:pgSz");
3972
3973 assert!(header_reference.is_some(), "{xml}");
3974 assert!(footer_reference.is_some(), "{xml}");
3975 assert!(
3978 sect_pr < header_reference.unwrap() && header_reference.unwrap() < pg_sz,
3979 "{xml}"
3980 );
3981 assert!(footer_reference.unwrap() < pg_sz, "{xml}");
3982 }
3983
3984 #[test]
3985 fn writes_header_and_footer_content_as_hdr_and_ftr_parts() {
3986 let header = HeaderFooter::new().with_paragraph(Paragraph::with_text("Header text"));
3987 let footer = HeaderFooter::new().with_paragraph(Paragraph::with_text("Footer text"));
3988
3989 let mut state = PackageState::new();
3990 let mut relationships = PartRelationships::new();
3991
3992 let header_xml =
3993 to_header_footer_xml("w:hdr", &header.blocks, &mut state, &mut relationships).unwrap();
3994 let header_xml = String::from_utf8(header_xml).unwrap();
3995 assert!(header_xml.starts_with("<?xml"), "{header_xml}");
3996 assert!(header_xml.contains("<w:hdr "), "{header_xml}");
3997 assert!(
3998 header_xml.contains("<w:t>Header text</w:t>"),
3999 "{header_xml}"
4000 );
4001 assert!(!header_xml.contains("w:body"), "{header_xml}");
4002 assert!(!header_xml.contains("w:sectPr"), "{header_xml}");
4003
4004 let footer_xml =
4005 to_header_footer_xml("w:ftr", &footer.blocks, &mut state, &mut relationships).unwrap();
4006 let footer_xml = String::from_utf8(footer_xml).unwrap();
4007 assert!(footer_xml.contains("<w:ftr "), "{footer_xml}");
4008 assert!(
4009 footer_xml.contains("<w:t>Footer text</w:t>"),
4010 "{footer_xml}"
4011 );
4012 }
4013
4014 #[test]
4015 fn write_to_produces_header1_and_footer1_parts_with_relationships() {
4016 use std::io::Cursor;
4017
4018 let document = Document::new()
4019 .with_paragraph(Paragraph::with_text("Body"))
4020 .with_header(HeaderFooter::new().with_paragraph(Paragraph::with_text("Header text")))
4021 .with_footer(HeaderFooter::new().with_paragraph(Paragraph::with_text("Footer text")));
4022
4023 let buffer = document.write_to(Cursor::new(Vec::new())).unwrap();
4024 let package = Package::read_from(buffer).unwrap();
4025
4026 let document_part = package
4027 .part(MAIN_DOCUMENT_PART_NAME)
4028 .expect("missing document.xml");
4029 let header_relationship = document_part
4030 .relationships
4031 .iter()
4032 .find(|relationship| relationship.rel_type == HEADER_RELATIONSHIP_TYPE)
4033 .expect("document.xml has no header relationship");
4034 let footer_relationship = document_part
4035 .relationships
4036 .iter()
4037 .find(|relationship| relationship.rel_type == FOOTER_RELATIONSHIP_TYPE)
4038 .expect("document.xml has no footer relationship");
4039 assert_eq!(header_relationship.target, HEADER_ENTRY_NAME);
4040 assert_eq!(footer_relationship.target, FOOTER_ENTRY_NAME);
4041
4042 let header_part = package.part(HEADER_PART_NAME).expect("missing header1.xml");
4043 let header_xml = std::str::from_utf8(&header_part.data).unwrap();
4044 assert!(
4045 header_xml.contains("<w:t>Header text</w:t>"),
4046 "{header_xml}"
4047 );
4048
4049 let footer_part = package.part(FOOTER_PART_NAME).expect("missing footer1.xml");
4050 let footer_xml = std::str::from_utf8(&footer_part.data).unwrap();
4051 assert!(
4052 footer_xml.contains("<w:t>Footer text</w:t>"),
4053 "{footer_xml}"
4054 );
4055
4056 let document_xml = std::str::from_utf8(&document_part.data).unwrap();
4057 assert!(
4058 document_xml.contains(&format!(
4059 r#"w:type="default" r:id="{}""#,
4060 header_relationship.id
4061 )),
4062 "{document_xml}"
4063 );
4064 }
4065
4066 #[test]
4067 fn writes_a_page_number_field_as_fldsimple_not_a_run() {
4068 use crate::model::Run;
4069
4070 let document = Document::new().with_paragraph(
4071 Paragraph::new()
4072 .with_run(Run::new("Page "))
4073 .with_run(Run::with_field(Field::PageNumber).with_bold(true)),
4074 );
4075
4076 let xml = document_xml(&document);
4077
4078 assert!(xml.contains(r#"<w:fldSimple w:instr="PAGE">"#), "{xml}");
4079 assert!(xml.contains(r#"<w:fldSimple w:instr="PAGE"><w:r><w:rPr><w:b/></w:rPr><w:t>1</w:t></w:r></w:fldSimple>"#), "{xml}");
4082 assert!(!xml.contains("<w:r><w:fldSimple"), "{xml}");
4084 }
4085
4086 #[test]
4087 fn settings_xml_never_sets_update_fields_even_with_a_field_present() {
4088 use crate::model::Run;
4095
4096 let document = Document::new()
4097 .with_paragraph(Paragraph::new().with_run(Run::with_field(Field::TotalPages)));
4098 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4099 let package = Package::read_from(buffer).unwrap();
4100
4101 let settings_part = package
4102 .part(SETTINGS_PART_NAME)
4103 .expect("missing word/settings.xml");
4104 let settings_xml = std::str::from_utf8(&settings_part.data).unwrap();
4105 assert!(!settings_xml.contains("updateFields"), "{settings_xml}");
4106 }
4107
4108 #[test]
4109 fn writes_styles_xml_with_name_based_on_ppr_then_rpr_in_order() {
4110 let heading = Style::new("Heading1", "Heading 1", StyleKind::Paragraph)
4111 .with_bold(true)
4112 .with_alignment(Alignment::Center);
4113 let emphasis = Style::new("Strong", "Strong", StyleKind::Character)
4118 .with_based_on("DefaultParagraphFont")
4119 .with_bold(true)
4120 .with_alignment(Alignment::Center);
4121
4122 let xml = String::from_utf8(to_styles_xml(&[heading, emphasis]).unwrap()).unwrap();
4123
4124 assert!(xml.starts_with("<?xml"), "{xml}");
4125 assert!(
4126 xml.contains(r#"<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="Heading 1"/><w:pPr><w:jc w:val="center"/></w:pPr><w:rPr><w:b/></w:rPr></w:style>"#),
4127 "{xml}"
4128 );
4129 assert!(
4130 xml.contains(
4131 r#"<w:style w:type="character" w:styleId="Strong"><w:name w:val="Strong"/><w:basedOn w:val="DefaultParagraphFont"/><w:rPr><w:b/></w:rPr></w:style>"#
4132 ),
4133 "{xml}"
4134 );
4135 }
4136
4137 #[test]
4138 fn writes_a_paragraph_style_with_spacing_shading_and_border() {
4139 let heading = Style::new("Heading1", "Heading 1", StyleKind::Paragraph)
4140 .with_line_spacing(360)
4141 .with_space_before(240)
4142 .with_space_after(120)
4143 .with_paragraph_shading_color("D9D9D9")
4144 .with_paragraph_border(true);
4145
4146 let xml = String::from_utf8(to_styles_xml(&[heading]).unwrap()).unwrap();
4147
4148 assert!(
4149 xml.contains(
4150 r#"<w:pPr><w:pBdr><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:pBdr><w:shd w:val="clear" w:color="auto" w:fill="D9D9D9"/><w:spacing w:before="240" w:after="120" w:line="360" w:lineRule="auto"/></w:pPr>"#
4151 ),
4152 "{xml}"
4153 );
4154 }
4155
4156 #[test]
4157 fn writes_a_paragraph_style_with_keep_settings_tabs_and_contextual_spacing() {
4158 use crate::model::TabStopAlignment;
4159
4160 let list_paragraph = Style::new("ListParagraph", "List Paragraph", StyleKind::Paragraph)
4161 .with_keep_with_next(true)
4162 .with_keep_lines_together(true)
4163 .with_page_break_before(true)
4164 .with_tab(TabStop::new(720).with_alignment(TabStopAlignment::Left))
4165 .with_contextual_spacing(true);
4166
4167 let xml = String::from_utf8(to_styles_xml(&[list_paragraph]).unwrap()).unwrap();
4168
4169 assert!(
4173 xml.contains(
4174 r#"<w:pPr><w:keepNext/><w:keepLines/><w:pageBreakBefore/><w:tabs><w:tab w:val="left" w:pos="720"/></w:tabs><w:contextualSpacing/></w:pPr>"#
4175 ),
4176 "{xml}"
4177 );
4178 }
4179
4180 #[test]
4181 fn a_character_style_ignores_keep_settings_tabs_and_contextual_spacing() {
4182 let emphasis = Style::new("Strong", "Strong", StyleKind::Character)
4183 .with_bold(true)
4184 .with_keep_with_next(true)
4185 .with_tab(TabStop::new(720))
4186 .with_contextual_spacing(true);
4187
4188 let xml = String::from_utf8(to_styles_xml(&[emphasis]).unwrap()).unwrap();
4189
4190 assert!(
4191 xml.contains(r#"<w:style w:type="character" w:styleId="Strong"><w:name w:val="Strong"/><w:rPr><w:b/></w:rPr></w:style>"#),
4192 "{xml}"
4193 );
4194 }
4195
4196 #[test]
4197 fn a_character_style_ignores_paragraph_level_spacing_shading_and_border() {
4198 let emphasis = Style::new("Strong", "Strong", StyleKind::Character)
4199 .with_bold(true)
4200 .with_line_spacing(360)
4201 .with_paragraph_shading_color("D9D9D9")
4202 .with_paragraph_border(true);
4203
4204 let xml = String::from_utf8(to_styles_xml(&[emphasis]).unwrap()).unwrap();
4205
4206 assert!(
4207 xml.contains(r#"<w:style w:type="character" w:styleId="Strong"><w:name w:val="Strong"/><w:rPr><w:b/></w:rPr></w:style>"#),
4208 "{xml}"
4209 );
4210 }
4211
4212 #[test]
4213 fn writes_a_style_with_color_size_and_font_family() {
4214 let heading = Style::new("Heading1", "Heading 1", StyleKind::Paragraph)
4215 .with_color("2E74B5")
4216 .with_font_size(16)
4217 .with_font_family("Cambria");
4218
4219 let xml = String::from_utf8(to_styles_xml(&[heading]).unwrap()).unwrap();
4220
4221 assert!(
4222 xml.contains(
4223 r#"<w:rPr><w:rFonts w:ascii="Cambria" w:hAnsi="Cambria"/><w:color w:val="2E74B5"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr>"#
4224 ),
4225 "{xml}"
4226 );
4227 }
4228
4229 #[test]
4230 fn writes_a_style_with_a_highlight() {
4231 use crate::model::Highlight;
4232
4233 let heading = Style::new("Heading1", "Heading 1", StyleKind::Paragraph)
4234 .with_highlight(Highlight::LightGray);
4235
4236 let xml = String::from_utf8(to_styles_xml(&[heading]).unwrap()).unwrap();
4237
4238 assert!(
4239 xml.contains(r#"<w:rPr><w:highlight w:val="lightGray"/></w:rPr>"#),
4240 "{xml}"
4241 );
4242 }
4243
4244 #[test]
4245 fn writes_a_style_with_strike_and_subscript() {
4246 use crate::model::VerticalAlign;
4247
4248 let footnote_ref = Style::new(
4249 "FootnoteReference",
4250 "footnote reference",
4251 StyleKind::Character,
4252 )
4253 .with_strike(true)
4254 .with_vertical_align(VerticalAlign::Subscript);
4255
4256 let xml = String::from_utf8(to_styles_xml(&[footnote_ref]).unwrap()).unwrap();
4257
4258 assert!(
4259 xml.contains(r#"<w:rPr><w:strike/><w:vertAlign w:val="subscript"/></w:rPr>"#),
4260 "{xml}"
4261 );
4262 }
4263
4264 #[test]
4265 fn writes_a_style_with_a_double_underline_and_color() {
4266 use crate::model::UnderlineStyle;
4267
4268 let heading = Style::new("Heading1", "Heading 1", StyleKind::Paragraph)
4269 .with_underline(UnderlineStyle::Double)
4270 .with_underline_color("2E74B5");
4271
4272 let xml = String::from_utf8(to_styles_xml(&[heading]).unwrap()).unwrap();
4273
4274 assert!(
4275 xml.contains(r#"<w:rPr><w:u w:val="double" w:color="2E74B5"/></w:rPr>"#),
4276 "{xml}"
4277 );
4278 }
4279
4280 #[test]
4281 fn writes_a_style_with_small_caps_shading_border_spacing_and_hidden() {
4282 let heading = Style::new("Redacted", "Redacted", StyleKind::Character)
4283 .with_small_caps(true)
4284 .with_shading_color("000000")
4285 .with_text_border(true)
4286 .with_character_spacing(10)
4287 .with_vertical_position(-2)
4288 .with_hidden(true);
4289
4290 let xml = String::from_utf8(to_styles_xml(&[heading]).unwrap()).unwrap();
4291
4292 assert!(
4293 xml.contains(
4294 r#"<w:rPr><w:smallCaps/><w:shd w:val="clear" w:color="auto" w:fill="000000"/><w:bdr w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:spacing w:val="10"/><w:position w:val="-2"/><w:vanish/></w:rPr>"#
4295 ),
4296 "{xml}"
4297 );
4298 }
4299
4300 #[test]
4301 fn a_document_with_no_styles_does_not_write_a_styles_part() {
4302 let document = Document::new().with_paragraph(Paragraph::with_text("No styles here"));
4303
4304 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4305 let package = Package::read_from(buffer).unwrap();
4306 assert!(
4307 package.part(STYLES_PART_NAME).is_none(),
4308 "word/styles.xml should not be written"
4309 );
4310 }
4311
4312 #[test]
4313 fn writes_pstyle_before_jc_and_rstyle_in_rpr() {
4314 let document = Document::new().with_paragraph(
4315 Paragraph::new()
4316 .with_style_id("Heading1")
4317 .with_alignment(Alignment::Center)
4318 .with_run(Run::new("Title").with_style_id("Strong")),
4319 );
4320
4321 let xml = document_xml(&document);
4322
4323 assert!(
4324 xml.contains(r#"<w:pPr><w:pStyle w:val="Heading1"/><w:jc w:val="center"/></w:pPr>"#),
4325 "{xml}"
4326 );
4327 assert!(
4328 xml.contains(r#"<w:rPr><w:rStyle w:val="Strong"/></w:rPr><w:t>Title</w:t>"#),
4329 "{xml}"
4330 );
4331 }
4332
4333 #[test]
4334 fn write_to_declares_a_styles_relationship_when_styles_are_present() {
4335 use std::io::Cursor;
4336
4337 let document = Document::new()
4338 .with_paragraph(Paragraph::with_text("Body"))
4339 .with_style(Style::new("Heading1", "Heading 1", StyleKind::Paragraph));
4340
4341 let buffer = document.write_to(Cursor::new(Vec::new())).unwrap();
4342 let package = Package::read_from(buffer).unwrap();
4343
4344 let document_part = package
4345 .part(MAIN_DOCUMENT_PART_NAME)
4346 .expect("missing document.xml");
4347 let styles_relationship = document_part
4348 .relationships
4349 .iter()
4350 .find(|relationship| relationship.rel_type == STYLES_RELATIONSHIP_TYPE)
4351 .expect("document.xml has no styles relationship");
4352 assert_eq!(styles_relationship.target, STYLES_ENTRY_NAME);
4353
4354 let styles_part = package
4355 .part(STYLES_PART_NAME)
4356 .expect("missing word/styles.xml");
4357 let styles_xml = std::str::from_utf8(&styles_part.data).unwrap();
4358 assert!(
4359 styles_xml.contains(r#"w:styleId="Heading1""#),
4360 "{styles_xml}"
4361 );
4362 }
4363
4364 #[test]
4365 fn writes_an_external_hyperlink_wrapping_a_run_not_wrapped_in_it() {
4366 let document = Document::new().with_paragraph(
4367 Paragraph::new().with_run(
4368 Run::new("Rust")
4369 .with_hyperlink(Hyperlink::External("https://www.rust-lang.org".to_string())),
4370 ),
4371 );
4372
4373 let mut state = PackageState::new();
4374 let mut relationships = PartRelationships::new();
4375 relationships.add(SETTINGS_RELATIONSHIP_TYPE, SETTINGS_ENTRY_NAME); let xml = document
4378 .to_document_xml(
4379 &mut state,
4380 &mut relationships,
4381 SectionReferenceIds::default(),
4382 )
4383 .unwrap();
4384 let xml = String::from_utf8(xml).unwrap();
4385
4386 assert!(
4388 xml.contains(r#"<w:hyperlink r:id="rId2"><w:r><w:t>Rust</w:t></w:r></w:hyperlink>"#),
4389 "{xml}"
4390 );
4391 assert!(!xml.contains("<w:r><w:hyperlink"), "{xml}");
4392
4393 let relationship = relationships
4394 .relationships
4395 .by_id("rId2")
4396 .expect("missing hyperlink relationship");
4397 assert_eq!(relationship.rel_type, HYPERLINK_RELATIONSHIP_TYPE);
4398 assert_eq!(relationship.target, "https://www.rust-lang.org");
4399 assert_eq!(relationship.target_mode, TargetMode::External);
4400 }
4401
4402 #[test]
4403 fn writes_an_internal_hyperlink_as_an_anchor_with_no_relationship() {
4404 let document = Document::new().with_paragraph(Paragraph::new().with_run(
4405 Run::new("See above").with_hyperlink(Hyperlink::Internal("Section1".to_string())),
4406 ));
4407
4408 let xml = document_xml(&document);
4409
4410 assert!(
4411 xml.contains(
4412 r#"<w:hyperlink w:anchor="Section1"><w:r><w:t>See above</w:t></w:r></w:hyperlink>"#
4413 ),
4414 "{xml}"
4415 );
4416 assert!(!xml.contains("r:id"), "{xml}");
4417 }
4418
4419 #[test]
4420 fn two_runs_with_the_same_url_each_get_their_own_relationship() {
4421 let url = "https://example.com";
4424 let document = Document::new().with_paragraph(
4425 Paragraph::new()
4426 .with_run(Run::new("first").with_hyperlink(Hyperlink::External(url.to_string())))
4427 .with_run(Run::new("second").with_hyperlink(Hyperlink::External(url.to_string()))),
4428 );
4429
4430 let mut state = PackageState::new();
4431 let mut relationships = PartRelationships::new();
4432 let xml = document
4433 .to_document_xml(
4434 &mut state,
4435 &mut relationships,
4436 SectionReferenceIds::default(),
4437 )
4438 .unwrap();
4439 let xml = String::from_utf8(xml).unwrap();
4440
4441 let hyperlink_relationships: Vec<_> = relationships
4442 .relationships
4443 .iter()
4444 .filter(|relationship| relationship.rel_type == HYPERLINK_RELATIONSHIP_TYPE)
4445 .collect();
4446 assert_eq!(hyperlink_relationships.len(), 2, "{xml}");
4447 assert_ne!(hyperlink_relationships[0].id, hyperlink_relationships[1].id);
4448 }
4449
4450 #[test]
4451 fn a_hyperlinked_run_still_carries_its_own_formatting() {
4452 let document = Document::new().with_paragraph(
4453 Paragraph::new().with_run(
4454 Run::new("Bold link")
4455 .with_bold(true)
4456 .with_hyperlink(Hyperlink::External("https://example.com".to_string())),
4457 ),
4458 );
4459
4460 let xml = document_xml(&document);
4461
4462 assert!(
4463 xml.contains(r#"<w:hyperlink r:id="rId1"><w:r><w:rPr><w:b/></w:rPr><w:t>Bold link</w:t></w:r></w:hyperlink>"#),
4464 "{xml}"
4465 );
4466 }
4467
4468 #[test]
4469 fn writes_numbering_xml_with_one_abstractnum_and_num_per_definition() {
4470 use crate::model::{ListLevel, NumberFormat};
4471
4472 let bullet = NumberingDefinition::new(
4473 1,
4474 vec![ListLevel::new(NumberFormat::Bullet, "\u{2022}", 720, 360)],
4475 );
4476 let decimal = NumberingDefinition::new(
4477 5,
4478 vec![ListLevel::new(NumberFormat::Decimal, "%1.", 360, 360)],
4479 );
4480
4481 let xml = String::from_utf8(to_numbering_xml(&[bullet, decimal]).unwrap()).unwrap();
4482
4483 assert!(xml.starts_with("<?xml"), "{xml}");
4484 let last_abstract_num = xml.rfind("<w:abstractNum ").expect("missing w:abstractNum");
4488 let first_num = xml.find("<w:num ").expect("missing w:num");
4489 assert!(last_abstract_num < first_num, "{xml}");
4490
4491 assert!(
4492 xml.contains(
4493 r#"<w:abstractNum w:abstractNumId="1"><w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="bullet"/><w:lvlText w:val="•"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="720" w:hanging="360"/></w:pPr></w:lvl></w:abstractNum>"#
4494 ),
4495 "{xml}"
4496 );
4497 assert!(
4498 xml.contains(r#"<w:num w:numId="1"><w:abstractNumId w:val="1"/></w:num>"#),
4499 "{xml}"
4500 );
4501 assert!(
4502 xml.contains(r#"<w:num w:numId="5"><w:abstractNumId w:val="5"/></w:num>"#),
4503 "{xml}"
4504 );
4505 }
4506
4507 #[test]
4508 fn writes_pstyle_before_numpr_before_jc_in_ppr() {
4509 let document = Document::new().with_paragraph(
4510 Paragraph::new()
4511 .with_style_id("ListParagraph")
4512 .with_numbering(1, 2)
4513 .with_alignment(Alignment::Center)
4514 .with_run(Run::new("Item")),
4515 );
4516
4517 let xml = document_xml(&document);
4518
4519 assert!(
4520 xml.contains(
4521 r#"<w:pPr><w:pStyle w:val="ListParagraph"/><w:numPr><w:ilvl w:val="2"/><w:numId w:val="1"/></w:numPr><w:jc w:val="center"/></w:pPr>"#
4522 ),
4523 "{xml}"
4524 );
4525 }
4526
4527 #[test]
4528 fn a_document_with_no_numbering_definitions_does_not_write_a_numbering_part() {
4529 let document = Document::new().with_paragraph(Paragraph::with_text("No lists here"));
4530
4531 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4532 let package = Package::read_from(buffer).unwrap();
4533 assert!(
4534 package.part(NUMBERING_PART_NAME).is_none(),
4535 "word/numbering.xml should not be written"
4536 );
4537 }
4538
4539 #[test]
4540 fn write_to_declares_a_numbering_relationship_when_definitions_are_present() {
4541 use std::io::Cursor;
4542
4543 let document = Document::new()
4544 .with_paragraph(Paragraph::with_text("Item one").with_numbering(1, 0))
4545 .with_numbering_definition(NumberingDefinition::bullet(1));
4546
4547 let buffer = document.write_to(Cursor::new(Vec::new())).unwrap();
4548 let package = Package::read_from(buffer).unwrap();
4549
4550 let document_part = package
4551 .part(MAIN_DOCUMENT_PART_NAME)
4552 .expect("missing document.xml");
4553 let numbering_relationship = document_part
4554 .relationships
4555 .iter()
4556 .find(|relationship| relationship.rel_type == NUMBERING_RELATIONSHIP_TYPE)
4557 .expect("document.xml has no numbering relationship");
4558 assert_eq!(numbering_relationship.target, NUMBERING_ENTRY_NAME);
4559
4560 let numbering_part = package
4561 .part(NUMBERING_PART_NAME)
4562 .expect("missing word/numbering.xml");
4563 let numbering_xml = std::str::from_utf8(&numbering_part.data).unwrap();
4564 assert!(numbering_xml.contains(r#"w:numId="1""#), "{numbering_xml}");
4565
4566 let document_xml = std::str::from_utf8(&document_part.data).unwrap();
4567 assert!(
4568 document_xml.contains(r#"<w:numId w:val="1"/>"#),
4569 "{document_xml}"
4570 );
4571 }
4572
4573 #[test]
4574 fn bullet_and_decimal_convenience_definitions_have_three_levels() {
4575 let bullet = NumberingDefinition::bullet(1);
4576 let decimal = NumberingDefinition::decimal(2);
4577
4578 assert_eq!(bullet.levels.len(), 3);
4579 assert_eq!(decimal.levels.len(), 3);
4580 assert_eq!(decimal.levels[1].text, "%1.%2.");
4581 }
4582
4583 #[test]
4584 fn writes_a_note_reference_run_with_its_id_and_reference_style() {
4585 let document = Document::new().with_paragraph(
4586 Paragraph::with_text("See the note.")
4587 .with_run(Run::with_note_reference(NoteReference::Footnote(1))),
4588 );
4589
4590 let xml = document_xml(&document);
4591
4592 assert!(
4593 xml.contains(
4594 r#"<w:r><w:rPr><w:rStyle w:val="FootnoteReference"/></w:rPr><w:footnoteReference w:id="1"/></w:r>"#
4595 ),
4596 "{xml}"
4597 );
4598 }
4599
4600 #[test]
4601 fn writes_an_endnote_reference_with_the_endnote_element_and_style() {
4602 let document = Document::new().with_paragraph(
4603 Paragraph::new().with_run(Run::with_note_reference(NoteReference::Endnote(3))),
4604 );
4605
4606 let xml = document_xml(&document);
4607
4608 assert!(
4609 xml.contains(
4610 r#"<w:rPr><w:rStyle w:val="EndnoteReference"/></w:rPr><w:endnoteReference w:id="3"/>"#
4611 ),
4612 "{xml}"
4613 );
4614 }
4615
4616 #[test]
4617 fn writes_footnotes_xml_with_one_footnote_element_per_note() {
4618 let note = Note::footnote_with_text(1, "This is a footnote.");
4619
4620 let xml = String::from_utf8(
4621 to_notes_xml(
4622 "w:footnotes",
4623 "w:footnote",
4624 &[note],
4625 &mut PackageState::new(),
4626 &mut PartRelationships::new(),
4627 )
4628 .unwrap(),
4629 )
4630 .unwrap();
4631
4632 assert!(xml.starts_with("<?xml"), "{xml}");
4633 assert!(xml.contains(r#"<w:footnote w:id="1">"#), "{xml}");
4634 assert!(
4637 xml.contains(
4638 r#"<w:r><w:rPr><w:rStyle w:val="FootnoteReference"/></w:rPr><w:footnoteRef/></w:r><w:r><w:t xml:space="preserve"> </w:t></w:r><w:r><w:t>This is a footnote.</w:t></w:r>"#
4639 ),
4640 "{xml}"
4641 );
4642 }
4643
4644 #[test]
4645 fn writes_an_endnote_marker_with_the_endnote_ref_element() {
4646 let note = Note::endnote_with_text(1, "This is an endnote.");
4647
4648 let xml = String::from_utf8(
4649 to_notes_xml(
4650 "w:endnotes",
4651 "w:endnote",
4652 &[note],
4653 &mut PackageState::new(),
4654 &mut PartRelationships::new(),
4655 )
4656 .unwrap(),
4657 )
4658 .unwrap();
4659
4660 assert!(xml.contains(r#"<w:endnote w:id="1">"#), "{xml}");
4661 assert!(xml.contains("<w:endnoteRef/>"), "{xml}");
4662 }
4663
4664 #[test]
4665 fn a_document_with_no_footnotes_or_endnotes_does_not_write_those_parts() {
4666 let document = Document::new().with_paragraph(Paragraph::with_text("No notes here"));
4667
4668 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4669 let package = Package::read_from(buffer).unwrap();
4670 assert!(
4671 package.part(FOOTNOTES_PART_NAME).is_none(),
4672 "word/footnotes.xml should not be written"
4673 );
4674 assert!(
4675 package.part(ENDNOTES_PART_NAME).is_none(),
4676 "word/endnotes.xml should not be written"
4677 );
4678 }
4679
4680 #[test]
4681 fn write_to_declares_footnote_and_endnote_relationships_when_notes_are_present() {
4682 use std::io::Cursor;
4683
4684 let document = Document::new()
4685 .with_paragraph(
4686 Paragraph::with_text("See the footnote")
4687 .with_run(Run::with_note_reference(NoteReference::Footnote(1))),
4688 )
4689 .with_footnote(Note::footnote_with_text(1, "Footnote text."))
4690 .with_endnote(Note::endnote_with_text(1, "Endnote text."));
4691
4692 let buffer = document.write_to(Cursor::new(Vec::new())).unwrap();
4693 let package = Package::read_from(buffer).unwrap();
4694
4695 let document_part = package
4696 .part(MAIN_DOCUMENT_PART_NAME)
4697 .expect("missing document.xml");
4698 assert!(
4699 document_part
4700 .relationships
4701 .iter()
4702 .any(|relationship| relationship.rel_type == FOOTNOTES_RELATIONSHIP_TYPE),
4703 "document.xml has no footnotes relationship"
4704 );
4705 assert!(
4706 document_part
4707 .relationships
4708 .iter()
4709 .any(|relationship| relationship.rel_type == ENDNOTES_RELATIONSHIP_TYPE),
4710 "document.xml has no endnotes relationship"
4711 );
4712
4713 let footnotes_part = package
4714 .part(FOOTNOTES_PART_NAME)
4715 .expect("missing word/footnotes.xml");
4716 let footnotes_xml = std::str::from_utf8(&footnotes_part.data).unwrap();
4717 assert!(footnotes_xml.contains("Footnote text."), "{footnotes_xml}");
4718
4719 let endnotes_part = package
4720 .part(ENDNOTES_PART_NAME)
4721 .expect("missing word/endnotes.xml");
4722 let endnotes_xml = std::str::from_utf8(&endnotes_part.data).unwrap();
4723 assert!(endnotes_xml.contains("Endnote text."), "{endnotes_xml}");
4724 }
4725
4726 #[test]
4727 fn footnote_and_endnote_with_text_convenience_builds_marker_space_and_text() {
4728 let footnote = Note::footnote_with_text(7, "Some detail.");
4729
4730 assert_eq!(footnote.id, 7);
4731 assert_eq!(footnote.blocks.len(), 1);
4732 let paragraph = footnote.paragraphs().next().expect("missing paragraph");
4733 assert_eq!(paragraph.runs.len(), 3);
4734 assert_eq!(
4735 paragraph.runs[0].content,
4736 RunContent::NoteMarker(NoteKind::Footnote)
4737 );
4738 assert_eq!(paragraph.runs[1].text(), Some(" "));
4739 assert_eq!(paragraph.runs[2].text(), Some("Some detail."));
4740 }
4741
4742 #[test]
4743 fn a_run_marked_with_a_comment_id_gets_a_range_start_end_and_reference() {
4744 let document = Document::new().with_paragraph(
4748 Paragraph::new()
4749 .with_run(Run::new("Some "))
4750 .with_run(Run::new("text.").with_comment(0)),
4751 );
4752
4753 let xml = document_xml(&document);
4754
4755 assert!(
4756 xml.contains(
4757 r#"<w:r><w:t xml:space="preserve">Some </w:t></w:r><w:commentRangeStart w:id="0"/><w:r><w:t>text.</w:t></w:r><w:commentRangeEnd w:id="0"/><w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>"#
4758 ),
4759 "{xml}"
4760 );
4761 }
4762
4763 #[test]
4764 fn consecutive_runs_sharing_a_comment_id_get_a_single_start_end_pair() {
4765 let document = Document::new().with_paragraph(
4770 Paragraph::new()
4771 .with_run(Run::new("first ").with_comment(0))
4772 .with_run(Run::new("second").with_comment(0)),
4773 );
4774
4775 let xml = document_xml(&document);
4776
4777 assert_eq!(xml.matches("w:commentRangeStart").count(), 1, "{xml}");
4778 assert_eq!(xml.matches("w:commentRangeEnd").count(), 1, "{xml}");
4779 assert_eq!(xml.matches("w:commentReference").count(), 1, "{xml}");
4780 assert!(
4781 xml.contains(r#"<w:commentRangeStart w:id="0"/><w:r>"#),
4782 "{xml}"
4783 );
4784 assert!(
4785 xml.contains(r#"</w:r><w:commentRangeEnd w:id="0"/>"#),
4786 "{xml}"
4787 );
4788 }
4789
4790 #[test]
4791 fn overlapping_comment_ranges_each_get_their_own_start_end_pair() {
4792 let document = Document::new().with_paragraph(
4796 Paragraph::new()
4797 .with_run(Run::new("a").with_comment(0))
4798 .with_run(Run::new("b").with_comment(0).with_comment(1))
4799 .with_run(Run::new("c").with_comment(0)),
4800 );
4801
4802 let xml = document_xml(&document);
4803
4804 assert_eq!(xml.matches("w:commentRangeStart").count(), 2, "{xml}");
4805 assert_eq!(xml.matches("w:commentRangeEnd").count(), 2, "{xml}");
4806 assert!(
4809 xml.contains(r#"w:id="1"/><w:r><w:t>b</w:t></w:r><w:commentRangeEnd w:id="1"/>"#),
4810 "{xml}"
4811 );
4812 }
4813
4814 #[test]
4815 fn a_comment_range_left_open_at_the_paragraph_s_end_still_closes() {
4816 let document = Document::new()
4817 .with_paragraph(Paragraph::new().with_run(Run::new("last run").with_comment(0)));
4818
4819 let xml = document_xml(&document);
4820
4821 assert!(xml.contains(r#"<w:commentRangeEnd w:id="0"/>"#), "{xml}");
4822 assert!(xml.contains("<w:commentReference"), "{xml}");
4823 }
4824
4825 #[test]
4826 fn a_run_marked_with_a_bookmark_gets_a_start_and_end_pair() {
4827 use crate::model::Bookmark;
4828
4829 let document = Document::new().with_paragraph(
4830 Paragraph::new()
4831 .with_run(Run::new("Some "))
4832 .with_run(Run::new("text.").with_bookmark(Bookmark::new(0, "MyBookmark"))),
4833 );
4834
4835 let xml = document_xml(&document);
4836
4837 assert!(
4838 xml.contains(
4839 r#"<w:r><w:t xml:space="preserve">Some </w:t></w:r><w:bookmarkStart w:id="0" w:name="MyBookmark"/><w:r><w:t>text.</w:t></w:r><w:bookmarkEnd w:id="0"/>"#
4840 ),
4841 "{xml}"
4842 );
4843 }
4844
4845 #[test]
4846 fn consecutive_runs_sharing_a_bookmark_get_a_single_start_end_pair() {
4847 use crate::model::Bookmark;
4848
4849 let document = Document::new().with_paragraph(
4850 Paragraph::new()
4851 .with_run(Run::new("first ").with_bookmark(Bookmark::new(0, "Target")))
4852 .with_run(Run::new("second").with_bookmark(Bookmark::new(0, "Target"))),
4853 );
4854
4855 let xml = document_xml(&document);
4856
4857 assert_eq!(xml.matches("w:bookmarkStart").count(), 1, "{xml}");
4858 assert_eq!(xml.matches("w:bookmarkEnd").count(), 1, "{xml}");
4859 assert!(
4860 xml.contains(r#"<w:bookmarkStart w:id="0" w:name="Target"/><w:r>"#),
4861 "{xml}"
4862 );
4863 assert!(xml.contains(r#"</w:r><w:bookmarkEnd w:id="0"/>"#), "{xml}");
4864 }
4865
4866 #[test]
4867 fn overlapping_bookmarks_each_get_their_own_start_end_pair() {
4868 use crate::model::Bookmark;
4869
4870 let document = Document::new().with_paragraph(
4871 Paragraph::new()
4872 .with_run(Run::new("a").with_bookmark(Bookmark::new(0, "Outer")))
4873 .with_run(
4874 Run::new("b")
4875 .with_bookmark(Bookmark::new(0, "Outer"))
4876 .with_bookmark(Bookmark::new(1, "Inner")),
4877 )
4878 .with_run(Run::new("c").with_bookmark(Bookmark::new(0, "Outer"))),
4879 );
4880
4881 let xml = document_xml(&document);
4882
4883 assert_eq!(xml.matches("w:bookmarkStart").count(), 2, "{xml}");
4884 assert_eq!(xml.matches("w:bookmarkEnd").count(), 2, "{xml}");
4885 assert!(
4886 xml.contains(
4887 r#"w:id="1" w:name="Inner"/><w:r><w:t>b</w:t></w:r><w:bookmarkEnd w:id="1"/>"#
4888 ),
4889 "{xml}"
4890 );
4891 }
4892
4893 #[test]
4894 fn a_bookmark_left_open_at_the_paragraph_s_end_still_closes() {
4895 use crate::model::Bookmark;
4896
4897 let document = Document::new().with_paragraph(
4898 Paragraph::new().with_run(Run::new("last run").with_bookmark(Bookmark::new(0, "End"))),
4899 );
4900
4901 let xml = document_xml(&document);
4902
4903 assert!(xml.contains(r#"<w:bookmarkEnd w:id="0"/>"#), "{xml}");
4904 }
4905
4906 #[test]
4907 fn an_internal_hyperlink_can_point_to_a_real_bookmark_written_elsewhere() {
4908 use crate::model::Bookmark;
4909
4910 let document = Document::new()
4911 .with_paragraph(Paragraph::new().with_run(
4912 Run::new("See below").with_hyperlink(Hyperlink::Internal("Target".to_string())),
4913 ))
4914 .with_paragraph(
4915 Paragraph::new()
4916 .with_run(Run::new("Here").with_bookmark(Bookmark::new(0, "Target"))),
4917 );
4918
4919 let xml = document_xml(&document);
4920
4921 assert!(xml.contains(r#"<w:hyperlink w:anchor="Target">"#), "{xml}");
4922 assert!(
4923 xml.contains(r#"<w:bookmarkStart w:id="0" w:name="Target"/>"#),
4924 "{xml}"
4925 );
4926 }
4927
4928 #[test]
4929 fn writes_comments_xml_with_author_date_and_initials() {
4930 let comment = Comment::with_text(0, "Please rephrase.")
4931 .with_author("Morgan")
4932 .with_initials("MR")
4933 .with_date("2026-07-16T12:00:00Z");
4934
4935 let xml = String::from_utf8(
4936 to_comments_xml(
4937 &[comment],
4938 &mut PackageState::new(),
4939 &mut PartRelationships::new(),
4940 )
4941 .unwrap(),
4942 )
4943 .unwrap();
4944
4945 assert!(xml.starts_with("<?xml"), "{xml}");
4946 assert!(
4947 xml.contains(r#"<w:comment w:id="0" w:author="Morgan" w:date="2026-07-16T12:00:00Z" w:initials="MR">"#),
4948 "{xml}"
4949 );
4950 assert!(xml.contains("Please rephrase."), "{xml}");
4951 }
4952
4953 #[test]
4954 fn a_document_with_no_comments_does_not_write_that_part() {
4955 let document = Document::new().with_paragraph(Paragraph::with_text("No comments here"));
4956
4957 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4958 let package = Package::read_from(buffer).unwrap();
4959 assert!(
4960 package.part(COMMENTS_PART_NAME).is_none(),
4961 "word/comments.xml should not be written"
4962 );
4963 }
4964
4965 #[test]
4966 fn write_to_declares_a_comments_relationship_when_comments_are_present() {
4967 use std::io::Cursor;
4968
4969 let document = Document::new()
4970 .with_paragraph(
4971 Paragraph::with_text("Reviewed text").with_run(Run::new("").with_comment(0)),
4972 )
4973 .with_comment(Comment::with_text(0, "A remark."));
4974
4975 let buffer = document.write_to(Cursor::new(Vec::new())).unwrap();
4976 let package = Package::read_from(buffer).unwrap();
4977
4978 let document_part = package
4979 .part(MAIN_DOCUMENT_PART_NAME)
4980 .expect("missing document.xml");
4981 assert!(
4982 document_part
4983 .relationships
4984 .iter()
4985 .any(|relationship| relationship.rel_type == COMMENTS_RELATIONSHIP_TYPE),
4986 "document.xml has no comments relationship"
4987 );
4988
4989 let comments_part = package
4990 .part(COMMENTS_PART_NAME)
4991 .expect("missing word/comments.xml");
4992 let comments_xml = std::str::from_utf8(&comments_part.data).unwrap();
4993 assert!(comments_xml.contains("A remark."), "{comments_xml}");
4994 }
4995
4996 #[test]
4997 fn writes_a_structured_document_tag_with_its_properties_and_content() {
4998 let document = Document::new().with_structured_document_tag(
4999 StructuredDocumentTag::new()
5000 .with_id(1)
5001 .with_tag("CustomerName")
5002 .with_alias("Customer Name")
5003 .with_paragraph(Paragraph::with_text("Acme Corp")),
5004 );
5005
5006 let xml = document_xml(&document);
5007
5008 assert!(
5009 xml.contains(
5010 r#"<w:sdt><w:sdtPr><w:alias w:val="Customer Name"/><w:tag w:val="CustomerName"/><w:id w:val="1"/></w:sdtPr><w:sdtContent><w:p><w:r><w:t>Acme Corp</w:t></w:r></w:p></w:sdtContent></w:sdt>"#
5011 ),
5012 "{xml}"
5013 );
5014 }
5015
5016 #[test]
5017 fn a_structured_document_tag_with_no_properties_omits_sdtpr() {
5018 let document = Document::new().with_structured_document_tag(
5019 StructuredDocumentTag::new().with_paragraph(Paragraph::with_text("Plain")),
5020 );
5021
5022 let xml = document_xml(&document);
5023
5024 assert!(!xml.contains("w:sdtPr"), "{xml}");
5025 assert!(
5026 xml.contains(r#"<w:sdt><w:sdtContent><w:p><w:r><w:t>Plain</w:t></w:r></w:p></w:sdtContent></w:sdt>"#),
5027 "{xml}"
5028 );
5029 }
5030
5031 #[test]
5032 fn a_nested_structured_document_tag_writes_correctly() {
5033 let mut outer = StructuredDocumentTag::new().with_tag("outer");
5039 outer.blocks.push(Block::StructuredDocumentTag(
5040 StructuredDocumentTag::new()
5041 .with_tag("inner")
5042 .with_paragraph(Paragraph::with_text("nested")),
5043 ));
5044 let document = Document::new().with_structured_document_tag(outer);
5045
5046 let xml = document_xml(&document);
5047
5048 assert!(xml.contains(r#"<w:tag w:val="outer"/>"#), "{xml}");
5049 assert!(xml.contains(r#"<w:tag w:val="inner"/>"#), "{xml}");
5050 assert!(xml.contains("nested"), "{xml}");
5051 }
5052
5053 #[test]
5054 fn a_document_with_a_structured_document_tag_interleaved_with_paragraphs_writes_all_blocks() {
5055 let document = Document::new()
5056 .with_paragraph(Paragraph::with_text("Before"))
5057 .with_structured_document_tag(
5058 StructuredDocumentTag::new().with_paragraph(Paragraph::with_text("Inside")),
5059 )
5060 .with_paragraph(Paragraph::with_text("After"));
5061
5062 let xml = document_xml(&document);
5063
5064 let before = xml.find("Before").expect("missing Before");
5065 let inside = xml.find("Inside").expect("missing Inside");
5066 let after = xml.find("After").expect("missing After");
5067 assert!(before < inside && inside < after, "{xml}");
5068 }
5069
5070 #[test]
5071 fn writes_a_theme_part_with_colors_and_fonts() {
5072 use std::io::Cursor;
5073
5074 let theme = Theme::new(
5075 "Custom",
5076 ColorScheme::new(
5077 "111111", "EEEEEE", "222222", "DDDDDD", "AA0000", "BB1100", "CC2200", "DD3300",
5078 "EE4400", "FF5500", "0000AA", "AA00AA",
5079 ),
5080 FontScheme::new("Georgia", "Verdana"),
5081 );
5082 let document = Document::new()
5083 .with_paragraph(Paragraph::with_text("Hello"))
5084 .with_theme(theme);
5085
5086 let buffer = document.write_to(Cursor::new(Vec::new())).unwrap();
5087 let package = Package::read_from(buffer).unwrap();
5088
5089 let theme_part = package
5090 .part(THEME_PART_NAME)
5091 .expect("missing word/theme/theme1.xml");
5092 let theme_xml = std::str::from_utf8(&theme_part.data).unwrap();
5093
5094 assert!(theme_xml.contains(r#"<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Custom">"#), "{theme_xml}");
5095 assert!(
5096 theme_xml.contains(r#"<a:dk1><a:srgbClr val="111111"/></a:dk1>"#),
5097 "{theme_xml}"
5098 );
5099 assert!(
5100 theme_xml.contains(r#"<a:accent6><a:srgbClr val="FF5500"/></a:accent6>"#),
5101 "{theme_xml}"
5102 );
5103 assert!(
5104 theme_xml.contains(r#"<a:folHlink><a:srgbClr val="AA00AA"/></a:folHlink>"#),
5105 "{theme_xml}"
5106 );
5107 assert!(
5108 theme_xml.contains(r#"<a:latin typeface="Georgia"/>"#),
5109 "{theme_xml}"
5110 );
5111 assert!(
5112 theme_xml.contains(r#"<a:latin typeface="Verdana"/>"#),
5113 "{theme_xml}"
5114 );
5115 assert!(
5116 theme_xml.contains("<a:fmtScheme"),
5117 "fmtScheme boilerplate missing: {theme_xml}"
5118 );
5119 }
5120
5121 #[test]
5122 fn a_document_with_no_theme_does_not_write_that_part() {
5123 let document = Document::new().with_paragraph(Paragraph::with_text("No theme here"));
5124
5125 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
5126 let package = Package::read_from(buffer).unwrap();
5127 assert!(
5128 package.part(THEME_PART_NAME).is_none(),
5129 "word/theme/theme1.xml should not be written"
5130 );
5131 }
5132
5133 #[test]
5134 fn write_to_declares_a_theme_relationship_when_a_theme_is_present() {
5135 use std::io::Cursor;
5136
5137 let document = Document::new().with_theme(Theme::office_default());
5138
5139 let buffer = document.write_to(Cursor::new(Vec::new())).unwrap();
5140 let package = Package::read_from(buffer).unwrap();
5141
5142 let document_part = package
5143 .part(MAIN_DOCUMENT_PART_NAME)
5144 .expect("missing document.xml");
5145 assert!(
5146 document_part
5147 .relationships
5148 .iter()
5149 .any(|relationship| relationship.rel_type == THEME_RELATIONSHIP_TYPE),
5150 "document.xml has no theme relationship"
5151 );
5152 }
5153
5154 #[test]
5155 fn a_document_with_no_explicit_page_setup_matches_the_old_fixed_a4_values() {
5156 let document = Document::new().with_paragraph(Paragraph::with_text("Body"));
5160
5161 let xml = document_xml(&document);
5162
5163 assert!(
5164 xml.contains(r#"<w:pgSz w:w="11906" w:h="16838"/>"#),
5165 "{xml}"
5166 );
5167 assert!(
5168 xml.contains(r#"<w:pgMar w:top="1417" w:right="1417" w:bottom="1417" w:left="1417" w:header="708" w:footer="708" w:gutter="0"/>"#),
5169 "{xml}"
5170 );
5171 assert!(!xml.contains("w:orient"), "{xml}");
5173 assert!(!xml.contains("<w:cols"), "{xml}");
5174 assert!(!xml.contains("<w:titlePg"), "{xml}");
5175 }
5176
5177 #[test]
5178 fn write_to_uses_a_configurable_landscape_page_setup_with_columns() {
5179 use crate::model::PageSetup;
5180
5181 let page_setup = PageSetup::new()
5182 .with_size_twips(16_838, 11_906)
5183 .with_orientation(Orientation::Landscape)
5184 .with_margins_twips(1000, 1000, 1000, 1000)
5185 .with_columns(2);
5186
5187 let document = Document::new()
5188 .with_paragraph(Paragraph::with_text("Body"))
5189 .with_page_setup(page_setup);
5190
5191 let xml = document_xml(&document);
5192
5193 assert!(
5194 xml.contains(r#"<w:pgSz w:w="16838" w:h="11906" w:orient="landscape"/>"#),
5195 "{xml}"
5196 );
5197 assert!(
5198 xml.contains(r#"w:top="1000" w:right="1000" w:bottom="1000" w:left="1000""#),
5199 "{xml}"
5200 );
5201 assert!(
5202 xml.contains(r#"<w:cols w:num="2" w:space="720" w:equalWidth="true"/>"#),
5203 "{xml}"
5204 );
5205 }
5206
5207 #[test]
5208 fn write_to_writes_an_a3_page_size_distinct_from_the_default_a4() {
5209 use crate::model::PageSetup;
5215
5216 let page_setup = PageSetup::new().with_size_twips(16_838, 23_811);
5217 let document = Document::new()
5218 .with_paragraph(Paragraph::with_text("Body"))
5219 .with_page_setup(page_setup);
5220
5221 let xml = document_xml(&document);
5222
5223 assert!(
5224 xml.contains(r#"<w:pgSz w:w="16838" w:h="23811"/>"#),
5225 "{xml}"
5226 );
5227 assert!(!xml.contains("w:orient"), "{xml}");
5229 }
5230
5231 #[test]
5232 fn writes_first_and_even_header_footer_references_titlepg_and_evenandoddheaders() {
5233 use std::io::Cursor;
5234
5235 let document = Document::new()
5236 .with_paragraph(Paragraph::with_text("Body"))
5237 .with_header(HeaderFooter::new().with_paragraph(Paragraph::with_text("Default header")))
5238 .with_header_first(
5239 HeaderFooter::new().with_paragraph(Paragraph::with_text("First page header")),
5240 )
5241 .with_footer_first(
5242 HeaderFooter::new().with_paragraph(Paragraph::with_text("First page footer")),
5243 )
5244 .with_header_even(
5245 HeaderFooter::new().with_paragraph(Paragraph::with_text("Even page header")),
5246 )
5247 .with_footer_even(
5248 HeaderFooter::new().with_paragraph(Paragraph::with_text("Even page footer")),
5249 );
5250
5251 let buffer = document.write_to(Cursor::new(Vec::new())).unwrap();
5252 let package = Package::read_from(buffer).unwrap();
5253
5254 let header_first_part = package
5255 .part(HEADER_FIRST_PART_NAME)
5256 .expect("missing header2.xml");
5257 assert!(
5258 std::str::from_utf8(&header_first_part.data)
5259 .unwrap()
5260 .contains("First page header")
5261 );
5262 let footer_first_part = package
5263 .part(FOOTER_FIRST_PART_NAME)
5264 .expect("missing footer2.xml");
5265 assert!(
5266 std::str::from_utf8(&footer_first_part.data)
5267 .unwrap()
5268 .contains("First page footer")
5269 );
5270 let header_even_part = package
5271 .part(HEADER_EVEN_PART_NAME)
5272 .expect("missing header3.xml");
5273 assert!(
5274 std::str::from_utf8(&header_even_part.data)
5275 .unwrap()
5276 .contains("Even page header")
5277 );
5278 let footer_even_part = package
5279 .part(FOOTER_EVEN_PART_NAME)
5280 .expect("missing footer3.xml");
5281 assert!(
5282 std::str::from_utf8(&footer_even_part.data)
5283 .unwrap()
5284 .contains("Even page footer")
5285 );
5286
5287 let document_part = package
5288 .part(MAIN_DOCUMENT_PART_NAME)
5289 .expect("missing document.xml");
5290 let document_xml = std::str::from_utf8(&document_part.data).unwrap();
5291 assert!(
5292 document_xml.contains(r#"w:headerReference w:type="first""#),
5293 "{document_xml}"
5294 );
5295 assert!(
5296 document_xml.contains(r#"w:footerReference w:type="first""#),
5297 "{document_xml}"
5298 );
5299 assert!(
5300 document_xml.contains(r#"w:headerReference w:type="even""#),
5301 "{document_xml}"
5302 );
5303 assert!(
5304 document_xml.contains(r#"w:footerReference w:type="even""#),
5305 "{document_xml}"
5306 );
5307 assert!(document_xml.contains("<w:titlePg/>"), "{document_xml}");
5308
5309 let settings_part = package
5310 .part(SETTINGS_PART_NAME)
5311 .expect("missing word/settings.xml");
5312 let settings_xml = std::str::from_utf8(&settings_part.data).unwrap();
5313 assert!(
5314 settings_xml.contains("<w:evenAndOddHeaders/>"),
5315 "{settings_xml}"
5316 );
5317 }
5318
5319 #[test]
5320 fn omits_titlepg_and_evenandoddheaders_when_only_the_default_header_is_set() {
5321 let document = Document::new()
5322 .with_paragraph(Paragraph::with_text("Body"))
5323 .with_header(
5324 HeaderFooter::new().with_paragraph(Paragraph::with_text("Default header")),
5325 );
5326
5327 let xml = document_xml(&document);
5328
5329 assert!(!xml.contains("titlePg"), "{xml}");
5330 }
5331
5332 #[test]
5333 fn writes_trackrevisions_when_track_changes_is_set() {
5334 let document = Document::new()
5335 .with_paragraph(Paragraph::with_text("Body"))
5336 .with_track_changes(true);
5337
5338 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
5339 let package = Package::read_from(buffer).unwrap();
5340
5341 let settings_part = package
5342 .part(SETTINGS_PART_NAME)
5343 .expect("missing word/settings.xml");
5344 let settings_xml = std::str::from_utf8(&settings_part.data).unwrap();
5345 assert!(
5346 settings_xml.contains("<w:trackRevisions/>"),
5347 "{settings_xml}"
5348 );
5349 }
5350
5351 #[test]
5352 fn a_document_with_track_changes_unset_omits_trackrevisions() {
5353 let document = Document::new().with_paragraph(Paragraph::with_text("Body"));
5354
5355 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
5356 let package = Package::read_from(buffer).unwrap();
5357
5358 let settings_part = package
5359 .part(SETTINGS_PART_NAME)
5360 .expect("missing word/settings.xml");
5361 let settings_xml = std::str::from_utf8(&settings_part.data).unwrap();
5362 assert!(!settings_xml.contains("trackRevisions"), "{settings_xml}");
5363 }
5364
5365 #[test]
5366 fn writes_trackrevisions_before_evenandoddheaders_when_both_are_set() {
5367 let document = Document::new()
5368 .with_paragraph(Paragraph::with_text("Body"))
5369 .with_track_changes(true)
5370 .with_header_even(
5371 HeaderFooter::new().with_paragraph(Paragraph::with_text("Even header")),
5372 );
5373
5374 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
5375 let package = Package::read_from(buffer).unwrap();
5376
5377 let settings_part = package
5378 .part(SETTINGS_PART_NAME)
5379 .expect("missing word/settings.xml");
5380 let settings_xml = std::str::from_utf8(&settings_part.data).unwrap();
5381 assert!(
5382 settings_xml.contains("<w:trackRevisions/><w:evenAndOddHeaders/>"),
5383 "{settings_xml}"
5384 );
5385 }
5386
5387 #[test]
5388 fn writes_documentprotection_with_the_given_edit_value_and_enforcement() {
5389 let document = Document::new()
5390 .with_paragraph(Paragraph::with_text("Body"))
5391 .with_protection(ProtectionKind::ReadOnly);
5392
5393 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
5394 let package = Package::read_from(buffer).unwrap();
5395
5396 let settings_part = package
5397 .part(SETTINGS_PART_NAME)
5398 .expect("missing word/settings.xml");
5399 let settings_xml = std::str::from_utf8(&settings_part.data).unwrap();
5400 assert!(
5401 settings_xml.contains(r#"<w:documentProtection w:edit="readOnly" w:enforcement="1"/>"#),
5402 "{settings_xml}"
5403 );
5404 }
5405
5406 #[test]
5407 fn a_document_with_no_protection_set_omits_documentprotection() {
5408 let document = Document::new().with_paragraph(Paragraph::with_text("Body"));
5409
5410 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
5411 let package = Package::read_from(buffer).unwrap();
5412
5413 let settings_part = package
5414 .part(SETTINGS_PART_NAME)
5415 .expect("missing word/settings.xml");
5416 let settings_xml = std::str::from_utf8(&settings_part.data).unwrap();
5417 assert!(
5418 !settings_xml.contains("documentProtection"),
5419 "{settings_xml}"
5420 );
5421 }
5422
5423 #[test]
5424 fn writes_documentprotection_between_trackrevisions_and_evenandoddheaders() {
5425 let document = Document::new()
5426 .with_paragraph(Paragraph::with_text("Body"))
5427 .with_track_changes(true)
5428 .with_protection(ProtectionKind::TrackedChanges)
5429 .with_header_even(
5430 HeaderFooter::new().with_paragraph(Paragraph::with_text("Even header")),
5431 );
5432
5433 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
5434 let package = Package::read_from(buffer).unwrap();
5435
5436 let settings_part = package
5437 .part(SETTINGS_PART_NAME)
5438 .expect("missing word/settings.xml");
5439 let settings_xml = std::str::from_utf8(&settings_part.data).unwrap();
5440 assert!(
5441 settings_xml.contains(
5442 r#"<w:trackRevisions/><w:documentProtection w:edit="trackedChanges" w:enforcement="1"/><w:evenAndOddHeaders/>"#
5443 ),
5444 "{settings_xml}"
5445 );
5446 }
5447
5448 #[test]
5449 fn writes_core_properties_with_every_field_set() {
5450 let properties = DocumentProperties::new()
5451 .with_title("Rapport trimestriel")
5452 .with_subject("Finances")
5453 .with_creator("Morgan")
5454 .with_keywords("rapport, finances, Q2")
5455 .with_description("Notes internes")
5456 .with_last_modified_by("Morgan")
5457 .with_revision("3")
5458 .with_created("2026-07-17T10:00:00Z")
5459 .with_modified("2026-07-17T11:30:00Z")
5460 .with_category("Interne")
5461 .with_content_status("Draft");
5462 let document = Document::new()
5463 .with_paragraph(Paragraph::with_text("Body"))
5464 .with_properties(properties);
5465
5466 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
5467 let package = Package::read_from(buffer).unwrap();
5468
5469 let core_properties_part = package
5470 .part(CORE_PROPERTIES_PART_NAME)
5471 .expect("missing docProps/core.xml");
5472 let core_properties_xml = std::str::from_utf8(&core_properties_part.data).unwrap();
5473 assert!(
5474 core_properties_xml.contains("<dc:title>Rapport trimestriel</dc:title>"),
5475 "{core_properties_xml}"
5476 );
5477 assert!(
5478 core_properties_xml.contains("<dc:subject>Finances</dc:subject>"),
5479 "{core_properties_xml}"
5480 );
5481 assert!(
5482 core_properties_xml.contains("<dc:creator>Morgan</dc:creator>"),
5483 "{core_properties_xml}"
5484 );
5485 assert!(
5486 core_properties_xml.contains("<cp:keywords>rapport, finances, Q2</cp:keywords>"),
5487 "{core_properties_xml}"
5488 );
5489 assert!(
5490 core_properties_xml.contains("<dc:description>Notes internes</dc:description>"),
5491 "{core_properties_xml}"
5492 );
5493 assert!(
5494 core_properties_xml.contains("<cp:lastModifiedBy>Morgan</cp:lastModifiedBy>"),
5495 "{core_properties_xml}"
5496 );
5497 assert!(
5498 core_properties_xml.contains("<cp:revision>3</cp:revision>"),
5499 "{core_properties_xml}"
5500 );
5501 assert!(
5502 core_properties_xml.contains(r#"<dcterms:created xsi:type="dcterms:W3CDTF">2026-07-17T10:00:00Z</dcterms:created>"#),
5503 "{core_properties_xml}"
5504 );
5505 assert!(
5506 core_properties_xml.contains(r#"<dcterms:modified xsi:type="dcterms:W3CDTF">2026-07-17T11:30:00Z</dcterms:modified>"#),
5507 "{core_properties_xml}"
5508 );
5509 assert!(
5510 core_properties_xml.contains("<cp:category>Interne</cp:category>"),
5511 "{core_properties_xml}"
5512 );
5513 assert!(
5514 core_properties_xml.contains("<cp:contentStatus>Draft</cp:contentStatus>"),
5515 "{core_properties_xml}"
5516 );
5517 }
5518
5519 #[test]
5520 fn a_document_with_no_properties_set_writes_an_empty_coreproperties_element() {
5521 let document = Document::new().with_paragraph(Paragraph::with_text("Body"));
5522
5523 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
5524 let package = Package::read_from(buffer).unwrap();
5525
5526 let core_properties_part = package
5527 .part(CORE_PROPERTIES_PART_NAME)
5528 .expect("missing docProps/core.xml");
5529 let core_properties_xml = std::str::from_utf8(&core_properties_part.data).unwrap();
5530 assert!(
5531 core_properties_xml.contains("<cp:coreProperties "),
5532 "{core_properties_xml}"
5533 );
5534 assert!(
5535 !core_properties_xml.contains("<dc:title>"),
5536 "{core_properties_xml}"
5537 );
5538 }
5539
5540 #[test]
5541 fn writes_custom_tab_stops_in_the_order_they_were_added() {
5542 use crate::model::{TabLeader, TabStop, TabStopAlignment};
5543
5544 let paragraph = Paragraph::with_text("Tabbed")
5545 .with_tab(TabStop::new(720))
5546 .with_tab(
5547 TabStop::new(-360)
5548 .with_alignment(TabStopAlignment::Decimal)
5549 .with_leader(TabLeader::Dot),
5550 );
5551 let document = Document::new().with_paragraph(paragraph);
5552
5553 let xml = document_xml(&document);
5554
5555 let tabs_start = xml.find("<w:tabs>").expect("missing w:tabs");
5556 let first_tab = xml
5557 .find(r#"<w:tab w:val="left" w:pos="720"/>"#)
5558 .expect("missing first tab");
5559 let second_tab = xml
5560 .find(r#"<w:tab w:val="decimal" w:leader="dot" w:pos="-360"/>"#)
5561 .expect("missing second tab");
5562 assert!(tabs_start < first_tab && first_tab < second_tab, "{xml}");
5563 }
5564
5565 #[test]
5566 fn writes_keep_next_keep_lines_page_break_before_and_contextual_spacing() {
5567 let paragraph = Paragraph::with_text("Heading")
5568 .with_keep_with_next(true)
5569 .with_keep_lines_together(true)
5570 .with_page_break_before(true)
5571 .with_contextual_spacing(true);
5572 let document = Document::new().with_paragraph(paragraph);
5573
5574 let xml = document_xml(&document);
5575
5576 assert!(xml.contains("<w:keepNext/>"), "{xml}");
5577 assert!(xml.contains("<w:keepLines/>"), "{xml}");
5578 assert!(xml.contains("<w:pageBreakBefore/>"), "{xml}");
5579 assert!(xml.contains("<w:contextualSpacing/>"), "{xml}");
5580 let p_pr = xml.find("<w:pPr>").expect("missing w:pPr");
5584 let keep_next = xml.find("<w:keepNext/>").unwrap();
5585 assert!(p_pr < keep_next, "{xml}");
5586 }
5587
5588 #[test]
5589 fn writes_page_and_column_breaks_and_a_carriage_return_in_runs() {
5590 use crate::model::BreakKind;
5591
5592 let document = Document::new().with_paragraph(
5593 Paragraph::new()
5594 .with_run(Run::with_break(BreakKind::TextWrapping))
5595 .with_run(Run::with_break(BreakKind::Page))
5596 .with_run(Run::with_break(BreakKind::Column))
5597 .with_run(Run::with_carriage_return()),
5598 );
5599
5600 let xml = document_xml(&document);
5601
5602 assert!(xml.contains("<w:br/>"), "{xml}");
5605 assert!(xml.contains(r#"<w:br w:type="page"/>"#), "{xml}");
5606 assert!(xml.contains(r#"<w:br w:type="column"/>"#), "{xml}");
5607 assert!(xml.contains("<w:cr/>"), "{xml}");
5608 }
5609
5610 #[test]
5611 fn writes_company_and_manager_in_extended_properties_when_set() {
5612 use crate::model::ExtendedProperties;
5613
5614 let extended_properties = ExtendedProperties::new()
5615 .with_company("Acme Corp")
5616 .with_manager("Morgan");
5617 let document = Document::new()
5618 .with_paragraph(Paragraph::with_text("Body"))
5619 .with_extended_properties(extended_properties);
5620
5621 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
5622 let package = Package::read_from(buffer).unwrap();
5623
5624 let extended_properties_part = package
5625 .part(EXTENDED_PROPERTIES_PART_NAME)
5626 .expect("missing docProps/app.xml");
5627 let extended_properties_xml = std::str::from_utf8(&extended_properties_part.data).unwrap();
5628 assert!(
5629 extended_properties_xml.contains("<Application>office-toolkit</Application>"),
5630 "{extended_properties_xml}"
5631 );
5632 assert!(
5633 extended_properties_xml.contains("<Manager>Morgan</Manager>"),
5634 "{extended_properties_xml}"
5635 );
5636 assert!(
5637 extended_properties_xml.contains("<Company>Acme Corp</Company>"),
5638 "{extended_properties_xml}"
5639 );
5640 }
5641
5642 #[test]
5643 fn a_document_with_no_extended_properties_set_only_writes_application() {
5644 let document = Document::new().with_paragraph(Paragraph::with_text("Body"));
5645
5646 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
5647 let package = Package::read_from(buffer).unwrap();
5648
5649 let extended_properties_part = package
5650 .part(EXTENDED_PROPERTIES_PART_NAME)
5651 .expect("missing docProps/app.xml");
5652 let extended_properties_xml = std::str::from_utf8(&extended_properties_part.data).unwrap();
5653 assert!(
5654 extended_properties_xml.contains("<Application>office-toolkit</Application>"),
5655 "{extended_properties_xml}"
5656 );
5657 assert!(
5658 !extended_properties_xml.contains("<Company>"),
5659 "{extended_properties_xml}"
5660 );
5661 assert!(
5662 !extended_properties_xml.contains("<Manager>"),
5663 "{extended_properties_xml}"
5664 );
5665 }
5666
5667 #[test]
5668 fn writes_custom_properties_with_sequential_pids_and_the_right_variant_types() {
5669 use crate::model::CustomPropertyValue;
5670
5671 let document = Document::new()
5672 .with_paragraph(Paragraph::with_text("Body"))
5673 .with_custom_property(
5674 "Client",
5675 CustomPropertyValue::Text("Northwind Traders".to_string()),
5676 )
5677 .with_custom_property("Confidentiel", CustomPropertyValue::Bool(true))
5678 .with_custom_property("Version interne", CustomPropertyValue::Int(7))
5679 .with_custom_property("Taux de remise", CustomPropertyValue::Number(0.15));
5680
5681 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
5682 let package = Package::read_from(buffer).unwrap();
5683
5684 let custom_properties_part = package
5685 .part(CUSTOM_PROPERTIES_PART_NAME)
5686 .expect("missing docProps/custom.xml");
5687 let custom_properties_xml = std::str::from_utf8(&custom_properties_part.data).unwrap();
5688 assert!(
5689 custom_properties_xml.contains(
5690 r#"<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="2" name="Client"><vt:lpwstr>Northwind Traders</vt:lpwstr></property>"#
5691 ),
5692 "{custom_properties_xml}"
5693 );
5694 assert!(
5695 custom_properties_xml.contains(
5696 r#"<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="3" name="Confidentiel"><vt:bool>true</vt:bool></property>"#
5697 ),
5698 "{custom_properties_xml}"
5699 );
5700 assert!(
5701 custom_properties_xml.contains(
5702 r#"<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="4" name="Version interne"><vt:i4>7</vt:i4></property>"#
5703 ),
5704 "{custom_properties_xml}"
5705 );
5706 assert!(
5707 custom_properties_xml.contains(
5708 r#"<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="5" name="Taux de remise"><vt:r8>0.15</vt:r8></property>"#
5709 ),
5710 "{custom_properties_xml}"
5711 );
5712 }
5713
5714 #[test]
5715 fn a_document_with_no_custom_properties_omits_the_custom_properties_part() {
5716 let document = Document::new().with_paragraph(Paragraph::with_text("Body"));
5717
5718 let buffer = document.write_to(std::io::Cursor::new(Vec::new())).unwrap();
5719 let package = Package::read_from(buffer).unwrap();
5720
5721 assert!(package.part(CUSTOM_PROPERTIES_PART_NAME).is_none());
5722 }
5723}