mod alt_chunk;
mod charts;
mod color;
mod comments;
mod embedded_fonts;
pub(crate) mod emf;
mod group;
mod headers_footers;
mod images;
mod math_speech;
pub(crate) mod numbering;
mod paragraph;
mod runs;
mod sections;
mod settings;
pub(crate) mod smartart;
mod styles;
mod tables;
mod textbox;
mod wmf;
mod wordart;
use std::collections::HashMap;
use std::io::Read;
use crate::error::Error;
use crate::model::{
Block, BorderStyle, CellBorder, DocGridType, Document, FrameProperties, HRelativeFrom,
HorizontalPosition, LineSpacing, ParagraphBorder, ParagraphBorders, Section, SectionBreakType,
SectionProperties, TabAlignment, TabStop, VRelativeFrom, VerticalPosition,
};
use styles::{ParagraphStyle, StyleDefaults, parse_line_spacing, parse_styles, parse_theme};
use embedded_fonts::parse_font_table;
use headers_footers::{parse_endnotes, parse_footnotes};
use numbering::parse_numbering;
use relationships::parse_relationships;
use sections::parse_section_properties;
use settings::parse_settings;
use tables::parse_table_node;
pub(super) const WML_NS: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
pub(super) const DML_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
pub(super) const WPD_NS: &str =
"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
pub(super) const WPS_NS: &str = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
pub(super) const REL_NS: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
pub(super) const MC_NS_TOP: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
pub(super) const CHART_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/chart";
pub(super) const DSP_NS: &str = "http://schemas.microsoft.com/office/drawing/2008/diagram";
pub(super) const W14_NS: &str = "http://schemas.microsoft.com/office/word/2010/wordml";
pub(super) const VML_NS: &str = "urn:schemas-microsoft-com:vml";
pub(super) const W10_NS: &str = "urn:schemas-microsoft-com:office:word";
pub(super) const OFFICE_NS: &str = "urn:schemas-microsoft-com:office:office";
pub(super) const PIC_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/picture";
pub(super) const DIAGRAM_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/diagram";
pub(super) const MATH_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/math";
pub(super) const WPC_NS: &str =
"http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas";
pub(super) const WPG_NS: &str = "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup";
pub(super) fn twips_to_pts(twips: f32) -> f32 {
twips / 20.0
}
pub(super) fn emu_to_pts(emu: f32) -> f32 {
emu / 12700.0
}
pub(super) fn f32_attr<'n, 'm>(
node: roxmltree::Node,
attr: impl Into<roxmltree::ExpandedName<'n, 'm>>,
) -> Option<f32> {
node.attribute(attr).and_then(|v| v.parse::<f32>().ok())
}
pub(super) fn emu_attr_opt<'n, 'm>(
node: roxmltree::Node,
attr: impl Into<roxmltree::ExpandedName<'n, 'm>>,
) -> Option<f32> {
f32_attr(node, attr).map(emu_to_pts)
}
pub(super) fn emu_attr(node: roxmltree::Node, attr: &str) -> f32 {
emu_attr_opt(node, attr).unwrap_or(0.0)
}
pub(super) fn angle_attr<'n, 'm>(
node: roxmltree::Node,
attr: impl Into<roxmltree::ExpandedName<'n, 'm>>,
) -> Option<f32> {
f32_attr(node, attr).map(|v| v / 60_000.0)
}
pub(super) fn frac_attr<'n, 'm>(
node: roxmltree::Node,
attr: impl Into<roxmltree::ExpandedName<'n, 'm>>,
) -> Option<f32> {
f32_attr(node, attr).map(|v| v / 100_000.0)
}
pub(super) fn parse_on_off(v: &str) -> bool {
matches!(v, "1" | "true" | "on")
}
pub(super) fn parse_pt(s: &str) -> Option<f32> {
s.trim().trim_end_matches("pt").trim().parse::<f32>().ok()
}
pub(super) fn find_child<'a>(
node: roxmltree::Node<'a, 'a>,
name: &str,
namespace: &str,
) -> Option<roxmltree::Node<'a, 'a>> {
node.children().find(|n| n.has_tag_name((namespace, name)))
}
pub(super) fn dml<'a>(
node: roxmltree::Node<'a, 'a>,
name: &str,
) -> Option<roxmltree::Node<'a, 'a>> {
find_child(node, name, DML_NS)
}
pub(super) fn find_children<'a>(
node: roxmltree::Node<'a, 'a>,
name: &str,
namespace: &str,
) -> impl Iterator<Item = roxmltree::Node<'a, 'a>> {
node.children()
.filter(move |n| n.has_tag_name((namespace, name)))
}
pub(super) fn dml_children<'a>(
parent: roxmltree::Node<'a, 'a>,
name: &str,
) -> impl Iterator<Item = roxmltree::Node<'a, 'a>> {
find_children(parent, name, DML_NS)
}
pub(super) fn wpd<'a>(
node: roxmltree::Node<'a, 'a>,
name: &str,
) -> Option<roxmltree::Node<'a, 'a>> {
find_child(node, name, WPD_NS)
}
pub(super) fn wps<'a>(
node: roxmltree::Node<'a, 'a>,
name: &str,
) -> Option<roxmltree::Node<'a, 'a>> {
find_child(node, name, WPS_NS)
}
pub(super) fn dsp<'a>(
node: roxmltree::Node<'a, 'a>,
name: &str,
) -> Option<roxmltree::Node<'a, 'a>> {
find_child(node, name, DSP_NS)
}
pub(super) fn chart_ns<'a>(
node: roxmltree::Node<'a, 'a>,
name: &str,
) -> Option<roxmltree::Node<'a, 'a>> {
find_child(node, name, CHART_NS)
}
pub(super) fn chart_ns_attr<'a>(node: roxmltree::Node<'a, 'a>, child: &str) -> Option<&'a str> {
chart_ns(node, child).and_then(|n| n.attribute("val"))
}
pub(super) fn chart_ns_children<'a>(
parent: roxmltree::Node<'a, 'a>,
name: &str,
) -> impl Iterator<Item = roxmltree::Node<'a, 'a>> {
find_children(parent, name, CHART_NS)
}
pub(in crate::docx) struct ParseContext<'a, R: std::io::Read + std::io::Seek> {
pub(in crate::docx) styles: &'a styles::StylesInfo,
pub(in crate::docx) theme: &'a styles::ThemeFonts,
pub(in crate::docx) rels: &'a HashMap<String, String>,
pub(in crate::docx) zip: &'a mut zip::ZipArchive<R>,
pub(in crate::docx) numbering: &'a numbering::NumberingInfo,
}
pub(crate) fn is_east_asian_char(ch: char) -> bool {
matches!(ch as u32,
0x2E80..=0x2EFF | 0x2F00..=0x2FDF | 0x2FF0..=0x2FFF | 0x3000..=0x303F | 0x3040..=0x309F | 0x30A0..=0x30FF | 0x3100..=0x312F | 0x3130..=0x318F | 0x31A0..=0x31BF | 0x31F0..=0x31FF | 0x3200..=0x32FF | 0x3300..=0x33FF | 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xAC00..=0xD7AF | 0xF900..=0xFAFF | 0xFE30..=0xFE4F | 0xFF00..=0xFFEF | 0x1100..=0x11FF | 0x20000..=0x2A6DF | 0x2A700..=0x2B73F | 0x2B740..=0x2B81F )
}
pub(crate) fn is_complex_script_char(ch: char) -> bool {
matches!(ch as u32,
0x0590..=0x07BF | 0x08A0..=0x08FF | 0xFB1D..=0xFDFF | 0xFE70..=0xFEFE )
}
pub(super) fn parse_hex_color(val: &str) -> Option<[u8; 3]> {
if val == "auto" || val.len() != 6 {
return None;
}
let r = u8::from_str_radix(&val[0..2], 16).ok()?;
let g = u8::from_str_radix(&val[2..4], 16).ok()?;
let b = u8::from_str_radix(&val[4..6], 16).ok()?;
Some([r, g, b])
}
pub(super) fn parse_text_color(val: &str) -> Option<[u8; 3]> {
if val == "auto" {
return Some([0, 0, 0]);
}
parse_hex_color(val)
}
pub(super) fn parse_run_shd(parent: roxmltree::Node) -> Option<[u8; 3]> {
wml(parent, "shd").and_then(shd_color)
}
pub(super) fn shd_color(shd: roxmltree::Node) -> Option<[u8; 3]> {
let fill = shd
.attribute((WML_NS, "fill"))
.filter(|f| *f != "auto" && *f != "none")
.and_then(parse_hex_color);
let coverage = match shd.attribute((WML_NS, "val")) {
Some("nil") => return None,
Some("solid") => 1.0,
Some(v) => v
.strip_prefix("pct")
.and_then(|p| p.parse::<f32>().ok())
.map_or(0.0, |p| (p / 100.0).clamp(0.0, 1.0)),
None => 0.0,
};
if coverage == 0.0 {
return fill;
}
let ink = shd
.attribute((WML_NS, "color"))
.and_then(parse_hex_color)
.unwrap_or([0, 0, 0]);
let bg = fill.unwrap_or([255, 255, 255]);
Some(std::array::from_fn(|i| {
(bg[i] as f32 * (1.0 - coverage) + ink[i] as f32 * coverage).round() as u8
}))
}
pub(super) fn highlight_color(name: &str) -> Option<[u8; 3]> {
match name {
"yellow" => Some([255, 255, 0]),
"green" => Some([0, 255, 0]),
"cyan" => Some([0, 255, 255]),
"magenta" => Some([255, 0, 255]),
"red" => Some([255, 0, 0]),
"blue" => Some([0, 0, 255]),
"darkYellow" => Some([128, 128, 0]),
"darkGreen" => Some([0, 128, 0]),
"darkCyan" => Some([0, 128, 128]),
"darkMagenta" => Some([128, 0, 128]),
"darkRed" => Some([128, 0, 0]),
"darkBlue" => Some([0, 0, 128]),
"lightGray" => Some([192, 192, 192]),
"darkGray" => Some([128, 128, 128]),
"black" => Some([0, 0, 0]),
"white" => Some([255, 255, 255]),
_ => None,
}
}
pub(super) fn wml_bool(parent: roxmltree::Node, name: &str) -> Option<bool> {
wml(parent, name).map(|n| {
n.attribute((WML_NS, "val"))
.is_none_or(|v| v != "0" && v != "false")
})
}
pub(super) fn wml<'a>(
node: roxmltree::Node<'a, 'a>,
name: &str,
) -> Option<roxmltree::Node<'a, 'a>> {
find_child(node, name, WML_NS)
}
pub(super) fn wml_attr<'a>(node: roxmltree::Node<'a, 'a>, child: &str) -> Option<&'a str> {
wml(node, child).and_then(|n| n.attribute((WML_NS, "val")))
}
pub(super) fn math_child<'a>(
parent: roxmltree::Node<'a, 'a>,
name: &str,
) -> Option<roxmltree::Node<'a, 'a>> {
find_child(parent, name, MATH_NS)
}
pub(super) fn math_val<'a>(node: roxmltree::Node<'a, 'a>, pr: &str, name: &str) -> Option<&'a str> {
math_child(math_child(node, pr)?, name)?.attribute((MATH_NS, "val"))
}
pub(super) fn math_run_text(r: roxmltree::Node) -> String {
find_children(r, "t", MATH_NS)
.filter_map(|t| t.text())
.collect()
}
pub(super) fn twips_attr(node: roxmltree::Node, attr: &str) -> Option<f32> {
f32_attr(node, (WML_NS, attr)).map(twips_to_pts)
}
pub(super) fn parse_one_border(node: roxmltree::Node) -> Option<ParagraphBorder> {
let val = node.attribute((WML_NS, "val")).unwrap_or("none");
if val == "none" || val == "nil" {
return None;
}
let width_pt = node
.attribute((WML_NS, "sz"))
.and_then(|v| v.parse::<f32>().ok())
.map(|v| v / 8.0)
.unwrap_or(0.5);
let space_pt = node
.attribute((WML_NS, "space"))
.and_then(|v| v.parse::<f32>().ok())
.unwrap_or(0.0);
let color = node
.attribute((WML_NS, "color"))
.and_then(parse_hex_color)
.unwrap_or([0, 0, 0]);
Some(ParagraphBorder {
width_pt,
space_pt,
color,
})
}
pub(super) fn parse_paragraph_borders(ppr: roxmltree::Node) -> Option<ParagraphBorders> {
let pbdr = wml(ppr, "pBdr")?;
Some(ParagraphBorders {
top: wml(pbdr, "top").and_then(parse_one_border),
bottom: wml(pbdr, "bottom").and_then(parse_one_border),
left: wml(pbdr, "left").and_then(parse_one_border),
right: wml(pbdr, "right").and_then(parse_one_border),
between: wml(pbdr, "between").and_then(parse_one_border),
})
}
pub(super) fn parse_cell_border(parent: roxmltree::Node, name: &str) -> CellBorder {
let Some(n) = wml(parent, name) else {
return CellBorder::default();
};
let val = n.attribute((WML_NS, "val")).unwrap_or("none");
if val == "nil" || val == "none" {
return CellBorder {
is_override: true,
..CellBorder::default()
};
}
let width = n
.attribute((WML_NS, "sz"))
.and_then(|v| v.parse::<f32>().ok())
.map(|v| v / 8.0)
.unwrap_or(0.5);
let color = n.attribute((WML_NS, "color")).and_then(parse_hex_color);
let style = match val {
"dotted" => BorderStyle::Dotted,
"dashed" => BorderStyle::Dashed,
"dashSmallGap" => BorderStyle::DashSmallGap,
"dashDotStroked" | "dashDot" => BorderStyle::DashDot,
"dashDotDot" => BorderStyle::DashDotDot,
"double" => BorderStyle::Double,
_ => BorderStyle::Single,
};
CellBorder::visible(color, width, style)
}
fn parse_cell_border_with_fallback(
parent: roxmltree::Node,
primary: &str,
fallback: &str,
) -> CellBorder {
let border = parse_cell_border(parent, primary);
if border.present || border.is_override {
border
} else {
parse_cell_border(parent, fallback)
}
}
pub(super) fn parse_cell_border_left(parent: roxmltree::Node) -> CellBorder {
parse_cell_border_with_fallback(parent, "left", "start")
}
pub(super) fn parse_cell_border_right(parent: roxmltree::Node) -> CellBorder {
parse_cell_border_with_fallback(parent, "right", "end")
}
pub(super) type FrameAttrs = HashMap<String, String>;
pub(super) fn frame_attrs(fp: roxmltree::Node) -> FrameAttrs {
fp.attributes()
.filter(|a| a.namespace() == Some(WML_NS))
.map(|a| (a.name().to_string(), a.value().to_string()))
.collect()
}
pub(super) fn parse_frame_props(
ppr: Option<roxmltree::Node>,
style_attrs: Option<&FrameAttrs>,
) -> Option<FrameProperties> {
let direct = ppr.and_then(|p| wml(p, "framePr"));
if direct.is_none() && style_attrs.is_none() {
return None;
}
let mut attrs = style_attrs.cloned().unwrap_or_default();
attrs.extend(direct.map(frame_attrs).unwrap_or_default());
let attr = |name: &str| attrs.get(name).map(String::as_str);
if attr("yAlign") == Some("inline") {
return None;
}
let twips = |name| {
attr(name)
.and_then(|v| v.parse::<f32>().ok())
.map_or(0.0, twips_to_pts)
};
let h_anchor = match attr("hAnchor").unwrap_or("text") {
"margin" => HRelativeFrom::Margin,
"page" => HRelativeFrom::Page,
_ => HRelativeFrom::Column,
};
let h_position = if let Some(xa) = attr("xAlign") {
match xa {
"center" => HorizontalPosition::AlignCenter,
"right" | "outside" => HorizontalPosition::AlignRight,
_ => HorizontalPosition::AlignLeft,
}
} else {
HorizontalPosition::Offset(twips("x"))
};
let v_anchor = match attr("vAnchor") {
Some("page") => VRelativeFrom::Page,
Some("text") => VRelativeFrom::Paragraph,
_ => VRelativeFrom::Margin,
};
Some(FrameProperties {
h_relative_from: h_anchor,
h_position,
v_relative_from: v_anchor,
v_position: match attr("yAlign") {
Some("top" | "inside") => VerticalPosition::AlignTop,
Some("center") => VerticalPosition::AlignCenter,
Some("bottom" | "outside") => VerticalPosition::AlignBottom,
_ => VerticalPosition::Offset(twips("y")),
},
width: twips("w"),
height: twips("h"),
text_below: matches!(attr("wrap"), Some("none") | Some("notBeside")),
h_space: twips("hSpace"),
v_space: twips("vSpace"),
})
}
pub(in crate::docx) fn resolve_tab_stops(
ppr: Option<roxmltree::Node>,
para_style: Option<&ParagraphStyle>,
) -> Vec<TabStop> {
let mut tab_stops = para_style.map(|s| s.tab_stops.clone()).unwrap_or_default();
let (para_tabs, para_clears) = ppr.map(parse_tab_stops_with_clears).unwrap_or_default();
if !para_tabs.is_empty() || !para_clears.is_empty() {
merge_tab_stops(&mut tab_stops, ¶_clears, para_tabs);
tab_stops.sort_by(|a, b| a.position.total_cmp(&b.position));
}
tab_stops
}
pub(super) fn merge_tab_stops(dst: &mut Vec<TabStop>, clears: &[f32], incoming: Vec<TabStop>) {
for &clear_pos in clears {
dst.retain(|t| (t.position - clear_pos).abs() >= 0.5);
}
for ts in incoming {
if let Some(existing) = dst
.iter_mut()
.find(|t| (t.position - ts.position).abs() < 0.5)
{
*existing = ts;
} else {
dst.push(ts);
}
}
}
pub(super) fn parse_tab_stops_with_clears(ppr: roxmltree::Node) -> (Vec<TabStop>, Vec<f32>) {
let Some(tabs) = wml(ppr, "tabs") else {
return (vec![], vec![]);
};
let mut stops = Vec::new();
let mut clears = Vec::new();
for n in tabs.children().filter(|n| n.has_tag_name((WML_NS, "tab"))) {
let Some(pos) = twips_attr(n, "pos") else {
continue;
};
let val = n.attribute((WML_NS, "val")).unwrap_or("left");
if val == "clear" {
clears.push(pos);
continue;
}
let alignment = match val {
"center" => TabAlignment::Center,
"right" => TabAlignment::Right,
"decimal" => TabAlignment::Decimal,
_ => TabAlignment::Left,
};
let leader = n.attribute((WML_NS, "leader")).and_then(|l| match l {
"dot" => Some('.'),
"hyphen" => Some('-'),
"underscore" => Some('_'),
_ => None,
});
stops.push(TabStop {
position: pos,
alignment,
leader,
});
}
stops.sort_by(|a, b| a.position.total_cmp(&b.position));
(stops, clears)
}
pub(super) fn resolve_theme_color_key(scheme_name: &str) -> &str {
match scheme_name {
"dk1" | "lt1" | "dk2" | "lt2" => scheme_name,
"tx1" => "dk1",
"tx2" => "dk2",
"bg1" => "lt1",
"bg2" => "lt2",
other => other,
}
}
pub(in crate::docx) fn autospacing(
ppr: Option<roxmltree::Node>,
para_style: Option<&ParagraphStyle>,
defaults: &StyleDefaults,
) -> (bool, bool) {
let inline_spacing = ppr.and_then(|ppr| wml(ppr, "spacing"));
let side = |attr: &str, style_val: Option<bool>, default: bool| {
inline_spacing
.and_then(|n| n.attribute((WML_NS, attr)).map(parse_on_off))
.or(style_val)
.unwrap_or(default)
};
(
side(
"beforeAutospacing",
para_style.and_then(|s| s.space_before_autospacing),
defaults.before_autospacing,
),
side(
"afterAutospacing",
para_style.and_then(|s| s.space_after_autospacing),
defaults.after_autospacing,
),
)
}
const AUTO_SPACING: f32 = 14.0;
pub(in crate::docx) fn parse_paragraph_spacing(
ppr: Option<roxmltree::Node>,
para_style: Option<&ParagraphStyle>,
defaults: &StyleDefaults,
) -> (Option<f32>, Option<f32>, Option<LineSpacing>) {
let inline_spacing = ppr.and_then(|ppr| wml(ppr, "spacing"));
let (before_auto, after_auto) = autospacing(ppr, para_style, defaults);
let space_before = if before_auto {
Some(AUTO_SPACING)
} else {
inline_spacing
.and_then(|n| twips_attr(n, "before"))
.or_else(|| para_style.and_then(|s| s.space_before))
};
let space_after = if after_auto {
Some(AUTO_SPACING)
} else {
inline_spacing
.and_then(|n| twips_attr(n, "after"))
.or_else(|| para_style.and_then(|s| s.space_after))
};
let line_spacing = inline_spacing
.and_then(parse_line_spacing)
.or_else(|| para_style.and_then(|s| s.line_spacing));
(space_before, space_after, line_spacing)
}
fn chars_to_pts(ind: roxmltree::Node, attr: &str, char_width: f32) -> Option<f32> {
ind.attribute((WML_NS, attr))
.and_then(|v| v.parse::<f32>().ok())
.map(|hundredths| hundredths / 100.0 * char_width)
}
pub(super) fn extract_indents(
ind: roxmltree::Node,
char_width: Option<f32>,
) -> (Option<f32>, Option<f32>, Option<f32>, Option<f32>) {
let cw = char_width.unwrap_or(0.0);
let has_cw = char_width.is_some();
(
twips_attr(ind, "start")
.or_else(|| twips_attr(ind, "left"))
.or_else(|| has_cw.then(|| chars_to_pts(ind, "leftChars", cw)).flatten()),
twips_attr(ind, "end")
.or_else(|| twips_attr(ind, "right"))
.or_else(|| {
has_cw
.then(|| chars_to_pts(ind, "rightChars", cw))
.flatten()
}),
twips_attr(ind, "hanging").or_else(|| {
has_cw
.then(|| chars_to_pts(ind, "hangingChars", cw))
.flatten()
}),
twips_attr(ind, "firstLine").or_else(|| {
has_cw
.then(|| chars_to_pts(ind, "firstLineChars", cw))
.flatten()
}),
)
}
pub(super) fn collect_block_nodes<'a>(
parent: roxmltree::Node<'a, 'a>,
) -> Vec<roxmltree::Node<'a, 'a>> {
let mut nodes = Vec::new();
for child in parent.children() {
if child.has_tag_name((WML_NS, "sdt")) {
if let Some(content) = wml(child, "sdtContent") {
nodes.extend(collect_block_nodes(content));
}
} else if child.has_tag_name((WML_NS, "customXml")) {
nodes.extend(collect_block_nodes(child));
} else if child.has_tag_name((MC_NS_TOP, "AlternateContent")) {
let fallback = child
.children()
.find(|n| n.has_tag_name((MC_NS_TOP, "Fallback")));
if let Some(fb) = fallback {
nodes.extend(collect_block_nodes(fb));
}
} else {
nodes.push(child);
}
}
nodes
}
pub(super) fn read_zip_text<R: Read + std::io::Seek>(
zip: &mut zip::ZipArchive<R>,
name: &str,
) -> Option<String> {
let mut content = String::new();
zip.by_name(name).ok()?.read_to_string(&mut content).ok()?;
Some(strict_to_transitional(content))
}
fn strict_to_transitional(xml: String) -> String {
if !xml.contains("http://purl.oclc.org/ooxml/") {
return xml;
}
["wordprocessingml", "drawingml", "officeDocument"]
.iter()
.fold(xml, |acc, area| {
acc.replace(
&format!("http://purl.oclc.org/ooxml/{area}/"),
&format!("http://schemas.openxmlformats.org/{area}/2006/"),
)
})
}
pub(super) fn read_zip_bytes<R: Read + std::io::Seek>(
zip: &mut zip::ZipArchive<R>,
name: &str,
) -> Option<Vec<u8>> {
let mut data = Vec::new();
zip.by_name(name).ok()?.read_to_end(&mut data).ok()?;
Some(data)
}
pub(super) fn part_path(target: &str) -> String {
match target.strip_prefix('/') {
Some(absolute) => absolute.to_string(),
None => format!("word/{target}"),
}
}
mod relationships {
use std::collections::HashMap;
use std::io::Read;
use super::read_zip_text;
fn parse_rels_xml(xml_content: &str) -> HashMap<String, String> {
let Ok(xml) = roxmltree::Document::parse(xml_content) else {
return HashMap::new();
};
xml.root_element()
.children()
.filter(|n| n.tag_name().name() == "Relationship")
.filter_map(|n| {
Some((
n.attribute("Id")?.to_string(),
n.attribute("Target")?.to_string(),
))
})
.collect()
}
pub(in crate::docx) fn parse_relationships<R: Read + std::io::Seek>(
zip: &mut zip::ZipArchive<R>,
) -> HashMap<String, String> {
let Some(xml_content) = read_zip_text(zip, "word/_rels/document.xml.rels") else {
return HashMap::new();
};
parse_rels_xml(&xml_content)
}
pub(in crate::docx) fn parse_part_relationships<R: Read + std::io::Seek>(
zip: &mut zip::ZipArchive<R>,
part_path: &str,
) -> HashMap<String, String> {
let (dir, file) = match part_path.rsplit_once('/') {
Some((d, f)) => (d, f),
None => ("", part_path),
};
let rels_path = if dir.is_empty() {
format!("_rels/{}.rels", file)
} else {
format!("{}/_rels/{}.rels", dir, file)
};
let Some(xml_content) = read_zip_text(zip, &rels_path) else {
return HashMap::new();
};
parse_rels_xml(&xml_content)
}
}
pub fn parse(path: &std::path::Path) -> Result<Document, Error> {
let file = std::fs::File::open(path).map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied => Error::Io(
std::io::Error::new(e.kind(), format!("{}: {}", e, path.display())),
),
_ => Error::Io(e),
})?;
let mut zip = zip::ZipArchive::new(file)
.map_err(|_| Error::InvalidDocx("file is not a ZIP archive".into()))?;
parse_zip(&mut zip)
}
pub fn parse_bytes(bytes: &[u8]) -> Result<Document, Error> {
let cursor = std::io::Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(cursor)
.map_err(|_| Error::InvalidDocx("data is not a valid ZIP/DOCX archive".into()))?;
parse_zip(&mut zip)
}
fn parse_core_props<R: Read + std::io::Seek>(
zip: &mut zip::ZipArchive<R>,
) -> (
Option<String>,
Option<String>,
Option<String>,
Option<String>,
) {
let Some(xml_content) = read_zip_text(zip, "docProps/core.xml") else {
return (None, None, None, None);
};
let Ok(xml) = roxmltree::Document::parse(&xml_content) else {
return (None, None, None, None);
};
const DC_NS: &str = "http://purl.org/dc/elements/1.1/";
const CP_NS: &str = "http://schemas.openxmlformats.org/package/2006/metadata/core-properties";
let root = xml.root_element();
let prop = |ns, name| {
find_children(root, name, ns)
.filter_map(|n| n.text())
.last()
.map(str::to_string)
};
(
prop(DC_NS, "title"),
prop(DC_NS, "creator"),
prop(DC_NS, "subject"),
prop(CP_NS, "keywords"),
)
}
fn parse_zip<R: Read + std::io::Seek>(zip: &mut zip::ZipArchive<R>) -> Result<Document, Error> {
let settings = parse_settings(zip);
let theme = parse_theme(
zip,
settings.east_asia_lang.as_deref(),
settings.bidi_lang.as_deref(),
);
let styles = parse_styles(zip, &theme, settings.styles_from_normal_template);
let numbering = parse_numbering(zip);
let rels = parse_relationships(zip);
let ft = parse_font_table(zip);
let (embedded_fonts, font_table) = (ft.embedded_fonts, ft.font_table);
let (footnotes, footnote_separator) = parse_footnotes(zip, &styles, &theme, &numbering);
let endnotes = parse_endnotes(zip, &styles, &theme, &numbering);
let comments = comments::parse_comments(zip);
let (title, author, subject, keywords) = parse_core_props(zip);
let mut xml_content = String::new();
zip.by_name("word/document.xml")
.map_err(|_| Error::InvalidDocx("missing word/document.xml (is this a DOCX file?)".into()))?
.read_to_string(&mut xml_content)?;
let xml_content = strict_to_transitional(xml_content);
let xml = roxmltree::Document::parse(&xml_content)?;
let root = xml.root_element();
let body = wml(root, "body").ok_or_else(|| Error::InvalidDocx("Missing w:body".into()))?;
let default_line_pitch = styles.defaults.font_size * 1.2;
let mut ctx = ParseContext {
styles: &styles,
theme: &theme,
rels: &rels,
zip,
numbering: &numbering,
};
let mut sections: Vec<Section> = Vec::new();
let mut blocks = Vec::new();
let mut lists = numbering::ListCounters::default();
let mut prev_list_num_id: Option<String> = None;
for node in collect_block_nodes(body) {
if node.tag_name().namespace() != Some(WML_NS) {
continue;
}
match node.tag_name().name() {
"tbl" => {
let table = parse_table_node(node, &mut ctx, &mut lists);
blocks.push(Block::Table(table));
prev_list_num_id = None;
}
"p" => {
let ppr = wml(node, "pPr");
let para_style_id = ppr
.and_then(|ppr| wml_attr(ppr, "pStyle"))
.unwrap_or(&styles.default_paragraph_style_id);
let para_style = styles.paragraph_styles.get(para_style_id);
let opts = paragraph::ParagraphOptions {
resolve_bookmarks: true,
resolve_outline_level: true,
resolve_drawings: true,
collect_extra_textboxes: true,
style_num_id: para_style.and_then(|s| s.num_id.clone()),
style_num_ilvl: para_style.and_then(|s| s.num_ilvl),
};
let mut para = paragraph::build_paragraph(node, &mut ctx, &mut lists, &opts);
let num_id = ppr
.and_then(|ppr| wml(ppr, "numPr"))
.and_then(|np| wml_attr(np, "numId"))
.map(str::to_string)
.or_else(|| para_style.and_then(|s| s.num_id.clone()))
.filter(|id| id != "0");
if num_id.is_some() && num_id == prev_list_num_id {
if para.space_before_auto {
para.space_before = 0.0;
}
if let Some(Block::Paragraph(prev)) = blocks.last_mut()
&& prev.space_after_auto
{
prev.space_after = 0.0;
}
}
prev_list_num_id = num_id;
let continuation = paragraph::split_at_page_break(&mut para);
blocks.push(Block::Paragraph(para));
blocks.extend(continuation.map(Block::Paragraph));
if let Some(sect_node) = ppr.and_then(|ppr| wml(ppr, "sectPr")) {
if let Some(Block::Paragraph(last_para)) = blocks.last_mut() {
last_para.is_section_break = true;
last_para.page_break_after = false;
}
let props = parse_section_properties(
sect_node,
&mut ctx,
default_line_pitch,
settings.gutter_at_top,
);
sections.push(Section {
properties: props,
blocks: std::mem::take(&mut blocks),
});
}
}
"altChunk" => {
if let Some(id) = node.attribute((REL_NS, "id")) {
blocks.extend(alt_chunk::parse_alt_chunk(id, ctx.rels, ctx.zip));
prev_list_num_id = None;
}
}
_ => {}
}
}
if matches!(blocks.last(), Some(Block::Table(_))) {
blocks.push(Block::Paragraph(crate::model::Paragraph::default()));
}
let final_props = if let Some(sect_node) = wml(body, "sectPr") {
parse_section_properties(
sect_node,
&mut ctx,
default_line_pitch,
settings.gutter_at_top,
)
} else {
SectionProperties {
page_width: 612.0,
page_height: 792.0,
margin_top: 72.0,
margin_bottom: 72.0,
margin_top_fixed: false,
margin_bottom_fixed: false,
margin_left: 72.0,
margin_right: 72.0,
header_margin: 36.0,
footer_margin: 36.0,
header_default: None,
header_first: None,
header_even: None,
footer_default: None,
footer_first: None,
footer_even: None,
different_first_page: false,
line_pitch: default_line_pitch,
grid_type: DocGridType::Default,
break_type: SectionBreakType::NextPage,
columns: None,
page_num_start: None,
page_num_format: None,
page_borders: None,
vertical_align: crate::model::PageVerticalAlign::Top,
line_numbering: None,
footnote_num_fmt: None,
endnote_num_fmt: None,
}
};
sections.push(Section {
properties: final_props,
blocks,
});
if let Some(Block::Paragraph(first)) = sections[0].blocks.first_mut()
&& first.space_before_auto
{
first.space_before = 0.0;
}
let mut first = sections[0].blocks.first_mut();
while let Some(Block::Table(table)) = first {
let Some(cell) = table.rows.first_mut().and_then(|r| r.cells.first_mut()) else {
break;
};
if cell.content.len() == 1 {
cell.hide_mark = false;
}
first = cell.content.first_mut();
}
for i in 1..sections.len() {
let prev = §ions[i - 1].properties;
let size = (prev.page_width, prev.page_height);
let sp = &mut sections[i].properties;
if sp.break_type == SectionBreakType::Continuous && (sp.page_width, sp.page_height) != size
{
sp.break_type = SectionBreakType::NextPage;
}
}
Ok(Document {
sections,
line_spacing: styles.defaults.line_spacing,
embedded_fonts,
footnotes,
footnote_separator,
endnotes,
comments,
font_table,
even_and_odd_headers: settings.even_and_odd_headers,
mirror_margins: settings.mirror_margins,
default_tab_stop: settings.default_tab_stop,
style_id_to_name: styles.style_id_to_name,
theme_minor_font: theme.minor.clone(),
title,
author,
subject,
keywords,
default_lang: styles.defaults.lang.clone().or(settings.default_lang),
compress_punctuation: settings.compress_punctuation,
compat_mode: settings.compat_mode,
do_not_expand_shift_return: settings.do_not_expand_shift_return,
adjust_line_height_in_table: settings.adjust_line_height_in_table,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strict_namespaces_read_as_transitional() {
let strict = r#"<w:document xmlns:w="http://purl.oclc.org/ooxml/wordprocessingml/main" xmlns:r="http://purl.oclc.org/ooxml/officeDocument/relationships"><w:body/></w:document>"#;
let xml = strict_to_transitional(strict.to_string());
let doc = roxmltree::Document::parse(&xml).unwrap();
assert!(wml(doc.root_element(), "body").is_some());
assert!(
xml.contains("http://schemas.openxmlformats.org/officeDocument/2006/relationships")
);
}
#[test]
fn frame_y_align_overrides_y_and_inline_stays_in_flow() {
let parse = |fp: &str| {
let xml = format!(r#"<w:pPr xmlns:w="{WML_NS}">{fp}</w:pPr>"#);
let doc = roxmltree::Document::parse(&xml).unwrap();
parse_frame_props(Some(doc.root_element()), None)
};
let bottom =
parse(r#"<w:framePr w:w="2659" w:hAnchor="page" w:x="8971" w:yAlign="bottom"/>"#)
.unwrap();
assert_eq!(bottom.v_position, VerticalPosition::AlignBottom);
assert_eq!(bottom.v_relative_from, VRelativeFrom::Margin);
let y = parse(r#"<w:framePr w:vAnchor="page" w:y="3222"/>"#).unwrap();
assert_eq!(y.v_position, VerticalPosition::Offset(161.1));
assert!(parse(r#"<w:framePr w:yAlign="inline"/>"#).is_none());
}
#[test]
fn test_twips_to_pts() {
assert_eq!(twips_to_pts(0.0), 0.0);
assert_eq!(twips_to_pts(20.0), 1.0);
assert_eq!(twips_to_pts(360.0), 18.0);
assert_eq!(twips_to_pts(1440.0), 72.0); }
#[test]
fn test_emu_to_pts() {
assert_eq!(emu_to_pts(0.0), 0.0);
assert_eq!(emu_to_pts(12700.0), 1.0);
assert_eq!(emu_to_pts(914400.0), 72.0); }
#[test]
fn test_parse_hex_color() {
assert_eq!(parse_hex_color("FF0000"), Some([255, 0, 0]));
assert_eq!(parse_hex_color("00FF00"), Some([0, 255, 0]));
assert_eq!(parse_hex_color("0000FF"), Some([0, 0, 255]));
assert_eq!(parse_hex_color("4472C4"), Some([68, 114, 196]));
assert_eq!(parse_hex_color("auto"), None);
assert_eq!(parse_hex_color("red"), None); assert_eq!(parse_hex_color(""), None);
assert_eq!(parse_hex_color("ZZZZZZ"), None); }
#[test]
fn test_parse_text_color() {
assert_eq!(parse_text_color("auto"), Some([0, 0, 0])); assert_eq!(parse_text_color("FF0000"), Some([255, 0, 0]));
assert_eq!(parse_text_color("invalid"), None);
}
#[test]
fn test_parse_run_shd() {
let ns = WML_NS;
let xml = format!(r#"<w:rPr xmlns:w="{ns}"><w:b/></w:rPr>"#);
let doc = roxmltree::Document::parse(&xml).unwrap();
assert_eq!(parse_run_shd(doc.root_element()), None);
let xml = format!(
r#"<w:rPr xmlns:w="{ns}"><w:shd w:val="clear" w:color="auto" w:fill="FFFF00"/></w:rPr>"#
);
let doc = roxmltree::Document::parse(&xml).unwrap();
assert_eq!(parse_run_shd(doc.root_element()), Some([0xFF, 0xFF, 0x00]));
let xml = format!(r#"<w:rPr xmlns:w="{ns}"><w:shd w:val="nil"/></w:rPr>"#);
let doc = roxmltree::Document::parse(&xml).unwrap();
assert_eq!(parse_run_shd(doc.root_element()), None);
let xml = format!(r#"<w:rPr xmlns:w="{ns}"><w:shd w:val="clear" w:fill="auto"/></w:rPr>"#);
let doc = roxmltree::Document::parse(&xml).unwrap();
assert_eq!(parse_run_shd(doc.root_element()), None);
let xml = format!(r#"<w:rPr xmlns:w="{ns}"><w:shd w:val="clear"/></w:rPr>"#);
let doc = roxmltree::Document::parse(&xml).unwrap();
assert_eq!(parse_run_shd(doc.root_element()), None);
let xml =
format!(r#"<w:rPr xmlns:w="{ns}"><w:shd w:val="pct25" w:fill="CCCCCC"/></w:rPr>"#);
let doc = roxmltree::Document::parse(&xml).unwrap();
assert_eq!(parse_run_shd(doc.root_element()), Some([153, 153, 153]));
let xml = format!(
r#"<w:rPr xmlns:w="{ns}"><w:shd w:val="solid" w:color="CC99FF" w:fill="auto"/></w:rPr>"#
);
let doc = roxmltree::Document::parse(&xml).unwrap();
assert_eq!(parse_run_shd(doc.root_element()), Some([0xCC, 0x99, 0xFF]));
}
#[test]
fn test_highlight_color() {
assert_eq!(highlight_color("yellow"), Some([255, 255, 0]));
assert_eq!(highlight_color("red"), Some([255, 0, 0]));
assert_eq!(highlight_color("blue"), Some([0, 0, 255]));
assert_eq!(highlight_color("darkGreen"), Some([0, 128, 0]));
assert_eq!(highlight_color("lightGray"), Some([192, 192, 192]));
assert_eq!(highlight_color("black"), Some([0, 0, 0]));
assert_eq!(highlight_color("white"), Some([255, 255, 255]));
assert_eq!(highlight_color("unknown"), None);
}
#[test]
fn test_is_east_asian_char() {
assert!(is_east_asian_char('漢')); assert!(is_east_asian_char('あ')); assert!(is_east_asian_char('ア')); assert!(is_east_asian_char('한')); assert!(is_east_asian_char('、')); assert!(is_east_asian_char('\u{FF01}')); assert!(!is_east_asian_char('A'));
assert!(!is_east_asian_char('1'));
assert!(!is_east_asian_char(' '));
assert!(!is_east_asian_char('é'));
}
#[test]
fn test_wml_bool() {
let xml = format!(
r#"<w:pPr xmlns:w="{}">
<w:b/>
<w:i w:val="0"/>
<w:caps w:val="true"/>
<w:vanish w:val="false"/>
</w:pPr>"#,
WML_NS
);
let doc = roxmltree::Document::parse(&xml).unwrap();
let node = doc.root_element();
assert_eq!(wml_bool(node, "b"), Some(true));
assert_eq!(wml_bool(node, "i"), Some(false));
assert_eq!(wml_bool(node, "caps"), Some(true));
assert_eq!(wml_bool(node, "vanish"), Some(false));
assert_eq!(wml_bool(node, "strike"), None);
}
#[test]
fn test_wml_attr() {
let xml = format!(
r#"<w:pPr xmlns:w="{}">
<w:jc w:val="center"/>
<w:sz w:val="24"/>
</w:pPr>"#,
WML_NS
);
let doc = roxmltree::Document::parse(&xml).unwrap();
let node = doc.root_element();
assert_eq!(wml_attr(node, "jc"), Some("center"));
assert_eq!(wml_attr(node, "sz"), Some("24"));
assert_eq!(wml_attr(node, "missing"), None);
}
#[test]
fn test_twips_attr() {
let xml = format!(
r#"<w:ind xmlns:w="{}" w:left="720" w:right="360"/>"#,
WML_NS
);
let doc = roxmltree::Document::parse(&xml).unwrap();
let node = doc.root_element();
assert_eq!(twips_attr(node, "left"), Some(36.0)); assert_eq!(twips_attr(node, "right"), Some(18.0)); assert_eq!(twips_attr(node, "hanging"), None);
}
#[test]
fn test_parse_tab_stops() {
let xml = format!(
r#"<w:pPr xmlns:w="{}">
<w:tabs>
<w:tab w:val="center" w:pos="4320"/>
<w:tab w:val="right" w:pos="8640" w:leader="dot"/>
<w:tab w:val="left" w:pos="1440"/>
</w:tabs>
</w:pPr>"#,
WML_NS
);
let doc = roxmltree::Document::parse(&xml).unwrap();
let node = doc.root_element();
let stops = parse_tab_stops_with_clears(node).0;
assert_eq!(stops.len(), 3);
assert_eq!(stops[0].position, 72.0); assert_eq!(stops[0].alignment, TabAlignment::Left);
assert_eq!(stops[0].leader, None);
assert_eq!(stops[1].position, 216.0); assert_eq!(stops[1].alignment, TabAlignment::Center);
assert_eq!(stops[2].position, 432.0); assert_eq!(stops[2].alignment, TabAlignment::Right);
assert_eq!(stops[2].leader, Some('.'));
}
#[test]
fn test_parse_tab_stops_with_clears() {
let xml = format!(
r#"<w:pPr xmlns:w="{}">
<w:tabs>
<w:tab w:val="clear" w:pos="720"/>
<w:tab w:val="right" w:pos="9360"/>
</w:tabs>
</w:pPr>"#,
WML_NS
);
let doc = roxmltree::Document::parse(&xml).unwrap();
let node = doc.root_element();
let (stops, clears) = parse_tab_stops_with_clears(node);
assert_eq!(stops.len(), 1);
assert_eq!(stops[0].alignment, TabAlignment::Right);
assert_eq!(clears.len(), 1);
assert_eq!(clears[0], 36.0); }
#[test]
fn test_extract_indents() {
let xml = format!(
r#"<w:ind xmlns:w="{}" w:left="720" w:right="360" w:hanging="360" w:firstLine="0"/>"#,
WML_NS
);
let doc = roxmltree::Document::parse(&xml).unwrap();
let node = doc.root_element();
let (left, right, hanging, first_line) = extract_indents(node, None);
assert_eq!(left, Some(36.0)); assert_eq!(right, Some(18.0)); assert_eq!(hanging, Some(18.0)); assert_eq!(first_line, Some(0.0));
}
#[test]
fn test_extract_indents_start_end() {
let xml = format!(
r#"<w:ind xmlns:w="{}" w:start="1440" w:end="720"/>"#,
WML_NS
);
let doc = roxmltree::Document::parse(&xml).unwrap();
let node = doc.root_element();
let (left, right, hanging, first_line) = extract_indents(node, None);
assert_eq!(left, Some(72.0)); assert_eq!(right, Some(36.0)); assert_eq!(hanging, None);
assert_eq!(first_line, None);
}
#[test]
fn test_extract_indents_left_chars() {
let xml = format!(
r#"<w:ind xmlns:w="{}" w:leftChars="200" w:left="480"/>"#,
WML_NS
);
let doc = roxmltree::Document::parse(&xml).unwrap();
let node = doc.root_element();
let (left, _right, _hanging, _first_line) = extract_indents(node, Some(5.5));
assert_eq!(left, Some(24.0)); let xml2 = format!(r#"<w:ind xmlns:w="{}" w:leftChars="200"/>"#, WML_NS);
let doc2 = roxmltree::Document::parse(&xml2).unwrap();
let node2 = doc2.root_element();
let (left2, _, _, _) = extract_indents(node2, Some(5.5));
assert_eq!(left2, Some(11.0)); }
#[test]
fn test_resolve_theme_color_key() {
assert_eq!(resolve_theme_color_key("dk1"), "dk1");
assert_eq!(resolve_theme_color_key("tx1"), "dk1");
assert_eq!(resolve_theme_color_key("tx2"), "dk2");
assert_eq!(resolve_theme_color_key("accent1"), "accent1");
assert_eq!(resolve_theme_color_key("accent6"), "accent6");
assert_eq!(resolve_theme_color_key("bg1"), "lt1");
assert_eq!(resolve_theme_color_key("bg2"), "lt2");
}
}