use std::collections::HashMap;
use std::io::{Read, Seek};
use drawing::{Geometry, PresetShape, ShapeProperties};
use opc::{Package, Relationships, TargetMode};
use xml_core::{BytesStart, Event, Reader};
use crate::error::{Error, Result};
use crate::model::{
Alignment, Block, Bookmark, BreakKind, CellVerticalAlign, ColorScheme, Comment,
CustomPropertyValue, Document, DocumentProperties, EmbeddedChart, ExtendedProperties, Field,
FontScheme, HeaderFooter, Highlight, Hyperlink, Image, ImageFormat, ListLevel, Note, NoteKind,
NoteReference, NumberFormat, NumberingDefinition, Orientation, PageSetup, Paragraph,
ProtectionKind, Run, RunContent, StructuredDocumentTag, Style, StyleKind, TabLeader, TabStop,
TabStopAlignment, Table, TableCell, TableRow, TextDirection, Theme, UnderlineStyle,
VerticalAlign, VerticalMerge,
};
const MAIN_DOCUMENT_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
const STYLES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
const NUMBERING_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
const FOOTNOTES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";
const ENDNOTES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes";
const COMMENTS_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
const THEME_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
const SETTINGS_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings";
const CORE_PROPERTIES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
const EXTENDED_PROPERTIES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties";
const CUSTOM_PROPERTIES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
impl Document {
pub fn read_from<R: Read + Seek>(reader: R) -> Result<Self> {
let package = Package::read_from(reader)?;
let main_document_relationship = package
.relationships()
.iter()
.find(|relationship| relationship.rel_type == MAIN_DOCUMENT_RELATIONSHIP_TYPE)
.ok_or(Error::MissingMainDocument)?;
let part_name = match main_document_relationship.target_mode {
TargetMode::Internal => format!("/{}", main_document_relationship.target),
TargetMode::External => return Err(Error::MissingMainDocument),
};
let part = package.part(&part_name).ok_or(Error::MissingMainDocument)?;
let xml = std::str::from_utf8(&part.data)?;
let resolver = ImageResolver {
package: &package,
part_relationships: &part.relationships,
};
let parsed = parse_document_xml(xml, &resolver)?;
let mut document = parsed.document;
if let Some(relationship_id) = parsed.header_relationship_id {
document.header = Some(resolve_header_footer(
&package,
&part.relationships,
&relationship_id,
)?);
}
if let Some(relationship_id) = parsed.footer_relationship_id {
document.footer = Some(resolve_header_footer(
&package,
&part.relationships,
&relationship_id,
)?);
}
if let Some(relationship_id) = parsed.header_first_relationship_id {
document.header_first = Some(resolve_header_footer(
&package,
&part.relationships,
&relationship_id,
)?);
}
if let Some(relationship_id) = parsed.footer_first_relationship_id {
document.footer_first = Some(resolve_header_footer(
&package,
&part.relationships,
&relationship_id,
)?);
}
if let Some(relationship_id) = parsed.header_even_relationship_id {
document.header_even = Some(resolve_header_footer(
&package,
&part.relationships,
&relationship_id,
)?);
}
if let Some(relationship_id) = parsed.footer_even_relationship_id {
document.footer_even = Some(resolve_header_footer(
&package,
&part.relationships,
&relationship_id,
)?);
}
let styles_relationship = part
.relationships
.iter()
.find(|relationship| relationship.rel_type == STYLES_RELATIONSHIP_TYPE);
if let Some(relationship) = styles_relationship {
let styles_part_name = format!("/word/{}", relationship.target);
let styles_part = package
.part(&styles_part_name)
.ok_or_else(|| Error::MissingStylesPart(styles_part_name.clone()))?;
let styles_xml = std::str::from_utf8(&styles_part.data)?;
document.styles = parse_styles_xml(styles_xml)?;
}
let numbering_relationship = part
.relationships
.iter()
.find(|relationship| relationship.rel_type == NUMBERING_RELATIONSHIP_TYPE);
if let Some(relationship) = numbering_relationship {
let numbering_part_name = format!("/word/{}", relationship.target);
let numbering_part = package
.part(&numbering_part_name)
.ok_or_else(|| Error::MissingNumberingPart(numbering_part_name.clone()))?;
let numbering_xml = std::str::from_utf8(&numbering_part.data)?;
document.numbering_definitions = parse_numbering_xml(numbering_xml)?;
}
let footnotes_relationship = part
.relationships
.iter()
.find(|relationship| relationship.rel_type == FOOTNOTES_RELATIONSHIP_TYPE);
if let Some(relationship) = footnotes_relationship {
let footnotes_part_name = format!("/word/{}", relationship.target);
let footnotes_part = package
.part(&footnotes_part_name)
.ok_or_else(|| Error::MissingFootnotesPart(footnotes_part_name.clone()))?;
let footnotes_xml = std::str::from_utf8(&footnotes_part.data)?;
let footnotes_resolver = ImageResolver {
package: &package,
part_relationships: &footnotes_part.relationships,
};
document.footnotes = parse_notes_xml(footnotes_xml, &footnotes_resolver, b"footnote")?;
}
let endnotes_relationship = part
.relationships
.iter()
.find(|relationship| relationship.rel_type == ENDNOTES_RELATIONSHIP_TYPE);
if let Some(relationship) = endnotes_relationship {
let endnotes_part_name = format!("/word/{}", relationship.target);
let endnotes_part = package
.part(&endnotes_part_name)
.ok_or_else(|| Error::MissingEndnotesPart(endnotes_part_name.clone()))?;
let endnotes_xml = std::str::from_utf8(&endnotes_part.data)?;
let endnotes_resolver = ImageResolver {
package: &package,
part_relationships: &endnotes_part.relationships,
};
document.endnotes = parse_notes_xml(endnotes_xml, &endnotes_resolver, b"endnote")?;
}
let comments_relationship = part
.relationships
.iter()
.find(|relationship| relationship.rel_type == COMMENTS_RELATIONSHIP_TYPE);
if let Some(relationship) = comments_relationship {
let comments_part_name = format!("/word/{}", relationship.target);
let comments_part = package
.part(&comments_part_name)
.ok_or_else(|| Error::MissingCommentsPart(comments_part_name.clone()))?;
let comments_xml = std::str::from_utf8(&comments_part.data)?;
let comments_resolver = ImageResolver {
package: &package,
part_relationships: &comments_part.relationships,
};
document.comments = parse_comments_xml(comments_xml, &comments_resolver)?;
}
let theme_relationship = part
.relationships
.iter()
.find(|relationship| relationship.rel_type == THEME_RELATIONSHIP_TYPE);
if let Some(relationship) = theme_relationship {
let theme_part_name = format!("/word/{}", relationship.target);
let theme_part = package
.part(&theme_part_name)
.ok_or_else(|| Error::MissingThemePart(theme_part_name.clone()))?;
let theme_xml = std::str::from_utf8(&theme_part.data)?;
document.theme = Some(parse_theme_xml(theme_xml)?);
}
let settings_relationship = part
.relationships
.iter()
.find(|relationship| relationship.rel_type == SETTINGS_RELATIONSHIP_TYPE);
if let Some(relationship) = settings_relationship {
let settings_part_name = format!("/word/{}", relationship.target);
if let Some(settings_part) = package.part(&settings_part_name) {
let settings_xml = std::str::from_utf8(&settings_part.data)?;
let (track_changes, protection) = parse_settings_xml(settings_xml)?;
document.track_changes = track_changes;
document.protection = protection;
}
}
let core_properties_relationship = package
.relationships()
.iter()
.find(|relationship| relationship.rel_type == CORE_PROPERTIES_RELATIONSHIP_TYPE);
if let Some(relationship) = core_properties_relationship {
let core_properties_part_name = format!("/{}", relationship.target);
if let Some(core_properties_part) = package.part(&core_properties_part_name) {
let core_properties_xml = std::str::from_utf8(&core_properties_part.data)?;
document.properties = parse_core_properties_xml(core_properties_xml)?;
}
}
let extended_properties_relationship = package
.relationships()
.iter()
.find(|relationship| relationship.rel_type == EXTENDED_PROPERTIES_RELATIONSHIP_TYPE);
if let Some(relationship) = extended_properties_relationship {
let extended_properties_part_name = format!("/{}", relationship.target);
if let Some(extended_properties_part) = package.part(&extended_properties_part_name) {
let extended_properties_xml = std::str::from_utf8(&extended_properties_part.data)?;
document.extended_properties =
parse_extended_properties_xml(extended_properties_xml)?;
}
}
let custom_properties_relationship = package
.relationships()
.iter()
.find(|relationship| relationship.rel_type == CUSTOM_PROPERTIES_RELATIONSHIP_TYPE);
if let Some(relationship) = custom_properties_relationship {
let custom_properties_part_name = format!("/{}", relationship.target);
if let Some(custom_properties_part) = package.part(&custom_properties_part_name) {
let custom_properties_xml = std::str::from_utf8(&custom_properties_part.data)?;
document.custom_properties = parse_custom_properties_xml(custom_properties_xml)?;
}
}
Ok(document)
}
}
fn resolve_header_footer(
package: &Package,
document_relationships: &Relationships,
relationship_id: &str,
) -> Result<HeaderFooter> {
let relationship = document_relationships
.by_id(relationship_id)
.ok_or_else(|| Error::MissingHeaderFooterRelationship(relationship_id.to_string()))?;
let part_name = format!("/word/{}", relationship.target);
let part = package
.part(&part_name)
.ok_or_else(|| Error::MissingHeaderFooterPart(part_name.clone()))?;
let xml = std::str::from_utf8(&part.data)?;
let resolver = ImageResolver {
package,
part_relationships: &part.relationships,
};
Ok(HeaderFooter {
blocks: parse_blocks(xml, &resolver)?,
})
}
struct ImageResolver<'a> {
package: &'a Package,
part_relationships: &'a Relationships,
}
impl ImageResolver<'_> {
fn resolve(
&self,
relationship_id: &str,
width_emu: i64,
height_emu: i64,
description: String,
) -> Result<Image> {
let relationship = self
.part_relationships
.by_id(relationship_id)
.ok_or_else(|| Error::MissingImageRelationship(relationship_id.to_string()))?;
let part_name = format!("/word/{}", relationship.target);
let part = self
.package
.part(&part_name)
.ok_or_else(|| Error::MissingImagePart(part_name.clone()))?;
let format = ImageFormat::from_extension(&part_name)
.ok_or_else(|| Error::UnsupportedImageFormat(part_name.clone()))?;
Ok(Image {
data: part.data.clone(),
format,
width_emu,
height_emu,
description,
shape_properties: None,
})
}
fn resolve_chart(&self, relationship_id: &str) -> Result<chart::ChartSpace> {
let relationship = self
.part_relationships
.by_id(relationship_id)
.ok_or_else(|| Error::MissingChartRelationship(relationship_id.to_string()))?;
let part_name = format!("/word/{}", relationship.target);
let part = self
.package
.part(&part_name)
.ok_or_else(|| Error::MissingChartPart(part_name.clone()))?;
let xml = std::str::from_utf8(&part.data)?;
Ok(chart::read_chart_space(xml)?)
}
}
struct ParsedDocumentXml {
document: Document,
header_relationship_id: Option<String>,
footer_relationship_id: Option<String>,
header_first_relationship_id: Option<String>,
footer_first_relationship_id: Option<String>,
header_even_relationship_id: Option<String>,
footer_even_relationship_id: Option<String>,
}
fn parse_document_xml(xml: &str, resolver: &ImageResolver) -> Result<ParsedDocumentXml> {
let mut document = Document::new();
let mut header_relationship_id: Option<String> = None;
let mut footer_relationship_id: Option<String> = None;
let mut header_first_relationship_id: Option<String> = None;
let mut footer_first_relationship_id: Option<String> = None;
let mut header_even_relationship_id: Option<String> = None;
let mut footer_even_relationship_id: Option<String> = None;
let mut reader = Reader::from_xml_str(xml);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) => match start.local_name().as_ref() {
b"p" => document
.blocks
.push(Block::Paragraph(parse_paragraph(&mut reader, resolver)?)),
b"tbl" => document
.blocks
.push(Block::Table(parse_table(&mut reader, resolver)?)),
b"sdt" => document.blocks.push(Block::StructuredDocumentTag(
parse_structured_document_tag(&mut reader, resolver)?,
)),
b"headerReference" => capture_header_footer_reference(
&start,
&mut header_relationship_id,
&mut header_first_relationship_id,
&mut header_even_relationship_id,
),
b"footerReference" => capture_header_footer_reference(
&start,
&mut footer_relationship_id,
&mut footer_first_relationship_id,
&mut footer_even_relationship_id,
),
b"pgSz" => apply_page_size(&start, &mut document.page_setup),
b"pgMar" => apply_page_margins(&start, &mut document.page_setup),
b"cols" => {
document.page_setup.columns =
raw_attribute(&start, b"num").and_then(|value| value.parse().ok())
}
_ => {}
},
Event::Empty(start) => match start.local_name().as_ref() {
b"p" => document.blocks.push(Block::Paragraph(Paragraph::new())),
b"sdt" => document
.blocks
.push(Block::StructuredDocumentTag(StructuredDocumentTag::new())),
b"headerReference" => capture_header_footer_reference(
&start,
&mut header_relationship_id,
&mut header_first_relationship_id,
&mut header_even_relationship_id,
),
b"footerReference" => capture_header_footer_reference(
&start,
&mut footer_relationship_id,
&mut footer_first_relationship_id,
&mut footer_even_relationship_id,
),
b"pgSz" => apply_page_size(&start, &mut document.page_setup),
b"pgMar" => apply_page_margins(&start, &mut document.page_setup),
b"cols" => {
document.page_setup.columns =
raw_attribute(&start, b"num").and_then(|value| value.parse().ok())
}
_ => {}
},
_ => {}
}
}
Ok(ParsedDocumentXml {
document,
header_relationship_id,
footer_relationship_id,
header_first_relationship_id,
footer_first_relationship_id,
header_even_relationship_id,
footer_even_relationship_id,
})
}
fn apply_page_size(start: &BytesStart, page_setup: &mut PageSetup) {
if let Some(width) = raw_attribute(start, b"w").and_then(|value| value.parse().ok()) {
page_setup.width_twips = width;
}
if let Some(height) = raw_attribute(start, b"h").and_then(|value| value.parse().ok()) {
page_setup.height_twips = height;
}
if let Some(orientation) =
raw_attribute(start, b"orient").and_then(|value| Orientation::from_attribute_value(&value))
{
page_setup.orientation = orientation;
}
}
fn apply_page_margins(start: &BytesStart, page_setup: &mut PageSetup) {
if let Some(value) = raw_attribute(start, b"top").and_then(|value| value.parse().ok()) {
page_setup.margin_top_twips = value;
}
if let Some(value) = raw_attribute(start, b"bottom").and_then(|value| value.parse().ok()) {
page_setup.margin_bottom_twips = value;
}
if let Some(value) = raw_attribute(start, b"left").and_then(|value| value.parse().ok()) {
page_setup.margin_left_twips = value;
}
if let Some(value) = raw_attribute(start, b"right").and_then(|value| value.parse().ok()) {
page_setup.margin_right_twips = value;
}
if let Some(value) = raw_attribute(start, b"header").and_then(|value| value.parse().ok()) {
page_setup.margin_header_twips = value;
}
if let Some(value) = raw_attribute(start, b"footer").and_then(|value| value.parse().ok()) {
page_setup.margin_footer_twips = value;
}
if let Some(value) = raw_attribute(start, b"gutter").and_then(|value| value.parse().ok()) {
page_setup.margin_gutter_twips = value;
}
}
fn parse_blocks(xml: &str, resolver: &ImageResolver) -> Result<Vec<Block>> {
let mut blocks = Vec::new();
let mut reader = Reader::from_xml_str(xml);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) => match start.local_name().as_ref() {
b"p" => blocks.push(Block::Paragraph(parse_paragraph(&mut reader, resolver)?)),
b"tbl" => blocks.push(Block::Table(parse_table(&mut reader, resolver)?)),
b"sdt" => blocks.push(Block::StructuredDocumentTag(parse_structured_document_tag(
&mut reader,
resolver,
)?)),
_ => {}
},
Event::Empty(start) => match start.local_name().as_ref() {
b"p" => blocks.push(Block::Paragraph(Paragraph::new())),
b"sdt" => blocks.push(Block::StructuredDocumentTag(StructuredDocumentTag::new())),
_ => {}
},
_ => {}
}
}
Ok(blocks)
}
fn capture_header_footer_reference(
start: &BytesStart,
default_target: &mut Option<String>,
first_target: &mut Option<String>,
even_target: &mut Option<String>,
) {
let type_value = raw_attribute(start, b"type");
match type_value.as_deref() {
Some("default") if default_target.is_none() => {
*default_target = raw_attribute(start, b"id")
}
Some("first") if first_target.is_none() => *first_target = raw_attribute(start, b"id"),
Some("even") if even_target.is_none() => *even_target = raw_attribute(start, b"id"),
_ => {}
}
}
fn parse_paragraph(reader: &mut Reader<'_>, resolver: &ImageResolver) -> Result<Paragraph> {
let mut paragraph = Paragraph::new();
let mut current_run_text: Option<String> = None;
let mut current_run_drawing: Option<RunContent> = None;
let mut current_run_note: Option<RunContent> = None;
let mut current_run_break: Option<RunContent> = None;
let mut current_run_bold = false;
let mut current_run_italic = false;
let mut current_run_underline: Option<UnderlineStyle> = None;
let mut current_run_underline_color: Option<String> = None;
let mut current_run_color: Option<String> = None;
let mut current_run_font_size: Option<u16> = None;
let mut current_run_font_family: Option<String> = None;
let mut current_run_highlight: Option<Highlight> = None;
let mut current_run_strike = false;
let mut current_run_vertical_align: Option<VerticalAlign> = None;
let mut current_run_small_caps = false;
let mut current_run_all_caps = false;
let mut current_run_shading_color: Option<String> = None;
let mut current_run_text_border = false;
let mut current_run_character_spacing: Option<i16> = None;
let mut current_run_vertical_position: Option<i16> = None;
let mut current_run_hidden = false;
let mut current_run_style_id: Option<String> = None;
let mut in_run = false;
let mut in_text = false;
let mut in_field = false;
let mut current_field: Option<Field> = None;
let mut current_hyperlink: Option<Hyperlink> = None;
let mut open_comment_ids: Vec<u32> = Vec::new();
let mut open_bookmarks: Vec<Bookmark> = Vec::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) => match start.local_name().as_ref() {
b"r" => {
in_run = true;
current_run_text = Some(String::new());
current_run_drawing = None;
current_run_note = None;
current_run_break = None;
current_run_bold = false;
current_run_italic = false;
current_run_underline = None;
current_run_underline_color = None;
current_run_color = None;
current_run_font_size = None;
current_run_font_family = None;
current_run_highlight = None;
current_run_strike = false;
current_run_vertical_align = None;
current_run_small_caps = false;
current_run_all_caps = false;
current_run_shading_color = None;
current_run_text_border = false;
current_run_character_spacing = None;
current_run_vertical_position = None;
current_run_hidden = false;
current_run_style_id = None;
}
b"t" => in_text = true,
b"drawing" => current_run_drawing = parse_drawing(reader, resolver)?,
b"pStyle" => paragraph.style_id = raw_attribute(&start, b"val"),
b"ilvl" => {
paragraph.numbering_level = raw_attribute(&start, b"val")
.and_then(|value| value.parse().ok())
.unwrap_or(0)
}
b"numId" => {
paragraph.numbering_id =
raw_attribute(&start, b"val").and_then(|value| value.parse().ok())
}
b"jc" => paragraph.alignment = alignment_from_attribute(&start),
b"b" => current_run_bold = on_off_attribute(&start),
b"i" => current_run_italic = on_off_attribute(&start),
b"u" => {
current_run_underline = underline_attribute(&start);
current_run_underline_color = raw_attribute(&start, b"color");
}
b"color" => current_run_color = raw_attribute(&start, b"val"),
b"sz" => {
current_run_font_size = raw_attribute(&start, b"val")
.and_then(|value| value.parse::<u16>().ok())
.map(|half_points| half_points / 2)
}
b"rFonts" => {
current_run_font_family =
raw_attribute(&start, b"ascii").or_else(|| raw_attribute(&start, b"hAnsi"))
}
b"highlight" => {
current_run_highlight = raw_attribute(&start, b"val")
.and_then(|value| Highlight::from_attribute_value(&value))
}
b"strike" => current_run_strike = on_off_attribute(&start),
b"vertAlign" => {
current_run_vertical_align = raw_attribute(&start, b"val")
.and_then(|value| VerticalAlign::from_attribute_value(&value))
}
b"smallCaps" => current_run_small_caps = on_off_attribute(&start),
b"caps" => current_run_all_caps = on_off_attribute(&start),
b"shd" => {
if in_run {
current_run_shading_color = raw_attribute(&start, b"fill");
} else {
paragraph.shading_color = raw_attribute(&start, b"fill");
}
}
b"bdr" => current_run_text_border = true,
b"spacing" => {
if in_run {
current_run_character_spacing = raw_attribute(&start, b"val")
.and_then(|value| value.parse::<i16>().ok());
} else {
paragraph.space_before = raw_attribute(&start, b"before")
.and_then(|value| value.parse::<u32>().ok());
paragraph.space_after = raw_attribute(&start, b"after")
.and_then(|value| value.parse::<u32>().ok());
paragraph.line_spacing = raw_attribute(&start, b"line")
.and_then(|value| value.parse::<u32>().ok());
}
}
b"position" => {
current_run_vertical_position =
raw_attribute(&start, b"val").and_then(|value| value.parse::<i16>().ok())
}
b"vanish" => current_run_hidden = true,
b"rStyle" => current_run_style_id = raw_attribute(&start, b"val"),
b"pBdr" => paragraph.border = true,
b"keepNext" => paragraph.keep_with_next = true,
b"keepLines" => paragraph.keep_lines_together = true,
b"pageBreakBefore" => paragraph.page_break_before = true,
b"contextualSpacing" => paragraph.contextual_spacing = true,
b"fldSimple" => {
in_field = true;
current_field =
field_from_instruction(raw_attribute(&start, b"instr").as_deref());
}
b"hyperlink" => current_hyperlink = hyperlink_from_start(&start, resolver),
_ => {}
},
Event::Empty(start) => match start.local_name().as_ref() {
b"r" if !in_field => {
paragraph.runs.push(Run {
hyperlink: current_hyperlink.clone(),
comment_ids: open_comment_ids.clone(),
bookmarks: open_bookmarks.clone(),
..Run::new("")
});
}
b"pStyle" => paragraph.style_id = raw_attribute(&start, b"val"),
b"ilvl" => {
paragraph.numbering_level = raw_attribute(&start, b"val")
.and_then(|value| value.parse().ok())
.unwrap_or(0)
}
b"numId" => {
paragraph.numbering_id =
raw_attribute(&start, b"val").and_then(|value| value.parse().ok())
}
b"jc" => paragraph.alignment = alignment_from_attribute(&start),
b"b" => current_run_bold = on_off_attribute(&start),
b"i" => current_run_italic = on_off_attribute(&start),
b"u" => {
current_run_underline = underline_attribute(&start);
current_run_underline_color = raw_attribute(&start, b"color");
}
b"color" => current_run_color = raw_attribute(&start, b"val"),
b"sz" => {
current_run_font_size = raw_attribute(&start, b"val")
.and_then(|value| value.parse::<u16>().ok())
.map(|half_points| half_points / 2)
}
b"rFonts" => {
current_run_font_family =
raw_attribute(&start, b"ascii").or_else(|| raw_attribute(&start, b"hAnsi"))
}
b"highlight" => {
current_run_highlight = raw_attribute(&start, b"val")
.and_then(|value| Highlight::from_attribute_value(&value))
}
b"strike" => current_run_strike = on_off_attribute(&start),
b"vertAlign" => {
current_run_vertical_align = raw_attribute(&start, b"val")
.and_then(|value| VerticalAlign::from_attribute_value(&value))
}
b"smallCaps" => current_run_small_caps = on_off_attribute(&start),
b"caps" => current_run_all_caps = on_off_attribute(&start),
b"shd" => {
if in_run {
current_run_shading_color = raw_attribute(&start, b"fill");
} else {
paragraph.shading_color = raw_attribute(&start, b"fill");
}
}
b"bdr" => current_run_text_border = true,
b"spacing" => {
if in_run {
current_run_character_spacing = raw_attribute(&start, b"val")
.and_then(|value| value.parse::<i16>().ok());
} else {
paragraph.space_before = raw_attribute(&start, b"before")
.and_then(|value| value.parse::<u32>().ok());
paragraph.space_after = raw_attribute(&start, b"after")
.and_then(|value| value.parse::<u32>().ok());
paragraph.line_spacing = raw_attribute(&start, b"line")
.and_then(|value| value.parse::<u32>().ok());
}
}
b"position" => {
current_run_vertical_position =
raw_attribute(&start, b"val").and_then(|value| value.parse::<i16>().ok())
}
b"vanish" => current_run_hidden = true,
b"rStyle" => current_run_style_id = raw_attribute(&start, b"val"),
b"pBdr" => paragraph.border = true,
b"keepNext" => paragraph.keep_with_next = true,
b"keepLines" => paragraph.keep_lines_together = true,
b"pageBreakBefore" => paragraph.page_break_before = true,
b"contextualSpacing" => paragraph.contextual_spacing = true,
b"tab" => {
if let Some(position) =
raw_attribute(&start, b"pos").and_then(|value| value.parse::<i32>().ok())
{
let mut tab = TabStop::new(position);
if let Some(alignment) = raw_attribute(&start, b"val")
.and_then(|value| TabStopAlignment::from_attribute_value(&value))
{
tab = tab.with_alignment(alignment);
}
if let Some(leader) = raw_attribute(&start, b"leader")
.and_then(|value| TabLeader::from_attribute_value(&value))
{
tab = tab.with_leader(leader);
}
paragraph.tabs.push(tab);
}
}
b"br" => {
current_run_break = Some(RunContent::Break(BreakKind::from_attribute_value(
raw_attribute(&start, b"type").as_deref(),
)));
}
b"cr" => current_run_break = Some(RunContent::CarriageReturn),
b"fldSimple" => {
if let Some(field) =
field_from_instruction(raw_attribute(&start, b"instr").as_deref())
{
paragraph.runs.push(Run {
hyperlink: current_hyperlink.clone(),
comment_ids: open_comment_ids.clone(),
bookmarks: open_bookmarks.clone(),
..Run::with_field(field)
});
}
}
b"commentRangeStart" => {
if let Some(id) =
raw_attribute(&start, b"id").and_then(|value| value.parse().ok())
{
open_comment_ids.push(id);
}
}
b"commentRangeEnd" => {
if let Some(id) =
raw_attribute(&start, b"id").and_then(|value| value.parse::<u32>().ok())
{
open_comment_ids.retain(|open_id| *open_id != id);
}
}
b"bookmarkStart" => {
if let (Some(id), Some(name)) = (
raw_attribute(&start, b"id").and_then(|value| value.parse().ok()),
raw_attribute(&start, b"name"),
) {
open_bookmarks.push(Bookmark::new(id, name));
}
}
b"bookmarkEnd" => {
if let Some(id) =
raw_attribute(&start, b"id").and_then(|value| value.parse::<u32>().ok())
{
open_bookmarks.retain(|bookmark| bookmark.id != id);
}
}
b"commentReference" => {}
b"footnoteReference" => {
current_run_note = raw_attribute(&start, b"id")
.and_then(|value| value.parse().ok())
.map(|id| RunContent::NoteReference(NoteReference::Footnote(id)));
}
b"endnoteReference" => {
current_run_note = raw_attribute(&start, b"id")
.and_then(|value| value.parse().ok())
.map(|id| RunContent::NoteReference(NoteReference::Endnote(id)));
}
b"footnoteRef" => {
current_run_note = Some(RunContent::NoteMarker(NoteKind::Footnote))
}
b"endnoteRef" => current_run_note = Some(RunContent::NoteMarker(NoteKind::Endnote)),
_ => {}
},
Event::Text(text) if in_text => {
if let Some(buffer) = current_run_text.as_mut() {
let text = xml_core::decode_text(&text)?;
buffer.push_str(&text);
}
}
Event::GeneralRef(reference) if in_text => {
if let Some(buffer) = current_run_text.as_mut() {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
buffer.push_str(&resolved);
}
}
}
Event::End(end) => match end.local_name().as_ref() {
b"t" => in_text = false,
b"r" if in_field => {
in_run = false;
}
b"r" => {
in_run = false;
let bold = current_run_bold;
let italic = current_run_italic;
let underline = current_run_underline;
let underline_color = current_run_underline_color.take();
let color = current_run_color.take();
let font_size = current_run_font_size;
let font_family = current_run_font_family.take();
let highlight = current_run_highlight.take();
let strike = current_run_strike;
let vertical_align = current_run_vertical_align.take();
let small_caps = current_run_small_caps;
let all_caps = current_run_all_caps;
let shading_color = current_run_shading_color.take();
let text_border = current_run_text_border;
let character_spacing = current_run_character_spacing;
let vertical_position = current_run_vertical_position;
let hidden = current_run_hidden;
let style_id = current_run_style_id.take();
let hyperlink = current_hyperlink.clone();
let comment_ids = open_comment_ids.clone();
let bookmarks = open_bookmarks.clone();
let run = if let Some(drawing_content) = current_run_drawing.take() {
Run {
content: drawing_content,
bold,
italic,
underline,
underline_color,
color,
font_size,
font_family,
highlight,
strike,
vertical_align,
small_caps,
all_caps,
shading_color,
text_border,
character_spacing,
vertical_position,
hidden,
style_id,
hyperlink,
comment_ids,
bookmarks,
}
} else if let Some(note_content) = current_run_note.take() {
Run {
content: note_content,
bold,
italic,
underline,
underline_color,
color,
font_size,
font_family,
highlight,
strike,
vertical_align,
small_caps,
all_caps,
shading_color,
text_border,
character_spacing,
vertical_position,
hidden,
style_id,
hyperlink,
comment_ids,
bookmarks,
}
} else if let Some(break_content) = current_run_break.take() {
Run {
content: break_content,
bold,
italic,
underline,
underline_color,
color,
font_size,
font_family,
highlight,
strike,
vertical_align,
small_caps,
all_caps,
shading_color,
text_border,
character_spacing,
vertical_position,
hidden,
style_id,
hyperlink,
comment_ids,
bookmarks,
}
} else {
let text = current_run_text.take().unwrap_or_default();
Run {
content: RunContent::Text(text),
bold,
italic,
underline,
underline_color,
color,
font_size,
font_family,
highlight,
strike,
vertical_align,
small_caps,
all_caps,
shading_color,
text_border,
character_spacing,
vertical_position,
hidden,
style_id,
hyperlink,
comment_ids,
bookmarks,
}
};
paragraph.runs.push(run);
}
b"fldSimple" => {
if let Some(field) = current_field.take() {
paragraph.runs.push(Run {
content: RunContent::Field(field),
bold: current_run_bold,
italic: current_run_italic,
underline: current_run_underline,
underline_color: current_run_underline_color.take(),
color: current_run_color.take(),
font_size: current_run_font_size,
font_family: current_run_font_family.take(),
highlight: current_run_highlight.take(),
strike: current_run_strike,
vertical_align: current_run_vertical_align.take(),
small_caps: current_run_small_caps,
all_caps: current_run_all_caps,
shading_color: current_run_shading_color.take(),
text_border: current_run_text_border,
character_spacing: current_run_character_spacing,
vertical_position: current_run_vertical_position,
hidden: current_run_hidden,
style_id: current_run_style_id.take(),
hyperlink: current_hyperlink.clone(),
comment_ids: open_comment_ids.clone(),
bookmarks: open_bookmarks.clone(),
});
}
in_field = false;
}
b"hyperlink" => current_hyperlink = None,
b"p" => break,
_ => {}
},
_ => {}
}
}
Ok(paragraph)
}
fn parse_table(reader: &mut Reader<'_>, resolver: &ImageResolver) -> Result<Table> {
let mut table = Table::new();
let mut current_row: Option<TableRow> = None;
let mut current_cell: Option<TableCell> = None;
let mut in_table_properties = false;
let mut in_tbl_borders = false;
let mut in_tc_borders = false;
let mut in_tc_mar = false;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) => match start.local_name().as_ref() {
b"tblPr" => in_table_properties = true,
b"tblBorders" => in_tbl_borders = true,
b"tcBorders" => in_tc_borders = true,
b"tcMar" => in_tc_mar = true,
b"tr" => current_row = Some(TableRow::new()),
b"tc" => current_cell = Some(TableCell::new()),
b"p" => {
let paragraph = parse_paragraph(reader, resolver)?;
if let Some(cell) = current_cell.as_mut() {
cell.blocks.push(Block::Paragraph(paragraph));
}
}
b"tbl" => {
let nested_table = parse_table(reader, resolver)?;
if let Some(cell) = current_cell.as_mut() {
cell.blocks.push(Block::Table(nested_table));
}
}
b"tblStyle" => {
table.style_id = raw_attribute(&start, b"val");
}
b"jc" if in_table_properties => {
table.alignment = alignment_from_attribute(&start);
}
b"tblInd" => {
table.indent_twips =
raw_attribute(&start, b"w").and_then(|value| value.parse::<u32>().ok());
}
b"top" | b"left" | b"bottom" | b"right" => {
apply_table_or_cell_side(
&start,
start.local_name().as_ref(),
in_tbl_borders,
in_table_properties,
in_tc_borders,
in_tc_mar,
&mut table,
current_cell.as_mut(),
);
}
b"shd" => {
if let Some(cell) = current_cell.as_mut() {
cell.shading_color = raw_attribute(&start, b"fill");
}
}
b"textDirection" => {
if let Some(cell) = current_cell.as_mut() {
cell.text_direction = raw_attribute(&start, b"val")
.and_then(|value| TextDirection::from_attribute_value(&value));
}
}
b"vAlign" => {
if let Some(cell) = current_cell.as_mut() {
cell.vertical_alignment = raw_attribute(&start, b"val")
.and_then(|value| CellVerticalAlign::from_attribute_value(&value));
}
}
b"cantSplit" => {
if let Some(row) = current_row.as_mut() {
row.cant_split = true;
}
}
b"trHeight" => {
if let Some(row) = current_row.as_mut() {
row.height_twips = raw_attribute(&start, b"val")
.and_then(|value| value.parse::<u32>().ok());
}
}
b"tblHeader" => {
if let Some(row) = current_row.as_mut() {
row.repeat_as_header_row = true;
}
}
b"gridSpan" => {
if let Some(cell) = current_cell.as_mut() {
cell.horizontal_span = raw_attribute(&start, b"val")
.and_then(|value| value.parse::<u32>().ok());
}
}
b"vMerge" => {
if let Some(cell) = current_cell.as_mut() {
cell.vertical_merge = Some(vertical_merge_attribute(&start));
}
}
b"gridCol" => {
if let Some(width) =
raw_attribute(&start, b"w").and_then(|value| value.parse::<u32>().ok())
{
table.column_widths.push(width);
}
}
_ => {}
},
Event::Empty(start) => match start.local_name().as_ref() {
b"p" => {
if let Some(cell) = current_cell.as_mut() {
cell.blocks.push(Block::Paragraph(Paragraph::new()));
}
}
b"tblStyle" => {
table.style_id = raw_attribute(&start, b"val");
}
b"jc" if in_table_properties => {
table.alignment = alignment_from_attribute(&start);
}
b"tblInd" => {
table.indent_twips =
raw_attribute(&start, b"w").and_then(|value| value.parse::<u32>().ok());
}
b"top" | b"left" | b"bottom" | b"right" => {
apply_table_or_cell_side(
&start,
start.local_name().as_ref(),
in_tbl_borders,
in_table_properties,
in_tc_borders,
in_tc_mar,
&mut table,
current_cell.as_mut(),
);
}
b"shd" => {
if let Some(cell) = current_cell.as_mut() {
cell.shading_color = raw_attribute(&start, b"fill");
}
}
b"textDirection" => {
if let Some(cell) = current_cell.as_mut() {
cell.text_direction = raw_attribute(&start, b"val")
.and_then(|value| TextDirection::from_attribute_value(&value));
}
}
b"vAlign" => {
if let Some(cell) = current_cell.as_mut() {
cell.vertical_alignment = raw_attribute(&start, b"val")
.and_then(|value| CellVerticalAlign::from_attribute_value(&value));
}
}
b"cantSplit" => {
if let Some(row) = current_row.as_mut() {
row.cant_split = true;
}
}
b"trHeight" => {
if let Some(row) = current_row.as_mut() {
row.height_twips = raw_attribute(&start, b"val")
.and_then(|value| value.parse::<u32>().ok());
}
}
b"tblHeader" => {
if let Some(row) = current_row.as_mut() {
row.repeat_as_header_row = true;
}
}
b"gridSpan" => {
if let Some(cell) = current_cell.as_mut() {
cell.horizontal_span = raw_attribute(&start, b"val")
.and_then(|value| value.parse::<u32>().ok());
}
}
b"vMerge" => {
if let Some(cell) = current_cell.as_mut() {
cell.vertical_merge = Some(vertical_merge_attribute(&start));
}
}
b"gridCol" => {
if let Some(width) =
raw_attribute(&start, b"w").and_then(|value| value.parse::<u32>().ok())
{
table.column_widths.push(width);
}
}
_ => {}
},
Event::End(end) => match end.local_name().as_ref() {
b"tblPr" => in_table_properties = false,
b"tblBorders" => in_tbl_borders = false,
b"tcBorders" => in_tc_borders = false,
b"tcMar" => in_tc_mar = false,
b"tc" => {
if let (Some(cell), Some(row)) = (current_cell.take(), current_row.as_mut()) {
row.cells.push(cell);
}
}
b"tr" => {
if let Some(row) = current_row.take() {
table.rows.push(row);
}
}
b"tbl" => break,
_ => {}
},
_ => {}
}
}
Ok(table)
}
#[allow(clippy::too_many_arguments)]
fn apply_table_or_cell_side(
start: &BytesStart,
tag_name: &[u8],
in_tbl_borders: bool,
in_table_properties: bool,
in_tc_borders: bool,
in_tc_mar: bool,
table: &mut Table,
current_cell: Option<&mut TableCell>,
) {
if in_tc_mar {
if let Some(cell) = current_cell {
let value = raw_attribute(start, b"w").and_then(|value| value.parse::<u32>().ok());
match tag_name {
b"top" => cell.margin_top_twips = value,
b"left" => cell.margin_left_twips = value,
b"bottom" => cell.margin_bottom_twips = value,
b"right" => cell.margin_right_twips = value,
_ => {}
}
}
} else if in_tc_borders {
if let Some(cell) = current_cell {
cell.border = true;
}
} else if in_tbl_borders && in_table_properties {
if let Some(color) = raw_attribute(start, b"color") {
if color != "auto" {
table.border_color = Some(color);
}
}
}
}
fn parse_drawing(reader: &mut Reader<'_>, resolver: &ImageResolver) -> Result<Option<RunContent>> {
let mut width_emu: i64 = 0;
let mut height_emu: i64 = 0;
let mut description = String::new();
let mut image_relationship_id: Option<String> = None;
let mut chart_relationship_id: Option<String> = None;
let mut shape_properties: Option<ShapeProperties> = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"spPr" => {
shape_properties = Some(drawing::read_shape_properties(reader)?);
}
Event::Empty(start) if start.local_name().as_ref() == b"spPr" => {
shape_properties = Some(ShapeProperties::new());
}
Event::Start(start) | Event::Empty(start) => match start.local_name().as_ref() {
b"extent" => {
width_emu = raw_attribute(&start, b"cx")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
height_emu = raw_attribute(&start, b"cy")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
}
b"docPr" => description = raw_attribute(&start, b"descr").unwrap_or_default(),
b"blip" => image_relationship_id = raw_attribute(&start, b"embed"),
b"chart" => chart_relationship_id = raw_attribute(&start, b"id"),
_ => {}
},
Event::End(end) if end.local_name().as_ref() == b"drawing" => break,
_ => {}
}
}
if let Some(id) = image_relationship_id {
let mut image = resolver.resolve(&id, width_emu, height_emu, description)?;
if let Some(mut shape_properties) = shape_properties {
shape_properties.transform = None;
if matches!(
shape_properties.geometry,
Some(Geometry::Preset(PresetShape::Rectangle))
) {
shape_properties.geometry = None;
}
let is_fixed_default = shape_properties.geometry.is_none()
&& shape_properties.fill.is_none()
&& shape_properties.line.is_none()
&& shape_properties.effects.is_none();
if !is_fixed_default {
image = image.with_shape_properties(shape_properties);
}
}
return Ok(Some(RunContent::Image(Box::new(image))));
}
if let Some(id) = chart_relationship_id {
let chart_space = resolver.resolve_chart(&id)?;
let chart = EmbeddedChart::new(chart_space, width_emu, height_emu).with_name(description);
return Ok(Some(RunContent::Chart(Box::new(chart))));
}
Ok(None)
}
fn on_off_attribute(start: &BytesStart) -> bool {
match raw_attribute(start, b"val") {
None => true,
Some(value) => matches!(value.as_str(), "true" | "1" | "on"),
}
}
fn underline_attribute(start: &BytesStart) -> Option<UnderlineStyle> {
match raw_attribute(start, b"val") {
None => Some(UnderlineStyle::Single),
Some(value) => UnderlineStyle::from_attribute_value(&value),
}
}
fn vertical_merge_attribute(start: &BytesStart) -> VerticalMerge {
match raw_attribute(start, b"val").as_deref() {
Some("restart") => VerticalMerge::Restart,
_ => VerticalMerge::Continue,
}
}
fn alignment_from_attribute(start: &BytesStart) -> Option<Alignment> {
match raw_attribute(start, b"val")?.as_str() {
"left" | "start" => Some(Alignment::Left),
"center" => Some(Alignment::Center),
"right" | "end" => Some(Alignment::Right),
"both" => Some(Alignment::Justify),
_ => None,
}
}
fn raw_attribute(start: &BytesStart, name: &[u8]) -> Option<String> {
start
.attributes()
.flatten()
.find(|attribute| attribute.key.local_name().as_ref() == name)
.and_then(|attribute| {
xml_core::decode_attribute_value(attribute.value.as_ref())
.ok()
.flatten()
})
}
fn field_from_instruction(instr: Option<&str>) -> Option<Field> {
let first_token = instr?.split_whitespace().next()?;
if first_token.eq_ignore_ascii_case("PAGE") {
Some(Field::PageNumber)
} else if first_token.eq_ignore_ascii_case("NUMPAGES") {
Some(Field::TotalPages)
} else {
None
}
}
fn hyperlink_from_start(start: &BytesStart, resolver: &ImageResolver) -> Option<Hyperlink> {
if let Some(relationship_id) = raw_attribute(start, b"id") {
return resolver
.part_relationships
.by_id(&relationship_id)
.map(|relationship| Hyperlink::External(relationship.target.clone()));
}
raw_attribute(start, b"anchor").map(Hyperlink::Internal)
}
fn parse_styles_xml(xml: &str) -> Result<Vec<Style>> {
let mut styles = Vec::new();
let mut reader = Reader::from_xml_str(xml);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"style" => {
if let Some(style) = parse_style(&mut reader, &start)? {
styles.push(style);
}
}
_ => {}
}
}
Ok(styles)
}
fn parse_style(reader: &mut Reader<'_>, start: &BytesStart) -> Result<Option<Style>> {
let kind = match raw_attribute(start, b"type").as_deref() {
Some("paragraph") => StyleKind::Paragraph,
Some("character") => StyleKind::Character,
_ => {
skip_to_end(reader, b"style")?;
return Ok(None);
}
};
let id = raw_attribute(start, b"styleId").unwrap_or_default();
let mut name = String::new();
let mut based_on: Option<String> = None;
let mut alignment: Option<Alignment> = None;
let mut bold = false;
let mut italic = false;
let mut underline: Option<UnderlineStyle> = None;
let mut underline_color: Option<String> = None;
let mut color: Option<String> = None;
let mut font_size: Option<u16> = None;
let mut font_family: Option<String> = None;
let mut highlight: Option<Highlight> = None;
let mut strike = false;
let mut vertical_align: Option<VerticalAlign> = None;
let mut small_caps = false;
let mut all_caps = false;
let mut shading_color: Option<String> = None;
let mut text_border = false;
let mut character_spacing: Option<i16> = None;
let mut vertical_position: Option<i16> = None;
let mut hidden = false;
let mut line_spacing: Option<u32> = None;
let mut space_before: Option<u32> = None;
let mut space_after: Option<u32> = None;
let mut paragraph_shading_color: Option<String> = None;
let mut paragraph_border = false;
let mut keep_with_next = false;
let mut keep_lines_together = false;
let mut page_break_before = false;
let mut tabs: Vec<TabStop> = Vec::new();
let mut contextual_spacing = false;
let mut in_paragraph_properties = false;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) | Event::Empty(start) => match start.local_name().as_ref() {
b"name" => name = raw_attribute(&start, b"val").unwrap_or_default(),
b"basedOn" => based_on = raw_attribute(&start, b"val"),
b"pPr" => in_paragraph_properties = true,
b"rPr" => in_paragraph_properties = false,
b"pBdr" => paragraph_border = true,
b"keepNext" => keep_with_next = true,
b"keepLines" => keep_lines_together = true,
b"pageBreakBefore" => page_break_before = true,
b"contextualSpacing" => contextual_spacing = true,
b"tab" => {
if let Some(position) =
raw_attribute(&start, b"pos").and_then(|value| value.parse::<i32>().ok())
{
let mut tab = TabStop::new(position);
if let Some(alignment) = raw_attribute(&start, b"val")
.and_then(|value| TabStopAlignment::from_attribute_value(&value))
{
tab = tab.with_alignment(alignment);
}
if let Some(leader) = raw_attribute(&start, b"leader")
.and_then(|value| TabLeader::from_attribute_value(&value))
{
tab = tab.with_leader(leader);
}
tabs.push(tab);
}
}
b"jc" => alignment = alignment_from_attribute(&start),
b"b" => bold = on_off_attribute(&start),
b"i" => italic = on_off_attribute(&start),
b"u" => {
underline = underline_attribute(&start);
underline_color = raw_attribute(&start, b"color");
}
b"color" => color = raw_attribute(&start, b"val"),
b"sz" => {
font_size = raw_attribute(&start, b"val")
.and_then(|value| value.parse::<u16>().ok())
.map(|half_points| half_points / 2)
}
b"rFonts" => {
font_family =
raw_attribute(&start, b"ascii").or_else(|| raw_attribute(&start, b"hAnsi"))
}
b"highlight" => {
highlight = raw_attribute(&start, b"val")
.and_then(|value| Highlight::from_attribute_value(&value))
}
b"strike" => strike = on_off_attribute(&start),
b"vertAlign" => {
vertical_align = raw_attribute(&start, b"val")
.and_then(|value| VerticalAlign::from_attribute_value(&value))
}
b"smallCaps" => small_caps = on_off_attribute(&start),
b"caps" => all_caps = on_off_attribute(&start),
b"shd" => {
if in_paragraph_properties {
paragraph_shading_color = raw_attribute(&start, b"fill");
} else {
shading_color = raw_attribute(&start, b"fill");
}
}
b"bdr" => text_border = true,
b"spacing" => {
if in_paragraph_properties {
space_before = raw_attribute(&start, b"before")
.and_then(|value| value.parse::<u32>().ok());
space_after = raw_attribute(&start, b"after")
.and_then(|value| value.parse::<u32>().ok());
line_spacing = raw_attribute(&start, b"line")
.and_then(|value| value.parse::<u32>().ok());
} else {
character_spacing = raw_attribute(&start, b"val")
.and_then(|value| value.parse::<i16>().ok());
}
}
b"position" => {
vertical_position =
raw_attribute(&start, b"val").and_then(|value| value.parse::<i16>().ok())
}
b"vanish" => hidden = true,
_ => {}
},
Event::End(end) if end.local_name().as_ref() == b"style" => break,
_ => {}
}
}
let mut style = Style::new(id, name, kind)
.with_bold(bold)
.with_italic(italic);
if let Some(underline) = underline {
style = style.with_underline(underline);
}
if let Some(underline_color) = underline_color {
style = style.with_underline_color(underline_color);
}
if let Some(based_on) = based_on {
style = style.with_based_on(based_on);
}
if let Some(alignment) = alignment {
style = style.with_alignment(alignment);
}
if let Some(color) = color {
style = style.with_color(color);
}
if let Some(font_size) = font_size {
style = style.with_font_size(font_size);
}
if let Some(font_family) = font_family {
style = style.with_font_family(font_family);
}
if let Some(highlight) = highlight {
style = style.with_highlight(highlight);
}
style = style.with_strike(strike);
if let Some(vertical_align) = vertical_align {
style = style.with_vertical_align(vertical_align);
}
style = style
.with_small_caps(small_caps)
.with_all_caps(all_caps)
.with_text_border(text_border)
.with_hidden(hidden);
if let Some(shading_color) = shading_color {
style = style.with_shading_color(shading_color);
}
if let Some(character_spacing) = character_spacing {
style = style.with_character_spacing(character_spacing);
}
if let Some(vertical_position) = vertical_position {
style = style.with_vertical_position(vertical_position);
}
style = style.with_paragraph_border(paragraph_border);
if let Some(paragraph_shading_color) = paragraph_shading_color {
style = style.with_paragraph_shading_color(paragraph_shading_color);
}
if let Some(line_spacing) = line_spacing {
style = style.with_line_spacing(line_spacing);
}
if let Some(space_before) = space_before {
style = style.with_space_before(space_before);
}
if let Some(space_after) = space_after {
style = style.with_space_after(space_after);
}
style = style
.with_keep_with_next(keep_with_next)
.with_keep_lines_together(keep_lines_together)
.with_page_break_before(page_break_before)
.with_contextual_spacing(contextual_spacing);
for tab in tabs {
style = style.with_tab(tab);
}
Ok(Some(style))
}
fn skip_to_end(reader: &mut Reader<'_>, local_name: &[u8]) -> Result<()> {
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == local_name => break,
_ => {}
}
}
Ok(())
}
fn parse_numbering_xml(xml: &str) -> Result<Vec<NumberingDefinition>> {
let mut abstract_nums: HashMap<u32, Vec<ListLevel>> = HashMap::new();
let mut definitions = Vec::new();
let mut reader = Reader::from_xml_str(xml);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) => match start.local_name().as_ref() {
b"abstractNum" => match raw_attribute(&start, b"abstractNumId")
.and_then(|value| value.parse().ok())
{
Some(id) => {
let levels = parse_abstract_num(&mut reader)?;
abstract_nums.insert(id, levels);
}
None => skip_to_end(&mut reader, b"abstractNum")?,
},
b"num" => {
match raw_attribute(&start, b"numId").and_then(|value| value.parse().ok()) {
Some(num_id) => {
let abstract_num_id = parse_num_abstract_ref(&mut reader)?;
let levels = abstract_num_id
.and_then(|id| abstract_nums.get(&id).cloned())
.unwrap_or_default();
definitions.push(NumberingDefinition::new(num_id, levels));
}
None => skip_to_end(&mut reader, b"num")?,
}
}
_ => {}
},
_ => {}
}
}
Ok(definitions)
}
fn parse_abstract_num(reader: &mut Reader<'_>) -> Result<Vec<ListLevel>> {
let mut levels = Vec::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"lvl" => {
levels.push(parse_lvl(reader)?)
}
Event::End(end) if end.local_name().as_ref() == b"abstractNum" => break,
_ => {}
}
}
Ok(levels)
}
fn parse_lvl(reader: &mut Reader<'_>) -> Result<ListLevel> {
let mut format = NumberFormat::Decimal;
let mut text = String::new();
let mut indent_twips: i32 = 0;
let mut hanging_twips: i32 = 0;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) | Event::Empty(start) => match start.local_name().as_ref() {
b"numFmt" => {
format = number_format_from_attribute(&start).unwrap_or(NumberFormat::Decimal)
}
b"lvlText" => text = raw_attribute(&start, b"val").unwrap_or_default(),
b"ind" => {
indent_twips = raw_attribute(&start, b"left")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
hanging_twips = raw_attribute(&start, b"hanging")
.and_then(|value| value.parse().ok())
.unwrap_or(0);
}
_ => {}
},
Event::End(end) if end.local_name().as_ref() == b"lvl" => break,
_ => {}
}
}
Ok(ListLevel::new(format, text, indent_twips, hanging_twips))
}
fn parse_num_abstract_ref(reader: &mut Reader<'_>) -> Result<Option<u32>> {
let mut abstract_num_id = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) | Event::Empty(start)
if start.local_name().as_ref() == b"abstractNumId" =>
{
abstract_num_id =
raw_attribute(&start, b"val").and_then(|value| value.parse().ok());
}
Event::End(end) if end.local_name().as_ref() == b"num" => break,
_ => {}
}
}
Ok(abstract_num_id)
}
fn number_format_from_attribute(start: &BytesStart) -> Option<NumberFormat> {
match raw_attribute(start, b"val")?.as_str() {
"bullet" => Some(NumberFormat::Bullet),
"decimal" => Some(NumberFormat::Decimal),
"lowerLetter" => Some(NumberFormat::LowerLetter),
"upperLetter" => Some(NumberFormat::UpperLetter),
"lowerRoman" => Some(NumberFormat::LowerRoman),
"upperRoman" => Some(NumberFormat::UpperRoman),
_ => None,
}
}
fn parse_notes_xml(
xml: &str,
resolver: &ImageResolver,
note_element_name: &[u8],
) -> Result<Vec<Note>> {
let mut notes = Vec::new();
let mut reader = Reader::from_xml_str(xml);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == note_element_name => {
let is_special_note = matches!(
raw_attribute(&start, b"type").as_deref(),
Some("separator" | "continuationSeparator" | "continuationNotice")
);
let id = raw_attribute(&start, b"id").and_then(|value| value.parse().ok());
match (is_special_note, id) {
(false, Some(id)) => {
let blocks = parse_note_blocks(&mut reader, resolver, note_element_name)?;
notes.push(Note { id, blocks });
}
_ => skip_to_end(&mut reader, note_element_name)?,
}
}
_ => {}
}
}
Ok(notes)
}
fn parse_note_blocks(
reader: &mut Reader<'_>,
resolver: &ImageResolver,
stop_local_name: &[u8],
) -> Result<Vec<Block>> {
let mut blocks = Vec::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) => match start.local_name().as_ref() {
b"p" => blocks.push(Block::Paragraph(parse_paragraph(reader, resolver)?)),
b"tbl" => blocks.push(Block::Table(parse_table(reader, resolver)?)),
b"sdt" => blocks.push(Block::StructuredDocumentTag(parse_structured_document_tag(
reader, resolver,
)?)),
_ => {}
},
Event::Empty(start) => match start.local_name().as_ref() {
b"p" => blocks.push(Block::Paragraph(Paragraph::new())),
b"sdt" => blocks.push(Block::StructuredDocumentTag(StructuredDocumentTag::new())),
_ => {}
},
Event::End(end) if end.local_name().as_ref() == stop_local_name => break,
_ => {}
}
}
Ok(blocks)
}
fn parse_structured_document_tag(
reader: &mut Reader<'_>,
resolver: &ImageResolver,
) -> Result<StructuredDocumentTag> {
let mut sdt = StructuredDocumentTag::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) => match start.local_name().as_ref() {
b"sdtPr" => parse_sdt_properties(reader, &mut sdt)?,
b"sdtContent" => sdt.blocks = parse_note_blocks(reader, resolver, b"sdtContent")?,
_ => {}
},
Event::End(end) if end.local_name().as_ref() == b"sdt" => break,
_ => {}
}
}
Ok(sdt)
}
fn parse_sdt_properties(reader: &mut Reader<'_>, sdt: &mut StructuredDocumentTag) -> Result<()> {
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Empty(start) => match start.local_name().as_ref() {
b"id" => {
sdt.id = raw_attribute(&start, b"val").and_then(|value| value.parse().ok())
}
b"tag" => sdt.tag = raw_attribute(&start, b"val"),
b"alias" => sdt.alias = raw_attribute(&start, b"val"),
_ => {}
},
Event::End(end) if end.local_name().as_ref() == b"sdtPr" => break,
_ => {}
}
}
Ok(())
}
fn parse_comments_xml(xml: &str, resolver: &ImageResolver) -> Result<Vec<Comment>> {
let mut comments = Vec::new();
let mut reader = Reader::from_xml_str(xml);
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"comment" => {
match raw_attribute(&start, b"id").and_then(|value| value.parse().ok()) {
Some(id) => {
let author = raw_attribute(&start, b"author");
let initials = raw_attribute(&start, b"initials");
let date = raw_attribute(&start, b"date");
let blocks = parse_note_blocks(&mut reader, resolver, b"comment")?;
comments.push(Comment {
id,
author,
initials,
date,
blocks,
});
}
None => skip_to_end(&mut reader, b"comment")?,
}
}
_ => {}
}
}
Ok(comments)
}
fn parse_theme_xml(xml: &str) -> Result<Theme> {
let mut reader = Reader::from_xml_str(xml);
let mut name = String::new();
let mut colors = ColorScheme::office_default();
let mut fonts = FontScheme::office_default();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) => match start.local_name().as_ref() {
b"theme" => {
if let Some(value) = raw_attribute(&start, b"name") {
name = value;
}
}
b"clrScheme" => colors = parse_color_scheme(&mut reader)?,
b"fontScheme" => fonts = parse_font_scheme(&mut reader)?,
_ => {}
},
_ => {}
}
}
Ok(Theme {
name,
colors,
fonts,
})
}
fn parse_settings_xml(xml: &str) -> Result<(bool, Option<ProtectionKind>)> {
let mut reader = Reader::from_xml_str(xml);
let mut track_changes = false;
let mut protection = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) | Event::Empty(start) => match start.local_name().as_ref() {
b"trackRevisions" => track_changes = on_off_attribute(&start),
b"documentProtection" => {
protection = raw_attribute(&start, b"edit")
.and_then(|value| ProtectionKind::from_attribute_value(&value));
}
_ => {}
},
_ => {}
}
}
Ok((track_changes, protection))
}
fn parse_core_properties_xml(xml: &str) -> Result<DocumentProperties> {
let mut reader = Reader::from_xml_str(xml);
let mut properties = DocumentProperties::new();
let mut current_element: Option<Vec<u8>> = None;
let mut buffer = String::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) => {
let local_name = start.local_name().as_ref().to_vec();
if is_tracked_core_property_element(&local_name) {
current_element = Some(local_name);
buffer.clear();
}
}
Event::Text(text) if current_element.is_some() => {
buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if current_element.is_some() => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
buffer.push_str(&resolved);
}
}
Event::End(end) => {
if let Some(name) = current_element.take() {
if end.local_name().as_ref() == name.as_slice() {
apply_core_property(&mut properties, &name, std::mem::take(&mut buffer));
} else {
current_element = Some(name);
}
}
}
_ => {}
}
}
Ok(properties)
}
fn is_tracked_core_property_element(local_name: &[u8]) -> bool {
matches!(
local_name,
b"title"
| b"subject"
| b"creator"
| b"keywords"
| b"description"
| b"lastModifiedBy"
| b"revision"
| b"created"
| b"modified"
| b"category"
| b"contentStatus"
)
}
fn apply_core_property(properties: &mut DocumentProperties, local_name: &[u8], value: String) {
match local_name {
b"title" => properties.title = Some(value),
b"subject" => properties.subject = Some(value),
b"creator" => properties.creator = Some(value),
b"keywords" => properties.keywords = Some(value),
b"description" => properties.description = Some(value),
b"lastModifiedBy" => properties.last_modified_by = Some(value),
b"revision" => properties.revision = Some(value),
b"created" => properties.created = Some(value),
b"modified" => properties.modified = Some(value),
b"category" => properties.category = Some(value),
b"contentStatus" => properties.content_status = Some(value),
_ => {}
}
}
fn parse_extended_properties_xml(xml: &str) -> Result<ExtendedProperties> {
let mut reader = Reader::from_xml_str(xml);
let mut properties = ExtendedProperties::new();
let mut current_element: Option<Vec<u8>> = None;
let mut buffer = String::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) => {
let local_name = start.local_name().as_ref().to_vec();
if local_name == b"Company" || local_name == b"Manager" {
current_element = Some(local_name);
buffer.clear();
}
}
Event::Text(text) if current_element.is_some() => {
buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if current_element.is_some() => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
buffer.push_str(&resolved);
}
}
Event::End(end) => {
if let Some(name) = current_element.take() {
if end.local_name().as_ref() == name.as_slice() {
let value = std::mem::take(&mut buffer);
match name.as_slice() {
b"Company" => properties.company = Some(value),
b"Manager" => properties.manager = Some(value),
_ => {}
}
} else {
current_element = Some(name);
}
}
}
_ => {}
}
}
Ok(properties)
}
fn parse_custom_properties_xml(xml: &str) -> Result<Vec<(String, CustomPropertyValue)>> {
let mut reader = Reader::from_xml_str(xml);
let mut custom_properties = Vec::new();
let mut current_name: Option<String> = None;
let mut current_variant: Option<Vec<u8>> = None;
let mut buffer = String::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"property" => {
current_name = raw_attribute(&start, b"name");
}
Event::Start(start) | Event::Empty(start)
if current_name.is_some()
&& matches!(
start.local_name().as_ref(),
b"lpwstr" | b"bool" | b"i4" | b"r8"
) =>
{
current_variant = Some(start.local_name().as_ref().to_vec());
buffer.clear();
}
Event::Text(text) if current_variant.is_some() => {
buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if current_variant.is_some() => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
buffer.push_str(&resolved);
}
}
Event::End(end) => {
if let Some(variant) = current_variant.take() {
if end.local_name().as_ref() == variant.as_slice() {
if let Some(name) = current_name.clone() {
let value = std::mem::take(&mut buffer);
let parsed = match variant.as_slice() {
b"lpwstr" => Some(CustomPropertyValue::Text(value)),
b"bool" => {
Some(CustomPropertyValue::Bool(value == "true" || value == "1"))
}
b"i4" => value.parse::<i32>().ok().map(CustomPropertyValue::Int),
b"r8" => value.parse::<f64>().ok().map(CustomPropertyValue::Number),
_ => None,
};
if let Some(value) = parsed {
custom_properties.push((name, value));
}
}
} else {
current_variant = Some(variant);
}
} else if end.local_name().as_ref() == b"property" {
current_name = None;
}
}
_ => {}
}
}
Ok(custom_properties)
}
fn parse_color_scheme(reader: &mut Reader<'_>) -> Result<ColorScheme> {
let mut colors = ColorScheme::office_default();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"clrScheme" => break,
Event::Start(start) => {
let slot = start.local_name().as_ref().to_vec();
if let Some(value) = read_color_slot_value(reader, &slot)? {
assign_color_slot(&mut colors, &slot, value);
}
}
_ => {}
}
}
Ok(colors)
}
fn read_color_slot_value(
reader: &mut Reader<'_>,
wrapper_local_name: &[u8],
) -> Result<Option<String>> {
let mut value = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == wrapper_local_name => break,
Event::Empty(start) | Event::Start(start) => match start.local_name().as_ref() {
b"srgbClr" => value = raw_attribute(&start, b"val"),
b"sysClr" => {
value =
raw_attribute(&start, b"lastClr").or_else(|| raw_attribute(&start, b"val"))
}
_ => {}
},
_ => {}
}
}
Ok(value)
}
fn assign_color_slot(colors: &mut ColorScheme, slot: &[u8], value: String) {
match slot {
b"dk1" => colors.dark1 = value,
b"lt1" => colors.light1 = value,
b"dk2" => colors.dark2 = value,
b"lt2" => colors.light2 = value,
b"accent1" => colors.accent1 = value,
b"accent2" => colors.accent2 = value,
b"accent3" => colors.accent3 = value,
b"accent4" => colors.accent4 = value,
b"accent5" => colors.accent5 = value,
b"accent6" => colors.accent6 = value,
b"hlink" => colors.hyperlink = value,
b"folHlink" => colors.followed_hyperlink = value,
_ => {}
}
}
fn parse_font_scheme(reader: &mut Reader<'_>) -> Result<FontScheme> {
let mut fonts = FontScheme::office_default();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == b"fontScheme" => break,
Event::Start(start) if start.local_name().as_ref() == b"majorFont" => {
if let Some(typeface) = parse_font_collection_latin(reader, b"majorFont")? {
fonts.major_latin = typeface;
}
}
Event::Start(start) if start.local_name().as_ref() == b"minorFont" => {
if let Some(typeface) = parse_font_collection_latin(reader, b"minorFont")? {
fonts.minor_latin = typeface;
}
}
_ => {}
}
}
Ok(fonts)
}
fn parse_font_collection_latin(
reader: &mut Reader<'_>,
wrapper_local_name: &[u8],
) -> Result<Option<String>> {
let mut typeface = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::End(end) if end.local_name().as_ref() == wrapper_local_name => break,
Event::Empty(start) | Event::Start(start)
if start.local_name().as_ref() == b"latin" =>
{
typeface = raw_attribute(&start, b"typeface");
}
_ => {}
}
}
Ok(typeface)
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(xml: &str) -> Document {
let package = Package::new();
let relationships = Relationships::new();
let resolver = ImageResolver {
package: &package,
part_relationships: &relationships,
};
parse_document_xml(xml, &resolver).unwrap().document
}
#[test]
fn unescapes_xml_special_characters_from_hand_written_xml() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:r><w:t>A & B < C > D ' E " F</w:t></w:r></w:p></w:body>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].text(), r#"A & B < C > D ' E " F"#);
}
#[test]
fn parses_paragraphs_and_runs() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p><w:r><w:t>Hello</w:t></w:r></w:p>
<w:p><w:r><w:t>How are you?</w:t></w:r></w:p>
<w:p/>
<w:sectPr><w:pgSz w:w="11906" w:h="16838"/></w:sectPr>
</w:body>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs.len(), 3);
assert_eq!(paragraphs[0].text(), "Hello");
assert_eq!(paragraphs[1].text(), "How are you?");
assert_eq!(paragraphs[2].text(), "");
}
#[test]
fn parses_a_paragraph_with_multiple_runs() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs.len(), 1);
assert_eq!(paragraphs[0].runs.len(), 2);
assert_eq!(paragraphs[0].text(), "Hello, world!");
}
#[test]
fn parses_bold_italic_and_underline_run_properties() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
let run = ¶graphs[0].runs[0];
assert!(run.bold, "{run:?}");
assert!(run.italic, "{run:?}");
assert_eq!(run.underline, Some(UnderlineStyle::Single), "{run:?}");
}
#[test]
fn parses_color_size_and_font_family_run_properties() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
let run = ¶graphs[0].runs[0];
assert_eq!(run.color.as_deref(), Some("FF0000"));
assert_eq!(run.font_size, Some(14));
assert_eq!(run.font_family.as_deref(), Some("Georgia"));
}
#[test]
fn parses_a_highlighted_run() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs[0].highlight, Some(Highlight::Yellow));
}
#[test]
fn an_unrecognized_highlight_value_is_not_set() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs[0].highlight, None);
}
#[test]
fn parses_a_struck_through_run() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:r><w:rPr><w:strike/></w:rPr><w:t>Struck</w:t></w:r></w:p></w:body>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert!(paragraphs[0].runs[0].strike);
}
#[test]
fn parses_a_superscript_run() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(
paragraphs[0].runs[0].vertical_align,
Some(VerticalAlign::Superscript)
);
}
#[test]
fn a_baseline_vert_align_value_is_not_set() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs[0].vertical_align, None);
}
#[test]
fn a_u_element_with_val_none_is_not_underlined() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs[0].underline, None);
}
#[test]
fn parses_a_wave_underline_and_color() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
let run = ¶graphs[0].runs[0];
assert_eq!(run.underline, Some(UnderlineStyle::Wave));
assert_eq!(run.underline_color.as_deref(), Some("FF0000"));
}
#[test]
fn parses_small_caps_and_all_caps() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert!(paragraphs[0].runs[0].small_caps);
assert!(!paragraphs[0].runs[0].all_caps);
assert!(paragraphs[0].runs[1].all_caps);
assert!(!paragraphs[0].runs[1].small_caps);
}
#[test]
fn parses_a_shading_color() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(
paragraphs[0].runs[0].shading_color.as_deref(),
Some("FFFF00")
);
}
#[test]
fn parses_a_text_border() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert!(paragraphs[0].runs[0].text_border);
}
#[test]
fn parses_character_spacing_and_vertical_position() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
let run = ¶graphs[0].runs[0];
assert_eq!(run.character_spacing, Some(40));
assert_eq!(run.vertical_position, Some(-6));
}
#[test]
fn parses_hidden_text() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:r><w:rPr><w:vanish/></w:rPr><w:t>Hidden</w:t></w:r></w:p></w:body>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert!(paragraphs[0].runs[0].hidden);
}
#[test]
fn parses_paragraph_alignment() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].alignment, Some(Alignment::Center));
}
#[test]
fn paragraphs_without_jc_have_no_alignment() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:r><w:t>Default</w:t></w:r></w:p></w:body>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].alignment, None);
}
#[test]
fn parses_paragraph_line_spacing_and_before_after() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].line_spacing, Some(480));
assert_eq!(paragraphs[0].space_before, Some(240));
assert_eq!(paragraphs[0].space_after, Some(120));
}
#[test]
fn parses_a_paragraph_shading_color() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].shading_color.as_deref(), Some("D9D9D9"));
}
#[test]
fn parses_a_paragraph_border() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert!(paragraphs[0].border);
}
#[test]
fn paragraph_and_run_level_shading_and_spacing_do_not_collide() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
let paragraph = ¶graphs[0];
let run = ¶graph.runs[0];
assert_eq!(paragraph.shading_color.as_deref(), Some("D9D9D9"));
assert_eq!(paragraph.line_spacing, Some(480));
assert_eq!(paragraph.space_before, Some(240));
assert_eq!(paragraph.space_after, Some(120));
assert_eq!(run.shading_color.as_deref(), Some("FFFF00"));
assert_eq!(run.character_spacing, Some(40));
}
#[test]
fn parses_a_table_of_text_cells() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr>
<w:tblGrid><w:gridCol w:w="2000"/><w:gridCol w:w="2000"/></w:tblGrid>
<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>
<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>
</w:tbl>
</w:body>
</w:document>"#;
let document = parse(xml);
let tables: Vec<_> = document.tables().collect();
assert_eq!(tables.len(), 1);
let table = tables[0];
assert_eq!(table.rows.len(), 2);
assert_eq!(table.rows[0].cells.len(), 2);
assert_eq!(table.rows[0].cells[0].text(), "A1");
assert_eq!(table.rows[0].cells[1].text(), "B1");
assert_eq!(table.rows[1].cells[0].text(), "A2");
assert_eq!(table.rows[1].cells[1].text(), "");
}
#[test]
fn parses_a_nested_table_inside_a_cell() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr>
<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
<w:tr><w:tc>
<w:tbl>
<w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr>
<w:tblGrid><w:gridCol w:w="1000"/></w:tblGrid>
<w:tr><w:tc><w:p><w:r><w:t>Inner</w:t></w:r></w:p></w:tc></w:tr>
</w:tbl>
<w:p/>
</w:tc></w:tr>
</w:tbl>
</w:body>
</w:document>"#;
let document = parse(xml);
let outer_table = document.tables().next().unwrap();
let cell = &outer_table.rows[0].cells[0];
let nested_tables: Vec<_> = cell.tables().collect();
assert_eq!(nested_tables.len(), 1);
assert_eq!(nested_tables[0].rows[0].cells[0].text(), "Inner");
assert_eq!(cell.text(), "");
}
#[test]
fn parses_gridcol_widths_into_column_widths() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblPr><w:tblW w:w="4000" w:type="dxa"/><w:tblLayout w:type="fixed"/></w:tblPr>
<w:tblGrid><w:gridCol w:w="1000"/><w:gridCol w:w="3000"/></w:tblGrid>
<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>
</w:tbl>
</w:body>
</w:document>"#;
let document = parse(xml);
let tables: Vec<_> = document.tables().collect();
assert_eq!(tables[0].column_widths, vec![1000, 3000]);
}
#[test]
fn parses_a_horizontal_cell_merge() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblGrid><w:gridCol w:w="2000"/><w:gridCol w:w="2000"/></w:tblGrid>
<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>
</w:tbl>
</w:body>
</w:document>"#;
let document = parse(xml);
let tables: Vec<_> = document.tables().collect();
assert_eq!(tables[0].rows[0].cells[0].horizontal_span, Some(2));
}
#[test]
fn parses_a_vertical_cell_merge() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
<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>
<w:tr><w:tc><w:tcPr><w:vMerge/></w:tcPr><w:p/></w:tc></w:tr>
</w:tbl>
</w:body>
</w:document>"#;
let document = parse(xml);
let tables: Vec<_> = document.tables().collect();
assert_eq!(
tables[0].rows[0].cells[0].vertical_merge,
Some(VerticalMerge::Restart)
);
assert_eq!(
tables[0].rows[1].cells[0].vertical_merge,
Some(VerticalMerge::Continue)
);
}
#[test]
fn a_cell_with_no_tcpr_has_no_merge_properties() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
<w:tr><w:tc><w:p><w:r><w:t>Ordinary</w:t></w:r></w:p></w:tc></w:tr>
</w:tbl>
</w:body>
</w:document>"#;
let document = parse(xml);
let tables: Vec<_> = document.tables().collect();
assert_eq!(tables[0].rows[0].cells[0].horizontal_span, None);
assert_eq!(tables[0].rows[0].cells[0].vertical_merge, None);
}
#[test]
fn parses_a_custom_table_border_color() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="FF0000"/></w:tblBorders></w:tblPr>
<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
<w:tr><w:tc><w:p><w:r><w:t>A1</w:t></w:r></w:p></w:tc></w:tr>
</w:tbl>
</w:body>
</w:document>"#;
let document = parse(xml);
let tables: Vec<_> = document.tables().collect();
assert_eq!(tables[0].border_color.as_deref(), Some("FF0000"));
}
#[test]
fn a_table_with_the_fixed_auto_border_color_leaves_border_color_unset() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:tblBorders></w:tblPr>
<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
<w:tr><w:tc><w:p><w:r><w:t>A1</w:t></w:r></w:p></w:tc></w:tr>
</w:tbl>
</w:body>
</w:document>"#;
let document = parse(xml);
let tables: Vec<_> = document.tables().collect();
assert_eq!(tables[0].border_color, None);
}
#[test]
fn parses_table_style_alignment_and_indent() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblPr><w:tblStyle w:val="TableGrid"/><w:jc w:val="center"/><w:tblInd w:w="200" w:type="dxa"/></w:tblPr>
<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
<w:tr><w:tc><w:p><w:r><w:t>A1</w:t></w:r></w:p></w:tc></w:tr>
</w:tbl>
</w:body>
</w:document>"#;
let document = parse(xml);
let tables: Vec<_> = document.tables().collect();
assert_eq!(tables[0].style_id.as_deref(), Some("TableGrid"));
assert_eq!(tables[0].alignment, Some(Alignment::Center));
assert_eq!(tables[0].indent_twips, Some(200));
}
#[test]
fn parses_a_cell_border_shading_valign_margins_and_text_direction() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
<w:tr><w:tc><w:tcPr>
<w:tcBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:tcBorders>
<w:shd w:val="clear" w:color="auto" w:fill="00FF00"/>
<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>
<w:textDirection w:val="btLr"/>
<w:vAlign w:val="bottom"/>
</w:tcPr><w:p><w:r><w:t>Cell</w:t></w:r></w:p></w:tc></w:tr>
</w:tbl>
</w:body>
</w:document>"#;
let document = parse(xml);
let tables: Vec<_> = document.tables().collect();
let cell = &tables[0].rows[0].cells[0];
assert!(cell.border);
assert_eq!(cell.shading_color.as_deref(), Some("00FF00"));
assert_eq!(cell.margin_top_twips, Some(10));
assert_eq!(cell.margin_left_twips, Some(20));
assert_eq!(cell.margin_bottom_twips, Some(30));
assert_eq!(cell.margin_right_twips, Some(40));
assert_eq!(cell.text_direction, Some(TextDirection::BottomToTop));
assert_eq!(cell.vertical_alignment, Some(CellVerticalAlign::Bottom));
}
#[test]
fn cell_borders_and_margins_do_not_leak_into_the_table_level_border_color() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="0000FF"/></w:tblBorders></w:tblPr>
<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
<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>
</w:tbl>
</w:body>
</w:document>"#;
let document = parse(xml);
let tables: Vec<_> = document.tables().collect();
assert_eq!(tables[0].border_color.as_deref(), Some("0000FF"));
assert!(tables[0].rows[0].cells[0].border);
}
#[test]
fn parses_row_repeat_as_header_height_and_cant_split() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
<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>
</w:tbl>
</w:body>
</w:document>"#;
let document = parse(xml);
let tables: Vec<_> = document.tables().collect();
let row = &tables[0].rows[0];
assert!(row.repeat_as_header_row);
assert_eq!(row.height_twips, Some(500));
assert!(row.cant_split);
}
#[test]
fn a_row_with_no_trpr_has_default_row_properties() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblGrid><w:gridCol w:w="2000"/></w:tblGrid>
<w:tr><w:tc><w:p><w:r><w:t>Ordinary</w:t></w:r></w:p></w:tc></w:tr>
</w:tbl>
</w:body>
</w:document>"#;
let document = parse(xml);
let tables: Vec<_> = document.tables().collect();
let row = &tables[0].rows[0];
assert!(!row.repeat_as_header_row);
assert_eq!(row.height_twips, None);
assert!(!row.cant_split);
}
#[test]
fn paragraphs_and_tables_are_kept_in_reading_order() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p><w:r><w:t>Before</w:t></w:r></w:p>
<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>
<w:p><w:r><w:t>After</w:t></w:r></w:p>
</w:body>
</w:document>"#;
let document = parse(xml);
assert_eq!(document.blocks.len(), 3);
assert!(matches!(document.blocks[0], Block::Paragraph(ref p) if p.text() == "Before"));
assert!(matches!(document.blocks[1], Block::Table(_)));
assert!(matches!(document.blocks[2], Block::Paragraph(ref p) if p.text() == "After"));
}
#[test]
fn resolves_an_inline_image_via_its_relationship() {
use opc::{Part, Relationship};
let mut relationships = Relationships::new();
relationships.add(Relationship {
id: "rId2".to_string(),
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
.to_string(),
target: "media/image1.png".to_string(),
target_mode: TargetMode::Internal,
});
let mut package = Package::new();
package.add_part(Part::new(
"/word/media/image1.png",
"image/png",
vec![0u8, 1, 2, 3],
));
let resolver = ImageResolver {
package: &package,
part_relationships: &relationships,
};
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:r><w:drawing><wp:inline>
<wp:extent cx="914400" cy="457200"/>
<wp:docPr id="1" name="rId2" descr="a test image"/>
<a:graphic><a:graphicData uri="picture"><pic:pic><pic:blipFill><a:blip r:embed="rId2"/></pic:blipFill></pic:pic></a:graphicData></a:graphic>
</wp:inline></w:drawing></w:r></w:p></w:body>
</w:document>"#;
let document = parse_document_xml(xml, &resolver).unwrap().document;
let paragraphs: Vec<_> = document.paragraphs().collect();
let image = paragraphs[0].runs[0]
.image()
.expect("should be an image run");
assert_eq!(image.data, vec![0u8, 1, 2, 3]);
assert_eq!(image.format, ImageFormat::Png);
assert_eq!(image.width_emu, 914_400);
assert_eq!(image.height_emu, 457_200);
assert_eq!(image.description, "a test image");
}
#[test]
fn a_drawing_with_no_blip_yields_no_image_run() {
let package = Package::new();
let relationships = Relationships::new();
let resolver = ImageResolver {
package: &package,
part_relationships: &relationships,
};
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#;
let document = parse_document_xml(xml, &resolver).unwrap().document;
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs.len(), 1);
assert!(paragraphs[0].runs[0].image().is_none());
assert_eq!(paragraphs[0].runs[0].text(), Some(""));
}
#[test]
fn captures_the_default_header_and_footer_relationship_ids_from_sectpr() {
let package = Package::new();
let relationships = Relationships::new();
let resolver = ImageResolver {
package: &package,
part_relationships: &relationships,
};
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p><w:r><w:t>Body</w:t></w:r></w:p>
<w:sectPr>
<w:headerReference w:type="even" r:id="rId9"/>
<w:headerReference w:type="default" r:id="rId2"/>
<w:footerReference w:type="default" r:id="rId3"/>
<w:pgSz w:w="11906" w:h="16838"/>
</w:sectPr>
</w:body>
</w:document>"#;
let parsed = parse_document_xml(xml, &resolver).unwrap();
assert_eq!(parsed.header_relationship_id.as_deref(), Some("rId2"));
assert_eq!(parsed.header_even_relationship_id.as_deref(), Some("rId9"));
assert_eq!(parsed.header_first_relationship_id, None);
assert_eq!(parsed.footer_relationship_id.as_deref(), Some("rId3"));
}
#[test]
fn no_header_or_footer_reference_yields_none() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#,
);
assert!(document.header.is_none());
assert!(document.footer.is_none());
}
#[test]
fn parses_a_page_number_field() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs.len(), 2);
assert_eq!(paragraphs[0].runs[0].text(), Some("Page "));
let field_run = ¶graphs[0].runs[1];
assert_eq!(field_run.field(), Some(Field::PageNumber));
assert!(field_run.bold, "{field_run:?}");
}
#[test]
fn parses_a_total_pages_field_and_does_not_confuse_it_with_page() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:fldSimple w:instr="NUMPAGES"><w:r><w:t>1</w:t></w:r></w:fldSimple></w:p></w:body>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs[0].field(), Some(Field::TotalPages));
}
#[test]
fn an_unsupported_field_instruction_is_silently_dropped() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs.len(), 2);
assert_eq!(paragraphs[0].runs[0].text(), Some("Before"));
assert_eq!(paragraphs[0].runs[1].text(), Some("After"));
}
#[test]
fn a_self_closed_field_with_no_inner_run_is_parsed() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:fldSimple w:instr="PAGE"/></w:p></w:body>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs[0].field(), Some(Field::PageNumber));
}
#[test]
fn parses_a_paragraph_style_referenced_by_pstyle() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].style_id.as_deref(), Some("Heading1"));
assert_eq!(paragraphs[0].alignment, Some(Alignment::Center));
}
#[test]
fn parses_a_character_style_referenced_by_rstyle() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs[0].style_id.as_deref(), Some("Strong"));
assert!(paragraphs[0].runs[0].bold);
}
#[test]
fn a_page_number_field_can_carry_a_run_style() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(
paragraphs[0].runs[0].style_id.as_deref(),
Some("PageNumber")
);
}
#[test]
fn parses_styles_xml_into_paragraph_and_character_styles() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="Heading 1"/>
<w:pPr><w:jc w:val="center"/></w:pPr>
<w:rPr><w:b/></w:rPr>
</w:style>
<w:style w:type="character" w:styleId="Strong">
<w:name w:val="Strong"/>
<w:basedOn w:val="DefaultParagraphFont"/>
<w:rPr><w:b/><w:i/></w:rPr>
</w:style>
<w:style w:type="table" w:styleId="TableGrid">
<w:name w:val="Table Grid"/>
</w:style>
</w:styles>"#;
let styles = parse_styles_xml(xml).unwrap();
assert_eq!(styles.len(), 2);
assert_eq!(styles[0].id, "Heading1");
assert_eq!(styles[0].name, "Heading 1");
assert_eq!(styles[0].kind, StyleKind::Paragraph);
assert_eq!(styles[0].alignment, Some(Alignment::Center));
assert!(styles[0].bold);
assert_eq!(styles[0].based_on, None);
assert_eq!(styles[1].id, "Strong");
assert_eq!(styles[1].kind, StyleKind::Character);
assert_eq!(styles[1].based_on.as_deref(), Some("DefaultParagraphFont"));
assert!(styles[1].bold);
assert!(styles[1].italic);
}
#[test]
fn parses_a_style_with_color_size_and_font_family() {
let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="Heading 1"/>
<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>
</w:style>
</w:styles>"#;
let styles = parse_styles_xml(xml).unwrap();
assert_eq!(styles[0].color.as_deref(), Some("2E74B5"));
assert_eq!(styles[0].font_size, Some(16));
assert_eq!(styles[0].font_family.as_deref(), Some("Cambria"));
}
#[test]
fn parses_a_style_with_a_highlight() {
let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="Heading 1"/>
<w:rPr><w:highlight w:val="lightGray"/></w:rPr>
</w:style>
</w:styles>"#;
let styles = parse_styles_xml(xml).unwrap();
assert_eq!(styles[0].highlight, Some(Highlight::LightGray));
}
#[test]
fn parses_a_style_with_strike_and_subscript() {
let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="character" w:styleId="FootnoteReference">
<w:name w:val="footnote reference"/>
<w:rPr><w:strike/><w:vertAlign w:val="subscript"/></w:rPr>
</w:style>
</w:styles>"#;
let styles = parse_styles_xml(xml).unwrap();
assert!(styles[0].strike);
assert_eq!(styles[0].vertical_align, Some(VerticalAlign::Subscript));
}
#[test]
fn parses_a_style_with_a_double_underline_and_color() {
let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="Heading 1"/>
<w:rPr><w:u w:val="double" w:color="2E74B5"/></w:rPr>
</w:style>
</w:styles>"#;
let styles = parse_styles_xml(xml).unwrap();
assert_eq!(styles[0].underline, Some(UnderlineStyle::Double));
assert_eq!(styles[0].underline_color.as_deref(), Some("2E74B5"));
}
#[test]
fn parses_a_style_with_small_caps_shading_border_spacing_and_hidden() {
let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="character" w:styleId="Emphasis">
<w:name w:val="Emphasis"/>
<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>
</w:style>
</w:styles>"#;
let styles = parse_styles_xml(xml).unwrap();
let style = &styles[0];
assert!(style.small_caps);
assert!(style.all_caps);
assert_eq!(style.shading_color.as_deref(), Some("FFFF00"));
assert!(style.text_border);
assert_eq!(style.character_spacing, Some(40));
assert_eq!(style.vertical_position, Some(-6));
assert!(style.hidden);
}
#[test]
fn parses_a_paragraph_style_with_spacing_shading_and_border() {
let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="Heading 1"/>
<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>
</w:style>
</w:styles>"#;
let styles = parse_styles_xml(xml).unwrap();
let style = &styles[0];
assert!(style.paragraph_border);
assert_eq!(style.paragraph_shading_color.as_deref(), Some("D9D9D9"));
assert_eq!(style.line_spacing, Some(360));
assert_eq!(style.space_before, Some(240));
assert_eq!(style.space_after, Some(120));
}
#[test]
fn parses_a_paragraph_style_with_keep_settings_tabs_and_contextual_spacing() {
let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="paragraph" w:styleId="ListParagraph">
<w:name w:val="List Paragraph"/>
<w:pPr><w:keepNext/><w:keepLines/><w:pageBreakBefore/><w:tabs><w:tab w:val="left" w:pos="720"/></w:tabs><w:contextualSpacing/></w:pPr>
</w:style>
</w:styles>"#;
let styles = parse_styles_xml(xml).unwrap();
let style = &styles[0];
assert!(style.keep_with_next);
assert!(style.keep_lines_together);
assert!(style.page_break_before);
assert!(style.contextual_spacing);
assert_eq!(style.tabs.len(), 1);
assert_eq!(style.tabs[0].position_twips, 720);
}
#[test]
fn style_paragraph_and_run_level_shading_and_spacing_do_not_collide() {
let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="Heading 1"/>
<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:rPr><w:shd w:val="clear" w:color="auto" w:fill="FFFF00"/><w:spacing w:val="40"/></w:rPr>
</w:style>
</w:styles>"#;
let styles = parse_styles_xml(xml).unwrap();
let style = &styles[0];
assert_eq!(style.paragraph_shading_color.as_deref(), Some("D9D9D9"));
assert_eq!(style.line_spacing, Some(480));
assert_eq!(style.space_before, Some(240));
assert_eq!(style.space_after, Some(120));
assert_eq!(style.shading_color.as_deref(), Some("FFFF00"));
assert_eq!(style.character_spacing, Some(40));
}
#[test]
fn an_unrecognized_style_type_is_skipped_without_disturbing_the_next_style() {
let xml = r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="numbering" w:styleId="ListParagraph">
<w:name w:val="List Paragraph"/>
</w:style>
<w:style w:type="paragraph" w:styleId="Normal">
<w:name w:val="Normal"/>
</w:style>
</w:styles>"#;
let styles = parse_styles_xml(xml).unwrap();
assert_eq!(styles.len(), 1);
assert_eq!(styles[0].id, "Normal");
}
#[test]
fn a_document_without_a_styles_relationship_has_no_styles() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:r><w:t>Body</w:t></w:r></w:p></w:body>
</w:document>"#,
);
assert!(document.styles.is_empty());
}
#[test]
fn resolves_an_external_hyperlink_via_its_relationship() {
use opc::Relationship;
let mut relationships = Relationships::new();
relationships.add(Relationship {
id: "rId4".to_string(),
rel_type:
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
.to_string(),
target: "https://www.rust-lang.org".to_string(),
target_mode: TargetMode::External,
});
let package = Package::new();
let resolver = ImageResolver {
package: &package,
part_relationships: &relationships,
};
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:hyperlink r:id="rId4"><w:r><w:t>Rust</w:t></w:r></w:hyperlink></w:p></w:body>
</w:document>"#;
let document = parse_document_xml(xml, &resolver).unwrap().document;
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs.len(), 1);
assert_eq!(paragraphs[0].runs[0].text(), Some("Rust"));
assert_eq!(
paragraphs[0].runs[0].hyperlink,
Some(Hyperlink::External("https://www.rust-lang.org".to_string()))
);
}
#[test]
fn parses_an_internal_hyperlink_from_its_anchor_attribute() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(
paragraphs[0].runs[0].hyperlink,
Some(Hyperlink::Internal("Section1".to_string()))
);
}
#[test]
fn a_hyperlink_id_with_no_matching_relationship_is_dropped_but_its_run_is_kept() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs[0].text(), Some("Broken link"));
assert_eq!(paragraphs[0].runs[0].hyperlink, None);
}
#[test]
fn multiple_runs_inside_one_hyperlink_all_get_the_same_target() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
let runs = ¶graphs[0].runs;
assert_eq!(runs.len(), 2);
assert_eq!(
runs[0].hyperlink,
Some(Hyperlink::Internal("Top".to_string()))
);
assert_eq!(
runs[1].hyperlink,
Some(Hyperlink::Internal("Top".to_string()))
);
assert!(runs[0].bold);
assert!(!runs[1].bold);
}
#[test]
fn a_run_outside_any_hyperlink_has_no_hyperlink() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
let runs = ¶graphs[0].runs;
assert_eq!(runs.len(), 2);
assert!(runs[0].hyperlink.is_some());
assert_eq!(runs[1].hyperlink, None);
}
#[test]
fn parses_a_paragraphs_numpr_into_numbering_id_and_level() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].numbering_id, Some(7));
assert_eq!(paragraphs[0].numbering_level, 1);
}
#[test]
fn a_paragraph_without_numpr_has_no_numbering() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:r><w:t>Not a list item</w:t></w:r></w:p></w:body>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].numbering_id, None);
assert_eq!(paragraphs[0].numbering_level, 0);
}
#[test]
fn parses_numbering_xml_resolving_num_through_its_abstractnum() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:abstractNum w:abstractNumId="0">
<w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="bullet"/><w:lvlText w:val="•"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="720" w:hanging="360"/></w:pPr></w:lvl>
<w: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>
</w:abstractNum>
<w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>
</w:numbering>"#;
let definitions = parse_numbering_xml(xml).unwrap();
assert_eq!(definitions.len(), 1);
assert_eq!(definitions[0].id, 1);
assert_eq!(definitions[0].levels.len(), 2);
assert_eq!(definitions[0].levels[0].format, NumberFormat::Bullet);
assert_eq!(definitions[0].levels[0].text, "•");
assert_eq!(definitions[0].levels[0].indent_twips, 720);
assert_eq!(definitions[0].levels[0].hanging_twips, 360);
assert_eq!(definitions[0].levels[1].text, "◦");
}
#[test]
fn numid_and_abstractnumid_can_differ_like_in_a_real_word_document() {
let xml = r#"<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:abstractNum w:abstractNumId="4">
<w:lvl w:ilvl="0"><w:numFmt w:val="decimal"/><w:lvlText w:val="%1."/></w:lvl>
</w:abstractNum>
<w:num w:numId="2"><w:abstractNumId w:val="4"/></w:num>
</w:numbering>"#;
let definitions = parse_numbering_xml(xml).unwrap();
assert_eq!(definitions.len(), 1);
assert_eq!(definitions[0].id, 2);
assert_eq!(definitions[0].levels[0].format, NumberFormat::Decimal);
}
#[test]
fn a_num_referencing_an_undeclared_abstractnum_gets_empty_levels() {
let xml = r#"<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:num w:numId="1"><w:abstractNumId w:val="99"/></w:num>
</w:numbering>"#;
let definitions = parse_numbering_xml(xml).unwrap();
assert_eq!(definitions.len(), 1);
assert_eq!(definitions[0].id, 1);
assert!(definitions[0].levels.is_empty());
}
#[test]
fn a_document_without_a_numbering_relationship_has_no_numbering_definitions() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:r><w:t>Body</w:t></w:r></w:p></w:body>
</w:document>"#,
);
assert!(document.numbering_definitions.is_empty());
}
#[test]
fn parses_a_footnote_reference_run_into_a_run_content_variant() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs.len(), 1);
assert_eq!(
paragraphs[0].runs[0].content,
RunContent::NoteReference(NoteReference::Footnote(1))
);
assert_eq!(
paragraphs[0].runs[0].style_id.as_deref(),
Some("FootnoteReference")
);
}
#[test]
fn parses_an_endnote_reference_run() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:r><w:endnoteReference w:id="3"/></w:r></w:p></w:body>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(
paragraphs[0].runs[0].content,
RunContent::NoteReference(NoteReference::Endnote(3))
);
}
#[test]
fn parses_notes_xml_into_notes_with_their_blocks() {
let package = Package::new();
let relationships = Relationships::new();
let resolver = ImageResolver {
package: &package,
part_relationships: &relationships,
};
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:footnotes xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:footnotes>"#;
let notes = parse_notes_xml(xml, &resolver, b"footnote").unwrap();
assert_eq!(notes.len(), 1);
assert_eq!(notes[0].id, 1);
let paragraph = notes[0].paragraphs().next().expect("missing paragraph");
assert_eq!(paragraph.runs.len(), 3);
assert_eq!(
paragraph.runs[0].content,
RunContent::NoteMarker(NoteKind::Footnote)
);
assert_eq!(paragraph.runs[2].text(), Some("Some detail."));
}
#[test]
fn skips_separator_and_continuation_separator_notes() {
let package = Package::new();
let relationships = Relationships::new();
let resolver = ImageResolver {
package: &package,
part_relationships: &relationships,
};
let xml = r#"<w:footnotes xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:footnote w:type="separator" w:id="-1"><w:p><w:r><w:separator/></w:r></w:p></w:footnote>
<w:footnote w:type="continuationSeparator" w:id="0"><w:p><w:r><w:continuationSeparator/></w:r></w:p></w:footnote>
<w:footnote w:id="1"><w:p><w:r><w:t>Real footnote.</w:t></w:r></w:p></w:footnote>
</w:footnotes>"#;
let notes = parse_notes_xml(xml, &resolver, b"footnote").unwrap();
assert_eq!(notes.len(), 1);
assert_eq!(notes[0].id, 1);
}
#[test]
fn a_document_without_a_footnotes_or_endnotes_relationship_has_none() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:r><w:t>Body</w:t></w:r></w:p></w:body>
</w:document>"#,
);
assert!(document.footnotes.is_empty());
assert!(document.endnotes.is_empty());
}
#[test]
fn parses_a_comment_range_into_the_covered_run_s_comment_ids() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p>
<w:r><w:t xml:space="preserve">Some </w:t></w:r>
<w:commentRangeStart w:id="0"/>
<w:r><w:t>text.</w:t></w:r>
<w:commentRangeEnd w:id="0"/>
<w:r><w:commentReference w:id="0"/></w:r>
</w:p></w:body>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
let runs = ¶graphs[0].runs;
assert_eq!(runs.len(), 3);
assert_eq!(runs[0].text(), Some("Some "));
assert!(runs[0].comment_ids.is_empty(), "{:?}", runs[0].comment_ids);
assert_eq!(runs[1].text(), Some("text."));
assert_eq!(runs[1].comment_ids, vec![0]);
assert!(runs[2].comment_ids.is_empty(), "{:?}", runs[2].comment_ids);
}
#[test]
fn overlapping_comment_ranges_parse_back_into_each_run_s_own_id_set() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p>
<w:commentRangeStart w:id="0"/>
<w:r><w:t>a</w:t></w:r>
<w:commentRangeStart w:id="1"/>
<w:r><w:t>b</w:t></w:r>
<w:commentRangeEnd w:id="1"/>
<w:r><w:commentReference w:id="1"/></w:r>
<w:r><w:t>c</w:t></w:r>
<w:commentRangeEnd w:id="0"/>
<w:r><w:commentReference w:id="0"/></w:r>
</w:p></w:body>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
let text_runs: Vec<_> = paragraphs[0]
.runs
.iter()
.filter(|run| run.text().is_some_and(|text| !text.is_empty()))
.collect();
assert_eq!(text_runs[0].text(), Some("a"));
assert_eq!(text_runs[0].comment_ids, vec![0]);
assert_eq!(text_runs[1].text(), Some("b"));
assert_eq!(text_runs[1].comment_ids, vec![0, 1]);
assert_eq!(text_runs[2].text(), Some("c"));
assert_eq!(text_runs[2].comment_ids, vec![0]);
}
#[test]
fn parses_a_bookmark_range_into_the_covered_run_s_bookmarks() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p>
<w:r><w:t xml:space="preserve">Some </w:t></w:r>
<w:bookmarkStart w:id="0" w:name="MyBookmark"/>
<w:r><w:t>text.</w:t></w:r>
<w:bookmarkEnd w:id="0"/>
</w:p></w:body>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
let runs = ¶graphs[0].runs;
assert_eq!(runs.len(), 2);
assert_eq!(runs[0].text(), Some("Some "));
assert!(runs[0].bookmarks.is_empty(), "{:?}", runs[0].bookmarks);
assert_eq!(runs[1].text(), Some("text."));
assert_eq!(runs[1].bookmarks, vec![Bookmark::new(0, "MyBookmark")]);
}
#[test]
fn overlapping_bookmarks_parse_back_into_each_run_s_own_set() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p>
<w:bookmarkStart w:id="0" w:name="Outer"/>
<w:r><w:t>a</w:t></w:r>
<w:bookmarkStart w:id="1" w:name="Inner"/>
<w:r><w:t>b</w:t></w:r>
<w:bookmarkEnd w:id="1"/>
<w:r><w:t>c</w:t></w:r>
<w:bookmarkEnd w:id="0"/>
</w:p></w:body>
</w:document>"#,
);
let paragraphs: Vec<_> = document.paragraphs().collect();
let runs = ¶graphs[0].runs;
assert_eq!(runs[0].text(), Some("a"));
assert_eq!(runs[0].bookmarks, vec![Bookmark::new(0, "Outer")]);
assert_eq!(runs[1].text(), Some("b"));
assert_eq!(
runs[1].bookmarks,
vec![Bookmark::new(0, "Outer"), Bookmark::new(1, "Inner")]
);
assert_eq!(runs[2].text(), Some("c"));
assert_eq!(runs[2].bookmarks, vec![Bookmark::new(0, "Outer")]);
}
#[test]
fn parses_comments_xml_into_comments_with_author_date_initials_and_blocks() {
let package = Package::new();
let relationships = Relationships::new();
let resolver = ImageResolver {
package: &package,
part_relationships: &relationships,
};
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:comments>"#;
let comments = parse_comments_xml(xml, &resolver).unwrap();
assert_eq!(comments.len(), 1);
assert_eq!(comments[0].id, 0);
assert_eq!(comments[0].author.as_deref(), Some("Morgan"));
assert_eq!(comments[0].date.as_deref(), Some("2026-07-16T12:00:00Z"));
assert_eq!(comments[0].initials.as_deref(), Some("MR"));
let paragraph = comments[0].paragraphs().next().expect("missing paragraph");
assert_eq!(paragraph.text(), "Please rephrase.");
}
#[test]
fn a_comment_with_no_id_is_skipped() {
let package = Package::new();
let relationships = Relationships::new();
let resolver = ImageResolver {
package: &package,
part_relationships: &relationships,
};
let xml = r#"<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:comment w:author="No id"><w:p><w:r><w:t>Malformed.</w:t></w:r></w:p></w:comment>
<w:comment w:id="1"><w:p><w:r><w:t>Valid.</w:t></w:r></w:p></w:comment>
</w:comments>"#;
let comments = parse_comments_xml(xml, &resolver).unwrap();
assert_eq!(comments.len(), 1);
assert_eq!(comments[0].id, 1);
}
#[test]
fn a_document_without_a_comments_relationship_has_none() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:r><w:t>Body</w:t></w:r></w:p></w:body>
</w:document>"#,
);
assert!(document.comments.is_empty());
}
#[test]
fn parses_a_structured_document_tag_with_its_properties_and_content() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:sdt>
<w:sdtPr><w:alias w:val="Customer Name"/><w:tag w:val="CustomerName"/><w:id w:val="1"/></w:sdtPr>
<w:sdtContent><w:p><w:r><w:t>Acme Corp</w:t></w:r></w:p></w:sdtContent>
</w:sdt></w:body>
</w:document>"#,
);
assert_eq!(document.blocks.len(), 1);
match &document.blocks[0] {
Block::StructuredDocumentTag(sdt) => {
assert_eq!(sdt.id, Some(1));
assert_eq!(sdt.tag.as_deref(), Some("CustomerName"));
assert_eq!(sdt.alias.as_deref(), Some("Customer Name"));
assert_eq!(
sdt.paragraphs().next().expect("missing paragraph").text(),
"Acme Corp"
);
}
other => panic!("expected a StructuredDocumentTag block, got {other:?}"),
}
}
#[test]
fn a_structured_document_tag_with_no_sdtpr_has_no_properties() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<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>
</w:document>"#,
);
match &document.blocks[0] {
Block::StructuredDocumentTag(sdt) => {
assert!(sdt.id.is_none());
assert!(sdt.tag.is_none());
assert!(sdt.alias.is_none());
assert_eq!(sdt.paragraphs().next().unwrap().text(), "Plain");
}
other => panic!("expected a StructuredDocumentTag block, got {other:?}"),
}
}
#[test]
fn a_nested_structured_document_tag_parses_correctly() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:sdt>
<w:sdtPr><w:tag w:val="outer"/></w:sdtPr>
<w:sdtContent><w:sdt>
<w:sdtPr><w:tag w:val="inner"/></w:sdtPr>
<w:sdtContent><w:p><w:r><w:t>nested</w:t></w:r></w:p></w:sdtContent>
</w:sdt></w:sdtContent>
</w:sdt></w:body>
</w:document>"#,
);
match &document.blocks[0] {
Block::StructuredDocumentTag(outer) => {
assert_eq!(outer.tag.as_deref(), Some("outer"));
assert_eq!(outer.blocks.len(), 1);
match &outer.blocks[0] {
Block::StructuredDocumentTag(inner) => {
assert_eq!(inner.tag.as_deref(), Some("inner"));
assert_eq!(inner.paragraphs().next().unwrap().text(), "nested");
}
other => panic!("expected a nested StructuredDocumentTag block, got {other:?}"),
}
}
other => panic!("expected a StructuredDocumentTag block, got {other:?}"),
}
}
#[test]
fn a_structured_document_tag_interleaved_with_paragraphs_keeps_reading_order() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p><w:r><w:t>Before</w:t></w:r></w:p>
<w:sdt><w:sdtContent><w:p><w:r><w:t>Inside</w:t></w:r></w:p></w:sdtContent></w:sdt>
<w:p><w:r><w:t>After</w:t></w:r></w:p>
</w:body>
</w:document>"#,
);
assert_eq!(document.blocks.len(), 3);
assert!(matches!(&document.blocks[0], Block::Paragraph(p) if p.text() == "Before"));
assert!(matches!(
&document.blocks[1],
Block::StructuredDocumentTag(_)
));
assert!(matches!(&document.blocks[2], Block::Paragraph(p) if p.text() == "After"));
}
#[test]
fn parses_a_theme_with_colors_and_fonts() {
let theme = parse_theme_xml(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Custom">
<a:themeElements>
<a:clrScheme name="Custom">
<a:dk1><a:srgbClr val="111111"/></a:dk1>
<a:lt1><a:srgbClr val="EEEEEE"/></a:lt1>
<a:dk2><a:srgbClr val="222222"/></a:dk2>
<a:lt2><a:srgbClr val="DDDDDD"/></a:lt2>
<a:accent1><a:srgbClr val="AA0000"/></a:accent1>
<a:accent2><a:srgbClr val="BB1100"/></a:accent2>
<a:accent3><a:srgbClr val="CC2200"/></a:accent3>
<a:accent4><a:srgbClr val="DD3300"/></a:accent4>
<a:accent5><a:srgbClr val="EE4400"/></a:accent5>
<a:accent6><a:srgbClr val="FF5500"/></a:accent6>
<a:hlink><a:srgbClr val="0000AA"/></a:hlink>
<a:folHlink><a:srgbClr val="AA00AA"/></a:folHlink>
</a:clrScheme>
<a:fontScheme name="Custom">
<a:majorFont><a:latin typeface="Georgia"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>
<a:minorFont><a:latin typeface="Verdana"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
</a:fontScheme>
<a:fmtScheme name="Office"><a:fillStyleLst/><a:lnStyleLst/><a:effectStyleLst/><a:bgFillStyleLst/></a:fmtScheme>
</a:themeElements>
</a:theme>"#,
)
.unwrap();
assert_eq!(theme.name, "Custom");
assert_eq!(theme.colors.dark1, "111111");
assert_eq!(theme.colors.light1, "EEEEEE");
assert_eq!(theme.colors.accent6, "FF5500");
assert_eq!(theme.colors.followed_hyperlink, "AA00AA");
assert_eq!(theme.fonts.major_latin, "Georgia");
assert_eq!(theme.fonts.minor_latin, "Verdana");
}
#[test]
fn parses_trackrevisions_from_settings_xml() {
let xml = r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:trackRevisions/>
</w:settings>"#;
let (track_changes, _protection) = parse_settings_xml(xml).unwrap();
assert!(track_changes);
}
#[test]
fn a_settings_xml_with_no_trackrevisions_parses_as_false() {
let xml = r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:evenAndOddHeaders/>
</w:settings>"#;
let (track_changes, _protection) = parse_settings_xml(xml).unwrap();
assert!(!track_changes);
}
#[test]
fn an_explicit_trackrevisions_val_false_parses_as_false() {
let xml = r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:trackRevisions w:val="false"/>
</w:settings>"#;
let (track_changes, _protection) = parse_settings_xml(xml).unwrap();
assert!(!track_changes);
}
#[test]
fn parses_documentprotection_edit_value_into_protectionkind() {
let xml = r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:documentProtection w:edit="readOnly" w:enforcement="1"/>
</w:settings>"#;
let (_track_changes, protection) = parse_settings_xml(xml).unwrap();
assert_eq!(protection, Some(ProtectionKind::ReadOnly));
}
#[test]
fn a_settings_xml_with_no_documentprotection_parses_protection_as_none() {
let xml = r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:trackRevisions/>
</w:settings>"#;
let (_track_changes, protection) = parse_settings_xml(xml).unwrap();
assert_eq!(protection, None);
}
#[test]
fn an_unrecognized_documentprotection_edit_value_parses_as_none() {
let xml = r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:documentProtection w:edit="none" w:enforcement="1"/>
</w:settings>"#;
let (_track_changes, protection) = parse_settings_xml(xml).unwrap();
assert_eq!(protection, None);
}
#[test]
fn parses_core_properties_xml_into_documentproperties() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<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">
<dc:title>Rapport trimestriel</dc:title>
<dc:subject>Finances</dc:subject>
<dc:creator>Morgan & Associés</dc:creator>
<cp:keywords>rapport, finances</cp:keywords>
<dc:description>Notes internes</dc:description>
<cp:lastModifiedBy>Morgan</cp:lastModifiedBy>
<cp:revision>3</cp:revision>
<dcterms:created xsi:type="dcterms:W3CDTF">2026-07-17T10:00:00Z</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2026-07-17T11:30:00Z</dcterms:modified>
<cp:category>Interne</cp:category>
<cp:contentStatus>Draft</cp:contentStatus>
</cp:coreProperties>"#;
let properties = parse_core_properties_xml(xml).unwrap();
assert_eq!(properties.title.as_deref(), Some("Rapport trimestriel"));
assert_eq!(properties.subject.as_deref(), Some("Finances"));
assert_eq!(properties.creator.as_deref(), Some("Morgan & Associés"));
assert_eq!(properties.keywords.as_deref(), Some("rapport, finances"));
assert_eq!(properties.description.as_deref(), Some("Notes internes"));
assert_eq!(properties.last_modified_by.as_deref(), Some("Morgan"));
assert_eq!(properties.revision.as_deref(), Some("3"));
assert_eq!(properties.created.as_deref(), Some("2026-07-17T10:00:00Z"));
assert_eq!(properties.modified.as_deref(), Some("2026-07-17T11:30:00Z"));
assert_eq!(properties.category.as_deref(), Some("Interne"));
assert_eq!(properties.content_status.as_deref(), Some("Draft"));
}
#[test]
fn an_empty_core_properties_element_parses_as_all_none() {
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"/>"#;
let properties = parse_core_properties_xml(xml).unwrap();
assert_eq!(properties, DocumentProperties::new());
}
#[test]
fn a_sysclr_color_slot_uses_its_lastclr_fallback() {
let theme = parse_theme_xml(
r#"<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office">
<a:themeElements>
<a:clrScheme name="Office">
<a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
<a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
<a:dk2><a:srgbClr val="1F497D"/></a:dk2>
<a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
<a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
<a:accent2><a:srgbClr val="C0504D"/></a:accent2>
<a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
<a:accent4><a:srgbClr val="8064A2"/></a:accent4>
<a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
<a:accent6><a:srgbClr val="F79646"/></a:accent6>
<a:hlink><a:srgbClr val="0000FF"/></a:hlink>
<a:folHlink><a:srgbClr val="800080"/></a:folHlink>
</a:clrScheme>
</a:themeElements>
</a:theme>"#,
)
.unwrap();
assert_eq!(theme.colors.dark1, "000000");
assert_eq!(theme.colors.light1, "FFFFFF");
}
#[test]
fn a_theme_with_no_font_scheme_keeps_the_default_fonts() {
let theme = parse_theme_xml(
r#"<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="ColorsOnly">
<a:themeElements>
<a:clrScheme name="ColorsOnly">
<a:dk1><a:srgbClr val="000000"/></a:dk1>
<a:lt1><a:srgbClr val="FFFFFF"/></a:lt1>
<a:dk2><a:srgbClr val="1F497D"/></a:dk2>
<a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
<a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
<a:accent2><a:srgbClr val="C0504D"/></a:accent2>
<a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
<a:accent4><a:srgbClr val="8064A2"/></a:accent4>
<a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
<a:accent6><a:srgbClr val="F79646"/></a:accent6>
<a:hlink><a:srgbClr val="0000FF"/></a:hlink>
<a:folHlink><a:srgbClr val="800080"/></a:folHlink>
</a:clrScheme>
</a:themeElements>
</a:theme>"#,
)
.unwrap();
assert_eq!(theme.fonts, FontScheme::office_default());
}
#[test]
fn parses_page_size_margins_and_columns_from_sectpr() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p><w:r><w:t>Body</w:t></w:r></w:p>
<w:sectPr>
<w:pgSz w:w="16838" w:h="11906" w:orient="landscape"/>
<w:pgMar w:top="1000" w:right="1000" w:bottom="1000" w:left="1000" w:header="500" w:footer="500" w:gutter="0"/>
<w:cols w:num="2" w:space="720" w:equalWidth="true"/>
</w:sectPr>
</w:body>
</w:document>"#;
let document = parse(xml);
assert_eq!(document.page_setup.width_twips, 16838);
assert_eq!(document.page_setup.height_twips, 11906);
assert_eq!(document.page_setup.orientation, Orientation::Landscape);
assert_eq!(document.page_setup.margin_top_twips, 1000);
assert_eq!(document.page_setup.margin_right_twips, 1000);
assert_eq!(document.page_setup.margin_bottom_twips, 1000);
assert_eq!(document.page_setup.margin_left_twips, 1000);
assert_eq!(document.page_setup.margin_header_twips, 500);
assert_eq!(document.page_setup.margin_footer_twips, 500);
assert_eq!(document.page_setup.columns, Some(2));
}
#[test]
fn parses_an_a3_page_size_distinct_from_the_default_a4() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p><w:r><w:t>Body</w:t></w:r></w:p>
<w:sectPr>
<w:pgSz w:w="16838" w:h="23811"/>
</w:sectPr>
</w:body>
</w:document>"#;
let document = parse(xml);
assert_eq!(document.page_setup.width_twips, 16838);
assert_eq!(document.page_setup.height_twips, 23811);
assert_eq!(document.page_setup.orientation, Orientation::Portrait);
}
#[test]
fn a_missing_pgsz_keeps_the_default_page_setup() {
let document = parse(
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:r><w:t>Body</w:t></w:r></w:p></w:body>
</w:document>"#,
);
assert_eq!(document.page_setup, PageSetup::default());
}
#[test]
fn captures_first_and_even_header_footer_reference_ids_separately() {
let package = Package::new();
let relationships = Relationships::new();
let resolver = ImageResolver {
package: &package,
part_relationships: &relationships,
};
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p><w:r><w:t>Body</w:t></w:r></w:p>
<w:sectPr>
<w:headerReference w:type="default" r:id="rId2"/>
<w:headerReference w:type="first" r:id="rId4"/>
<w:footerReference w:type="first" r:id="rId5"/>
<w:headerReference w:type="even" r:id="rId6"/>
<w:footerReference w:type="even" r:id="rId7"/>
<w:pgSz w:w="11906" w:h="16838"/>
</w:sectPr>
</w:body>
</w:document>"#;
let parsed = parse_document_xml(xml, &resolver).unwrap();
assert_eq!(parsed.header_relationship_id.as_deref(), Some("rId2"));
assert_eq!(parsed.header_first_relationship_id.as_deref(), Some("rId4"));
assert_eq!(parsed.footer_first_relationship_id.as_deref(), Some("rId5"));
assert_eq!(parsed.header_even_relationship_id.as_deref(), Some("rId6"));
assert_eq!(parsed.footer_even_relationship_id.as_deref(), Some("rId7"));
}
#[test]
fn parses_custom_tab_stops_in_order() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p><w:pPr><w:tabs>
<w:tab w:val="left" w:pos="720"/>
<w:tab w:val="decimal" w:leader="dot" w:pos="-360"/>
</w:tabs></w:pPr><w:r><w:t>Tabbed</w:t></w:r></w:p>
</w:body>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].tabs.len(), 2);
assert_eq!(paragraphs[0].tabs[0].position_twips, 720);
assert_eq!(paragraphs[0].tabs[0].alignment, TabStopAlignment::Left);
assert_eq!(paragraphs[0].tabs[0].leader, None);
assert_eq!(paragraphs[0].tabs[1].position_twips, -360);
assert_eq!(paragraphs[0].tabs[1].alignment, TabStopAlignment::Decimal);
assert_eq!(paragraphs[0].tabs[1].leader, Some(TabLeader::Dot));
}
#[test]
fn parses_keep_next_keep_lines_page_break_before_and_contextual_spacing() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p><w:pPr>
<w:keepNext/><w:keepLines/><w:pageBreakBefore/><w:contextualSpacing/>
</w:pPr><w:r><w:t>Heading</w:t></w:r></w:p>
</w:body>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert!(paragraphs[0].keep_with_next);
assert!(paragraphs[0].keep_lines_together);
assert!(paragraphs[0].page_break_before);
assert!(paragraphs[0].contextual_spacing);
}
#[test]
fn parses_page_and_column_breaks_and_a_carriage_return() {
let xml = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p>
<w:r><w:br/></w:r>
<w:r><w:br w:type="page"/></w:r>
<w:r><w:br w:type="column"/></w:r>
<w:r><w:cr/></w:r>
</w:p>
</w:body>
</w:document>"#;
let document = parse(xml);
let paragraphs: Vec<_> = document.paragraphs().collect();
assert_eq!(paragraphs[0].runs.len(), 4);
assert_eq!(
paragraphs[0].runs[0].content,
RunContent::Break(BreakKind::TextWrapping)
);
assert_eq!(
paragraphs[0].runs[1].content,
RunContent::Break(BreakKind::Page)
);
assert_eq!(
paragraphs[0].runs[2].content,
RunContent::Break(BreakKind::Column)
);
assert_eq!(paragraphs[0].runs[3].content, RunContent::CarriageReturn);
}
}