use ahash::AHashMap;
use crate::error::{Result, XbergError};
use crate::extraction::ooxml_constants::WORDPROCESSINGML_NAMESPACE;
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, PartialEq)]
pub enum StyleType {
Paragraph,
Character,
Table,
Numbering,
}
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, Default, PartialEq)]
pub struct RunProperties {
pub bold: Option<bool>,
pub italic: Option<bool>,
pub underline: Option<bool>,
pub strikethrough: Option<bool>,
pub color: Option<String>,
pub font_size_half_points: Option<i32>,
pub font_ascii: Option<String>,
pub font_ascii_theme: Option<String>,
pub vert_align: Option<String>,
pub font_h_ansi: Option<String>,
pub font_cs: Option<String>,
pub font_east_asia: Option<String>,
pub highlight: Option<String>,
pub caps: Option<bool>,
pub small_caps: Option<bool>,
pub shadow: Option<bool>,
pub outline: Option<bool>,
pub emboss: Option<bool>,
pub imprint: Option<bool>,
pub char_spacing: Option<i32>,
pub position: Option<i32>,
pub kern: Option<i32>,
pub theme_color: Option<String>,
pub theme_tint: Option<String>,
pub theme_shade: Option<String>,
}
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ParagraphProperties {
pub alignment: Option<String>,
pub spacing_before: Option<i32>,
pub spacing_after: Option<i32>,
pub spacing_line: Option<i32>,
pub spacing_line_rule: Option<String>,
pub indent_left: Option<i32>,
pub indent_right: Option<i32>,
pub indent_first_line: Option<i32>,
pub indent_hanging: Option<i32>,
pub outline_level: Option<u8>,
pub numbering_id: Option<i64>,
pub numbering_level: Option<i64>,
pub keep_next: Option<bool>,
pub keep_lines: Option<bool>,
pub page_break_before: Option<bool>,
pub widow_control: Option<bool>,
pub suppress_auto_hyphens: Option<bool>,
pub bidi: Option<bool>,
pub shading_fill: Option<String>,
pub shading_val: Option<String>,
pub border_top: Option<String>,
pub border_bottom: Option<String>,
pub border_left: Option<String>,
pub border_right: Option<String>,
}
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone)]
pub struct StyleDefinition {
pub id: String,
pub name: Option<String>,
pub style_type: StyleType,
pub based_on: Option<String>,
pub next_style: Option<String>,
pub is_default: bool,
pub paragraph_properties: ParagraphProperties,
pub run_properties: RunProperties,
}
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ResolvedStyle {
pub paragraph_properties: ParagraphProperties,
pub run_properties: RunProperties,
}
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, Default)]
pub struct StyleCatalog {
pub styles: AHashMap<String, StyleDefinition>,
pub default_paragraph_properties: ParagraphProperties,
pub default_run_properties: RunProperties,
}
pub(crate) fn parse_styles_xml(xml: &str) -> Result<StyleCatalog> {
let doc = roxmltree::Document::parse(xml)
.map_err(|e| XbergError::parsing(format!("Failed to parse styles.xml: {}", e)))?;
let root = doc.root_element();
let mut catalog = StyleCatalog::default();
for child in root.children() {
if !child.is_element() {
continue;
}
match child.tag_name().name() {
"docDefaults" => parse_doc_defaults(&child, &mut catalog),
"style" => {
if let Some(style_def) = parse_style_element(&child) {
catalog.styles.insert(style_def.id.clone(), style_def);
}
}
_ => {}
}
}
Ok(catalog)
}
fn parse_doc_defaults(node: &roxmltree::Node, catalog: &mut StyleCatalog) {
for child in node.children() {
if !child.is_element() {
continue;
}
match child.tag_name().name() {
"rPrDefault" => {
if let Some(rpr) = find_child_element(&child, "rPr") {
catalog.default_run_properties = parse_run_properties(&rpr);
}
}
"pPrDefault" => {
if let Some(ppr) = find_child_element(&child, "pPr") {
catalog.default_paragraph_properties = parse_paragraph_properties(&ppr);
}
}
_ => {}
}
}
}
fn parse_style_element(node: &roxmltree::Node) -> Option<StyleDefinition> {
let style_id = node.attribute((WORDPROCESSINGML_NAMESPACE, "styleId"))?;
let type_str = node.attribute((WORDPROCESSINGML_NAMESPACE, "type"))?;
let style_type = match type_str {
"paragraph" => StyleType::Paragraph,
"character" => StyleType::Character,
"table" => StyleType::Table,
"numbering" => StyleType::Numbering,
_ => return None,
};
let is_default = node
.attribute((WORDPROCESSINGML_NAMESPACE, "default"))
.is_some_and(|v| v == "1" || v == "true");
let mut name = None;
let mut based_on = None;
let mut next_style = None;
let mut paragraph_properties = ParagraphProperties::default();
let mut run_properties = RunProperties::default();
for child in node.children() {
if !child.is_element() {
continue;
}
match child.tag_name().name() {
"name" => {
name = get_w_val(&child).map(String::from);
}
"basedOn" => {
based_on = get_w_val(&child).map(String::from);
}
"next" => {
next_style = get_w_val(&child).map(String::from);
}
"pPr" => {
paragraph_properties = parse_paragraph_properties(&child);
}
"rPr" => {
run_properties = parse_run_properties(&child);
}
_ => {}
}
}
Some(StyleDefinition {
id: style_id.to_string(),
name,
style_type,
based_on,
next_style,
is_default,
paragraph_properties,
run_properties,
})
}
fn parse_run_properties(node: &roxmltree::Node) -> RunProperties {
let mut props = RunProperties::default();
for child in node.children() {
if !child.is_element() {
continue;
}
match child.tag_name().name() {
"b" => props.bold = Some(parse_toggle_property(&child)),
"i" => props.italic = Some(parse_toggle_property(&child)),
"u" => {
let val = get_w_val(&child);
props.underline = Some(!matches!(val, Some("none")));
}
"strike" | "dstrike" => props.strikethrough = Some(parse_toggle_property(&child)),
"color" => {
props.color = get_w_val(&child).map(String::from);
props.theme_color = get_w_attr_string(&child, "themeColor");
props.theme_tint = get_w_attr_string(&child, "themeTint");
props.theme_shade = get_w_attr_string(&child, "themeShade");
}
"sz" => {
props.font_size_half_points = get_w_val(&child).and_then(|v| v.parse::<i32>().ok());
}
"rFonts" => {
props.font_ascii = child.attribute((WORDPROCESSINGML_NAMESPACE, "ascii")).map(String::from);
props.font_ascii_theme = child
.attribute((WORDPROCESSINGML_NAMESPACE, "asciiTheme"))
.map(String::from);
props.font_h_ansi = child.attribute((WORDPROCESSINGML_NAMESPACE, "hAnsi")).map(String::from);
props.font_cs = child.attribute((WORDPROCESSINGML_NAMESPACE, "cs")).map(String::from);
props.font_east_asia = child
.attribute((WORDPROCESSINGML_NAMESPACE, "eastAsia"))
.map(String::from);
}
"vertAlign" => {
props.vert_align = get_w_val(&child).map(String::from);
}
"highlight" => {
props.highlight = get_w_val(&child).map(String::from);
}
"caps" => {
props.caps = Some(parse_toggle_property(&child));
}
"smallCaps" => {
props.small_caps = Some(parse_toggle_property(&child));
}
"shadow" => {
props.shadow = Some(parse_toggle_property(&child));
}
"outline" => {
props.outline = Some(parse_toggle_property(&child));
}
"emboss" => {
props.emboss = Some(parse_toggle_property(&child));
}
"imprint" => {
props.imprint = Some(parse_toggle_property(&child));
}
"spacing" => {
props.char_spacing = get_w_val(&child).and_then(|v| v.parse::<i32>().ok());
}
"position" => {
props.position = get_w_val(&child).and_then(|v| v.parse::<i32>().ok());
}
"kern" => {
props.kern = get_w_val(&child).and_then(|v| v.parse::<i32>().ok());
}
_ => {}
}
}
props
}
fn parse_paragraph_properties(node: &roxmltree::Node) -> ParagraphProperties {
let mut props = ParagraphProperties::default();
for child in node.children() {
if !child.is_element() {
continue;
}
match child.tag_name().name() {
"jc" => {
props.alignment = get_w_val(&child).map(String::from);
}
"spacing" => {
props.spacing_before = get_w_attr_i32(&child, "before");
props.spacing_after = get_w_attr_i32(&child, "after");
props.spacing_line = get_w_attr_i32(&child, "line");
props.spacing_line_rule = get_w_attr_string(&child, "lineRule");
}
"ind" => {
props.indent_left = get_w_attr_i32(&child, "left");
props.indent_right = get_w_attr_i32(&child, "right");
props.indent_first_line = get_w_attr_i32(&child, "firstLine");
props.indent_hanging = get_w_attr_i32(&child, "hanging");
}
"outlineLvl" => {
props.outline_level = get_w_val(&child).and_then(|v| v.parse::<u8>().ok());
}
"numPr" => {
(props.numbering_id, props.numbering_level) = parse_numbering_properties(&child);
}
"keepNext" => {
props.keep_next = Some(parse_toggle_property(&child));
}
"keepLines" => {
props.keep_lines = Some(parse_toggle_property(&child));
}
"pageBreakBefore" => {
props.page_break_before = Some(parse_toggle_property(&child));
}
"widowControl" => {
props.widow_control = Some(parse_toggle_property(&child));
}
"suppressAutoHyphens" => {
props.suppress_auto_hyphens = Some(parse_toggle_property(&child));
}
"bidi" => {
props.bidi = Some(parse_toggle_property(&child));
}
"shd" => {
props.shading_fill = get_w_attr_string(&child, "fill");
props.shading_val = get_w_attr_string(&child, "val");
}
"pBdr" => {
for border_child in child.children() {
if !border_child.is_element() {
continue;
}
match border_child.tag_name().name() {
"top" => props.border_top = get_w_val(&border_child).map(String::from),
"bottom" => props.border_bottom = get_w_val(&border_child).map(String::from),
"left" | "start" => props.border_left = get_w_val(&border_child).map(String::from),
"right" | "end" => props.border_right = get_w_val(&border_child).map(String::from),
_ => {}
}
}
}
_ => {}
}
}
props
}
fn parse_numbering_properties(node: &roxmltree::Node) -> (Option<i64>, Option<i64>) {
let mut numbering_id = None;
let mut numbering_level = None;
for child in node.children() {
if !child.is_element() {
continue;
}
match child.tag_name().name() {
"numId" => numbering_id = get_w_val(&child).and_then(|v| v.parse::<i64>().ok()),
"ilvl" => numbering_level = get_w_val(&child).and_then(|v| v.parse::<i64>().ok()),
_ => {}
}
}
(numbering_id, numbering_level)
}
fn get_w_val<'a>(node: &'a roxmltree::Node) -> Option<&'a str> {
node.attribute((WORDPROCESSINGML_NAMESPACE, "val"))
.or_else(|| node.attribute("val"))
}
fn get_w_attr_i32(node: &roxmltree::Node, local_name: &str) -> Option<i32> {
node.attribute((WORDPROCESSINGML_NAMESPACE, local_name))
.or_else(|| node.attribute(local_name))
.and_then(|v| v.parse::<i32>().ok())
}
fn get_w_attr_string(node: &roxmltree::Node, local_name: &str) -> Option<String> {
node.attribute((WORDPROCESSINGML_NAMESPACE, local_name))
.or_else(|| node.attribute(local_name))
.map(String::from)
}
fn find_child_element<'a>(node: &'a roxmltree::Node, local_name: &str) -> Option<roxmltree::Node<'a, 'a>> {
node.children()
.find(|c| c.is_element() && c.tag_name().name() == local_name)
}
fn parse_toggle_property(node: &roxmltree::Node) -> bool {
match get_w_val(node) {
None => true,
Some(val) => !matches!(val, "0" | "false"),
}
}
#[cfg(test)]
mod test_support;
#[cfg(test)]
mod tests;