use std::collections::HashMap;
use crate::model::{Alignment, CellBorder, LineSpacing};
use super::{
DML_NS, WML_NS, parse_paragraph_borders, parse_hex_color, parse_text_color, read_zip_text,
twips_attr, wml, wml_attr, wml_bool,
};
fn dml<'a>(node: roxmltree::Node<'a, 'a>, name: &str) -> Option<roxmltree::Node<'a, 'a>> {
node.children()
.find(|n| n.tag_name().name() == name && n.tag_name().namespace() == Some(DML_NS))
}
fn latin_typeface<'a>(node: roxmltree::Node<'a, 'a>) -> Option<&'a str> {
dml(node, "latin")
.and_then(|n| n.attribute("typeface"))
.filter(|tf| !tf.is_empty())
}
pub(super) struct ThemeFonts {
pub(super) major: String,
pub(super) minor: String,
}
pub(super) struct StyleDefaults {
pub(super) font_size: f32,
pub(super) font_name: String,
pub(super) space_after: f32,
pub(super) line_spacing: LineSpacing,
}
pub(super) struct ParagraphStyle {
pub(super) font_size: Option<f32>,
pub(super) font_name: Option<String>,
pub(super) bold: Option<bool>,
pub(super) italic: Option<bool>,
pub(super) caps: Option<bool>,
pub(super) small_caps: Option<bool>,
pub(super) vanish: Option<bool>,
pub(super) color: Option<[u8; 3]>,
pub(super) space_before: Option<f32>,
pub(super) space_after: Option<f32>,
pub(super) alignment: Option<Alignment>,
pub(super) contextual_spacing: bool,
pub(super) keep_next: bool,
pub(super) keep_lines: bool,
pub(super) line_spacing: Option<LineSpacing>,
pub(super) indent_left: Option<f32>,
pub(super) indent_right: Option<f32>,
pub(super) indent_hanging: Option<f32>,
pub(super) indent_first_line: Option<f32>,
pub(super) borders_extra: f32,
pub(super) borders: crate::model::ParagraphBorders,
pub(super) based_on: Option<String>,
}
pub(super) struct CharacterStyle {
pub(super) font_size: Option<f32>,
pub(super) font_name: Option<String>,
pub(super) bold: Option<bool>,
pub(super) italic: Option<bool>,
pub(super) underline: Option<bool>,
pub(super) strikethrough: Option<bool>,
pub(super) caps: Option<bool>,
pub(super) small_caps: Option<bool>,
pub(super) vanish: Option<bool>,
pub(super) color: Option<[u8; 3]>,
}
pub(super) struct TableBordersDef {
pub(super) top: CellBorder,
pub(super) bottom: CellBorder,
pub(super) left: CellBorder,
pub(super) right: CellBorder,
pub(super) inside_h: CellBorder,
pub(super) inside_v: CellBorder,
}
pub(super) struct StylesInfo {
pub(super) defaults: StyleDefaults,
pub(super) paragraph_styles: HashMap<String, ParagraphStyle>,
pub(super) character_styles: HashMap<String, CharacterStyle>,
pub(super) table_border_styles: HashMap<String, TableBordersDef>,
}
pub(super) fn parse_alignment(val: &str) -> Alignment {
match val {
"center" => Alignment::Center,
"right" | "end" => Alignment::Right,
"both" => Alignment::Justify,
_ => Alignment::Left,
}
}
pub(super) fn parse_theme<R: std::io::Read + std::io::Seek>(zip: &mut zip::ZipArchive<R>) -> ThemeFonts {
let mut major = String::from("Aptos Display");
let mut minor = String::from("Aptos");
let names: Vec<String> = zip.file_names().map(|s| s.to_string()).collect();
let theme_name = names
.iter()
.find(|n| n.starts_with("word/theme/") && n.ends_with(".xml"));
let Some(xml_content) = theme_name.and_then(|name| read_zip_text(zip, name)) else {
return ThemeFonts { major, minor };
};
let Ok(xml) = roxmltree::Document::parse(&xml_content) else {
return ThemeFonts { major, minor };
};
for node in xml.descendants() {
if node.tag_name().namespace() != Some(DML_NS) {
continue;
}
match node.tag_name().name() {
"majorFont" => {
if let Some(tf) = latin_typeface(node) {
major = tf.to_string();
}
}
"minorFont" => {
if let Some(tf) = latin_typeface(node) {
minor = tf.to_string();
}
}
_ => {}
}
}
ThemeFonts { major, minor }
}
pub(super) fn resolve_font(
ascii: Option<&str>,
ascii_theme: Option<&str>,
theme: &ThemeFonts,
default_font: &str,
) -> String {
if let Some(f) = ascii {
return f.to_string();
}
match ascii_theme {
Some("majorHAnsi") => theme.major.clone(),
Some("minorHAnsi") => theme.minor.clone(),
_ => default_font.to_string(),
}
}
pub(super) fn resolve_font_from_node(
rfonts: roxmltree::Node,
theme: &ThemeFonts,
default_font: &str,
) -> String {
resolve_font(
rfonts.attribute((WML_NS, "ascii")),
rfonts.attribute((WML_NS, "asciiTheme")),
theme,
default_font,
)
}
pub(super) fn parse_line_spacing(spacing_node: roxmltree::Node, line_val: f32) -> LineSpacing {
match spacing_node.attribute((WML_NS, "lineRule")) {
Some("exact") => LineSpacing::Exact(line_val / 20.0),
Some("atLeast") => LineSpacing::AtLeast(line_val / 20.0),
_ => LineSpacing::Auto(line_val / 240.0),
}
}
pub(super) fn parse_styles<R: std::io::Read + std::io::Seek>(
zip: &mut zip::ZipArchive<R>,
theme: &ThemeFonts,
) -> StylesInfo {
let mut defaults = StyleDefaults {
font_size: 12.0,
font_name: theme.minor.clone(),
space_after: 0.0,
line_spacing: LineSpacing::Auto(1.0),
};
let mut paragraph_styles = HashMap::new();
let mut character_styles = HashMap::new();
let Some(xml_content) = read_zip_text(zip, "word/styles.xml") else {
return StylesInfo {
defaults,
paragraph_styles,
character_styles,
table_border_styles: HashMap::new(),
};
};
let Ok(xml) = roxmltree::Document::parse(&xml_content) else {
return StylesInfo {
defaults,
paragraph_styles,
character_styles,
table_border_styles: HashMap::new(),
};
};
let root = xml.root_element();
if let Some(doc_defaults) = wml(root, "docDefaults") {
if let Some(rpr) = wml(doc_defaults, "rPrDefault").and_then(|n| wml(n, "rPr")) {
if let Some(sz_val) = wml_attr(rpr, "sz").and_then(|v| v.parse::<f32>().ok()) {
defaults.font_size = sz_val / 2.0;
}
if let Some(rfonts) = wml(rpr, "rFonts") {
defaults.font_name = resolve_font_from_node(rfonts, theme, &theme.minor);
}
}
let default_spacing = wml(doc_defaults, "pPrDefault")
.and_then(|n| wml(n, "pPr"))
.and_then(|n| wml(n, "spacing"));
if let Some(spacing) = default_spacing {
if let Some(after_val) = twips_attr(spacing, "after") {
defaults.space_after = after_val;
}
if let Some(line_val) = spacing
.attribute((WML_NS, "line"))
.and_then(|v| v.parse::<f32>().ok())
{
defaults.line_spacing = parse_line_spacing(spacing, line_val);
}
}
}
for style_node in root.children() {
if style_node.tag_name().name() != "style"
|| style_node.tag_name().namespace() != Some(WML_NS)
{
continue;
}
if style_node.attribute((WML_NS, "type")) != Some("paragraph") {
continue;
}
let Some(style_id) = style_node.attribute((WML_NS, "styleId")) else {
continue;
};
let ppr = wml(style_node, "pPr");
let spacing = ppr.and_then(|n| wml(n, "spacing"));
let space_before = spacing.and_then(|n| twips_attr(n, "before"));
let space_after = spacing.and_then(|n| twips_attr(n, "after"));
let borders = ppr.map(parse_paragraph_borders).unwrap_or_default();
let bdr_extra = borders
.bottom
.as_ref()
.map(|b| b.space_pt + b.width_pt)
.unwrap_or(0.0);
let rpr = wml(style_node, "rPr");
let font_size = rpr
.and_then(|n| wml_attr(n, "sz"))
.and_then(|v| v.parse::<f32>().ok())
.map(|hp| hp / 2.0);
let font_name = rpr
.and_then(|n| wml(n, "rFonts"))
.map(|rfonts| resolve_font_from_node(rfonts, theme, &defaults.font_name));
let bold = rpr.and_then(|n| wml_bool(n, "b"));
let italic = rpr.and_then(|n| wml_bool(n, "i"));
let caps = rpr.and_then(|n| wml_bool(n, "caps"));
let small_caps = rpr.and_then(|n| wml_bool(n, "smallCaps"));
let vanish = rpr.and_then(|n| wml_bool(n, "vanish"));
let color = rpr
.and_then(|n| wml_attr(n, "color"))
.and_then(parse_text_color);
let alignment = ppr.and_then(|ppr| wml_attr(ppr, "jc")).map(parse_alignment);
let contextual_spacing = ppr.and_then(|ppr| wml(ppr, "contextualSpacing")).is_some();
let keep_next = ppr.and_then(|ppr| wml(ppr, "keepNext")).is_some();
let keep_lines = ppr.and_then(|ppr| wml(ppr, "keepLines")).is_some();
let line_spacing = spacing.and_then(|n| {
n.attribute((WML_NS, "line"))
.and_then(|v| v.parse::<f32>().ok())
.map(|line_val| parse_line_spacing(n, line_val))
});
let ind = ppr.and_then(|n| wml(n, "ind"));
let indent_left = ind.and_then(|n| twips_attr(n, "left"));
let indent_right = ind.and_then(|n| twips_attr(n, "right"));
let indent_hanging = ind.and_then(|n| twips_attr(n, "hanging"));
let indent_first_line = ind.and_then(|n| twips_attr(n, "firstLine"));
let based_on = wml(style_node, "basedOn")
.and_then(|n| n.attribute((WML_NS, "val")))
.map(|s| s.to_string());
paragraph_styles.insert(
style_id.to_string(),
ParagraphStyle {
font_size,
font_name,
bold,
italic,
caps,
small_caps,
vanish,
color,
space_before,
space_after,
alignment,
contextual_spacing,
keep_next,
keep_lines,
line_spacing,
indent_left,
indent_right,
indent_hanging,
indent_first_line,
borders_extra: bdr_extra,
borders,
based_on,
},
);
}
resolve_based_on(&mut paragraph_styles);
for style_node in root.children() {
if style_node.tag_name().name() != "style"
|| style_node.tag_name().namespace() != Some(WML_NS)
{
continue;
}
if style_node.attribute((WML_NS, "type")) != Some("character") {
continue;
}
let Some(style_id) = style_node.attribute((WML_NS, "styleId")) else {
continue;
};
let Some(rpr) = wml(style_node, "rPr") else {
continue;
};
let font_size = wml_attr(rpr, "sz")
.and_then(|v| v.parse::<f32>().ok())
.map(|hp| hp / 2.0);
let font_name = wml(rpr, "rFonts")
.map(|rfonts| resolve_font_from_node(rfonts, theme, &defaults.font_name));
let bold = wml_bool(rpr, "b");
let italic = wml_bool(rpr, "i");
let underline = wml(rpr, "u")
.and_then(|n| n.attribute((WML_NS, "val")))
.map(|v| v != "none");
let strikethrough = wml_bool(rpr, "strike");
let caps = wml_bool(rpr, "caps");
let small_caps = wml_bool(rpr, "smallCaps");
let vanish = wml_bool(rpr, "vanish");
let color = wml_attr(rpr, "color").and_then(parse_text_color);
character_styles.insert(
style_id.to_string(),
CharacterStyle {
font_size,
font_name,
bold,
italic,
underline,
strikethrough,
caps,
small_caps,
vanish,
color,
},
);
}
let mut table_border_styles = HashMap::new();
for style_node in root.children() {
if style_node.tag_name().name() != "style"
|| style_node.tag_name().namespace() != Some(WML_NS)
{
continue;
}
if style_node.attribute((WML_NS, "type")) != Some("table") {
continue;
}
let Some(style_id) = style_node.attribute((WML_NS, "styleId")) else {
continue;
};
if let Some(tbl_borders) =
wml(style_node, "tblPr").and_then(|pr| wml(pr, "tblBorders"))
{
let parse_bdr = |name: &str| -> CellBorder {
let Some(n) = wml(tbl_borders, name) else {
return CellBorder::default();
};
let val = n.attribute((WML_NS, "val")).unwrap_or("none");
if val == "nil" || val == "none" {
return 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);
CellBorder::visible(color, width)
};
let left = parse_bdr("left");
let left = if left.present { left } else { parse_bdr("start") };
let right = parse_bdr("right");
let right = if right.present { right } else { parse_bdr("end") };
table_border_styles.insert(
style_id.to_string(),
TableBordersDef {
top: parse_bdr("top"),
bottom: parse_bdr("bottom"),
left,
right,
inside_h: parse_bdr("insideH"),
inside_v: parse_bdr("insideV"),
},
);
}
}
StylesInfo {
defaults,
paragraph_styles,
character_styles,
table_border_styles,
}
}
fn resolve_based_on(styles: &mut HashMap<String, ParagraphStyle>) {
let ids: Vec<String> = styles.keys().cloned().collect();
for id in ids {
let mut chain: Vec<String> = Vec::new();
let mut current = id.clone();
loop {
if chain.contains(¤t) {
break;
}
chain.push(current.clone());
match styles.get(¤t).and_then(|s| s.based_on.clone()) {
Some(parent) => current = parent,
None => break,
}
}
macro_rules! inherit {
($field:ident, $inherited:expr, $s:expr) => {
if $s.$field.is_some() {
$inherited = $s.$field.clone();
}
};
}
let mut inh = ParagraphStyle {
font_size: None,
font_name: None,
bold: None,
italic: None,
caps: None,
small_caps: None,
vanish: None,
color: None,
space_before: None,
space_after: None,
alignment: None,
contextual_spacing: false,
keep_next: false,
keep_lines: false,
line_spacing: None,
indent_left: None,
indent_right: None,
indent_hanging: None,
indent_first_line: None,
borders_extra: 0.0,
borders: crate::model::ParagraphBorders::default(),
based_on: None,
};
for ancestor_id in chain.iter().rev() {
if let Some(s) = styles.get(ancestor_id) {
inherit!(font_name, inh.font_name, s);
inherit!(font_size, inh.font_size, s);
inherit!(bold, inh.bold, s);
inherit!(italic, inh.italic, s);
inherit!(caps, inh.caps, s);
inherit!(small_caps, inh.small_caps, s);
inherit!(vanish, inh.vanish, s);
inherit!(color, inh.color, s);
inherit!(alignment, inh.alignment, s);
inherit!(space_before, inh.space_before, s);
inherit!(space_after, inh.space_after, s);
inherit!(line_spacing, inh.line_spacing, s);
inherit!(indent_left, inh.indent_left, s);
inherit!(indent_right, inh.indent_right, s);
inherit!(indent_hanging, inh.indent_hanging, s);
inherit!(indent_first_line, inh.indent_first_line, s);
}
}
if let Some(s) = styles.get_mut(&id) {
s.font_name = s.font_name.take().or(inh.font_name);
s.font_size = s.font_size.or(inh.font_size);
s.bold = s.bold.or(inh.bold);
s.italic = s.italic.or(inh.italic);
s.caps = s.caps.or(inh.caps);
s.small_caps = s.small_caps.or(inh.small_caps);
s.vanish = s.vanish.or(inh.vanish);
s.color = s.color.or(inh.color);
s.alignment = s.alignment.or(inh.alignment);
s.space_before = s.space_before.or(inh.space_before);
s.space_after = s.space_after.or(inh.space_after);
s.line_spacing = s.line_spacing.or(inh.line_spacing);
s.indent_left = s.indent_left.or(inh.indent_left);
s.indent_right = s.indent_right.or(inh.indent_right);
s.indent_hanging = s.indent_hanging.or(inh.indent_hanging);
s.indent_first_line = s.indent_first_line.or(inh.indent_first_line);
}
}
}