1use std::collections::HashMap;
4use std::io::{Read, Seek};
5
6use drawing::{Geometry, PresetShape, ShapeProperties};
7use opc::{Package, Relationships, TargetMode};
8use xml_core::{BytesStart, Event, Reader};
9
10use crate::error::{Error, Result};
11use crate::model::{
12 Alignment, Block, Bookmark, BreakKind, CellVerticalAlign, ColorScheme, Comment,
13 CustomPropertyValue, Document, DocumentProperties, EmbeddedChart, ExtendedProperties, Field,
14 FontScheme, HeaderFooter, Highlight, Hyperlink, Image, ImageFormat, ListLevel, Note, NoteKind,
15 NoteReference, NumberFormat, NumberingDefinition, Orientation, PageSetup, Paragraph,
16 ProtectionKind, Run, RunContent, StructuredDocumentTag, Style, StyleKind, TabLeader, TabStop,
17 TabStopAlignment, Table, TableCell, TableRow, TextDirection, Theme, UnderlineStyle,
18 VerticalAlign, VerticalMerge,
19};
20
21const MAIN_DOCUMENT_RELATIONSHIP_TYPE: &str =
23 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
24
25const STYLES_RELATIONSHIP_TYPE: &str =
29 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
30
31const NUMBERING_RELATIONSHIP_TYPE: &str =
35 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
36
37const FOOTNOTES_RELATIONSHIP_TYPE: &str =
41 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";
42
43const ENDNOTES_RELATIONSHIP_TYPE: &str =
45 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes";
46
47const COMMENTS_RELATIONSHIP_TYPE: &str =
51 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
52
53const THEME_RELATIONSHIP_TYPE: &str =
56 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
57
58const SETTINGS_RELATIONSHIP_TYPE: &str =
65 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings";
66
67const CORE_PROPERTIES_RELATIONSHIP_TYPE: &str =
73 "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
74
75const EXTENDED_PROPERTIES_RELATIONSHIP_TYPE: &str =
78 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties";
79
80const CUSTOM_PROPERTIES_RELATIONSHIP_TYPE: &str =
85 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
86
87impl Document {
88 pub fn read_from<R: Read + Seek>(reader: R) -> Result<Self> {
90 let package = Package::read_from(reader)?;
91 let main_document_relationship = package
92 .relationships()
93 .iter()
94 .find(|relationship| relationship.rel_type == MAIN_DOCUMENT_RELATIONSHIP_TYPE)
95 .ok_or(Error::MissingMainDocument)?;
96
97 let part_name = match main_document_relationship.target_mode {
100 TargetMode::Internal => format!("/{}", main_document_relationship.target),
101 TargetMode::External => return Err(Error::MissingMainDocument),
102 };
103
104 let part = package.part(&part_name).ok_or(Error::MissingMainDocument)?;
105 let xml = std::str::from_utf8(&part.data)?;
106
107 let resolver = ImageResolver {
112 package: &package,
113 part_relationships: &part.relationships,
114 };
115
116 let parsed = parse_document_xml(xml, &resolver)?;
117 let mut document = parsed.document;
118
119 if let Some(relationship_id) = parsed.header_relationship_id {
123 document.header = Some(resolve_header_footer(
124 &package,
125 &part.relationships,
126 &relationship_id,
127 )?);
128 }
129 if let Some(relationship_id) = parsed.footer_relationship_id {
130 document.footer = Some(resolve_header_footer(
131 &package,
132 &part.relationships,
133 &relationship_id,
134 )?);
135 }
136 if let Some(relationship_id) = parsed.header_first_relationship_id {
137 document.header_first = Some(resolve_header_footer(
138 &package,
139 &part.relationships,
140 &relationship_id,
141 )?);
142 }
143 if let Some(relationship_id) = parsed.footer_first_relationship_id {
144 document.footer_first = Some(resolve_header_footer(
145 &package,
146 &part.relationships,
147 &relationship_id,
148 )?);
149 }
150 if let Some(relationship_id) = parsed.header_even_relationship_id {
151 document.header_even = Some(resolve_header_footer(
152 &package,
153 &part.relationships,
154 &relationship_id,
155 )?);
156 }
157 if let Some(relationship_id) = parsed.footer_even_relationship_id {
158 document.footer_even = Some(resolve_header_footer(
159 &package,
160 &part.relationships,
161 &relationship_id,
162 )?);
163 }
164
165 let styles_relationship = part
169 .relationships
170 .iter()
171 .find(|relationship| relationship.rel_type == STYLES_RELATIONSHIP_TYPE);
172 if let Some(relationship) = styles_relationship {
173 let styles_part_name = format!("/word/{}", relationship.target);
174 let styles_part = package
175 .part(&styles_part_name)
176 .ok_or_else(|| Error::MissingStylesPart(styles_part_name.clone()))?;
177 let styles_xml = std::str::from_utf8(&styles_part.data)?;
178 document.styles = parse_styles_xml(styles_xml)?;
179 }
180
181 let numbering_relationship = part
183 .relationships
184 .iter()
185 .find(|relationship| relationship.rel_type == NUMBERING_RELATIONSHIP_TYPE);
186 if let Some(relationship) = numbering_relationship {
187 let numbering_part_name = format!("/word/{}", relationship.target);
188 let numbering_part = package
189 .part(&numbering_part_name)
190 .ok_or_else(|| Error::MissingNumberingPart(numbering_part_name.clone()))?;
191 let numbering_xml = std::str::from_utf8(&numbering_part.data)?;
192 document.numbering_definitions = parse_numbering_xml(numbering_xml)?;
193 }
194
195 let footnotes_relationship = part
199 .relationships
200 .iter()
201 .find(|relationship| relationship.rel_type == FOOTNOTES_RELATIONSHIP_TYPE);
202 if let Some(relationship) = footnotes_relationship {
203 let footnotes_part_name = format!("/word/{}", relationship.target);
204 let footnotes_part = package
205 .part(&footnotes_part_name)
206 .ok_or_else(|| Error::MissingFootnotesPart(footnotes_part_name.clone()))?;
207 let footnotes_xml = std::str::from_utf8(&footnotes_part.data)?;
208 let footnotes_resolver = ImageResolver {
209 package: &package,
210 part_relationships: &footnotes_part.relationships,
211 };
212 document.footnotes = parse_notes_xml(footnotes_xml, &footnotes_resolver, b"footnote")?;
213 }
214
215 let endnotes_relationship = part
216 .relationships
217 .iter()
218 .find(|relationship| relationship.rel_type == ENDNOTES_RELATIONSHIP_TYPE);
219 if let Some(relationship) = endnotes_relationship {
220 let endnotes_part_name = format!("/word/{}", relationship.target);
221 let endnotes_part = package
222 .part(&endnotes_part_name)
223 .ok_or_else(|| Error::MissingEndnotesPart(endnotes_part_name.clone()))?;
224 let endnotes_xml = std::str::from_utf8(&endnotes_part.data)?;
225 let endnotes_resolver = ImageResolver {
226 package: &package,
227 part_relationships: &endnotes_part.relationships,
228 };
229 document.endnotes = parse_notes_xml(endnotes_xml, &endnotes_resolver, b"endnote")?;
230 }
231
232 let comments_relationship = part
233 .relationships
234 .iter()
235 .find(|relationship| relationship.rel_type == COMMENTS_RELATIONSHIP_TYPE);
236 if let Some(relationship) = comments_relationship {
237 let comments_part_name = format!("/word/{}", relationship.target);
238 let comments_part = package
239 .part(&comments_part_name)
240 .ok_or_else(|| Error::MissingCommentsPart(comments_part_name.clone()))?;
241 let comments_xml = std::str::from_utf8(&comments_part.data)?;
242 let comments_resolver = ImageResolver {
243 package: &package,
244 part_relationships: &comments_part.relationships,
245 };
246 document.comments = parse_comments_xml(comments_xml, &comments_resolver)?;
247 }
248
249 let theme_relationship = part
250 .relationships
251 .iter()
252 .find(|relationship| relationship.rel_type == THEME_RELATIONSHIP_TYPE);
253 if let Some(relationship) = theme_relationship {
254 let theme_part_name = format!("/word/{}", relationship.target);
255 let theme_part = package
256 .part(&theme_part_name)
257 .ok_or_else(|| Error::MissingThemePart(theme_part_name.clone()))?;
258 let theme_xml = std::str::from_utf8(&theme_part.data)?;
259 document.theme = Some(parse_theme_xml(theme_xml)?);
260 }
261
262 let settings_relationship = part
267 .relationships
268 .iter()
269 .find(|relationship| relationship.rel_type == SETTINGS_RELATIONSHIP_TYPE);
270 if let Some(relationship) = settings_relationship {
271 let settings_part_name = format!("/word/{}", relationship.target);
272 if let Some(settings_part) = package.part(&settings_part_name) {
273 let settings_xml = std::str::from_utf8(&settings_part.data)?;
274 let (track_changes, protection) = parse_settings_xml(settings_xml)?;
275 document.track_changes = track_changes;
276 document.protection = protection;
277 }
278 }
279
280 let core_properties_relationship = package
284 .relationships()
285 .iter()
286 .find(|relationship| relationship.rel_type == CORE_PROPERTIES_RELATIONSHIP_TYPE);
287 if let Some(relationship) = core_properties_relationship {
288 let core_properties_part_name = format!("/{}", relationship.target);
289 if let Some(core_properties_part) = package.part(&core_properties_part_name) {
290 let core_properties_xml = std::str::from_utf8(&core_properties_part.data)?;
291 document.properties = parse_core_properties_xml(core_properties_xml)?;
292 }
293 }
294
295 let extended_properties_relationship = package
298 .relationships()
299 .iter()
300 .find(|relationship| relationship.rel_type == EXTENDED_PROPERTIES_RELATIONSHIP_TYPE);
301 if let Some(relationship) = extended_properties_relationship {
302 let extended_properties_part_name = format!("/{}", relationship.target);
303 if let Some(extended_properties_part) = package.part(&extended_properties_part_name) {
304 let extended_properties_xml = std::str::from_utf8(&extended_properties_part.data)?;
305 document.extended_properties =
306 parse_extended_properties_xml(extended_properties_xml)?;
307 }
308 }
309
310 let custom_properties_relationship = package
315 .relationships()
316 .iter()
317 .find(|relationship| relationship.rel_type == CUSTOM_PROPERTIES_RELATIONSHIP_TYPE);
318 if let Some(relationship) = custom_properties_relationship {
319 let custom_properties_part_name = format!("/{}", relationship.target);
320 if let Some(custom_properties_part) = package.part(&custom_properties_part_name) {
321 let custom_properties_xml = std::str::from_utf8(&custom_properties_part.data)?;
322 document.custom_properties = parse_custom_properties_xml(custom_properties_xml)?;
323 }
324 }
325
326 Ok(document)
327 }
328}
329
330fn resolve_header_footer(
335 package: &Package,
336 document_relationships: &Relationships,
337 relationship_id: &str,
338) -> Result<HeaderFooter> {
339 let relationship = document_relationships
340 .by_id(relationship_id)
341 .ok_or_else(|| Error::MissingHeaderFooterRelationship(relationship_id.to_string()))?;
342
343 let part_name = format!("/word/{}", relationship.target);
346 let part = package
347 .part(&part_name)
348 .ok_or_else(|| Error::MissingHeaderFooterPart(part_name.clone()))?;
349 let xml = std::str::from_utf8(&part.data)?;
350
351 let resolver = ImageResolver {
354 package,
355 part_relationships: &part.relationships,
356 };
357
358 Ok(HeaderFooter {
359 blocks: parse_blocks(xml, &resolver)?,
360 })
361}
362
363struct ImageResolver<'a> {
370 package: &'a Package,
371 part_relationships: &'a Relationships,
372}
373
374impl ImageResolver<'_> {
375 fn resolve(
376 &self,
377 relationship_id: &str,
378 width_emu: i64,
379 height_emu: i64,
380 description: String,
381 ) -> Result<Image> {
382 let relationship = self
383 .part_relationships
384 .by_id(relationship_id)
385 .ok_or_else(|| Error::MissingImageRelationship(relationship_id.to_string()))?;
386
387 let part_name = format!("/word/{}", relationship.target);
390 let part = self
391 .package
392 .part(&part_name)
393 .ok_or_else(|| Error::MissingImagePart(part_name.clone()))?;
394 let format = ImageFormat::from_extension(&part_name)
395 .ok_or_else(|| Error::UnsupportedImageFormat(part_name.clone()))?;
396
397 Ok(Image {
398 data: part.data.clone(),
399 format,
400 width_emu,
401 height_emu,
402 description,
403 shape_properties: None,
406 })
407 }
408
409 fn resolve_chart(&self, relationship_id: &str) -> Result<chart::ChartSpace> {
414 let relationship = self
415 .part_relationships
416 .by_id(relationship_id)
417 .ok_or_else(|| Error::MissingChartRelationship(relationship_id.to_string()))?;
418
419 let part_name = format!("/word/{}", relationship.target);
420 let part = self
421 .package
422 .part(&part_name)
423 .ok_or_else(|| Error::MissingChartPart(part_name.clone()))?;
424 let xml = std::str::from_utf8(&part.data)?;
425
426 Ok(chart::read_chart_space(xml)?)
427 }
428}
429
430struct ParsedDocumentXml {
437 document: Document,
438 header_relationship_id: Option<String>,
439 footer_relationship_id: Option<String>,
440 header_first_relationship_id: Option<String>,
441 footer_first_relationship_id: Option<String>,
442 header_even_relationship_id: Option<String>,
443 footer_even_relationship_id: Option<String>,
444}
445
446fn parse_document_xml(xml: &str, resolver: &ImageResolver) -> Result<ParsedDocumentXml> {
457 let mut document = Document::new();
458 let mut header_relationship_id: Option<String> = None;
459 let mut footer_relationship_id: Option<String> = None;
460 let mut header_first_relationship_id: Option<String> = None;
461 let mut footer_first_relationship_id: Option<String> = None;
462 let mut header_even_relationship_id: Option<String> = None;
463 let mut footer_even_relationship_id: Option<String> = None;
464 let mut reader = Reader::from_xml_str(xml);
465
466 loop {
467 match reader.read_event()? {
468 Event::Eof => break,
469
470 Event::Start(start) => match start.local_name().as_ref() {
471 b"p" => document
472 .blocks
473 .push(Block::Paragraph(parse_paragraph(&mut reader, resolver)?)),
474 b"tbl" => document
475 .blocks
476 .push(Block::Table(parse_table(&mut reader, resolver)?)),
477 b"sdt" => document.blocks.push(Block::StructuredDocumentTag(
478 parse_structured_document_tag(&mut reader, resolver)?,
479 )),
480 b"headerReference" => capture_header_footer_reference(
481 &start,
482 &mut header_relationship_id,
483 &mut header_first_relationship_id,
484 &mut header_even_relationship_id,
485 ),
486 b"footerReference" => capture_header_footer_reference(
487 &start,
488 &mut footer_relationship_id,
489 &mut footer_first_relationship_id,
490 &mut footer_even_relationship_id,
491 ),
492 b"pgSz" => apply_page_size(&start, &mut document.page_setup),
493 b"pgMar" => apply_page_margins(&start, &mut document.page_setup),
494 b"cols" => {
495 document.page_setup.columns =
496 raw_attribute(&start, b"num").and_then(|value| value.parse().ok())
497 }
498 _ => {}
499 },
500
501 Event::Empty(start) => match start.local_name().as_ref() {
502 b"p" => document.blocks.push(Block::Paragraph(Paragraph::new())),
503 b"sdt" => document
504 .blocks
505 .push(Block::StructuredDocumentTag(StructuredDocumentTag::new())),
506 b"headerReference" => capture_header_footer_reference(
507 &start,
508 &mut header_relationship_id,
509 &mut header_first_relationship_id,
510 &mut header_even_relationship_id,
511 ),
512 b"footerReference" => capture_header_footer_reference(
513 &start,
514 &mut footer_relationship_id,
515 &mut footer_first_relationship_id,
516 &mut footer_even_relationship_id,
517 ),
518 b"pgSz" => apply_page_size(&start, &mut document.page_setup),
519 b"pgMar" => apply_page_margins(&start, &mut document.page_setup),
520 b"cols" => {
521 document.page_setup.columns =
522 raw_attribute(&start, b"num").and_then(|value| value.parse().ok())
523 }
524 _ => {}
525 },
526
527 _ => {}
528 }
529 }
530
531 Ok(ParsedDocumentXml {
532 document,
533 header_relationship_id,
534 footer_relationship_id,
535 header_first_relationship_id,
536 footer_first_relationship_id,
537 header_even_relationship_id,
538 footer_even_relationship_id,
539 })
540}
541
542fn apply_page_size(start: &BytesStart, page_setup: &mut PageSetup) {
546 if let Some(width) = raw_attribute(start, b"w").and_then(|value| value.parse().ok()) {
547 page_setup.width_twips = width;
548 }
549 if let Some(height) = raw_attribute(start, b"h").and_then(|value| value.parse().ok()) {
550 page_setup.height_twips = height;
551 }
552 if let Some(orientation) =
553 raw_attribute(start, b"orient").and_then(|value| Orientation::from_attribute_value(&value))
554 {
555 page_setup.orientation = orientation;
556 }
557}
558
559fn apply_page_margins(start: &BytesStart, page_setup: &mut PageSetup) {
562 if let Some(value) = raw_attribute(start, b"top").and_then(|value| value.parse().ok()) {
563 page_setup.margin_top_twips = value;
564 }
565 if let Some(value) = raw_attribute(start, b"bottom").and_then(|value| value.parse().ok()) {
566 page_setup.margin_bottom_twips = value;
567 }
568 if let Some(value) = raw_attribute(start, b"left").and_then(|value| value.parse().ok()) {
569 page_setup.margin_left_twips = value;
570 }
571 if let Some(value) = raw_attribute(start, b"right").and_then(|value| value.parse().ok()) {
572 page_setup.margin_right_twips = value;
573 }
574 if let Some(value) = raw_attribute(start, b"header").and_then(|value| value.parse().ok()) {
575 page_setup.margin_header_twips = value;
576 }
577 if let Some(value) = raw_attribute(start, b"footer").and_then(|value| value.parse().ok()) {
578 page_setup.margin_footer_twips = value;
579 }
580 if let Some(value) = raw_attribute(start, b"gutter").and_then(|value| value.parse().ok()) {
581 page_setup.margin_gutter_twips = value;
582 }
583}
584
585fn parse_blocks(xml: &str, resolver: &ImageResolver) -> Result<Vec<Block>> {
590 let mut blocks = Vec::new();
591 let mut reader = Reader::from_xml_str(xml);
592
593 loop {
594 match reader.read_event()? {
595 Event::Eof => break,
596
597 Event::Start(start) => match start.local_name().as_ref() {
598 b"p" => blocks.push(Block::Paragraph(parse_paragraph(&mut reader, resolver)?)),
599 b"tbl" => blocks.push(Block::Table(parse_table(&mut reader, resolver)?)),
600 b"sdt" => blocks.push(Block::StructuredDocumentTag(parse_structured_document_tag(
601 &mut reader,
602 resolver,
603 )?)),
604 _ => {}
605 },
606
607 Event::Empty(start) => match start.local_name().as_ref() {
608 b"p" => blocks.push(Block::Paragraph(Paragraph::new())),
609 b"sdt" => blocks.push(Block::StructuredDocumentTag(StructuredDocumentTag::new())),
610 _ => {}
611 },
612
613 _ => {}
614 }
615 }
616
617 Ok(blocks)
618}
619
620fn capture_header_footer_reference(
625 start: &BytesStart,
626 default_target: &mut Option<String>,
627 first_target: &mut Option<String>,
628 even_target: &mut Option<String>,
629) {
630 let type_value = raw_attribute(start, b"type");
631 match type_value.as_deref() {
632 Some("default") if default_target.is_none() => {
633 *default_target = raw_attribute(start, b"id")
634 }
635 Some("first") if first_target.is_none() => *first_target = raw_attribute(start, b"id"),
636 Some("even") if even_target.is_none() => *even_target = raw_attribute(start, b"id"),
637 _ => {}
638 }
639}
640
641fn parse_paragraph(reader: &mut Reader<'_>, resolver: &ImageResolver) -> Result<Paragraph> {
645 let mut paragraph = Paragraph::new();
646 let mut current_run_text: Option<String> = None;
647 let mut current_run_drawing: Option<RunContent> = None;
650 let mut current_run_note: Option<RunContent> = None;
656 let mut current_run_break: Option<RunContent> = None;
660 let mut current_run_bold = false;
661 let mut current_run_italic = false;
662 let mut current_run_underline: Option<UnderlineStyle> = None;
663 let mut current_run_underline_color: Option<String> = None;
664 let mut current_run_color: Option<String> = None;
665 let mut current_run_font_size: Option<u16> = None;
666 let mut current_run_font_family: Option<String> = None;
667 let mut current_run_highlight: Option<Highlight> = None;
668 let mut current_run_strike = false;
669 let mut current_run_vertical_align: Option<VerticalAlign> = None;
670 let mut current_run_small_caps = false;
671 let mut current_run_all_caps = false;
672 let mut current_run_shading_color: Option<String> = None;
673 let mut current_run_text_border = false;
674 let mut current_run_character_spacing: Option<i16> = None;
675 let mut current_run_vertical_position: Option<i16> = None;
676 let mut current_run_hidden = false;
677 let mut current_run_style_id: Option<String> = None;
678 let mut in_run = false;
686 let mut in_text = false;
687 let mut in_field = false;
695 let mut current_field: Option<Field> = None;
696 let mut current_hyperlink: Option<Hyperlink> = None;
705 let mut open_comment_ids: Vec<u32> = Vec::new();
713 let mut open_bookmarks: Vec<Bookmark> = Vec::new();
719
720 loop {
721 match reader.read_event()? {
722 Event::Eof => break,
723
724 Event::Start(start) => match start.local_name().as_ref() {
725 b"r" => {
726 in_run = true;
727 current_run_text = Some(String::new());
728 current_run_drawing = None;
729 current_run_note = None;
730 current_run_break = None;
731 current_run_bold = false;
732 current_run_italic = false;
733 current_run_underline = None;
734 current_run_underline_color = None;
735 current_run_color = None;
736 current_run_font_size = None;
737 current_run_font_family = None;
738 current_run_highlight = None;
739 current_run_strike = false;
740 current_run_vertical_align = None;
741 current_run_small_caps = false;
742 current_run_all_caps = false;
743 current_run_shading_color = None;
744 current_run_text_border = false;
745 current_run_character_spacing = None;
746 current_run_vertical_position = None;
747 current_run_hidden = false;
748 current_run_style_id = None;
749 }
750 b"t" => in_text = true,
751 b"drawing" => current_run_drawing = parse_drawing(reader, resolver)?,
752 b"pStyle" => paragraph.style_id = raw_attribute(&start, b"val"),
753 b"ilvl" => {
754 paragraph.numbering_level = raw_attribute(&start, b"val")
755 .and_then(|value| value.parse().ok())
756 .unwrap_or(0)
757 }
758 b"numId" => {
759 paragraph.numbering_id =
760 raw_attribute(&start, b"val").and_then(|value| value.parse().ok())
761 }
762 b"jc" => paragraph.alignment = alignment_from_attribute(&start),
763 b"b" => current_run_bold = on_off_attribute(&start),
764 b"i" => current_run_italic = on_off_attribute(&start),
765 b"u" => {
766 current_run_underline = underline_attribute(&start);
767 current_run_underline_color = raw_attribute(&start, b"color");
768 }
769 b"color" => current_run_color = raw_attribute(&start, b"val"),
770 b"sz" => {
771 current_run_font_size = raw_attribute(&start, b"val")
772 .and_then(|value| value.parse::<u16>().ok())
773 .map(|half_points| half_points / 2)
774 }
775 b"rFonts" => {
776 current_run_font_family =
777 raw_attribute(&start, b"ascii").or_else(|| raw_attribute(&start, b"hAnsi"))
778 }
779 b"highlight" => {
780 current_run_highlight = raw_attribute(&start, b"val")
781 .and_then(|value| Highlight::from_attribute_value(&value))
782 }
783 b"strike" => current_run_strike = on_off_attribute(&start),
784 b"vertAlign" => {
785 current_run_vertical_align = raw_attribute(&start, b"val")
786 .and_then(|value| VerticalAlign::from_attribute_value(&value))
787 }
788 b"smallCaps" => current_run_small_caps = on_off_attribute(&start),
789 b"caps" => current_run_all_caps = on_off_attribute(&start),
790 b"shd" => {
796 if in_run {
797 current_run_shading_color = raw_attribute(&start, b"fill");
798 } else {
799 paragraph.shading_color = raw_attribute(&start, b"fill");
800 }
801 }
802 b"bdr" => current_run_text_border = true,
803 b"spacing" => {
804 if in_run {
805 current_run_character_spacing = raw_attribute(&start, b"val")
806 .and_then(|value| value.parse::<i16>().ok());
807 } else {
808 paragraph.space_before = raw_attribute(&start, b"before")
809 .and_then(|value| value.parse::<u32>().ok());
810 paragraph.space_after = raw_attribute(&start, b"after")
811 .and_then(|value| value.parse::<u32>().ok());
812 paragraph.line_spacing = raw_attribute(&start, b"line")
813 .and_then(|value| value.parse::<u32>().ok());
814 }
815 }
816 b"position" => {
817 current_run_vertical_position =
818 raw_attribute(&start, b"val").and_then(|value| value.parse::<i16>().ok())
819 }
820 b"vanish" => current_run_hidden = true,
821 b"rStyle" => current_run_style_id = raw_attribute(&start, b"val"),
822 b"pBdr" => paragraph.border = true,
823 b"keepNext" => paragraph.keep_with_next = true,
824 b"keepLines" => paragraph.keep_lines_together = true,
825 b"pageBreakBefore" => paragraph.page_break_before = true,
826 b"contextualSpacing" => paragraph.contextual_spacing = true,
827 b"fldSimple" => {
828 in_field = true;
829 current_field =
830 field_from_instruction(raw_attribute(&start, b"instr").as_deref());
831 }
832 b"hyperlink" => current_hyperlink = hyperlink_from_start(&start, resolver),
833 _ => {}
834 },
835
836 Event::Empty(start) => match start.local_name().as_ref() {
837 b"r" if !in_field => {
838 paragraph.runs.push(Run {
839 hyperlink: current_hyperlink.clone(),
840 comment_ids: open_comment_ids.clone(),
841 bookmarks: open_bookmarks.clone(),
842 ..Run::new("")
843 });
844 }
845 b"pStyle" => paragraph.style_id = raw_attribute(&start, b"val"),
846 b"ilvl" => {
847 paragraph.numbering_level = raw_attribute(&start, b"val")
848 .and_then(|value| value.parse().ok())
849 .unwrap_or(0)
850 }
851 b"numId" => {
852 paragraph.numbering_id =
853 raw_attribute(&start, b"val").and_then(|value| value.parse().ok())
854 }
855 b"jc" => paragraph.alignment = alignment_from_attribute(&start),
856 b"b" => current_run_bold = on_off_attribute(&start),
857 b"i" => current_run_italic = on_off_attribute(&start),
858 b"u" => {
859 current_run_underline = underline_attribute(&start);
860 current_run_underline_color = raw_attribute(&start, b"color");
861 }
862 b"color" => current_run_color = raw_attribute(&start, b"val"),
863 b"sz" => {
864 current_run_font_size = raw_attribute(&start, b"val")
865 .and_then(|value| value.parse::<u16>().ok())
866 .map(|half_points| half_points / 2)
867 }
868 b"rFonts" => {
869 current_run_font_family =
870 raw_attribute(&start, b"ascii").or_else(|| raw_attribute(&start, b"hAnsi"))
871 }
872 b"highlight" => {
873 current_run_highlight = raw_attribute(&start, b"val")
874 .and_then(|value| Highlight::from_attribute_value(&value))
875 }
876 b"strike" => current_run_strike = on_off_attribute(&start),
877 b"vertAlign" => {
878 current_run_vertical_align = raw_attribute(&start, b"val")
879 .and_then(|value| VerticalAlign::from_attribute_value(&value))
880 }
881 b"smallCaps" => current_run_small_caps = on_off_attribute(&start),
882 b"caps" => current_run_all_caps = on_off_attribute(&start),
883 b"shd" => {
884 if in_run {
885 current_run_shading_color = raw_attribute(&start, b"fill");
886 } else {
887 paragraph.shading_color = raw_attribute(&start, b"fill");
888 }
889 }
890 b"bdr" => current_run_text_border = true,
891 b"spacing" => {
892 if in_run {
893 current_run_character_spacing = raw_attribute(&start, b"val")
894 .and_then(|value| value.parse::<i16>().ok());
895 } else {
896 paragraph.space_before = raw_attribute(&start, b"before")
897 .and_then(|value| value.parse::<u32>().ok());
898 paragraph.space_after = raw_attribute(&start, b"after")
899 .and_then(|value| value.parse::<u32>().ok());
900 paragraph.line_spacing = raw_attribute(&start, b"line")
901 .and_then(|value| value.parse::<u32>().ok());
902 }
903 }
904 b"position" => {
905 current_run_vertical_position =
906 raw_attribute(&start, b"val").and_then(|value| value.parse::<i16>().ok())
907 }
908 b"vanish" => current_run_hidden = true,
909 b"rStyle" => current_run_style_id = raw_attribute(&start, b"val"),
910 b"pBdr" => paragraph.border = true,
911 b"keepNext" => paragraph.keep_with_next = true,
912 b"keepLines" => paragraph.keep_lines_together = true,
913 b"pageBreakBefore" => paragraph.page_break_before = true,
914 b"contextualSpacing" => paragraph.contextual_spacing = true,
915 b"tab" => {
919 if let Some(position) =
920 raw_attribute(&start, b"pos").and_then(|value| value.parse::<i32>().ok())
921 {
922 let mut tab = TabStop::new(position);
923 if let Some(alignment) = raw_attribute(&start, b"val")
924 .and_then(|value| TabStopAlignment::from_attribute_value(&value))
925 {
926 tab = tab.with_alignment(alignment);
927 }
928 if let Some(leader) = raw_attribute(&start, b"leader")
929 .and_then(|value| TabLeader::from_attribute_value(&value))
930 {
931 tab = tab.with_leader(leader);
932 }
933 paragraph.tabs.push(tab);
934 }
935 }
936 b"br" => {
938 current_run_break = Some(RunContent::Break(BreakKind::from_attribute_value(
939 raw_attribute(&start, b"type").as_deref(),
940 )));
941 }
942 b"cr" => current_run_break = Some(RunContent::CarriageReturn),
943 b"fldSimple" => {
946 if let Some(field) =
947 field_from_instruction(raw_attribute(&start, b"instr").as_deref())
948 {
949 paragraph.runs.push(Run {
950 hyperlink: current_hyperlink.clone(),
951 comment_ids: open_comment_ids.clone(),
952 bookmarks: open_bookmarks.clone(),
953 ..Run::with_field(field)
954 });
955 }
956 }
957 b"commentRangeStart" => {
963 if let Some(id) =
964 raw_attribute(&start, b"id").and_then(|value| value.parse().ok())
965 {
966 open_comment_ids.push(id);
967 }
968 }
969 b"commentRangeEnd" => {
970 if let Some(id) =
971 raw_attribute(&start, b"id").and_then(|value| value.parse::<u32>().ok())
972 {
973 open_comment_ids.retain(|open_id| *open_id != id);
974 }
975 }
976 b"bookmarkStart" => {
981 if let (Some(id), Some(name)) = (
982 raw_attribute(&start, b"id").and_then(|value| value.parse().ok()),
983 raw_attribute(&start, b"name"),
984 ) {
985 open_bookmarks.push(Bookmark::new(id, name));
986 }
987 }
988 b"bookmarkEnd" => {
989 if let Some(id) =
990 raw_attribute(&start, b"id").and_then(|value| value.parse::<u32>().ok())
991 {
992 open_bookmarks.retain(|bookmark| bookmark.id != id);
993 }
994 }
995 b"commentReference" => {}
1001 b"footnoteReference" => {
1002 current_run_note = raw_attribute(&start, b"id")
1003 .and_then(|value| value.parse().ok())
1004 .map(|id| RunContent::NoteReference(NoteReference::Footnote(id)));
1005 }
1006 b"endnoteReference" => {
1007 current_run_note = raw_attribute(&start, b"id")
1008 .and_then(|value| value.parse().ok())
1009 .map(|id| RunContent::NoteReference(NoteReference::Endnote(id)));
1010 }
1011 b"footnoteRef" => {
1012 current_run_note = Some(RunContent::NoteMarker(NoteKind::Footnote))
1013 }
1014 b"endnoteRef" => current_run_note = Some(RunContent::NoteMarker(NoteKind::Endnote)),
1015 _ => {}
1016 },
1017
1018 Event::Text(text) if in_text => {
1019 if let Some(buffer) = current_run_text.as_mut() {
1020 let text = xml_core::decode_text(&text)?;
1021 buffer.push_str(&text);
1022 }
1023 }
1024
1025 Event::GeneralRef(reference) if in_text => {
1033 if let Some(buffer) = current_run_text.as_mut() {
1034 if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
1035 buffer.push_str(&resolved);
1036 }
1037 }
1038 }
1039
1040 Event::End(end) => match end.local_name().as_ref() {
1041 b"t" => in_text = false,
1042 b"r" if in_field => {
1043 in_run = false;
1046 }
1047 b"r" => {
1048 in_run = false;
1049 let bold = current_run_bold;
1050 let italic = current_run_italic;
1051 let underline = current_run_underline;
1052 let underline_color = current_run_underline_color.take();
1053 let color = current_run_color.take();
1054 let font_size = current_run_font_size;
1055 let font_family = current_run_font_family.take();
1056 let highlight = current_run_highlight.take();
1057 let strike = current_run_strike;
1058 let vertical_align = current_run_vertical_align.take();
1059 let small_caps = current_run_small_caps;
1060 let all_caps = current_run_all_caps;
1061 let shading_color = current_run_shading_color.take();
1062 let text_border = current_run_text_border;
1063 let character_spacing = current_run_character_spacing;
1064 let vertical_position = current_run_vertical_position;
1065 let hidden = current_run_hidden;
1066 let style_id = current_run_style_id.take();
1067 let hyperlink = current_hyperlink.clone();
1068 let comment_ids = open_comment_ids.clone();
1069 let bookmarks = open_bookmarks.clone();
1070 let run = if let Some(drawing_content) = current_run_drawing.take() {
1071 Run {
1072 content: drawing_content,
1073 bold,
1074 italic,
1075 underline,
1076 underline_color,
1077 color,
1078 font_size,
1079 font_family,
1080 highlight,
1081 strike,
1082 vertical_align,
1083 small_caps,
1084 all_caps,
1085 shading_color,
1086 text_border,
1087 character_spacing,
1088 vertical_position,
1089 hidden,
1090 style_id,
1091 hyperlink,
1092 comment_ids,
1093 bookmarks,
1094 }
1095 } else if let Some(note_content) = current_run_note.take() {
1096 Run {
1097 content: note_content,
1098 bold,
1099 italic,
1100 underline,
1101 underline_color,
1102 color,
1103 font_size,
1104 font_family,
1105 highlight,
1106 strike,
1107 vertical_align,
1108 small_caps,
1109 all_caps,
1110 shading_color,
1111 text_border,
1112 character_spacing,
1113 vertical_position,
1114 hidden,
1115 style_id,
1116 hyperlink,
1117 comment_ids,
1118 bookmarks,
1119 }
1120 } else if let Some(break_content) = current_run_break.take() {
1121 Run {
1122 content: break_content,
1123 bold,
1124 italic,
1125 underline,
1126 underline_color,
1127 color,
1128 font_size,
1129 font_family,
1130 highlight,
1131 strike,
1132 vertical_align,
1133 small_caps,
1134 all_caps,
1135 shading_color,
1136 text_border,
1137 character_spacing,
1138 vertical_position,
1139 hidden,
1140 style_id,
1141 hyperlink,
1142 comment_ids,
1143 bookmarks,
1144 }
1145 } else {
1146 let text = current_run_text.take().unwrap_or_default();
1147 Run {
1148 content: RunContent::Text(text),
1149 bold,
1150 italic,
1151 underline,
1152 underline_color,
1153 color,
1154 font_size,
1155 font_family,
1156 highlight,
1157 strike,
1158 vertical_align,
1159 small_caps,
1160 all_caps,
1161 shading_color,
1162 text_border,
1163 character_spacing,
1164 vertical_position,
1165 hidden,
1166 style_id,
1167 hyperlink,
1168 comment_ids,
1169 bookmarks,
1170 }
1171 };
1172 paragraph.runs.push(run);
1173 }
1174 b"fldSimple" => {
1175 if let Some(field) = current_field.take() {
1176 paragraph.runs.push(Run {
1177 content: RunContent::Field(field),
1178 bold: current_run_bold,
1179 italic: current_run_italic,
1180 underline: current_run_underline,
1181 underline_color: current_run_underline_color.take(),
1182 color: current_run_color.take(),
1183 font_size: current_run_font_size,
1184 font_family: current_run_font_family.take(),
1185 highlight: current_run_highlight.take(),
1186 strike: current_run_strike,
1187 vertical_align: current_run_vertical_align.take(),
1188 small_caps: current_run_small_caps,
1189 all_caps: current_run_all_caps,
1190 shading_color: current_run_shading_color.take(),
1191 text_border: current_run_text_border,
1192 character_spacing: current_run_character_spacing,
1193 vertical_position: current_run_vertical_position,
1194 hidden: current_run_hidden,
1195 style_id: current_run_style_id.take(),
1196 hyperlink: current_hyperlink.clone(),
1197 comment_ids: open_comment_ids.clone(),
1198 bookmarks: open_bookmarks.clone(),
1199 });
1200 }
1201 in_field = false;
1202 }
1203 b"hyperlink" => current_hyperlink = None,
1204 b"p" => break,
1205 _ => {}
1206 },
1207
1208 _ => {}
1209 }
1210 }
1211
1212 Ok(paragraph)
1213}
1214
1215fn parse_table(reader: &mut Reader<'_>, resolver: &ImageResolver) -> Result<Table> {
1221 let mut table = Table::new();
1222 let mut current_row: Option<TableRow> = None;
1223 let mut current_cell: Option<TableCell> = None;
1224
1225 let mut in_table_properties = false;
1233 let mut in_tbl_borders = false;
1234 let mut in_tc_borders = false;
1235 let mut in_tc_mar = false;
1236
1237 loop {
1238 match reader.read_event()? {
1239 Event::Eof => break,
1240
1241 Event::Start(start) => match start.local_name().as_ref() {
1242 b"tblPr" => in_table_properties = true,
1243 b"tblBorders" => in_tbl_borders = true,
1244 b"tcBorders" => in_tc_borders = true,
1245 b"tcMar" => in_tc_mar = true,
1246 b"tr" => current_row = Some(TableRow::new()),
1247 b"tc" => current_cell = Some(TableCell::new()),
1248 b"p" => {
1249 let paragraph = parse_paragraph(reader, resolver)?;
1250 if let Some(cell) = current_cell.as_mut() {
1251 cell.blocks.push(Block::Paragraph(paragraph));
1252 }
1253 }
1254 b"tbl" => {
1258 let nested_table = parse_table(reader, resolver)?;
1259 if let Some(cell) = current_cell.as_mut() {
1260 cell.blocks.push(Block::Table(nested_table));
1261 }
1262 }
1263 b"tblStyle" => {
1264 table.style_id = raw_attribute(&start, b"val");
1265 }
1266 b"jc" if in_table_properties => {
1267 table.alignment = alignment_from_attribute(&start);
1268 }
1269 b"tblInd" => {
1270 table.indent_twips =
1271 raw_attribute(&start, b"w").and_then(|value| value.parse::<u32>().ok());
1272 }
1273 b"top" | b"left" | b"bottom" | b"right" => {
1274 apply_table_or_cell_side(
1275 &start,
1276 start.local_name().as_ref(),
1277 in_tbl_borders,
1278 in_table_properties,
1279 in_tc_borders,
1280 in_tc_mar,
1281 &mut table,
1282 current_cell.as_mut(),
1283 );
1284 }
1285 b"shd" => {
1286 if let Some(cell) = current_cell.as_mut() {
1287 cell.shading_color = raw_attribute(&start, b"fill");
1288 }
1289 }
1290 b"textDirection" => {
1291 if let Some(cell) = current_cell.as_mut() {
1292 cell.text_direction = raw_attribute(&start, b"val")
1293 .and_then(|value| TextDirection::from_attribute_value(&value));
1294 }
1295 }
1296 b"vAlign" => {
1297 if let Some(cell) = current_cell.as_mut() {
1298 cell.vertical_alignment = raw_attribute(&start, b"val")
1299 .and_then(|value| CellVerticalAlign::from_attribute_value(&value));
1300 }
1301 }
1302 b"cantSplit" => {
1303 if let Some(row) = current_row.as_mut() {
1304 row.cant_split = true;
1305 }
1306 }
1307 b"trHeight" => {
1308 if let Some(row) = current_row.as_mut() {
1309 row.height_twips = raw_attribute(&start, b"val")
1310 .and_then(|value| value.parse::<u32>().ok());
1311 }
1312 }
1313 b"tblHeader" => {
1314 if let Some(row) = current_row.as_mut() {
1315 row.repeat_as_header_row = true;
1316 }
1317 }
1318 b"gridSpan" => {
1319 if let Some(cell) = current_cell.as_mut() {
1320 cell.horizontal_span = raw_attribute(&start, b"val")
1321 .and_then(|value| value.parse::<u32>().ok());
1322 }
1323 }
1324 b"vMerge" => {
1325 if let Some(cell) = current_cell.as_mut() {
1326 cell.vertical_merge = Some(vertical_merge_attribute(&start));
1327 }
1328 }
1329 b"gridCol" => {
1337 if let Some(width) =
1338 raw_attribute(&start, b"w").and_then(|value| value.parse::<u32>().ok())
1339 {
1340 table.column_widths.push(width);
1341 }
1342 }
1343 _ => {}
1344 },
1345
1346 Event::Empty(start) => match start.local_name().as_ref() {
1347 b"p" => {
1348 if let Some(cell) = current_cell.as_mut() {
1349 cell.blocks.push(Block::Paragraph(Paragraph::new()));
1350 }
1351 }
1352 b"tblStyle" => {
1353 table.style_id = raw_attribute(&start, b"val");
1354 }
1355 b"jc" if in_table_properties => {
1356 table.alignment = alignment_from_attribute(&start);
1357 }
1358 b"tblInd" => {
1359 table.indent_twips =
1360 raw_attribute(&start, b"w").and_then(|value| value.parse::<u32>().ok());
1361 }
1362 b"top" | b"left" | b"bottom" | b"right" => {
1363 apply_table_or_cell_side(
1364 &start,
1365 start.local_name().as_ref(),
1366 in_tbl_borders,
1367 in_table_properties,
1368 in_tc_borders,
1369 in_tc_mar,
1370 &mut table,
1371 current_cell.as_mut(),
1372 );
1373 }
1374 b"shd" => {
1375 if let Some(cell) = current_cell.as_mut() {
1376 cell.shading_color = raw_attribute(&start, b"fill");
1377 }
1378 }
1379 b"textDirection" => {
1380 if let Some(cell) = current_cell.as_mut() {
1381 cell.text_direction = raw_attribute(&start, b"val")
1382 .and_then(|value| TextDirection::from_attribute_value(&value));
1383 }
1384 }
1385 b"vAlign" => {
1386 if let Some(cell) = current_cell.as_mut() {
1387 cell.vertical_alignment = raw_attribute(&start, b"val")
1388 .and_then(|value| CellVerticalAlign::from_attribute_value(&value));
1389 }
1390 }
1391 b"cantSplit" => {
1392 if let Some(row) = current_row.as_mut() {
1393 row.cant_split = true;
1394 }
1395 }
1396 b"trHeight" => {
1397 if let Some(row) = current_row.as_mut() {
1398 row.height_twips = raw_attribute(&start, b"val")
1399 .and_then(|value| value.parse::<u32>().ok());
1400 }
1401 }
1402 b"tblHeader" => {
1403 if let Some(row) = current_row.as_mut() {
1404 row.repeat_as_header_row = true;
1405 }
1406 }
1407 b"gridSpan" => {
1408 if let Some(cell) = current_cell.as_mut() {
1409 cell.horizontal_span = raw_attribute(&start, b"val")
1410 .and_then(|value| value.parse::<u32>().ok());
1411 }
1412 }
1413 b"vMerge" => {
1414 if let Some(cell) = current_cell.as_mut() {
1415 cell.vertical_merge = Some(vertical_merge_attribute(&start));
1416 }
1417 }
1418 b"gridCol" => {
1419 if let Some(width) =
1420 raw_attribute(&start, b"w").and_then(|value| value.parse::<u32>().ok())
1421 {
1422 table.column_widths.push(width);
1423 }
1424 }
1425 _ => {}
1426 },
1427
1428 Event::End(end) => match end.local_name().as_ref() {
1429 b"tblPr" => in_table_properties = false,
1430 b"tblBorders" => in_tbl_borders = false,
1431 b"tcBorders" => in_tc_borders = false,
1432 b"tcMar" => in_tc_mar = false,
1433 b"tc" => {
1434 if let (Some(cell), Some(row)) = (current_cell.take(), current_row.as_mut()) {
1435 row.cells.push(cell);
1436 }
1437 }
1438 b"tr" => {
1439 if let Some(row) = current_row.take() {
1440 table.rows.push(row);
1441 }
1442 }
1443 b"tbl" => break,
1444 _ => {}
1445 },
1446
1447 _ => {}
1448 }
1449 }
1450
1451 Ok(table)
1452}
1453
1454#[allow(clippy::too_many_arguments)]
1462fn apply_table_or_cell_side(
1463 start: &BytesStart,
1464 tag_name: &[u8],
1465 in_tbl_borders: bool,
1466 in_table_properties: bool,
1467 in_tc_borders: bool,
1468 in_tc_mar: bool,
1469 table: &mut Table,
1470 current_cell: Option<&mut TableCell>,
1471) {
1472 if in_tc_mar {
1473 if let Some(cell) = current_cell {
1474 let value = raw_attribute(start, b"w").and_then(|value| value.parse::<u32>().ok());
1475 match tag_name {
1476 b"top" => cell.margin_top_twips = value,
1477 b"left" => cell.margin_left_twips = value,
1478 b"bottom" => cell.margin_bottom_twips = value,
1479 b"right" => cell.margin_right_twips = value,
1480 _ => {}
1481 }
1482 }
1483 } else if in_tc_borders {
1484 if let Some(cell) = current_cell {
1485 cell.border = true;
1486 }
1487 } else if in_tbl_borders && in_table_properties {
1488 if let Some(color) = raw_attribute(start, b"color") {
1489 if color != "auto" {
1490 table.border_color = Some(color);
1491 }
1492 }
1493 }
1494}
1495
1496fn parse_drawing(reader: &mut Reader<'_>, resolver: &ImageResolver) -> Result<Option<RunContent>> {
1505 let mut width_emu: i64 = 0;
1506 let mut height_emu: i64 = 0;
1507 let mut description = String::new();
1508 let mut image_relationship_id: Option<String> = None;
1509 let mut chart_relationship_id: Option<String> = None;
1510 let mut shape_properties: Option<ShapeProperties> = None;
1514
1515 loop {
1516 match reader.read_event()? {
1517 Event::Eof => break,
1518
1519 Event::Start(start) if start.local_name().as_ref() == b"spPr" => {
1523 shape_properties = Some(drawing::read_shape_properties(reader)?);
1524 }
1525 Event::Empty(start) if start.local_name().as_ref() == b"spPr" => {
1526 shape_properties = Some(ShapeProperties::new());
1527 }
1528
1529 Event::Start(start) | Event::Empty(start) => match start.local_name().as_ref() {
1530 b"extent" => {
1531 width_emu = raw_attribute(&start, b"cx")
1532 .and_then(|value| value.parse().ok())
1533 .unwrap_or(0);
1534 height_emu = raw_attribute(&start, b"cy")
1535 .and_then(|value| value.parse().ok())
1536 .unwrap_or(0);
1537 }
1538 b"docPr" => description = raw_attribute(&start, b"descr").unwrap_or_default(),
1539 b"blip" => image_relationship_id = raw_attribute(&start, b"embed"),
1540 b"chart" => chart_relationship_id = raw_attribute(&start, b"id"),
1544 _ => {}
1545 },
1546
1547 Event::End(end) if end.local_name().as_ref() == b"drawing" => break,
1548
1549 _ => {}
1550 }
1551 }
1552
1553 if let Some(id) = image_relationship_id {
1554 let mut image = resolver.resolve(&id, width_emu, height_emu, description)?;
1555 if let Some(mut shape_properties) = shape_properties {
1556 shape_properties.transform = None;
1563
1564 if matches!(
1571 shape_properties.geometry,
1572 Some(Geometry::Preset(PresetShape::Rectangle))
1573 ) {
1574 shape_properties.geometry = None;
1575 }
1576
1577 let is_fixed_default = shape_properties.geometry.is_none()
1582 && shape_properties.fill.is_none()
1583 && shape_properties.line.is_none()
1584 && shape_properties.effects.is_none();
1585 if !is_fixed_default {
1586 image = image.with_shape_properties(shape_properties);
1587 }
1588 }
1589 return Ok(Some(RunContent::Image(Box::new(image))));
1590 }
1591
1592 if let Some(id) = chart_relationship_id {
1593 let chart_space = resolver.resolve_chart(&id)?;
1594 let chart = EmbeddedChart::new(chart_space, width_emu, height_emu).with_name(description);
1595 return Ok(Some(RunContent::Chart(Box::new(chart))));
1596 }
1597
1598 Ok(None)
1599}
1600
1601fn on_off_attribute(start: &BytesStart) -> bool {
1605 match raw_attribute(start, b"val") {
1606 None => true,
1607 Some(value) => matches!(value.as_str(), "true" | "1" | "on"),
1608 }
1609}
1610
1611fn underline_attribute(start: &BytesStart) -> Option<UnderlineStyle> {
1616 match raw_attribute(start, b"val") {
1617 None => Some(UnderlineStyle::Single),
1618 Some(value) => UnderlineStyle::from_attribute_value(&value),
1619 }
1620}
1621
1622fn vertical_merge_attribute(start: &BytesStart) -> VerticalMerge {
1629 match raw_attribute(start, b"val").as_deref() {
1630 Some("restart") => VerticalMerge::Restart,
1631 _ => VerticalMerge::Continue,
1632 }
1633}
1634
1635fn alignment_from_attribute(start: &BytesStart) -> Option<Alignment> {
1640 match raw_attribute(start, b"val")?.as_str() {
1641 "left" | "start" => Some(Alignment::Left),
1642 "center" => Some(Alignment::Center),
1643 "right" | "end" => Some(Alignment::Right),
1644 "both" => Some(Alignment::Justify),
1645 _ => None,
1646 }
1647}
1648
1649fn raw_attribute(start: &BytesStart, name: &[u8]) -> Option<String> {
1660 start
1661 .attributes()
1662 .flatten()
1663 .find(|attribute| attribute.key.local_name().as_ref() == name)
1664 .and_then(|attribute| {
1665 xml_core::decode_attribute_value(attribute.value.as_ref())
1666 .ok()
1667 .flatten()
1668 })
1669}
1670
1671fn field_from_instruction(instr: Option<&str>) -> Option<Field> {
1677 let first_token = instr?.split_whitespace().next()?;
1678 if first_token.eq_ignore_ascii_case("PAGE") {
1679 Some(Field::PageNumber)
1680 } else if first_token.eq_ignore_ascii_case("NUMPAGES") {
1681 Some(Field::TotalPages)
1682 } else {
1683 None
1684 }
1685}
1686
1687fn hyperlink_from_start(start: &BytesStart, resolver: &ImageResolver) -> Option<Hyperlink> {
1695 if let Some(relationship_id) = raw_attribute(start, b"id") {
1696 return resolver
1697 .part_relationships
1698 .by_id(&relationship_id)
1699 .map(|relationship| Hyperlink::External(relationship.target.clone()));
1700 }
1701 raw_attribute(start, b"anchor").map(Hyperlink::Internal)
1702}
1703
1704fn parse_styles_xml(xml: &str) -> Result<Vec<Style>> {
1709 let mut styles = Vec::new();
1710 let mut reader = Reader::from_xml_str(xml);
1711
1712 loop {
1713 match reader.read_event()? {
1714 Event::Eof => break,
1715 Event::Start(start) if start.local_name().as_ref() == b"style" => {
1716 if let Some(style) = parse_style(&mut reader, &start)? {
1717 styles.push(style);
1718 }
1719 }
1720 _ => {}
1721 }
1722 }
1723
1724 Ok(styles)
1725}
1726
1727fn parse_style(reader: &mut Reader<'_>, start: &BytesStart) -> Result<Option<Style>> {
1733 let kind = match raw_attribute(start, b"type").as_deref() {
1734 Some("paragraph") => StyleKind::Paragraph,
1735 Some("character") => StyleKind::Character,
1736 _ => {
1737 skip_to_end(reader, b"style")?;
1738 return Ok(None);
1739 }
1740 };
1741 let id = raw_attribute(start, b"styleId").unwrap_or_default();
1742
1743 let mut name = String::new();
1744 let mut based_on: Option<String> = None;
1745 let mut alignment: Option<Alignment> = None;
1746 let mut bold = false;
1747 let mut italic = false;
1748 let mut underline: Option<UnderlineStyle> = None;
1749 let mut underline_color: Option<String> = None;
1750 let mut color: Option<String> = None;
1751 let mut font_size: Option<u16> = None;
1752 let mut font_family: Option<String> = None;
1753 let mut highlight: Option<Highlight> = None;
1754 let mut strike = false;
1755 let mut vertical_align: Option<VerticalAlign> = None;
1756 let mut small_caps = false;
1757 let mut all_caps = false;
1758 let mut shading_color: Option<String> = None;
1759 let mut text_border = false;
1760 let mut character_spacing: Option<i16> = None;
1761 let mut vertical_position: Option<i16> = None;
1762 let mut hidden = false;
1763 let mut line_spacing: Option<u32> = None;
1764 let mut space_before: Option<u32> = None;
1765 let mut space_after: Option<u32> = None;
1766 let mut paragraph_shading_color: Option<String> = None;
1767 let mut paragraph_border = false;
1768 let mut keep_with_next = false;
1769 let mut keep_lines_together = false;
1770 let mut page_break_before = false;
1771 let mut tabs: Vec<TabStop> = Vec::new();
1772 let mut contextual_spacing = false;
1773 let mut in_paragraph_properties = false;
1780
1781 loop {
1782 match reader.read_event()? {
1783 Event::Eof => break,
1784
1785 Event::Start(start) | Event::Empty(start) => match start.local_name().as_ref() {
1786 b"name" => name = raw_attribute(&start, b"val").unwrap_or_default(),
1787 b"basedOn" => based_on = raw_attribute(&start, b"val"),
1788 b"pPr" => in_paragraph_properties = true,
1789 b"rPr" => in_paragraph_properties = false,
1790 b"pBdr" => paragraph_border = true,
1791 b"keepNext" => keep_with_next = true,
1792 b"keepLines" => keep_lines_together = true,
1793 b"pageBreakBefore" => page_break_before = true,
1794 b"contextualSpacing" => contextual_spacing = true,
1795 b"tab" => {
1796 if let Some(position) =
1797 raw_attribute(&start, b"pos").and_then(|value| value.parse::<i32>().ok())
1798 {
1799 let mut tab = TabStop::new(position);
1800 if let Some(alignment) = raw_attribute(&start, b"val")
1801 .and_then(|value| TabStopAlignment::from_attribute_value(&value))
1802 {
1803 tab = tab.with_alignment(alignment);
1804 }
1805 if let Some(leader) = raw_attribute(&start, b"leader")
1806 .and_then(|value| TabLeader::from_attribute_value(&value))
1807 {
1808 tab = tab.with_leader(leader);
1809 }
1810 tabs.push(tab);
1811 }
1812 }
1813 b"jc" => alignment = alignment_from_attribute(&start),
1814 b"b" => bold = on_off_attribute(&start),
1815 b"i" => italic = on_off_attribute(&start),
1816 b"u" => {
1817 underline = underline_attribute(&start);
1818 underline_color = raw_attribute(&start, b"color");
1819 }
1820 b"color" => color = raw_attribute(&start, b"val"),
1821 b"sz" => {
1822 font_size = raw_attribute(&start, b"val")
1823 .and_then(|value| value.parse::<u16>().ok())
1824 .map(|half_points| half_points / 2)
1825 }
1826 b"rFonts" => {
1827 font_family =
1828 raw_attribute(&start, b"ascii").or_else(|| raw_attribute(&start, b"hAnsi"))
1829 }
1830 b"highlight" => {
1831 highlight = raw_attribute(&start, b"val")
1832 .and_then(|value| Highlight::from_attribute_value(&value))
1833 }
1834 b"strike" => strike = on_off_attribute(&start),
1835 b"vertAlign" => {
1836 vertical_align = raw_attribute(&start, b"val")
1837 .and_then(|value| VerticalAlign::from_attribute_value(&value))
1838 }
1839 b"smallCaps" => small_caps = on_off_attribute(&start),
1840 b"caps" => all_caps = on_off_attribute(&start),
1841 b"shd" => {
1842 if in_paragraph_properties {
1843 paragraph_shading_color = raw_attribute(&start, b"fill");
1844 } else {
1845 shading_color = raw_attribute(&start, b"fill");
1846 }
1847 }
1848 b"bdr" => text_border = true,
1849 b"spacing" => {
1850 if in_paragraph_properties {
1851 space_before = raw_attribute(&start, b"before")
1852 .and_then(|value| value.parse::<u32>().ok());
1853 space_after = raw_attribute(&start, b"after")
1854 .and_then(|value| value.parse::<u32>().ok());
1855 line_spacing = raw_attribute(&start, b"line")
1856 .and_then(|value| value.parse::<u32>().ok());
1857 } else {
1858 character_spacing = raw_attribute(&start, b"val")
1859 .and_then(|value| value.parse::<i16>().ok());
1860 }
1861 }
1862 b"position" => {
1863 vertical_position =
1864 raw_attribute(&start, b"val").and_then(|value| value.parse::<i16>().ok())
1865 }
1866 b"vanish" => hidden = true,
1867 _ => {}
1868 },
1869
1870 Event::End(end) if end.local_name().as_ref() == b"style" => break,
1871
1872 _ => {}
1873 }
1874 }
1875
1876 let mut style = Style::new(id, name, kind)
1877 .with_bold(bold)
1878 .with_italic(italic);
1879 if let Some(underline) = underline {
1880 style = style.with_underline(underline);
1881 }
1882 if let Some(underline_color) = underline_color {
1883 style = style.with_underline_color(underline_color);
1884 }
1885 if let Some(based_on) = based_on {
1886 style = style.with_based_on(based_on);
1887 }
1888 if let Some(alignment) = alignment {
1889 style = style.with_alignment(alignment);
1890 }
1891 if let Some(color) = color {
1892 style = style.with_color(color);
1893 }
1894 if let Some(font_size) = font_size {
1895 style = style.with_font_size(font_size);
1896 }
1897 if let Some(font_family) = font_family {
1898 style = style.with_font_family(font_family);
1899 }
1900 if let Some(highlight) = highlight {
1901 style = style.with_highlight(highlight);
1902 }
1903 style = style.with_strike(strike);
1904 if let Some(vertical_align) = vertical_align {
1905 style = style.with_vertical_align(vertical_align);
1906 }
1907 style = style
1908 .with_small_caps(small_caps)
1909 .with_all_caps(all_caps)
1910 .with_text_border(text_border)
1911 .with_hidden(hidden);
1912 if let Some(shading_color) = shading_color {
1913 style = style.with_shading_color(shading_color);
1914 }
1915 if let Some(character_spacing) = character_spacing {
1916 style = style.with_character_spacing(character_spacing);
1917 }
1918 if let Some(vertical_position) = vertical_position {
1919 style = style.with_vertical_position(vertical_position);
1920 }
1921 style = style.with_paragraph_border(paragraph_border);
1922 if let Some(paragraph_shading_color) = paragraph_shading_color {
1923 style = style.with_paragraph_shading_color(paragraph_shading_color);
1924 }
1925 if let Some(line_spacing) = line_spacing {
1926 style = style.with_line_spacing(line_spacing);
1927 }
1928 if let Some(space_before) = space_before {
1929 style = style.with_space_before(space_before);
1930 }
1931 if let Some(space_after) = space_after {
1932 style = style.with_space_after(space_after);
1933 }
1934 style = style
1935 .with_keep_with_next(keep_with_next)
1936 .with_keep_lines_together(keep_lines_together)
1937 .with_page_break_before(page_break_before)
1938 .with_contextual_spacing(contextual_spacing);
1939 for tab in tabs {
1940 style = style.with_tab(tab);
1941 }
1942
1943 Ok(Some(style))
1944}
1945
1946fn skip_to_end(reader: &mut Reader<'_>, local_name: &[u8]) -> Result<()> {
1950 loop {
1951 match reader.read_event()? {
1952 Event::Eof => break,
1953 Event::End(end) if end.local_name().as_ref() == local_name => break,
1954 _ => {}
1955 }
1956 }
1957 Ok(())
1958}
1959
1960fn parse_numbering_xml(xml: &str) -> Result<Vec<NumberingDefinition>> {
1970 let mut abstract_nums: HashMap<u32, Vec<ListLevel>> = HashMap::new();
1971 let mut definitions = Vec::new();
1972 let mut reader = Reader::from_xml_str(xml);
1973
1974 loop {
1975 match reader.read_event()? {
1976 Event::Eof => break,
1977
1978 Event::Start(start) => match start.local_name().as_ref() {
1979 b"abstractNum" => match raw_attribute(&start, b"abstractNumId")
1980 .and_then(|value| value.parse().ok())
1981 {
1982 Some(id) => {
1983 let levels = parse_abstract_num(&mut reader)?;
1984 abstract_nums.insert(id, levels);
1985 }
1986 None => skip_to_end(&mut reader, b"abstractNum")?,
1987 },
1988 b"num" => {
1989 match raw_attribute(&start, b"numId").and_then(|value| value.parse().ok()) {
1990 Some(num_id) => {
1991 let abstract_num_id = parse_num_abstract_ref(&mut reader)?;
1992 let levels = abstract_num_id
1993 .and_then(|id| abstract_nums.get(&id).cloned())
1994 .unwrap_or_default();
1995 definitions.push(NumberingDefinition::new(num_id, levels));
1996 }
1997 None => skip_to_end(&mut reader, b"num")?,
1998 }
1999 }
2000 _ => {}
2001 },
2002
2003 _ => {}
2004 }
2005 }
2006
2007 Ok(definitions)
2008}
2009
2010fn parse_abstract_num(reader: &mut Reader<'_>) -> Result<Vec<ListLevel>> {
2016 let mut levels = Vec::new();
2017
2018 loop {
2019 match reader.read_event()? {
2020 Event::Eof => break,
2021 Event::Start(start) if start.local_name().as_ref() == b"lvl" => {
2022 levels.push(parse_lvl(reader)?)
2023 }
2024 Event::End(end) if end.local_name().as_ref() == b"abstractNum" => break,
2025 _ => {}
2026 }
2027 }
2028
2029 Ok(levels)
2030}
2031
2032fn parse_lvl(reader: &mut Reader<'_>) -> Result<ListLevel> {
2037 let mut format = NumberFormat::Decimal;
2038 let mut text = String::new();
2039 let mut indent_twips: i32 = 0;
2040 let mut hanging_twips: i32 = 0;
2041
2042 loop {
2043 match reader.read_event()? {
2044 Event::Eof => break,
2045
2046 Event::Start(start) | Event::Empty(start) => match start.local_name().as_ref() {
2047 b"numFmt" => {
2048 format = number_format_from_attribute(&start).unwrap_or(NumberFormat::Decimal)
2049 }
2050 b"lvlText" => text = raw_attribute(&start, b"val").unwrap_or_default(),
2051 b"ind" => {
2052 indent_twips = raw_attribute(&start, b"left")
2053 .and_then(|value| value.parse().ok())
2054 .unwrap_or(0);
2055 hanging_twips = raw_attribute(&start, b"hanging")
2056 .and_then(|value| value.parse().ok())
2057 .unwrap_or(0);
2058 }
2059 _ => {}
2060 },
2061
2062 Event::End(end) if end.local_name().as_ref() == b"lvl" => break,
2063
2064 _ => {}
2065 }
2066 }
2067
2068 Ok(ListLevel::new(format, text, indent_twips, hanging_twips))
2069}
2070
2071fn parse_num_abstract_ref(reader: &mut Reader<'_>) -> Result<Option<u32>> {
2075 let mut abstract_num_id = None;
2076
2077 loop {
2078 match reader.read_event()? {
2079 Event::Eof => break,
2080
2081 Event::Start(start) | Event::Empty(start)
2082 if start.local_name().as_ref() == b"abstractNumId" =>
2083 {
2084 abstract_num_id =
2085 raw_attribute(&start, b"val").and_then(|value| value.parse().ok());
2086 }
2087
2088 Event::End(end) if end.local_name().as_ref() == b"num" => break,
2089
2090 _ => {}
2091 }
2092 }
2093
2094 Ok(abstract_num_id)
2095}
2096
2097fn number_format_from_attribute(start: &BytesStart) -> Option<NumberFormat> {
2100 match raw_attribute(start, b"val")?.as_str() {
2101 "bullet" => Some(NumberFormat::Bullet),
2102 "decimal" => Some(NumberFormat::Decimal),
2103 "lowerLetter" => Some(NumberFormat::LowerLetter),
2104 "upperLetter" => Some(NumberFormat::UpperLetter),
2105 "lowerRoman" => Some(NumberFormat::LowerRoman),
2106 "upperRoman" => Some(NumberFormat::UpperRoman),
2107 _ => None,
2108 }
2109}
2110
2111fn parse_notes_xml(
2120 xml: &str,
2121 resolver: &ImageResolver,
2122 note_element_name: &[u8],
2123) -> Result<Vec<Note>> {
2124 let mut notes = Vec::new();
2125 let mut reader = Reader::from_xml_str(xml);
2126
2127 loop {
2128 match reader.read_event()? {
2129 Event::Eof => break,
2130
2131 Event::Start(start) if start.local_name().as_ref() == note_element_name => {
2132 let is_special_note = matches!(
2133 raw_attribute(&start, b"type").as_deref(),
2134 Some("separator" | "continuationSeparator" | "continuationNotice")
2135 );
2136 let id = raw_attribute(&start, b"id").and_then(|value| value.parse().ok());
2137
2138 match (is_special_note, id) {
2139 (false, Some(id)) => {
2140 let blocks = parse_note_blocks(&mut reader, resolver, note_element_name)?;
2141 notes.push(Note { id, blocks });
2142 }
2143 _ => skip_to_end(&mut reader, note_element_name)?,
2144 }
2145 }
2146
2147 _ => {}
2148 }
2149 }
2150
2151 Ok(notes)
2152}
2153
2154fn parse_note_blocks(
2166 reader: &mut Reader<'_>,
2167 resolver: &ImageResolver,
2168 stop_local_name: &[u8],
2169) -> Result<Vec<Block>> {
2170 let mut blocks = Vec::new();
2171
2172 loop {
2173 match reader.read_event()? {
2174 Event::Eof => break,
2175
2176 Event::Start(start) => match start.local_name().as_ref() {
2177 b"p" => blocks.push(Block::Paragraph(parse_paragraph(reader, resolver)?)),
2178 b"tbl" => blocks.push(Block::Table(parse_table(reader, resolver)?)),
2179 b"sdt" => blocks.push(Block::StructuredDocumentTag(parse_structured_document_tag(
2180 reader, resolver,
2181 )?)),
2182 _ => {}
2183 },
2184
2185 Event::Empty(start) => match start.local_name().as_ref() {
2186 b"p" => blocks.push(Block::Paragraph(Paragraph::new())),
2187 b"sdt" => blocks.push(Block::StructuredDocumentTag(StructuredDocumentTag::new())),
2188 _ => {}
2189 },
2190
2191 Event::End(end) if end.local_name().as_ref() == stop_local_name => break,
2192
2193 _ => {}
2194 }
2195 }
2196
2197 Ok(blocks)
2198}
2199
2200fn parse_structured_document_tag(
2209 reader: &mut Reader<'_>,
2210 resolver: &ImageResolver,
2211) -> Result<StructuredDocumentTag> {
2212 let mut sdt = StructuredDocumentTag::new();
2213
2214 loop {
2215 match reader.read_event()? {
2216 Event::Eof => break,
2217
2218 Event::Start(start) => match start.local_name().as_ref() {
2219 b"sdtPr" => parse_sdt_properties(reader, &mut sdt)?,
2220 b"sdtContent" => sdt.blocks = parse_note_blocks(reader, resolver, b"sdtContent")?,
2221 _ => {}
2222 },
2223
2224 Event::End(end) if end.local_name().as_ref() == b"sdt" => break,
2225
2226 _ => {}
2227 }
2228 }
2229
2230 Ok(sdt)
2231}
2232
2233fn parse_sdt_properties(reader: &mut Reader<'_>, sdt: &mut StructuredDocumentTag) -> Result<()> {
2240 loop {
2241 match reader.read_event()? {
2242 Event::Eof => break,
2243
2244 Event::Empty(start) => match start.local_name().as_ref() {
2245 b"id" => {
2246 sdt.id = raw_attribute(&start, b"val").and_then(|value| value.parse().ok())
2247 }
2248 b"tag" => sdt.tag = raw_attribute(&start, b"val"),
2249 b"alias" => sdt.alias = raw_attribute(&start, b"val"),
2250 _ => {}
2251 },
2252
2253 Event::End(end) if end.local_name().as_ref() == b"sdtPr" => break,
2254
2255 _ => {}
2256 }
2257 }
2258
2259 Ok(())
2260}
2261
2262fn parse_comments_xml(xml: &str, resolver: &ImageResolver) -> Result<Vec<Comment>> {
2269 let mut comments = Vec::new();
2270 let mut reader = Reader::from_xml_str(xml);
2271
2272 loop {
2273 match reader.read_event()? {
2274 Event::Eof => break,
2275
2276 Event::Start(start) if start.local_name().as_ref() == b"comment" => {
2277 match raw_attribute(&start, b"id").and_then(|value| value.parse().ok()) {
2278 Some(id) => {
2279 let author = raw_attribute(&start, b"author");
2280 let initials = raw_attribute(&start, b"initials");
2281 let date = raw_attribute(&start, b"date");
2282 let blocks = parse_note_blocks(&mut reader, resolver, b"comment")?;
2283 comments.push(Comment {
2284 id,
2285 author,
2286 initials,
2287 date,
2288 blocks,
2289 });
2290 }
2291 None => skip_to_end(&mut reader, b"comment")?,
2292 }
2293 }
2294
2295 _ => {}
2296 }
2297 }
2298
2299 Ok(comments)
2300}
2301
2302fn parse_theme_xml(xml: &str) -> Result<Theme> {
2308 let mut reader = Reader::from_xml_str(xml);
2309 let mut name = String::new();
2310 let mut colors = ColorScheme::office_default();
2311 let mut fonts = FontScheme::office_default();
2312
2313 loop {
2314 match reader.read_event()? {
2315 Event::Eof => break,
2316
2317 Event::Start(start) => match start.local_name().as_ref() {
2318 b"theme" => {
2319 if let Some(value) = raw_attribute(&start, b"name") {
2320 name = value;
2321 }
2322 }
2323 b"clrScheme" => colors = parse_color_scheme(&mut reader)?,
2324 b"fontScheme" => fonts = parse_font_scheme(&mut reader)?,
2325 _ => {}
2326 },
2327
2328 _ => {}
2329 }
2330 }
2331
2332 Ok(Theme {
2333 name,
2334 colors,
2335 fonts,
2336 })
2337}
2338
2339fn parse_settings_xml(xml: &str) -> Result<(bool, Option<ProtectionKind>)> {
2355 let mut reader = Reader::from_xml_str(xml);
2356 let mut track_changes = false;
2357 let mut protection = None;
2358
2359 loop {
2360 match reader.read_event()? {
2361 Event::Eof => break,
2362
2363 Event::Start(start) | Event::Empty(start) => match start.local_name().as_ref() {
2364 b"trackRevisions" => track_changes = on_off_attribute(&start),
2365 b"documentProtection" => {
2366 protection = raw_attribute(&start, b"edit")
2367 .and_then(|value| ProtectionKind::from_attribute_value(&value));
2368 }
2369 _ => {}
2370 },
2371
2372 _ => {}
2373 }
2374 }
2375
2376 Ok((track_changes, protection))
2377}
2378
2379fn parse_core_properties_xml(xml: &str) -> Result<DocumentProperties> {
2394 let mut reader = Reader::from_xml_str(xml);
2395 let mut properties = DocumentProperties::new();
2396 let mut current_element: Option<Vec<u8>> = None;
2397 let mut buffer = String::new();
2398
2399 loop {
2400 match reader.read_event()? {
2401 Event::Eof => break,
2402
2403 Event::Start(start) => {
2404 let local_name = start.local_name().as_ref().to_vec();
2405 if is_tracked_core_property_element(&local_name) {
2406 current_element = Some(local_name);
2407 buffer.clear();
2408 }
2409 }
2410
2411 Event::Text(text) if current_element.is_some() => {
2412 buffer.push_str(&xml_core::decode_text(&text)?);
2413 }
2414
2415 Event::GeneralRef(reference) if current_element.is_some() => {
2416 if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
2417 buffer.push_str(&resolved);
2418 }
2419 }
2420
2421 Event::End(end) => {
2422 if let Some(name) = current_element.take() {
2423 if end.local_name().as_ref() == name.as_slice() {
2424 apply_core_property(&mut properties, &name, std::mem::take(&mut buffer));
2425 } else {
2426 current_element = Some(name);
2429 }
2430 }
2431 }
2432
2433 _ => {}
2434 }
2435 }
2436
2437 Ok(properties)
2438}
2439
2440fn is_tracked_core_property_element(local_name: &[u8]) -> bool {
2443 matches!(
2444 local_name,
2445 b"title"
2446 | b"subject"
2447 | b"creator"
2448 | b"keywords"
2449 | b"description"
2450 | b"lastModifiedBy"
2451 | b"revision"
2452 | b"created"
2453 | b"modified"
2454 | b"category"
2455 | b"contentStatus"
2456 )
2457}
2458
2459fn apply_core_property(properties: &mut DocumentProperties, local_name: &[u8], value: String) {
2462 match local_name {
2463 b"title" => properties.title = Some(value),
2464 b"subject" => properties.subject = Some(value),
2465 b"creator" => properties.creator = Some(value),
2466 b"keywords" => properties.keywords = Some(value),
2467 b"description" => properties.description = Some(value),
2468 b"lastModifiedBy" => properties.last_modified_by = Some(value),
2469 b"revision" => properties.revision = Some(value),
2470 b"created" => properties.created = Some(value),
2471 b"modified" => properties.modified = Some(value),
2472 b"category" => properties.category = Some(value),
2473 b"contentStatus" => properties.content_status = Some(value),
2474 _ => {}
2475 }
2476}
2477
2478fn parse_extended_properties_xml(xml: &str) -> Result<ExtendedProperties> {
2484 let mut reader = Reader::from_xml_str(xml);
2485 let mut properties = ExtendedProperties::new();
2486 let mut current_element: Option<Vec<u8>> = None;
2487 let mut buffer = String::new();
2488
2489 loop {
2490 match reader.read_event()? {
2491 Event::Eof => break,
2492
2493 Event::Start(start) => {
2494 let local_name = start.local_name().as_ref().to_vec();
2495 if local_name == b"Company" || local_name == b"Manager" {
2496 current_element = Some(local_name);
2497 buffer.clear();
2498 }
2499 }
2500
2501 Event::Text(text) if current_element.is_some() => {
2502 buffer.push_str(&xml_core::decode_text(&text)?);
2503 }
2504
2505 Event::GeneralRef(reference) if current_element.is_some() => {
2506 if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
2507 buffer.push_str(&resolved);
2508 }
2509 }
2510
2511 Event::End(end) => {
2512 if let Some(name) = current_element.take() {
2513 if end.local_name().as_ref() == name.as_slice() {
2514 let value = std::mem::take(&mut buffer);
2515 match name.as_slice() {
2516 b"Company" => properties.company = Some(value),
2517 b"Manager" => properties.manager = Some(value),
2518 _ => {}
2519 }
2520 } else {
2521 current_element = Some(name);
2522 }
2523 }
2524 }
2525
2526 _ => {}
2527 }
2528 }
2529
2530 Ok(properties)
2531}
2532
2533fn parse_custom_properties_xml(xml: &str) -> Result<Vec<(String, CustomPropertyValue)>> {
2543 let mut reader = Reader::from_xml_str(xml);
2544 let mut custom_properties = Vec::new();
2545 let mut current_name: Option<String> = None;
2546 let mut current_variant: Option<Vec<u8>> = None;
2547 let mut buffer = String::new();
2548
2549 loop {
2550 match reader.read_event()? {
2551 Event::Eof => break,
2552
2553 Event::Start(start) if start.local_name().as_ref() == b"property" => {
2554 current_name = raw_attribute(&start, b"name");
2555 }
2556
2557 Event::Start(start) | Event::Empty(start)
2558 if current_name.is_some()
2559 && matches!(
2560 start.local_name().as_ref(),
2561 b"lpwstr" | b"bool" | b"i4" | b"r8"
2562 ) =>
2563 {
2564 current_variant = Some(start.local_name().as_ref().to_vec());
2565 buffer.clear();
2566 }
2567
2568 Event::Text(text) if current_variant.is_some() => {
2569 buffer.push_str(&xml_core::decode_text(&text)?);
2570 }
2571
2572 Event::GeneralRef(reference) if current_variant.is_some() => {
2573 if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
2574 buffer.push_str(&resolved);
2575 }
2576 }
2577
2578 Event::End(end) => {
2579 if let Some(variant) = current_variant.take() {
2580 if end.local_name().as_ref() == variant.as_slice() {
2581 if let Some(name) = current_name.clone() {
2582 let value = std::mem::take(&mut buffer);
2583 let parsed = match variant.as_slice() {
2584 b"lpwstr" => Some(CustomPropertyValue::Text(value)),
2585 b"bool" => {
2586 Some(CustomPropertyValue::Bool(value == "true" || value == "1"))
2587 }
2588 b"i4" => value.parse::<i32>().ok().map(CustomPropertyValue::Int),
2589 b"r8" => value.parse::<f64>().ok().map(CustomPropertyValue::Number),
2590 _ => None,
2591 };
2592 if let Some(value) = parsed {
2593 custom_properties.push((name, value));
2594 }
2595 }
2596 } else {
2597 current_variant = Some(variant);
2598 }
2599 } else if end.local_name().as_ref() == b"property" {
2600 current_name = None;
2601 }
2602 }
2603
2604 _ => {}
2605 }
2606 }
2607
2608 Ok(custom_properties)
2609}
2610
2611fn parse_color_scheme(reader: &mut Reader<'_>) -> Result<ColorScheme> {
2616 let mut colors = ColorScheme::office_default();
2617
2618 loop {
2619 match reader.read_event()? {
2620 Event::Eof => break,
2621 Event::End(end) if end.local_name().as_ref() == b"clrScheme" => break,
2622
2623 Event::Start(start) => {
2624 let slot = start.local_name().as_ref().to_vec();
2625 if let Some(value) = read_color_slot_value(reader, &slot)? {
2626 assign_color_slot(&mut colors, &slot, value);
2627 }
2628 }
2629
2630 _ => {}
2631 }
2632 }
2633
2634 Ok(colors)
2635}
2636
2637fn read_color_slot_value(
2643 reader: &mut Reader<'_>,
2644 wrapper_local_name: &[u8],
2645) -> Result<Option<String>> {
2646 let mut value = None;
2647
2648 loop {
2649 match reader.read_event()? {
2650 Event::Eof => break,
2651 Event::End(end) if end.local_name().as_ref() == wrapper_local_name => break,
2652
2653 Event::Empty(start) | Event::Start(start) => match start.local_name().as_ref() {
2654 b"srgbClr" => value = raw_attribute(&start, b"val"),
2655 b"sysClr" => {
2656 value =
2657 raw_attribute(&start, b"lastClr").or_else(|| raw_attribute(&start, b"val"))
2658 }
2659 _ => {}
2660 },
2661
2662 _ => {}
2663 }
2664 }
2665
2666 Ok(value)
2667}
2668
2669fn assign_color_slot(colors: &mut ColorScheme, slot: &[u8], value: String) {
2673 match slot {
2674 b"dk1" => colors.dark1 = value,
2675 b"lt1" => colors.light1 = value,
2676 b"dk2" => colors.dark2 = value,
2677 b"lt2" => colors.light2 = value,
2678 b"accent1" => colors.accent1 = value,
2679 b"accent2" => colors.accent2 = value,
2680 b"accent3" => colors.accent3 = value,
2681 b"accent4" => colors.accent4 = value,
2682 b"accent5" => colors.accent5 = value,
2683 b"accent6" => colors.accent6 = value,
2684 b"hlink" => colors.hyperlink = value,
2685 b"folHlink" => colors.followed_hyperlink = value,
2686 _ => {}
2687 }
2688}
2689
2690fn parse_font_scheme(reader: &mut Reader<'_>) -> Result<FontScheme> {
2695 let mut fonts = FontScheme::office_default();
2696
2697 loop {
2698 match reader.read_event()? {
2699 Event::Eof => break,
2700 Event::End(end) if end.local_name().as_ref() == b"fontScheme" => break,
2701
2702 Event::Start(start) if start.local_name().as_ref() == b"majorFont" => {
2703 if let Some(typeface) = parse_font_collection_latin(reader, b"majorFont")? {
2704 fonts.major_latin = typeface;
2705 }
2706 }
2707 Event::Start(start) if start.local_name().as_ref() == b"minorFont" => {
2708 if let Some(typeface) = parse_font_collection_latin(reader, b"minorFont")? {
2709 fonts.minor_latin = typeface;
2710 }
2711 }
2712
2713 _ => {}
2714 }
2715 }
2716
2717 Ok(fonts)
2718}
2719
2720fn parse_font_collection_latin(
2724 reader: &mut Reader<'_>,
2725 wrapper_local_name: &[u8],
2726) -> Result<Option<String>> {
2727 let mut typeface = None;
2728
2729 loop {
2730 match reader.read_event()? {
2731 Event::Eof => break,
2732 Event::End(end) if end.local_name().as_ref() == wrapper_local_name => break,
2733
2734 Event::Empty(start) | Event::Start(start)
2735 if start.local_name().as_ref() == b"latin" =>
2736 {
2737 typeface = raw_attribute(&start, b"typeface");
2738 }
2739
2740 _ => {}
2741 }
2742 }
2743
2744 Ok(typeface)
2745}
2746
2747#[cfg(test)]
2748mod tests {
2749 use super::*;
2750
2751 fn parse(xml: &str) -> Document {
2754 let package = Package::new();
2755 let relationships = Relationships::new();
2756 let resolver = ImageResolver {
2757 package: &package,
2758 part_relationships: &relationships,
2759 };
2760 parse_document_xml(xml, &resolver).unwrap().document
2761 }
2762
2763 #[test]
2764 fn unescapes_xml_special_characters_from_hand_written_xml() {
2765 let document = parse(
2770 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2771<w:body><w:p><w:r><w:t>A & B < C > D ' E " F</w:t></w:r></w:p></w:body>
2772</w:document>"#,
2773 );
2774
2775 let paragraphs: Vec<_> = document.paragraphs().collect();
2776 assert_eq!(paragraphs[0].text(), r#"A & B < C > D ' E " F"#);
2777 }
2778
2779 #[test]
2780 fn parses_paragraphs_and_runs() {
2781 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2782<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2783<w:body>
2784<w:p><w:r><w:t>Hello</w:t></w:r></w:p>
2785<w:p><w:r><w:t>How are you?</w:t></w:r></w:p>
2786<w:p/>
2787<w:sectPr><w:pgSz w:w="11906" w:h="16838"/></w:sectPr>
2788</w:body>
2789</w:document>"#;
2790
2791 let document = parse(xml);
2792 let paragraphs: Vec<_> = document.paragraphs().collect();
2793
2794 assert_eq!(paragraphs.len(), 3);
2795 assert_eq!(paragraphs[0].text(), "Hello");
2796 assert_eq!(paragraphs[1].text(), "How are you?");
2797 assert_eq!(paragraphs[2].text(), "");
2798 }
2799
2800 #[test]
2801 fn parses_a_paragraph_with_multiple_runs() {
2802 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2803<w:body><w:p><w:r><w:t>Hello, </w:t></w:r><w:r><w:t>world!</w:t></w:r></w:p></w:body>
2804</w:document>"#;
2805
2806 let document = parse(xml);
2807 let paragraphs: Vec<_> = document.paragraphs().collect();
2808
2809 assert_eq!(paragraphs.len(), 1);
2810 assert_eq!(paragraphs[0].runs.len(), 2);
2811 assert_eq!(paragraphs[0].text(), "Hello, world!");
2812 }
2813
2814 #[test]
2815 fn parses_bold_italic_and_underline_run_properties() {
2816 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2817<w:body><w:p><w:r><w:rPr><w:b/><w:i/><w:u w:val="single"/></w:rPr><w:t>Styled</w:t></w:r></w:p></w:body>
2818</w:document>"#;
2819
2820 let document = parse(xml);
2821 let paragraphs: Vec<_> = document.paragraphs().collect();
2822
2823 let run = ¶graphs[0].runs[0];
2824 assert!(run.bold, "{run:?}");
2825 assert!(run.italic, "{run:?}");
2826 assert_eq!(run.underline, Some(UnderlineStyle::Single), "{run:?}");
2827 }
2828
2829 #[test]
2830 fn parses_color_size_and_font_family_run_properties() {
2831 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2832<w:body><w:p><w: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></w:r></w:p></w:body>
2833</w:document>"#;
2834
2835 let document = parse(xml);
2836 let paragraphs: Vec<_> = document.paragraphs().collect();
2837 let run = ¶graphs[0].runs[0];
2838
2839 assert_eq!(run.color.as_deref(), Some("FF0000"));
2840 assert_eq!(run.font_size, Some(14));
2841 assert_eq!(run.font_family.as_deref(), Some("Georgia"));
2842 }
2843
2844 #[test]
2845 fn parses_a_highlighted_run() {
2846 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2847<w:body><w:p><w:r><w:rPr><w:highlight w:val="yellow"/></w:rPr><w:t>Highlighted</w:t></w:r></w:p></w:body>
2848</w:document>"#;
2849
2850 let document = parse(xml);
2851 let paragraphs: Vec<_> = document.paragraphs().collect();
2852
2853 assert_eq!(paragraphs[0].runs[0].highlight, Some(Highlight::Yellow));
2854 }
2855
2856 #[test]
2857 fn an_unrecognized_highlight_value_is_not_set() {
2858 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2859<w:body><w:p><w:r><w:rPr><w:highlight w:val="none"/></w:rPr><w:t>No highlight</w:t></w:r></w:p></w:body>
2860</w:document>"#;
2861
2862 let document = parse(xml);
2863 let paragraphs: Vec<_> = document.paragraphs().collect();
2864
2865 assert_eq!(paragraphs[0].runs[0].highlight, None);
2866 }
2867
2868 #[test]
2869 fn parses_a_struck_through_run() {
2870 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2871<w:body><w:p><w:r><w:rPr><w:strike/></w:rPr><w:t>Struck</w:t></w:r></w:p></w:body>
2872</w:document>"#;
2873
2874 let document = parse(xml);
2875 let paragraphs: Vec<_> = document.paragraphs().collect();
2876
2877 assert!(paragraphs[0].runs[0].strike);
2878 }
2879
2880 #[test]
2881 fn parses_a_superscript_run() {
2882 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2883<w:body><w:p><w:r><w:rPr><w:vertAlign w:val="superscript"/></w:rPr><w:t>Superscript</w:t></w:r></w:p></w:body>
2884</w:document>"#;
2885
2886 let document = parse(xml);
2887 let paragraphs: Vec<_> = document.paragraphs().collect();
2888
2889 assert_eq!(
2890 paragraphs[0].runs[0].vertical_align,
2891 Some(VerticalAlign::Superscript)
2892 );
2893 }
2894
2895 #[test]
2896 fn a_baseline_vert_align_value_is_not_set() {
2897 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2898<w:body><w:p><w:r><w:rPr><w:vertAlign w:val="baseline"/></w:rPr><w:t>Normal</w:t></w:r></w:p></w:body>
2899</w:document>"#;
2900
2901 let document = parse(xml);
2902 let paragraphs: Vec<_> = document.paragraphs().collect();
2903
2904 assert_eq!(paragraphs[0].runs[0].vertical_align, None);
2905 }
2906
2907 #[test]
2908 fn a_u_element_with_val_none_is_not_underlined() {
2909 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2910<w:body><w:p><w:r><w:rPr><w:u w:val="none"/></w:rPr><w:t>Not underlined</w:t></w:r></w:p></w:body>
2911</w:document>"#;
2912
2913 let document = parse(xml);
2914 let paragraphs: Vec<_> = document.paragraphs().collect();
2915
2916 assert_eq!(paragraphs[0].runs[0].underline, None);
2917 }
2918
2919 #[test]
2920 fn parses_a_wave_underline_and_color() {
2921 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2922<w:body><w:p><w:r><w:rPr><w:u w:val="wave" w:color="FF0000"/></w:rPr><w:t>Red wave</w:t></w:r></w:p></w:body>
2923</w:document>"#;
2924
2925 let document = parse(xml);
2926 let paragraphs: Vec<_> = document.paragraphs().collect();
2927 let run = ¶graphs[0].runs[0];
2928
2929 assert_eq!(run.underline, Some(UnderlineStyle::Wave));
2930 assert_eq!(run.underline_color.as_deref(), Some("FF0000"));
2931 }
2932
2933 #[test]
2934 fn parses_small_caps_and_all_caps() {
2935 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2936<w:body><w:p><w:r><w:rPr><w:smallCaps/></w:rPr><w:t>Small caps</w:t></w:r><w:r><w:rPr><w:caps/></w:rPr><w:t>All caps</w:t></w:r></w:p></w:body>
2937</w:document>"#;
2938
2939 let document = parse(xml);
2940 let paragraphs: Vec<_> = document.paragraphs().collect();
2941
2942 assert!(paragraphs[0].runs[0].small_caps);
2943 assert!(!paragraphs[0].runs[0].all_caps);
2944 assert!(paragraphs[0].runs[1].all_caps);
2945 assert!(!paragraphs[0].runs[1].small_caps);
2946 }
2947
2948 #[test]
2949 fn parses_a_shading_color() {
2950 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2951<w:body><w:p><w:r><w:rPr><w:shd w:val="clear" w:color="auto" w:fill="FFFF00"/></w:rPr><w:t>Highlighted background</w:t></w:r></w:p></w:body>
2952</w:document>"#;
2953
2954 let document = parse(xml);
2955 let paragraphs: Vec<_> = document.paragraphs().collect();
2956
2957 assert_eq!(
2958 paragraphs[0].runs[0].shading_color.as_deref(),
2959 Some("FFFF00")
2960 );
2961 }
2962
2963 #[test]
2964 fn parses_a_text_border() {
2965 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2966<w:body><w:p><w:r><w:rPr><w:bdr w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:rPr><w:t>Bordered</w:t></w:r></w:p></w:body>
2967</w:document>"#;
2968
2969 let document = parse(xml);
2970 let paragraphs: Vec<_> = document.paragraphs().collect();
2971
2972 assert!(paragraphs[0].runs[0].text_border);
2973 }
2974
2975 #[test]
2976 fn parses_character_spacing_and_vertical_position() {
2977 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2978<w:body><w:p><w:r><w:rPr><w:spacing w:val="40"/><w:position w:val="-6"/></w:rPr><w:t>Spaced and lowered</w:t></w:r></w:p></w:body>
2979</w:document>"#;
2980
2981 let document = parse(xml);
2982 let paragraphs: Vec<_> = document.paragraphs().collect();
2983 let run = ¶graphs[0].runs[0];
2984
2985 assert_eq!(run.character_spacing, Some(40));
2986 assert_eq!(run.vertical_position, Some(-6));
2987 }
2988
2989 #[test]
2990 fn parses_hidden_text() {
2991 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2992<w:body><w:p><w:r><w:rPr><w:vanish/></w:rPr><w:t>Hidden</w:t></w:r></w:p></w:body>
2993</w:document>"#;
2994
2995 let document = parse(xml);
2996 let paragraphs: Vec<_> = document.paragraphs().collect();
2997
2998 assert!(paragraphs[0].runs[0].hidden);
2999 }
3000
3001 #[test]
3002 fn parses_paragraph_alignment() {
3003 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3004<w:body><w:p><w:pPr><w:jc w:val="center"/></w:pPr><w:r><w:t>Centered</w:t></w:r></w:p></w:body>
3005</w:document>"#;
3006
3007 let document = parse(xml);
3008 let paragraphs: Vec<_> = document.paragraphs().collect();
3009
3010 assert_eq!(paragraphs[0].alignment, Some(Alignment::Center));
3011 }
3012
3013 #[test]
3014 fn paragraphs_without_jc_have_no_alignment() {
3015 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3016<w:body><w:p><w:r><w:t>Default</w:t></w:r></w:p></w:body>
3017</w:document>"#;
3018
3019 let document = parse(xml);
3020 let paragraphs: Vec<_> = document.paragraphs().collect();
3021
3022 assert_eq!(paragraphs[0].alignment, None);
3023 }
3024
3025 #[test]
3026 fn parses_paragraph_line_spacing_and_before_after() {
3027 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3028<w:body><w:p><w:pPr><w:spacing w:before="240" w:after="120" w:line="480" w:lineRule="auto"/></w:pPr><w:r><w:t>Spaced out</w:t></w:r></w:p></w:body>
3029</w:document>"#;
3030
3031 let document = parse(xml);
3032 let paragraphs: Vec<_> = document.paragraphs().collect();
3033
3034 assert_eq!(paragraphs[0].line_spacing, Some(480));
3035 assert_eq!(paragraphs[0].space_before, Some(240));
3036 assert_eq!(paragraphs[0].space_after, Some(120));
3037 }
3038
3039 #[test]
3040 fn parses_a_paragraph_shading_color() {
3041 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3042<w:body><w:p><w:pPr><w:shd w:val="clear" w:color="auto" w:fill="D9D9D9"/></w:pPr><w:r><w:t>Shaded paragraph</w:t></w:r></w:p></w:body>
3043</w:document>"#;
3044
3045 let document = parse(xml);
3046 let paragraphs: Vec<_> = document.paragraphs().collect();
3047
3048 assert_eq!(paragraphs[0].shading_color.as_deref(), Some("D9D9D9"));
3049 }
3050
3051 #[test]
3052 fn parses_a_paragraph_border() {
3053 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3054<w:body><w:p><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><w:r><w:t>Boxed paragraph</w:t></w:r></w:p></w:body>
3055</w:document>"#;
3056
3057 let document = parse(xml);
3058 let paragraphs: Vec<_> = document.paragraphs().collect();
3059
3060 assert!(paragraphs[0].border);
3061 }
3062
3063 #[test]
3068 fn paragraph_and_run_level_shading_and_spacing_do_not_collide() {
3069 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3070<w:body><w:p><w:pPr><w:shd w:val="clear" w:color="auto" w:fill="D9D9D9"/><w:spacing w:before="240" w:after="120" w:line="480" w:lineRule="auto"/></w:pPr><w:r><w:rPr><w:shd w:val="clear" w:color="auto" w:fill="FFFF00"/><w:spacing w:val="40"/></w:rPr><w:t>Run</w:t></w:r></w:p></w:body>
3071</w:document>"#;
3072
3073 let document = parse(xml);
3074 let paragraphs: Vec<_> = document.paragraphs().collect();
3075 let paragraph = ¶graphs[0];
3076 let run = ¶graph.runs[0];
3077
3078 assert_eq!(paragraph.shading_color.as_deref(), Some("D9D9D9"));
3079 assert_eq!(paragraph.line_spacing, Some(480));
3080 assert_eq!(paragraph.space_before, Some(240));
3081 assert_eq!(paragraph.space_after, Some(120));
3082 assert_eq!(run.shading_color.as_deref(), Some("FFFF00"));
3083 assert_eq!(run.character_spacing, Some(40));
3084 }
3085
3086 #[test]
3087 fn parses_a_table_of_text_cells() {
3088 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3089<w:body>
3090<w:tbl>
3091<w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr>
3092<w:tblGrid><w:gridCol w:w="2000"/><w:gridCol w:w="2000"/></w:tblGrid>
3093<w:tr><w:tc><w:p><w:r><w:t>A1</w:t></w:r></w:p></w:tc><w:tc><w:p><w:r><w:t>B1</w:t></w:r></w:p></w:tc></w:tr>
3094<w:tr><w:tc><w:p><w:r><w:t>A2</w:t></w:r></w:p></w:tc><w:tc><w:p/></w:tc></w:tr>
3095</w:tbl>
3096</w:body>
3097</w:document>"#;
3098
3099 let document = parse(xml);
3100 let tables: Vec<_> = document.tables().collect();
3101
3102 assert_eq!(tables.len(), 1);
3103 let table = tables[0];
3104 assert_eq!(table.rows.len(), 2);
3105 assert_eq!(table.rows[0].cells.len(), 2);
3106 assert_eq!(table.rows[0].cells[0].text(), "A1");
3107 assert_eq!(table.rows[0].cells[1].text(), "B1");
3108 assert_eq!(table.rows[1].cells[0].text(), "A2");
3109 assert_eq!(table.rows[1].cells[1].text(), "");
3110 }
3111
3112 #[test]
3113 fn parses_a_nested_table_inside_a_cell() {
3114 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3115<w:body>
3116<w:tbl>
3117<w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr>
3118<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
3119<w:tr><w:tc>
3120<w:tbl>
3121<w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr>
3122<w:tblGrid><w:gridCol w:w="1000"/></w:tblGrid>
3123<w:tr><w:tc><w:p><w:r><w:t>Inner</w:t></w:r></w:p></w:tc></w:tr>
3124</w:tbl>
3125<w:p/>
3126</w:tc></w:tr>
3127</w:tbl>
3128</w:body>
3129</w:document>"#;
3130
3131 let document = parse(xml);
3132 let outer_table = document.tables().next().unwrap();
3133 let cell = &outer_table.rows[0].cells[0];
3134
3135 let nested_tables: Vec<_> = cell.tables().collect();
3136 assert_eq!(nested_tables.len(), 1);
3137 assert_eq!(nested_tables[0].rows[0].cells[0].text(), "Inner");
3138 assert_eq!(cell.text(), "");
3141 }
3142
3143 #[test]
3144 fn parses_gridcol_widths_into_column_widths() {
3145 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3146<w:body>
3147<w:tbl>
3148<w:tblPr><w:tblW w:w="4000" w:type="dxa"/><w:tblLayout w:type="fixed"/></w:tblPr>
3149<w:tblGrid><w:gridCol w:w="1000"/><w:gridCol w:w="3000"/></w:tblGrid>
3150<w:tr><w:tc><w:p><w:r><w:t>A1</w:t></w:r></w:p></w:tc><w:tc><w:p><w:r><w:t>B1</w:t></w:r></w:p></w:tc></w:tr>
3151</w:tbl>
3152</w:body>
3153</w:document>"#;
3154
3155 let document = parse(xml);
3156 let tables: Vec<_> = document.tables().collect();
3157
3158 assert_eq!(tables[0].column_widths, vec![1000, 3000]);
3159 }
3160
3161 #[test]
3162 fn parses_a_horizontal_cell_merge() {
3163 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3164<w:body>
3165<w:tbl>
3166<w:tblGrid><w:gridCol w:w="2000"/><w:gridCol w:w="2000"/></w:tblGrid>
3167<w:tr><w:tc><w:tcPr><w:gridSpan w:val="2"/></w:tcPr><w:p><w:r><w:t>Spans two columns</w:t></w:r></w:p></w:tc></w:tr>
3168</w:tbl>
3169</w:body>
3170</w:document>"#;
3171
3172 let document = parse(xml);
3173 let tables: Vec<_> = document.tables().collect();
3174
3175 assert_eq!(tables[0].rows[0].cells[0].horizontal_span, Some(2));
3176 }
3177
3178 #[test]
3179 fn parses_a_vertical_cell_merge() {
3180 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3181<w:body>
3182<w:tbl>
3183<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
3184<w:tr><w:tc><w:tcPr><w:vMerge w:val="restart"/></w:tcPr><w:p><w:r><w:t>Merged</w:t></w:r></w:p></w:tc></w:tr>
3185<w:tr><w:tc><w:tcPr><w:vMerge/></w:tcPr><w:p/></w:tc></w:tr>
3186</w:tbl>
3187</w:body>
3188</w:document>"#;
3189
3190 let document = parse(xml);
3191 let tables: Vec<_> = document.tables().collect();
3192
3193 assert_eq!(
3194 tables[0].rows[0].cells[0].vertical_merge,
3195 Some(VerticalMerge::Restart)
3196 );
3197 assert_eq!(
3201 tables[0].rows[1].cells[0].vertical_merge,
3202 Some(VerticalMerge::Continue)
3203 );
3204 }
3205
3206 #[test]
3207 fn a_cell_with_no_tcpr_has_no_merge_properties() {
3208 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3209<w:body>
3210<w:tbl>
3211<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
3212<w:tr><w:tc><w:p><w:r><w:t>Ordinary</w:t></w:r></w:p></w:tc></w:tr>
3213</w:tbl>
3214</w:body>
3215</w:document>"#;
3216
3217 let document = parse(xml);
3218 let tables: Vec<_> = document.tables().collect();
3219
3220 assert_eq!(tables[0].rows[0].cells[0].horizontal_span, None);
3221 assert_eq!(tables[0].rows[0].cells[0].vertical_merge, None);
3222 }
3223
3224 #[test]
3225 fn parses_a_custom_table_border_color() {
3226 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3227<w:body>
3228<w:tbl>
3229<w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="FF0000"/></w:tblBorders></w:tblPr>
3230<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
3231<w:tr><w:tc><w:p><w:r><w:t>A1</w:t></w:r></w:p></w:tc></w:tr>
3232</w:tbl>
3233</w:body>
3234</w:document>"#;
3235
3236 let document = parse(xml);
3237 let tables: Vec<_> = document.tables().collect();
3238
3239 assert_eq!(tables[0].border_color.as_deref(), Some("FF0000"));
3240 }
3241
3242 #[test]
3243 fn a_table_with_the_fixed_auto_border_color_leaves_border_color_unset() {
3244 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3245<w:body>
3246<w:tbl>
3247<w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:tblBorders></w:tblPr>
3248<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
3249<w:tr><w:tc><w:p><w:r><w:t>A1</w:t></w:r></w:p></w:tc></w:tr>
3250</w:tbl>
3251</w:body>
3252</w:document>"#;
3253
3254 let document = parse(xml);
3255 let tables: Vec<_> = document.tables().collect();
3256
3257 assert_eq!(tables[0].border_color, None);
3262 }
3263
3264 #[test]
3265 fn parses_table_style_alignment_and_indent() {
3266 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3267<w:body>
3268<w:tbl>
3269<w:tblPr><w:tblStyle w:val="TableGrid"/><w:jc w:val="center"/><w:tblInd w:w="200" w:type="dxa"/></w:tblPr>
3270<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
3271<w:tr><w:tc><w:p><w:r><w:t>A1</w:t></w:r></w:p></w:tc></w:tr>
3272</w:tbl>
3273</w:body>
3274</w:document>"#;
3275
3276 let document = parse(xml);
3277 let tables: Vec<_> = document.tables().collect();
3278
3279 assert_eq!(tables[0].style_id.as_deref(), Some("TableGrid"));
3280 assert_eq!(tables[0].alignment, Some(Alignment::Center));
3281 assert_eq!(tables[0].indent_twips, Some(200));
3282 }
3283
3284 #[test]
3285 fn parses_a_cell_border_shading_valign_margins_and_text_direction() {
3286 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3287<w:body>
3288<w:tbl>
3289<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
3290<w:tr><w:tc><w:tcPr>
3291<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>
3292<w:shd w:val="clear" w:color="auto" w:fill="00FF00"/>
3293<w:tcMar><w:top w:w="10" w:type="dxa"/><w:left w:w="20" w:type="dxa"/><w:bottom w:w="30" w:type="dxa"/><w:right w:w="40" w:type="dxa"/></w:tcMar>
3294<w:textDirection w:val="btLr"/>
3295<w:vAlign w:val="bottom"/>
3296</w:tcPr><w:p><w:r><w:t>Cell</w:t></w:r></w:p></w:tc></w:tr>
3297</w:tbl>
3298</w:body>
3299</w:document>"#;
3300
3301 let document = parse(xml);
3302 let tables: Vec<_> = document.tables().collect();
3303 let cell = &tables[0].rows[0].cells[0];
3304
3305 assert!(cell.border);
3306 assert_eq!(cell.shading_color.as_deref(), Some("00FF00"));
3307 assert_eq!(cell.margin_top_twips, Some(10));
3308 assert_eq!(cell.margin_left_twips, Some(20));
3309 assert_eq!(cell.margin_bottom_twips, Some(30));
3310 assert_eq!(cell.margin_right_twips, Some(40));
3311 assert_eq!(cell.text_direction, Some(TextDirection::BottomToTop));
3312 assert_eq!(cell.vertical_alignment, Some(CellVerticalAlign::Bottom));
3313 }
3314
3315 #[test]
3316 fn cell_borders_and_margins_do_not_leak_into_the_table_level_border_color() {
3317 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3321<w:body>
3322<w:tbl>
3323<w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="0000FF"/></w:tblBorders></w:tblPr>
3324<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
3325<w:tr><w:tc><w:tcPr><w:tcBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:tcBorders></w:tcPr><w:p><w:r><w:t>Cell</w:t></w:r></w:p></w:tc></w:tr>
3326</w:tbl>
3327</w:body>
3328</w:document>"#;
3329
3330 let document = parse(xml);
3331 let tables: Vec<_> = document.tables().collect();
3332
3333 assert_eq!(tables[0].border_color.as_deref(), Some("0000FF"));
3334 assert!(tables[0].rows[0].cells[0].border);
3335 }
3336
3337 #[test]
3338 fn parses_row_repeat_as_header_height_and_cant_split() {
3339 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3340<w:body>
3341<w:tbl>
3342<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
3343<w:tr><w:trPr><w:cantSplit/><w:trHeight w:val="500" w:hRule="atLeast"/><w:tblHeader/></w:trPr><w:tc><w:p><w:r><w:t>Header</w:t></w:r></w:p></w:tc></w:tr>
3344</w:tbl>
3345</w:body>
3346</w:document>"#;
3347
3348 let document = parse(xml);
3349 let tables: Vec<_> = document.tables().collect();
3350 let row = &tables[0].rows[0];
3351
3352 assert!(row.repeat_as_header_row);
3353 assert_eq!(row.height_twips, Some(500));
3354 assert!(row.cant_split);
3355 }
3356
3357 #[test]
3358 fn a_row_with_no_trpr_has_default_row_properties() {
3359 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3360<w:body>
3361<w:tbl>
3362<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
3363<w:tr><w:tc><w:p><w:r><w:t>Ordinary</w:t></w:r></w:p></w:tc></w:tr>
3364</w:tbl>
3365</w:body>
3366</w:document>"#;
3367
3368 let document = parse(xml);
3369 let tables: Vec<_> = document.tables().collect();
3370 let row = &tables[0].rows[0];
3371
3372 assert!(!row.repeat_as_header_row);
3373 assert_eq!(row.height_twips, None);
3374 assert!(!row.cant_split);
3375 }
3376
3377 #[test]
3378 fn paragraphs_and_tables_are_kept_in_reading_order() {
3379 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3380<w:body>
3381<w:p><w:r><w:t>Before</w:t></w:r></w:p>
3382<w:tbl><w:tr><w:tc><w:p><w:r><w:t>Cell</w:t></w:r></w:p></w:tc></w:tr></w:tbl>
3383<w:p><w:r><w:t>After</w:t></w:r></w:p>
3384</w:body>
3385</w:document>"#;
3386
3387 let document = parse(xml);
3388
3389 assert_eq!(document.blocks.len(), 3);
3390 assert!(matches!(document.blocks[0], Block::Paragraph(ref p) if p.text() == "Before"));
3391 assert!(matches!(document.blocks[1], Block::Table(_)));
3392 assert!(matches!(document.blocks[2], Block::Paragraph(ref p) if p.text() == "After"));
3393 }
3394
3395 #[test]
3396 fn resolves_an_inline_image_via_its_relationship() {
3397 use opc::{Part, Relationship};
3398
3399 let mut relationships = Relationships::new();
3400 relationships.add(Relationship {
3401 id: "rId2".to_string(),
3402 rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
3403 .to_string(),
3404 target: "media/image1.png".to_string(),
3405 target_mode: TargetMode::Internal,
3406 });
3407
3408 let mut package = Package::new();
3409 package.add_part(Part::new(
3410 "/word/media/image1.png",
3411 "image/png",
3412 vec![0u8, 1, 2, 3],
3413 ));
3414
3415 let resolver = ImageResolver {
3416 package: &package,
3417 part_relationships: &relationships,
3418 };
3419
3420 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3421<w:body><w:p><w:r><w:drawing><wp:inline>
3422<wp:extent cx="914400" cy="457200"/>
3423<wp:docPr id="1" name="rId2" descr="a test image"/>
3424<a:graphic><a:graphicData uri="picture"><pic:pic><pic:blipFill><a:blip r:embed="rId2"/></pic:blipFill></pic:pic></a:graphicData></a:graphic>
3425</wp:inline></w:drawing></w:r></w:p></w:body>
3426</w:document>"#;
3427
3428 let document = parse_document_xml(xml, &resolver).unwrap().document;
3429 let paragraphs: Vec<_> = document.paragraphs().collect();
3430 let image = paragraphs[0].runs[0]
3431 .image()
3432 .expect("should be an image run");
3433
3434 assert_eq!(image.data, vec![0u8, 1, 2, 3]);
3435 assert_eq!(image.format, ImageFormat::Png);
3436 assert_eq!(image.width_emu, 914_400);
3437 assert_eq!(image.height_emu, 457_200);
3438 assert_eq!(image.description, "a test image");
3439 }
3440
3441 #[test]
3442 fn a_drawing_with_no_blip_yields_no_image_run() {
3443 let package = Package::new();
3444 let relationships = Relationships::new();
3445 let resolver = ImageResolver {
3446 package: &package,
3447 part_relationships: &relationships,
3448 };
3449
3450 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3453<w:body><w:p><w:r><w:drawing><wp:inline><wp:extent cx="1" cy="1"/></wp:inline></w:drawing></w:r></w:p></w:body>
3454</w:document>"#;
3455
3456 let document = parse_document_xml(xml, &resolver).unwrap().document;
3457 let paragraphs: Vec<_> = document.paragraphs().collect();
3458
3459 assert_eq!(paragraphs[0].runs.len(), 1);
3460 assert!(paragraphs[0].runs[0].image().is_none());
3461 assert_eq!(paragraphs[0].runs[0].text(), Some(""));
3462 }
3463
3464 #[test]
3465 fn captures_the_default_header_and_footer_relationship_ids_from_sectpr() {
3466 let package = Package::new();
3467 let relationships = Relationships::new();
3468 let resolver = ImageResolver {
3469 package: &package,
3470 part_relationships: &relationships,
3471 };
3472
3473 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3474<w:body>
3475<w:p><w:r><w:t>Body</w:t></w:r></w:p>
3476<w:sectPr>
3477<w:headerReference w:type="even" r:id="rId9"/>
3478<w:headerReference w:type="default" r:id="rId2"/>
3479<w:footerReference w:type="default" r:id="rId3"/>
3480<w:pgSz w:w="11906" w:h="16838"/>
3481</w:sectPr>
3482</w:body>
3483</w:document>"#;
3484
3485 let parsed = parse_document_xml(xml, &resolver).unwrap();
3486
3487 assert_eq!(parsed.header_relationship_id.as_deref(), Some("rId2"));
3491 assert_eq!(parsed.header_even_relationship_id.as_deref(), Some("rId9"));
3492 assert_eq!(parsed.header_first_relationship_id, None);
3493 assert_eq!(parsed.footer_relationship_id.as_deref(), Some("rId3"));
3494 }
3495
3496 #[test]
3497 fn no_header_or_footer_reference_yields_none() {
3498 let document = parse(
3499 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3500<w:body><w:p><w:r><w:t>Body</w:t></w:r></w:p><w:sectPr><w:pgSz w:w="11906" w:h="16838"/></w:sectPr></w:body>
3501</w:document>"#,
3502 );
3503
3504 assert!(document.header.is_none());
3505 assert!(document.footer.is_none());
3506 }
3507
3508 #[test]
3509 fn parses_a_page_number_field() {
3510 let document = parse(
3511 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3512<w:body><w:p><w:r><w:t>Page </w:t></w:r><w:fldSimple w:instr="PAGE"><w:r><w:rPr><w:b/></w:rPr><w:t>1</w:t></w:r></w:fldSimple></w:p></w:body>
3513</w:document>"#,
3514 );
3515 let paragraphs: Vec<_> = document.paragraphs().collect();
3516
3517 assert_eq!(paragraphs[0].runs.len(), 2);
3518 assert_eq!(paragraphs[0].runs[0].text(), Some("Page "));
3519 let field_run = ¶graphs[0].runs[1];
3520 assert_eq!(field_run.field(), Some(Field::PageNumber));
3521 assert!(field_run.bold, "{field_run:?}");
3523 }
3524
3525 #[test]
3526 fn parses_a_total_pages_field_and_does_not_confuse_it_with_page() {
3527 let document = parse(
3528 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3529<w:body><w:p><w:fldSimple w:instr="NUMPAGES"><w:r><w:t>1</w:t></w:r></w:fldSimple></w:p></w:body>
3530</w:document>"#,
3531 );
3532 let paragraphs: Vec<_> = document.paragraphs().collect();
3533
3534 assert_eq!(paragraphs[0].runs[0].field(), Some(Field::TotalPages));
3535 }
3536
3537 #[test]
3538 fn an_unsupported_field_instruction_is_silently_dropped() {
3539 let document = parse(
3540 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3541<w:body><w:p><w:r><w:t>Before</w:t></w:r><w:fldSimple w:instr="DATE \@ "MM/dd/yyyy""><w:r><w:t>1/1/2026</w:t></w:r></w:fldSimple><w:r><w:t>After</w:t></w:r></w:p></w:body>
3542</w:document>"#,
3543 );
3544 let paragraphs: Vec<_> = document.paragraphs().collect();
3545
3546 assert_eq!(paragraphs[0].runs.len(), 2);
3548 assert_eq!(paragraphs[0].runs[0].text(), Some("Before"));
3549 assert_eq!(paragraphs[0].runs[1].text(), Some("After"));
3550 }
3551
3552 #[test]
3553 fn a_self_closed_field_with_no_inner_run_is_parsed() {
3554 let document = parse(
3555 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3556<w:body><w:p><w:fldSimple w:instr="PAGE"/></w:p></w:body>
3557</w:document>"#,
3558 );
3559 let paragraphs: Vec<_> = document.paragraphs().collect();
3560
3561 assert_eq!(paragraphs[0].runs[0].field(), Some(Field::PageNumber));
3562 }
3563
3564 #[test]
3565 fn parses_a_paragraph_style_referenced_by_pstyle() {
3566 let document = parse(
3567 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3568<w:body><w:p><w:pPr><w:pStyle w:val="Heading1"/><w:jc w:val="center"/></w:pPr><w:r><w:t>Title</w:t></w:r></w:p></w:body>
3569</w:document>"#,
3570 );
3571 let paragraphs: Vec<_> = document.paragraphs().collect();
3572
3573 assert_eq!(paragraphs[0].style_id.as_deref(), Some("Heading1"));
3574 assert_eq!(paragraphs[0].alignment, Some(Alignment::Center));
3575 }
3576
3577 #[test]
3578 fn parses_a_character_style_referenced_by_rstyle() {
3579 let document = parse(
3580 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3581<w:body><w:p><w:r><w:rPr><w:rStyle w:val="Strong"/><w:b/></w:rPr><w:t>Bold</w:t></w:r></w:p></w:body>
3582</w:document>"#,
3583 );
3584 let paragraphs: Vec<_> = document.paragraphs().collect();
3585
3586 assert_eq!(paragraphs[0].runs[0].style_id.as_deref(), Some("Strong"));
3587 assert!(paragraphs[0].runs[0].bold);
3588 }
3589
3590 #[test]
3591 fn a_page_number_field_can_carry_a_run_style() {
3592 let document = parse(
3593 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3594<w:body><w:p><w:fldSimple w:instr="PAGE"><w:r><w:rPr><w:rStyle w:val="PageNumber"/></w:rPr><w:t>1</w:t></w:r></w:fldSimple></w:p></w:body>
3595</w:document>"#,
3596 );
3597 let paragraphs: Vec<_> = document.paragraphs().collect();
3598
3599 assert_eq!(
3600 paragraphs[0].runs[0].style_id.as_deref(),
3601 Some("PageNumber")
3602 );
3603 }
3604
3605 #[test]
3606 fn parses_styles_xml_into_paragraph_and_character_styles() {
3607 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3608<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3609<w:style w:type="paragraph" w:styleId="Heading1">
3610<w:name w:val="Heading 1"/>
3611<w:pPr><w:jc w:val="center"/></w:pPr>
3612<w:rPr><w:b/></w:rPr>
3613</w:style>
3614<w:style w:type="character" w:styleId="Strong">
3615<w:name w:val="Strong"/>
3616<w:basedOn w:val="DefaultParagraphFont"/>
3617<w:rPr><w:b/><w:i/></w:rPr>
3618</w:style>
3619<w:style w:type="table" w:styleId="TableGrid">
3620<w:name w:val="Table Grid"/>
3621</w:style>
3622</w:styles>"#;
3623
3624 let styles = parse_styles_xml(xml).unwrap();
3625
3626 assert_eq!(styles.len(), 2);
3628
3629 assert_eq!(styles[0].id, "Heading1");
3630 assert_eq!(styles[0].name, "Heading 1");
3631 assert_eq!(styles[0].kind, StyleKind::Paragraph);
3632 assert_eq!(styles[0].alignment, Some(Alignment::Center));
3633 assert!(styles[0].bold);
3634 assert_eq!(styles[0].based_on, None);
3635
3636 assert_eq!(styles[1].id, "Strong");
3637 assert_eq!(styles[1].kind, StyleKind::Character);
3638 assert_eq!(styles[1].based_on.as_deref(), Some("DefaultParagraphFont"));
3639 assert!(styles[1].bold);
3640 assert!(styles[1].italic);
3641 }
3642
3643 #[test]
3644 fn parses_a_style_with_color_size_and_font_family() {
3645 let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3646<w:style w:type="paragraph" w:styleId="Heading1">
3647<w:name w:val="Heading 1"/>
3648<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>
3649</w:style>
3650</w:styles>"#;
3651
3652 let styles = parse_styles_xml(xml).unwrap();
3653
3654 assert_eq!(styles[0].color.as_deref(), Some("2E74B5"));
3655 assert_eq!(styles[0].font_size, Some(16));
3656 assert_eq!(styles[0].font_family.as_deref(), Some("Cambria"));
3657 }
3658
3659 #[test]
3660 fn parses_a_style_with_a_highlight() {
3661 let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3662<w:style w:type="paragraph" w:styleId="Heading1">
3663<w:name w:val="Heading 1"/>
3664<w:rPr><w:highlight w:val="lightGray"/></w:rPr>
3665</w:style>
3666</w:styles>"#;
3667
3668 let styles = parse_styles_xml(xml).unwrap();
3669
3670 assert_eq!(styles[0].highlight, Some(Highlight::LightGray));
3671 }
3672
3673 #[test]
3674 fn parses_a_style_with_strike_and_subscript() {
3675 let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3676<w:style w:type="character" w:styleId="FootnoteReference">
3677<w:name w:val="footnote reference"/>
3678<w:rPr><w:strike/><w:vertAlign w:val="subscript"/></w:rPr>
3679</w:style>
3680</w:styles>"#;
3681
3682 let styles = parse_styles_xml(xml).unwrap();
3683
3684 assert!(styles[0].strike);
3685 assert_eq!(styles[0].vertical_align, Some(VerticalAlign::Subscript));
3686 }
3687
3688 #[test]
3689 fn parses_a_style_with_a_double_underline_and_color() {
3690 let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3691<w:style w:type="paragraph" w:styleId="Heading1">
3692<w:name w:val="Heading 1"/>
3693<w:rPr><w:u w:val="double" w:color="2E74B5"/></w:rPr>
3694</w:style>
3695</w:styles>"#;
3696
3697 let styles = parse_styles_xml(xml).unwrap();
3698
3699 assert_eq!(styles[0].underline, Some(UnderlineStyle::Double));
3700 assert_eq!(styles[0].underline_color.as_deref(), Some("2E74B5"));
3701 }
3702
3703 #[test]
3704 fn parses_a_style_with_small_caps_shading_border_spacing_and_hidden() {
3705 let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3706<w:style w:type="character" w:styleId="Emphasis">
3707<w:name w:val="Emphasis"/>
3708<w:rPr><w:smallCaps/><w:caps/><w:shd w:val="clear" w:color="auto" w:fill="FFFF00"/><w:bdr w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:spacing w:val="40"/><w:position w:val="-6"/><w:vanish/></w:rPr>
3709</w:style>
3710</w:styles>"#;
3711
3712 let styles = parse_styles_xml(xml).unwrap();
3713 let style = &styles[0];
3714
3715 assert!(style.small_caps);
3716 assert!(style.all_caps);
3717 assert_eq!(style.shading_color.as_deref(), Some("FFFF00"));
3718 assert!(style.text_border);
3719 assert_eq!(style.character_spacing, Some(40));
3720 assert_eq!(style.vertical_position, Some(-6));
3721 assert!(style.hidden);
3722 }
3723
3724 #[test]
3725 fn parses_a_paragraph_style_with_spacing_shading_and_border() {
3726 let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3727<w:style w:type="paragraph" w:styleId="Heading1">
3728<w:name w:val="Heading 1"/>
3729<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>
3730</w:style>
3731</w:styles>"#;
3732
3733 let styles = parse_styles_xml(xml).unwrap();
3734 let style = &styles[0];
3735
3736 assert!(style.paragraph_border);
3737 assert_eq!(style.paragraph_shading_color.as_deref(), Some("D9D9D9"));
3738 assert_eq!(style.line_spacing, Some(360));
3739 assert_eq!(style.space_before, Some(240));
3740 assert_eq!(style.space_after, Some(120));
3741 }
3742
3743 #[test]
3744 fn parses_a_paragraph_style_with_keep_settings_tabs_and_contextual_spacing() {
3745 let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3746<w:style w:type="paragraph" w:styleId="ListParagraph">
3747<w:name w:val="List Paragraph"/>
3748<w:pPr><w:keepNext/><w:keepLines/><w:pageBreakBefore/><w:tabs><w:tab w:val="left" w:pos="720"/></w:tabs><w:contextualSpacing/></w:pPr>
3749</w:style>
3750</w:styles>"#;
3751
3752 let styles = parse_styles_xml(xml).unwrap();
3753 let style = &styles[0];
3754
3755 assert!(style.keep_with_next);
3756 assert!(style.keep_lines_together);
3757 assert!(style.page_break_before);
3758 assert!(style.contextual_spacing);
3759 assert_eq!(style.tabs.len(), 1);
3760 assert_eq!(style.tabs[0].position_twips, 720);
3761 }
3762
3763 #[test]
3768 fn style_paragraph_and_run_level_shading_and_spacing_do_not_collide() {
3769 let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3770<w:style w:type="paragraph" w:styleId="Heading1">
3771<w:name w:val="Heading 1"/>
3772<w:pPr><w:shd w:val="clear" w:color="auto" w:fill="D9D9D9"/><w:spacing w:before="240" w:after="120" w:line="480" w:lineRule="auto"/></w:pPr>
3773<w:rPr><w:shd w:val="clear" w:color="auto" w:fill="FFFF00"/><w:spacing w:val="40"/></w:rPr>
3774</w:style>
3775</w:styles>"#;
3776
3777 let styles = parse_styles_xml(xml).unwrap();
3778 let style = &styles[0];
3779
3780 assert_eq!(style.paragraph_shading_color.as_deref(), Some("D9D9D9"));
3781 assert_eq!(style.line_spacing, Some(480));
3782 assert_eq!(style.space_before, Some(240));
3783 assert_eq!(style.space_after, Some(120));
3784 assert_eq!(style.shading_color.as_deref(), Some("FFFF00"));
3785 assert_eq!(style.character_spacing, Some(40));
3786 }
3787
3788 #[test]
3789 fn an_unrecognized_style_type_is_skipped_without_disturbing_the_next_style() {
3790 let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3791<w:style w:type="numbering" w:styleId="ListParagraph">
3792<w:name w:val="List Paragraph"/>
3793</w:style>
3794<w:style w:type="paragraph" w:styleId="Normal">
3795<w:name w:val="Normal"/>
3796</w:style>
3797</w:styles>"#;
3798
3799 let styles = parse_styles_xml(xml).unwrap();
3800
3801 assert_eq!(styles.len(), 1);
3802 assert_eq!(styles[0].id, "Normal");
3803 }
3804
3805 #[test]
3806 fn a_document_without_a_styles_relationship_has_no_styles() {
3807 let document = parse(
3808 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3809<w:body><w:p><w:r><w:t>Body</w:t></w:r></w:p></w:body>
3810</w:document>"#,
3811 );
3812
3813 assert!(document.styles.is_empty());
3814 }
3815
3816 #[test]
3817 fn resolves_an_external_hyperlink_via_its_relationship() {
3818 use opc::Relationship;
3819
3820 let mut relationships = Relationships::new();
3821 relationships.add(Relationship {
3822 id: "rId4".to_string(),
3823 rel_type:
3824 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
3825 .to_string(),
3826 target: "https://www.rust-lang.org".to_string(),
3827 target_mode: TargetMode::External,
3828 });
3829
3830 let package = Package::new();
3831 let resolver = ImageResolver {
3832 package: &package,
3833 part_relationships: &relationships,
3834 };
3835
3836 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3837<w:body><w:p><w:hyperlink r:id="rId4"><w:r><w:t>Rust</w:t></w:r></w:hyperlink></w:p></w:body>
3838</w:document>"#;
3839
3840 let document = parse_document_xml(xml, &resolver).unwrap().document;
3841 let paragraphs: Vec<_> = document.paragraphs().collect();
3842
3843 assert_eq!(paragraphs[0].runs.len(), 1);
3844 assert_eq!(paragraphs[0].runs[0].text(), Some("Rust"));
3845 assert_eq!(
3846 paragraphs[0].runs[0].hyperlink,
3847 Some(Hyperlink::External("https://www.rust-lang.org".to_string()))
3848 );
3849 }
3850
3851 #[test]
3852 fn parses_an_internal_hyperlink_from_its_anchor_attribute() {
3853 let document = parse(
3854 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3855<w:body><w:p><w:hyperlink w:anchor="Section1"><w:r><w:t>See above</w:t></w:r></w:hyperlink></w:p></w:body>
3856</w:document>"#,
3857 );
3858 let paragraphs: Vec<_> = document.paragraphs().collect();
3859
3860 assert_eq!(
3861 paragraphs[0].runs[0].hyperlink,
3862 Some(Hyperlink::Internal("Section1".to_string()))
3863 );
3864 }
3865
3866 #[test]
3867 fn a_hyperlink_id_with_no_matching_relationship_is_dropped_but_its_run_is_kept() {
3868 let document = parse(
3872 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3873<w:body><w:p><w:hyperlink r:id="rId99"><w:r><w:t>Broken link</w:t></w:r></w:hyperlink></w:p></w:body>
3874</w:document>"#,
3875 );
3876 let paragraphs: Vec<_> = document.paragraphs().collect();
3877
3878 assert_eq!(paragraphs[0].runs[0].text(), Some("Broken link"));
3879 assert_eq!(paragraphs[0].runs[0].hyperlink, None);
3880 }
3881
3882 #[test]
3883 fn multiple_runs_inside_one_hyperlink_all_get_the_same_target() {
3884 let document = parse(
3885 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3886<w:body><w:p><w:hyperlink w:anchor="Top"><w:r><w:rPr><w:b/></w:rPr><w:t>Back </w:t></w:r><w:r><w:t>to top</w:t></w:r></w:hyperlink></w:p></w:body>
3887</w:document>"#,
3888 );
3889 let paragraphs: Vec<_> = document.paragraphs().collect();
3890 let runs = ¶graphs[0].runs;
3891
3892 assert_eq!(runs.len(), 2);
3893 assert_eq!(
3894 runs[0].hyperlink,
3895 Some(Hyperlink::Internal("Top".to_string()))
3896 );
3897 assert_eq!(
3898 runs[1].hyperlink,
3899 Some(Hyperlink::Internal("Top".to_string()))
3900 );
3901 assert!(runs[0].bold);
3902 assert!(!runs[1].bold);
3903 }
3904
3905 #[test]
3906 fn a_run_outside_any_hyperlink_has_no_hyperlink() {
3907 let document = parse(
3908 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3909<w:body><w:p><w:hyperlink w:anchor="Top"><w:r><w:t>link</w:t></w:r></w:hyperlink><w:r><w:t> plain</w:t></w:r></w:p></w:body>
3910</w:document>"#,
3911 );
3912 let paragraphs: Vec<_> = document.paragraphs().collect();
3913 let runs = ¶graphs[0].runs;
3914
3915 assert_eq!(runs.len(), 2);
3916 assert!(runs[0].hyperlink.is_some());
3917 assert_eq!(runs[1].hyperlink, None);
3918 }
3919
3920 #[test]
3921 fn parses_a_paragraphs_numpr_into_numbering_id_and_level() {
3922 let document = parse(
3923 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3924<w:body><w:p><w:pPr><w:numPr><w:ilvl w:val="1"/><w:numId w:val="7"/></w:numPr></w:pPr><w:r><w:t>Item</w:t></w:r></w:p></w:body>
3925</w:document>"#,
3926 );
3927 let paragraphs: Vec<_> = document.paragraphs().collect();
3928
3929 assert_eq!(paragraphs[0].numbering_id, Some(7));
3930 assert_eq!(paragraphs[0].numbering_level, 1);
3931 }
3932
3933 #[test]
3934 fn a_paragraph_without_numpr_has_no_numbering() {
3935 let document = parse(
3936 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3937<w:body><w:p><w:r><w:t>Not a list item</w:t></w:r></w:p></w:body>
3938</w:document>"#,
3939 );
3940 let paragraphs: Vec<_> = document.paragraphs().collect();
3941
3942 assert_eq!(paragraphs[0].numbering_id, None);
3943 assert_eq!(paragraphs[0].numbering_level, 0);
3944 }
3945
3946 #[test]
3947 fn parses_numbering_xml_resolving_num_through_its_abstractnum() {
3948 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3949<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3950<w:abstractNum w:abstractNumId="0">
3951<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>
3952<w:lvl w:ilvl="1"><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="1440" w:hanging="360"/></w:pPr></w:lvl>
3953</w:abstractNum>
3954<w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>
3955</w:numbering>"#;
3956
3957 let definitions = parse_numbering_xml(xml).unwrap();
3958
3959 assert_eq!(definitions.len(), 1);
3960 assert_eq!(definitions[0].id, 1);
3961 assert_eq!(definitions[0].levels.len(), 2);
3962 assert_eq!(definitions[0].levels[0].format, NumberFormat::Bullet);
3963 assert_eq!(definitions[0].levels[0].text, "•");
3964 assert_eq!(definitions[0].levels[0].indent_twips, 720);
3965 assert_eq!(definitions[0].levels[0].hanging_twips, 360);
3966 assert_eq!(definitions[0].levels[1].text, "◦");
3967 }
3968
3969 #[test]
3970 fn numid_and_abstractnumid_can_differ_like_in_a_real_word_document() {
3971 let xml = r#"<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3976<w:abstractNum w:abstractNumId="4">
3977<w:lvl w:ilvl="0"><w:numFmt w:val="decimal"/><w:lvlText w:val="%1."/></w:lvl>
3978</w:abstractNum>
3979<w:num w:numId="2"><w:abstractNumId w:val="4"/></w:num>
3980</w:numbering>"#;
3981
3982 let definitions = parse_numbering_xml(xml).unwrap();
3983
3984 assert_eq!(definitions.len(), 1);
3985 assert_eq!(definitions[0].id, 2);
3986 assert_eq!(definitions[0].levels[0].format, NumberFormat::Decimal);
3987 }
3988
3989 #[test]
3990 fn a_num_referencing_an_undeclared_abstractnum_gets_empty_levels() {
3991 let xml = r#"<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3992<w:num w:numId="1"><w:abstractNumId w:val="99"/></w:num>
3993</w:numbering>"#;
3994
3995 let definitions = parse_numbering_xml(xml).unwrap();
3996
3997 assert_eq!(definitions.len(), 1);
3998 assert_eq!(definitions[0].id, 1);
3999 assert!(definitions[0].levels.is_empty());
4000 }
4001
4002 #[test]
4003 fn a_document_without_a_numbering_relationship_has_no_numbering_definitions() {
4004 let document = parse(
4005 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4006<w:body><w:p><w:r><w:t>Body</w:t></w:r></w:p></w:body>
4007</w:document>"#,
4008 );
4009
4010 assert!(document.numbering_definitions.is_empty());
4011 }
4012
4013 #[test]
4014 fn parses_a_footnote_reference_run_into_a_run_content_variant() {
4015 let document = parse(
4016 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4017<w:body><w:p><w:r><w:rPr><w:rStyle w:val="FootnoteReference"/></w:rPr><w:footnoteReference w:id="1"/></w:r></w:p></w:body>
4018</w:document>"#,
4019 );
4020
4021 let paragraphs: Vec<_> = document.paragraphs().collect();
4022 assert_eq!(paragraphs[0].runs.len(), 1);
4023 assert_eq!(
4024 paragraphs[0].runs[0].content,
4025 RunContent::NoteReference(NoteReference::Footnote(1))
4026 );
4027 assert_eq!(
4028 paragraphs[0].runs[0].style_id.as_deref(),
4029 Some("FootnoteReference")
4030 );
4031 }
4032
4033 #[test]
4034 fn parses_an_endnote_reference_run() {
4035 let document = parse(
4036 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4037<w:body><w:p><w:r><w:endnoteReference w:id="3"/></w:r></w:p></w:body>
4038</w:document>"#,
4039 );
4040
4041 let paragraphs: Vec<_> = document.paragraphs().collect();
4042 assert_eq!(
4043 paragraphs[0].runs[0].content,
4044 RunContent::NoteReference(NoteReference::Endnote(3))
4045 );
4046 }
4047
4048 #[test]
4049 fn parses_notes_xml_into_notes_with_their_blocks() {
4050 let package = Package::new();
4051 let relationships = Relationships::new();
4052 let resolver = ImageResolver {
4053 package: &package,
4054 part_relationships: &relationships,
4055 };
4056
4057 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4058<w:footnotes xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4059<w:footnote w:id="1"><w:p><w:r><w:footnoteRef/></w:r><w:r><w:t xml:space="preserve"> </w:t></w:r><w:r><w:t>Some detail.</w:t></w:r></w:p></w:footnote>
4060</w:footnotes>"#;
4061
4062 let notes = parse_notes_xml(xml, &resolver, b"footnote").unwrap();
4063
4064 assert_eq!(notes.len(), 1);
4065 assert_eq!(notes[0].id, 1);
4066 let paragraph = notes[0].paragraphs().next().expect("missing paragraph");
4067 assert_eq!(paragraph.runs.len(), 3);
4068 assert_eq!(
4069 paragraph.runs[0].content,
4070 RunContent::NoteMarker(NoteKind::Footnote)
4071 );
4072 assert_eq!(paragraph.runs[2].text(), Some("Some detail."));
4073 }
4074
4075 #[test]
4076 fn skips_separator_and_continuation_separator_notes() {
4077 let package = Package::new();
4078 let relationships = Relationships::new();
4079 let resolver = ImageResolver {
4080 package: &package,
4081 part_relationships: &relationships,
4082 };
4083
4084 let xml = r#"<w:footnotes xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4087<w:footnote w:type="separator" w:id="-1"><w:p><w:r><w:separator/></w:r></w:p></w:footnote>
4088<w:footnote w:type="continuationSeparator" w:id="0"><w:p><w:r><w:continuationSeparator/></w:r></w:p></w:footnote>
4089<w:footnote w:id="1"><w:p><w:r><w:t>Real footnote.</w:t></w:r></w:p></w:footnote>
4090</w:footnotes>"#;
4091
4092 let notes = parse_notes_xml(xml, &resolver, b"footnote").unwrap();
4093
4094 assert_eq!(notes.len(), 1);
4095 assert_eq!(notes[0].id, 1);
4096 }
4097
4098 #[test]
4099 fn a_document_without_a_footnotes_or_endnotes_relationship_has_none() {
4100 let document = parse(
4101 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4102<w:body><w:p><w:r><w:t>Body</w:t></w:r></w:p></w:body>
4103</w:document>"#,
4104 );
4105
4106 assert!(document.footnotes.is_empty());
4107 assert!(document.endnotes.is_empty());
4108 }
4109
4110 #[test]
4111 fn parses_a_comment_range_into_the_covered_run_s_comment_ids() {
4112 let document = parse(
4115 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4116<w:body><w:p>
4117<w:r><w:t xml:space="preserve">Some </w:t></w:r>
4118<w:commentRangeStart w:id="0"/>
4119<w:r><w:t>text.</w:t></w:r>
4120<w:commentRangeEnd w:id="0"/>
4121<w:r><w:commentReference w:id="0"/></w:r>
4122</w:p></w:body>
4123</w:document>"#,
4124 );
4125
4126 let paragraphs: Vec<_> = document.paragraphs().collect();
4127 let runs = ¶graphs[0].runs;
4128 assert_eq!(runs.len(), 3);
4132 assert_eq!(runs[0].text(), Some("Some "));
4133 assert!(runs[0].comment_ids.is_empty(), "{:?}", runs[0].comment_ids);
4134 assert_eq!(runs[1].text(), Some("text."));
4135 assert_eq!(runs[1].comment_ids, vec![0]);
4136 assert!(runs[2].comment_ids.is_empty(), "{:?}", runs[2].comment_ids);
4137 }
4138
4139 #[test]
4140 fn overlapping_comment_ranges_parse_back_into_each_run_s_own_id_set() {
4141 let document = parse(
4142 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4143<w:body><w:p>
4144<w:commentRangeStart w:id="0"/>
4145<w:r><w:t>a</w:t></w:r>
4146<w:commentRangeStart w:id="1"/>
4147<w:r><w:t>b</w:t></w:r>
4148<w:commentRangeEnd w:id="1"/>
4149<w:r><w:commentReference w:id="1"/></w:r>
4150<w:r><w:t>c</w:t></w:r>
4151<w:commentRangeEnd w:id="0"/>
4152<w:r><w:commentReference w:id="0"/></w:r>
4153</w:p></w:body>
4154</w:document>"#,
4155 );
4156
4157 let paragraphs: Vec<_> = document.paragraphs().collect();
4158 let text_runs: Vec<_> = paragraphs[0]
4159 .runs
4160 .iter()
4161 .filter(|run| run.text().is_some_and(|text| !text.is_empty()))
4162 .collect();
4163 assert_eq!(text_runs[0].text(), Some("a"));
4164 assert_eq!(text_runs[0].comment_ids, vec![0]);
4165 assert_eq!(text_runs[1].text(), Some("b"));
4166 assert_eq!(text_runs[1].comment_ids, vec![0, 1]);
4167 assert_eq!(text_runs[2].text(), Some("c"));
4168 assert_eq!(text_runs[2].comment_ids, vec![0]);
4169 }
4170
4171 #[test]
4172 fn parses_a_bookmark_range_into_the_covered_run_s_bookmarks() {
4173 let document = parse(
4174 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4175<w:body><w:p>
4176<w:r><w:t xml:space="preserve">Some </w:t></w:r>
4177<w:bookmarkStart w:id="0" w:name="MyBookmark"/>
4178<w:r><w:t>text.</w:t></w:r>
4179<w:bookmarkEnd w:id="0"/>
4180</w:p></w:body>
4181</w:document>"#,
4182 );
4183
4184 let paragraphs: Vec<_> = document.paragraphs().collect();
4185 let runs = ¶graphs[0].runs;
4186 assert_eq!(runs.len(), 2);
4187 assert_eq!(runs[0].text(), Some("Some "));
4188 assert!(runs[0].bookmarks.is_empty(), "{:?}", runs[0].bookmarks);
4189 assert_eq!(runs[1].text(), Some("text."));
4190 assert_eq!(runs[1].bookmarks, vec![Bookmark::new(0, "MyBookmark")]);
4191 }
4192
4193 #[test]
4194 fn overlapping_bookmarks_parse_back_into_each_run_s_own_set() {
4195 let document = parse(
4196 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4197<w:body><w:p>
4198<w:bookmarkStart w:id="0" w:name="Outer"/>
4199<w:r><w:t>a</w:t></w:r>
4200<w:bookmarkStart w:id="1" w:name="Inner"/>
4201<w:r><w:t>b</w:t></w:r>
4202<w:bookmarkEnd w:id="1"/>
4203<w:r><w:t>c</w:t></w:r>
4204<w:bookmarkEnd w:id="0"/>
4205</w:p></w:body>
4206</w:document>"#,
4207 );
4208
4209 let paragraphs: Vec<_> = document.paragraphs().collect();
4210 let runs = ¶graphs[0].runs;
4211 assert_eq!(runs[0].text(), Some("a"));
4212 assert_eq!(runs[0].bookmarks, vec![Bookmark::new(0, "Outer")]);
4213 assert_eq!(runs[1].text(), Some("b"));
4214 assert_eq!(
4215 runs[1].bookmarks,
4216 vec![Bookmark::new(0, "Outer"), Bookmark::new(1, "Inner")]
4217 );
4218 assert_eq!(runs[2].text(), Some("c"));
4219 assert_eq!(runs[2].bookmarks, vec![Bookmark::new(0, "Outer")]);
4220 }
4221
4222 #[test]
4223 fn parses_comments_xml_into_comments_with_author_date_initials_and_blocks() {
4224 let package = Package::new();
4225 let relationships = Relationships::new();
4226 let resolver = ImageResolver {
4227 package: &package,
4228 part_relationships: &relationships,
4229 };
4230
4231 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4232<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4233<w:comment w:id="0" w:author="Morgan" w:date="2026-07-16T12:00:00Z" w:initials="MR"><w:p><w:r><w:t>Please rephrase.</w:t></w:r></w:p></w:comment>
4234</w:comments>"#;
4235
4236 let comments = parse_comments_xml(xml, &resolver).unwrap();
4237
4238 assert_eq!(comments.len(), 1);
4239 assert_eq!(comments[0].id, 0);
4240 assert_eq!(comments[0].author.as_deref(), Some("Morgan"));
4241 assert_eq!(comments[0].date.as_deref(), Some("2026-07-16T12:00:00Z"));
4242 assert_eq!(comments[0].initials.as_deref(), Some("MR"));
4243 let paragraph = comments[0].paragraphs().next().expect("missing paragraph");
4244 assert_eq!(paragraph.text(), "Please rephrase.");
4245 }
4246
4247 #[test]
4248 fn a_comment_with_no_id_is_skipped() {
4249 let package = Package::new();
4250 let relationships = Relationships::new();
4251 let resolver = ImageResolver {
4252 package: &package,
4253 part_relationships: &relationships,
4254 };
4255
4256 let xml = r#"<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4257<w:comment w:author="No id"><w:p><w:r><w:t>Malformed.</w:t></w:r></w:p></w:comment>
4258<w:comment w:id="1"><w:p><w:r><w:t>Valid.</w:t></w:r></w:p></w:comment>
4259</w:comments>"#;
4260
4261 let comments = parse_comments_xml(xml, &resolver).unwrap();
4262
4263 assert_eq!(comments.len(), 1);
4264 assert_eq!(comments[0].id, 1);
4265 }
4266
4267 #[test]
4268 fn a_document_without_a_comments_relationship_has_none() {
4269 let document = parse(
4270 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4271<w:body><w:p><w:r><w:t>Body</w:t></w:r></w:p></w:body>
4272</w:document>"#,
4273 );
4274
4275 assert!(document.comments.is_empty());
4276 }
4277
4278 #[test]
4279 fn parses_a_structured_document_tag_with_its_properties_and_content() {
4280 let document = parse(
4281 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4282<w:body><w:sdt>
4283<w:sdtPr><w:alias w:val="Customer Name"/><w:tag w:val="CustomerName"/><w:id w:val="1"/></w:sdtPr>
4284<w:sdtContent><w:p><w:r><w:t>Acme Corp</w:t></w:r></w:p></w:sdtContent>
4285</w:sdt></w:body>
4286</w:document>"#,
4287 );
4288
4289 assert_eq!(document.blocks.len(), 1);
4290 match &document.blocks[0] {
4291 Block::StructuredDocumentTag(sdt) => {
4292 assert_eq!(sdt.id, Some(1));
4293 assert_eq!(sdt.tag.as_deref(), Some("CustomerName"));
4294 assert_eq!(sdt.alias.as_deref(), Some("Customer Name"));
4295 assert_eq!(
4296 sdt.paragraphs().next().expect("missing paragraph").text(),
4297 "Acme Corp"
4298 );
4299 }
4300 other => panic!("expected a StructuredDocumentTag block, got {other:?}"),
4301 }
4302 }
4303
4304 #[test]
4305 fn a_structured_document_tag_with_no_sdtpr_has_no_properties() {
4306 let document = parse(
4307 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4308<w:body><w:sdt><w:sdtContent><w:p><w:r><w:t>Plain</w:t></w:r></w:p></w:sdtContent></w:sdt></w:body>
4309</w:document>"#,
4310 );
4311
4312 match &document.blocks[0] {
4313 Block::StructuredDocumentTag(sdt) => {
4314 assert!(sdt.id.is_none());
4315 assert!(sdt.tag.is_none());
4316 assert!(sdt.alias.is_none());
4317 assert_eq!(sdt.paragraphs().next().unwrap().text(), "Plain");
4318 }
4319 other => panic!("expected a StructuredDocumentTag block, got {other:?}"),
4320 }
4321 }
4322
4323 #[test]
4324 fn a_nested_structured_document_tag_parses_correctly() {
4325 let document = parse(
4326 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4327<w:body><w:sdt>
4328<w:sdtPr><w:tag w:val="outer"/></w:sdtPr>
4329<w:sdtContent><w:sdt>
4330<w:sdtPr><w:tag w:val="inner"/></w:sdtPr>
4331<w:sdtContent><w:p><w:r><w:t>nested</w:t></w:r></w:p></w:sdtContent>
4332</w:sdt></w:sdtContent>
4333</w:sdt></w:body>
4334</w:document>"#,
4335 );
4336
4337 match &document.blocks[0] {
4338 Block::StructuredDocumentTag(outer) => {
4339 assert_eq!(outer.tag.as_deref(), Some("outer"));
4340 assert_eq!(outer.blocks.len(), 1);
4341 match &outer.blocks[0] {
4342 Block::StructuredDocumentTag(inner) => {
4343 assert_eq!(inner.tag.as_deref(), Some("inner"));
4344 assert_eq!(inner.paragraphs().next().unwrap().text(), "nested");
4345 }
4346 other => panic!("expected a nested StructuredDocumentTag block, got {other:?}"),
4347 }
4348 }
4349 other => panic!("expected a StructuredDocumentTag block, got {other:?}"),
4350 }
4351 }
4352
4353 #[test]
4354 fn a_structured_document_tag_interleaved_with_paragraphs_keeps_reading_order() {
4355 let document = parse(
4356 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4357<w:body>
4358<w:p><w:r><w:t>Before</w:t></w:r></w:p>
4359<w:sdt><w:sdtContent><w:p><w:r><w:t>Inside</w:t></w:r></w:p></w:sdtContent></w:sdt>
4360<w:p><w:r><w:t>After</w:t></w:r></w:p>
4361</w:body>
4362</w:document>"#,
4363 );
4364
4365 assert_eq!(document.blocks.len(), 3);
4366 assert!(matches!(&document.blocks[0], Block::Paragraph(p) if p.text() == "Before"));
4367 assert!(matches!(
4368 &document.blocks[1],
4369 Block::StructuredDocumentTag(_)
4370 ));
4371 assert!(matches!(&document.blocks[2], Block::Paragraph(p) if p.text() == "After"));
4372 }
4373
4374 #[test]
4375 fn parses_a_theme_with_colors_and_fonts() {
4376 let theme = parse_theme_xml(
4377 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4378<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Custom">
4379<a:themeElements>
4380<a:clrScheme name="Custom">
4381<a:dk1><a:srgbClr val="111111"/></a:dk1>
4382<a:lt1><a:srgbClr val="EEEEEE"/></a:lt1>
4383<a:dk2><a:srgbClr val="222222"/></a:dk2>
4384<a:lt2><a:srgbClr val="DDDDDD"/></a:lt2>
4385<a:accent1><a:srgbClr val="AA0000"/></a:accent1>
4386<a:accent2><a:srgbClr val="BB1100"/></a:accent2>
4387<a:accent3><a:srgbClr val="CC2200"/></a:accent3>
4388<a:accent4><a:srgbClr val="DD3300"/></a:accent4>
4389<a:accent5><a:srgbClr val="EE4400"/></a:accent5>
4390<a:accent6><a:srgbClr val="FF5500"/></a:accent6>
4391<a:hlink><a:srgbClr val="0000AA"/></a:hlink>
4392<a:folHlink><a:srgbClr val="AA00AA"/></a:folHlink>
4393</a:clrScheme>
4394<a:fontScheme name="Custom">
4395<a:majorFont><a:latin typeface="Georgia"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>
4396<a:minorFont><a:latin typeface="Verdana"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
4397</a:fontScheme>
4398<a:fmtScheme name="Office"><a:fillStyleLst/><a:lnStyleLst/><a:effectStyleLst/><a:bgFillStyleLst/></a:fmtScheme>
4399</a:themeElements>
4400</a:theme>"#,
4401 )
4402 .unwrap();
4403
4404 assert_eq!(theme.name, "Custom");
4405 assert_eq!(theme.colors.dark1, "111111");
4406 assert_eq!(theme.colors.light1, "EEEEEE");
4407 assert_eq!(theme.colors.accent6, "FF5500");
4408 assert_eq!(theme.colors.followed_hyperlink, "AA00AA");
4409 assert_eq!(theme.fonts.major_latin, "Georgia");
4410 assert_eq!(theme.fonts.minor_latin, "Verdana");
4411 }
4412
4413 #[test]
4414 fn parses_trackrevisions_from_settings_xml() {
4415 let xml = r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4416<w:trackRevisions/>
4417</w:settings>"#;
4418
4419 let (track_changes, _protection) = parse_settings_xml(xml).unwrap();
4420 assert!(track_changes);
4421 }
4422
4423 #[test]
4424 fn a_settings_xml_with_no_trackrevisions_parses_as_false() {
4425 let xml = r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4426<w:evenAndOddHeaders/>
4427</w:settings>"#;
4428
4429 let (track_changes, _protection) = parse_settings_xml(xml).unwrap();
4430 assert!(!track_changes);
4431 }
4432
4433 #[test]
4434 fn an_explicit_trackrevisions_val_false_parses_as_false() {
4435 let xml = r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4436<w:trackRevisions w:val="false"/>
4437</w:settings>"#;
4438
4439 let (track_changes, _protection) = parse_settings_xml(xml).unwrap();
4440 assert!(!track_changes);
4441 }
4442
4443 #[test]
4444 fn parses_documentprotection_edit_value_into_protectionkind() {
4445 let xml = r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4446<w:documentProtection w:edit="readOnly" w:enforcement="1"/>
4447</w:settings>"#;
4448
4449 let (_track_changes, protection) = parse_settings_xml(xml).unwrap();
4450 assert_eq!(protection, Some(ProtectionKind::ReadOnly));
4451 }
4452
4453 #[test]
4454 fn a_settings_xml_with_no_documentprotection_parses_protection_as_none() {
4455 let xml = r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4456<w:trackRevisions/>
4457</w:settings>"#;
4458
4459 let (_track_changes, protection) = parse_settings_xml(xml).unwrap();
4460 assert_eq!(protection, None);
4461 }
4462
4463 #[test]
4464 fn an_unrecognized_documentprotection_edit_value_parses_as_none() {
4465 let xml = r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4466<w:documentProtection w:edit="none" w:enforcement="1"/>
4467</w:settings>"#;
4468
4469 let (_track_changes, protection) = parse_settings_xml(xml).unwrap();
4470 assert_eq!(protection, None);
4471 }
4472
4473 #[test]
4474 fn parses_core_properties_xml_into_documentproperties() {
4475 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4476<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4477<dc:title>Rapport trimestriel</dc:title>
4478<dc:subject>Finances</dc:subject>
4479<dc:creator>Morgan & Associés</dc:creator>
4480<cp:keywords>rapport, finances</cp:keywords>
4481<dc:description>Notes internes</dc:description>
4482<cp:lastModifiedBy>Morgan</cp:lastModifiedBy>
4483<cp:revision>3</cp:revision>
4484<dcterms:created xsi:type="dcterms:W3CDTF">2026-07-17T10:00:00Z</dcterms:created>
4485<dcterms:modified xsi:type="dcterms:W3CDTF">2026-07-17T11:30:00Z</dcterms:modified>
4486<cp:category>Interne</cp:category>
4487<cp:contentStatus>Draft</cp:contentStatus>
4488</cp:coreProperties>"#;
4489
4490 let properties = parse_core_properties_xml(xml).unwrap();
4491
4492 assert_eq!(properties.title.as_deref(), Some("Rapport trimestriel"));
4493 assert_eq!(properties.subject.as_deref(), Some("Finances"));
4494 assert_eq!(properties.creator.as_deref(), Some("Morgan & Associés"));
4498 assert_eq!(properties.keywords.as_deref(), Some("rapport, finances"));
4499 assert_eq!(properties.description.as_deref(), Some("Notes internes"));
4500 assert_eq!(properties.last_modified_by.as_deref(), Some("Morgan"));
4501 assert_eq!(properties.revision.as_deref(), Some("3"));
4502 assert_eq!(properties.created.as_deref(), Some("2026-07-17T10:00:00Z"));
4503 assert_eq!(properties.modified.as_deref(), Some("2026-07-17T11:30:00Z"));
4504 assert_eq!(properties.category.as_deref(), Some("Interne"));
4505 assert_eq!(properties.content_status.as_deref(), Some("Draft"));
4506 }
4507
4508 #[test]
4509 fn an_empty_core_properties_element_parses_as_all_none() {
4510 let xml = r#"<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>"#;
4511
4512 let properties = parse_core_properties_xml(xml).unwrap();
4513
4514 assert_eq!(properties, DocumentProperties::new());
4515 }
4516
4517 #[test]
4518 fn a_sysclr_color_slot_uses_its_lastclr_fallback() {
4519 let theme = parse_theme_xml(
4520 r#"<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office">
4521<a:themeElements>
4522<a:clrScheme name="Office">
4523<a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
4524<a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
4525<a:dk2><a:srgbClr val="1F497D"/></a:dk2>
4526<a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
4527<a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
4528<a:accent2><a:srgbClr val="C0504D"/></a:accent2>
4529<a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
4530<a:accent4><a:srgbClr val="8064A2"/></a:accent4>
4531<a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
4532<a:accent6><a:srgbClr val="F79646"/></a:accent6>
4533<a:hlink><a:srgbClr val="0000FF"/></a:hlink>
4534<a:folHlink><a:srgbClr val="800080"/></a:folHlink>
4535</a:clrScheme>
4536</a:themeElements>
4537</a:theme>"#,
4538 )
4539 .unwrap();
4540
4541 assert_eq!(theme.colors.dark1, "000000");
4544 assert_eq!(theme.colors.light1, "FFFFFF");
4545 }
4546
4547 #[test]
4548 fn a_theme_with_no_font_scheme_keeps_the_default_fonts() {
4549 let theme = parse_theme_xml(
4550 r#"<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="ColorsOnly">
4551<a:themeElements>
4552<a:clrScheme name="ColorsOnly">
4553<a:dk1><a:srgbClr val="000000"/></a:dk1>
4554<a:lt1><a:srgbClr val="FFFFFF"/></a:lt1>
4555<a:dk2><a:srgbClr val="1F497D"/></a:dk2>
4556<a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
4557<a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
4558<a:accent2><a:srgbClr val="C0504D"/></a:accent2>
4559<a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
4560<a:accent4><a:srgbClr val="8064A2"/></a:accent4>
4561<a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
4562<a:accent6><a:srgbClr val="F79646"/></a:accent6>
4563<a:hlink><a:srgbClr val="0000FF"/></a:hlink>
4564<a:folHlink><a:srgbClr val="800080"/></a:folHlink>
4565</a:clrScheme>
4566</a:themeElements>
4567</a:theme>"#,
4568 )
4569 .unwrap();
4570
4571 assert_eq!(theme.fonts, FontScheme::office_default());
4572 }
4573
4574 #[test]
4575 fn parses_page_size_margins_and_columns_from_sectpr() {
4576 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4577<w:body>
4578<w:p><w:r><w:t>Body</w:t></w:r></w:p>
4579<w:sectPr>
4580<w:pgSz w:w="16838" w:h="11906" w:orient="landscape"/>
4581<w:pgMar w:top="1000" w:right="1000" w:bottom="1000" w:left="1000" w:header="500" w:footer="500" w:gutter="0"/>
4582<w:cols w:num="2" w:space="720" w:equalWidth="true"/>
4583</w:sectPr>
4584</w:body>
4585</w:document>"#;
4586
4587 let document = parse(xml);
4588
4589 assert_eq!(document.page_setup.width_twips, 16838);
4590 assert_eq!(document.page_setup.height_twips, 11906);
4591 assert_eq!(document.page_setup.orientation, Orientation::Landscape);
4592 assert_eq!(document.page_setup.margin_top_twips, 1000);
4593 assert_eq!(document.page_setup.margin_right_twips, 1000);
4594 assert_eq!(document.page_setup.margin_bottom_twips, 1000);
4595 assert_eq!(document.page_setup.margin_left_twips, 1000);
4596 assert_eq!(document.page_setup.margin_header_twips, 500);
4597 assert_eq!(document.page_setup.margin_footer_twips, 500);
4598 assert_eq!(document.page_setup.columns, Some(2));
4599 }
4600
4601 #[test]
4602 fn parses_an_a3_page_size_distinct_from_the_default_a4() {
4603 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4607<w:body>
4608<w:p><w:r><w:t>Body</w:t></w:r></w:p>
4609<w:sectPr>
4610<w:pgSz w:w="16838" w:h="23811"/>
4611</w:sectPr>
4612</w:body>
4613</w:document>"#;
4614
4615 let document = parse(xml);
4616
4617 assert_eq!(document.page_setup.width_twips, 16838);
4618 assert_eq!(document.page_setup.height_twips, 23811);
4619 assert_eq!(document.page_setup.orientation, Orientation::Portrait);
4620 }
4621
4622 #[test]
4623 fn a_missing_pgsz_keeps_the_default_page_setup() {
4624 let document = parse(
4625 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4626<w:body><w:p><w:r><w:t>Body</w:t></w:r></w:p></w:body>
4627</w:document>"#,
4628 );
4629
4630 assert_eq!(document.page_setup, PageSetup::default());
4631 }
4632
4633 #[test]
4634 fn captures_first_and_even_header_footer_reference_ids_separately() {
4635 let package = Package::new();
4636 let relationships = Relationships::new();
4637 let resolver = ImageResolver {
4638 package: &package,
4639 part_relationships: &relationships,
4640 };
4641
4642 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4643<w:body>
4644<w:p><w:r><w:t>Body</w:t></w:r></w:p>
4645<w:sectPr>
4646<w:headerReference w:type="default" r:id="rId2"/>
4647<w:headerReference w:type="first" r:id="rId4"/>
4648<w:footerReference w:type="first" r:id="rId5"/>
4649<w:headerReference w:type="even" r:id="rId6"/>
4650<w:footerReference w:type="even" r:id="rId7"/>
4651<w:pgSz w:w="11906" w:h="16838"/>
4652</w:sectPr>
4653</w:body>
4654</w:document>"#;
4655
4656 let parsed = parse_document_xml(xml, &resolver).unwrap();
4657
4658 assert_eq!(parsed.header_relationship_id.as_deref(), Some("rId2"));
4659 assert_eq!(parsed.header_first_relationship_id.as_deref(), Some("rId4"));
4660 assert_eq!(parsed.footer_first_relationship_id.as_deref(), Some("rId5"));
4661 assert_eq!(parsed.header_even_relationship_id.as_deref(), Some("rId6"));
4662 assert_eq!(parsed.footer_even_relationship_id.as_deref(), Some("rId7"));
4663 }
4664
4665 #[test]
4666 fn parses_custom_tab_stops_in_order() {
4667 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4668<w:body>
4669<w:p><w:pPr><w:tabs>
4670<w:tab w:val="left" w:pos="720"/>
4671<w:tab w:val="decimal" w:leader="dot" w:pos="-360"/>
4672</w:tabs></w:pPr><w:r><w:t>Tabbed</w:t></w:r></w:p>
4673</w:body>
4674</w:document>"#;
4675
4676 let document = parse(xml);
4677 let paragraphs: Vec<_> = document.paragraphs().collect();
4678
4679 assert_eq!(paragraphs[0].tabs.len(), 2);
4680 assert_eq!(paragraphs[0].tabs[0].position_twips, 720);
4681 assert_eq!(paragraphs[0].tabs[0].alignment, TabStopAlignment::Left);
4682 assert_eq!(paragraphs[0].tabs[0].leader, None);
4683 assert_eq!(paragraphs[0].tabs[1].position_twips, -360);
4684 assert_eq!(paragraphs[0].tabs[1].alignment, TabStopAlignment::Decimal);
4685 assert_eq!(paragraphs[0].tabs[1].leader, Some(TabLeader::Dot));
4686 }
4687
4688 #[test]
4689 fn parses_keep_next_keep_lines_page_break_before_and_contextual_spacing() {
4690 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4691<w:body>
4692<w:p><w:pPr>
4693<w:keepNext/><w:keepLines/><w:pageBreakBefore/><w:contextualSpacing/>
4694</w:pPr><w:r><w:t>Heading</w:t></w:r></w:p>
4695</w:body>
4696</w:document>"#;
4697
4698 let document = parse(xml);
4699 let paragraphs: Vec<_> = document.paragraphs().collect();
4700
4701 assert!(paragraphs[0].keep_with_next);
4702 assert!(paragraphs[0].keep_lines_together);
4703 assert!(paragraphs[0].page_break_before);
4704 assert!(paragraphs[0].contextual_spacing);
4705 }
4706
4707 #[test]
4708 fn parses_page_and_column_breaks_and_a_carriage_return() {
4709 let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
4710<w:body>
4711<w:p>
4712<w:r><w:br/></w:r>
4713<w:r><w:br w:type="page"/></w:r>
4714<w:r><w:br w:type="column"/></w:r>
4715<w:r><w:cr/></w:r>
4716</w:p>
4717</w:body>
4718</w:document>"#;
4719
4720 let document = parse(xml);
4721 let paragraphs: Vec<_> = document.paragraphs().collect();
4722
4723 assert_eq!(paragraphs[0].runs.len(), 4);
4724 assert_eq!(
4725 paragraphs[0].runs[0].content,
4726 RunContent::Break(BreakKind::TextWrapping)
4727 );
4728 assert_eq!(
4729 paragraphs[0].runs[1].content,
4730 RunContent::Break(BreakKind::Page)
4731 );
4732 assert_eq!(
4733 paragraphs[0].runs[2].content,
4734 RunContent::Break(BreakKind::Column)
4735 );
4736 assert_eq!(paragraphs[0].runs[3].content, RunContent::CarriageReturn);
4737 }
4738}