use std::collections::HashMap;
use roxmltree::Node;
use super::props::*;
use super::xml::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StyleKind {
Paragraph,
Character,
Table,
Numbering,
}
#[derive(Clone, Debug, Default)]
pub struct Region {
pub ppr: ParaProps,
pub rpr: RunProps,
pub tbl: TblProps,
pub tc: TcProps,
pub tr: TrProps,
}
impl Region {
fn parse(n: Node) -> Region {
Region {
ppr: ParaProps::parse(child(n, "pPr")),
rpr: RunProps::parse(child(n, "rPr")),
tbl: TblProps::parse(child(n, "tblPr")),
tc: TcProps::parse(child(n, "tcPr")),
tr: TrProps::parse(child(n, "trPr")),
}
}
}
#[derive(Clone, Debug)]
pub struct Style {
pub id: String,
pub name: String,
pub quick: bool,
pub kind: StyleKind,
pub based_on: Option<String>,
pub next: Option<String>,
pub default: bool,
pub base: Region,
pub regions: HashMap<String, Region>,
}
pub const REGION_ORDER: [&str; 12] =
["band1Horz", "band2Horz", "band1Vert", "band2Vert", "firstCol", "lastCol", "firstRow", "lastRow", "nwCell", "neCell", "swCell", "seCell"];
#[derive(Clone, Debug, Default)]
pub struct ThemeFonts {
pub major: String,
pub minor: String,
pub major_ea: Option<String>,
pub minor_ea: Option<String>,
pub major_cs: Option<String>,
pub minor_cs: Option<String>,
}
impl ThemeFonts {
pub fn parse(theme: Option<&str>) -> ThemeFonts {
let mut t = ThemeFonts { major: "Calibri Light".into(), minor: "Calibri".into(), ..Default::default() };
let Some(xml) = theme else { return t };
let Ok(doc) = roxmltree::Document::parse(xml) else { return t };
let face = |scheme: Node, kind: &str| {
scheme.children().find(|c| is(*c, A, kind)).and_then(|n| n.attribute("typeface")).filter(|s| !s.is_empty()).map(str::to_string)
};
for n in doc.descendants().filter(|n| is(*n, A, "majorFont") || is(*n, A, "minorFont")) {
let major = n.tag_name().name() == "majorFont";
let (latin, ea, cs) = (face(n, "latin"), face(n, "ea"), face(n, "cs"));
if major {
if let Some(l) = latin {
t.major = l;
}
t.major_ea = ea;
t.major_cs = cs;
} else {
if let Some(l) = latin {
t.minor = l;
}
t.minor_ea = ea;
t.minor_cs = cs;
}
}
t
}
pub fn ea_unset(&self, theme: &str) -> bool {
theme.ends_with("EastAsia") && if theme.starts_with("major") { self.major_ea.is_none() } else { self.minor_ea.is_none() }
}
pub fn resolve(&self, theme: &str) -> String {
let major = theme.starts_with("major");
let (latin, ea, cs) = if major { (&self.major, &self.major_ea, &self.major_cs) } else { (&self.minor, &self.minor_ea, &self.minor_cs) };
if theme.ends_with("EastAsia") {
ea.clone().unwrap_or_else(|| latin.clone())
} else if theme.ends_with("Bidi") {
cs.clone().unwrap_or_else(|| latin.clone())
} else {
latin.clone()
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Styles {
pub by_id: HashMap<String, Style>,
pub paragraph_order: Vec<String>,
pub default_paragraph: Option<String>,
pub default_table: Option<String>,
pub doc_rpr: RunProps,
pub doc_ppr: ParaProps,
}
impl Styles {
pub fn parse(xml: Option<&str>) -> Styles {
let mut s = Styles::default();
let doc = xml.and_then(|x| roxmltree::Document::parse(x).ok());
let Some(doc) = doc else {
s.app_defaults();
return s;
};
let root = doc.root_element();
match child(root, "docDefaults") {
Some(dd) => {
s.doc_rpr = RunProps::parse(child(dd, "rPrDefault").and_then(|n| child(n, "rPr")));
s.doc_ppr = ParaProps::parse(child(dd, "pPrDefault").and_then(|n| child(n, "pPr")));
}
None => s.app_defaults(),
}
for n in children(root, "style") {
let Some(id) = attr(n, "styleId") else { continue };
let kind = match attr(n, "type") {
Some("character") => StyleKind::Character,
Some("table") => StyleKind::Table,
Some("numbering") => StyleKind::Numbering,
_ => StyleKind::Paragraph,
};
let default = attr(n, "default").map(|v| on_off_str(Some(v))).unwrap_or(false);
let mut regions = HashMap::new();
for r in children(n, "tblStylePr") {
if let Some(t) = attr(r, "type") {
regions.insert(t.to_string(), Region::parse(r));
}
}
let style = Style {
id: id.to_string(),
name: child(n, "name").and_then(val).unwrap_or("").to_string(),
quick: child(n, "qFormat").is_some(),
kind,
based_on: child(n, "basedOn").and_then(val).map(str::to_string),
next: child(n, "next").and_then(val).map(str::to_string),
default,
base: Region::parse(n),
regions,
};
if kind == StyleKind::Paragraph {
s.paragraph_order.push(style.id.clone());
if default {
s.default_paragraph = Some(style.id.clone());
}
}
if kind == StyleKind::Table && default && s.default_table.is_none() {
s.default_table = Some(style.id.clone());
}
s.by_id.insert(style.id.clone(), style);
}
if s.default_paragraph.is_none() && s.by_id.get("Normal").is_some_and(|st| st.kind == StyleKind::Paragraph) {
s.default_paragraph = Some("Normal".into());
}
s
}
fn app_defaults(&mut self) {
let mut r = RunProps::default();
let aptos = Some(FontRef::Name("Aptos".into()));
r.ascii = aptos.clone();
r.h_ansi = aptos.clone();
r.east_asia = aptos.clone();
r.cs = aptos;
r.size = Some(12.0);
r.kern = Some(1.0);
self.doc_rpr = r;
self.doc_ppr = ParaProps { space_after: Some(8.0), line: Some(278.0), line_rule: Some(LineRule::Auto), ..Default::default() };
}
pub fn get(&self, id: &str) -> Option<&Style> {
self.by_id.get(id)
}
pub fn paragraph_style<'a>(&'a self, id: Option<&'a str>) -> Option<&'a str> {
match id {
Some(id) if self.by_id.get(id).is_some_and(|s| s.kind == StyleKind::Paragraph) => Some(id),
_ => self.default_paragraph.as_deref(),
}
}
pub fn chain(&self, id: Option<&str>) -> Vec<&Style> {
let mut out = Vec::new();
let mut cur = id.and_then(|i| self.by_id.get(i));
while let Some(s) = cur {
if out.iter().any(|o: &&Style| o.id == s.id) || out.len() > 32 {
break;
}
out.push(s);
cur = s.based_on.as_deref().and_then(|b| self.by_id.get(b));
}
out.reverse();
out
}
pub fn chain_ppr(&self, id: Option<&str>) -> ParaProps {
let mut p = ParaProps::default();
for s in self.chain(id) {
p.merge(&s.base.ppr);
}
p
}
pub fn chain_rpr(&self, id: Option<&str>) -> RunProps {
let chain = self.chain(id);
RunProps::fold_chain(chain.iter().map(|s| &s.base.rpr))
}
pub fn table_style<'a>(&'a self, id: Option<&'a str>) -> Option<&'a str> {
match id {
Some(id) if self.by_id.contains_key(id) => Some(id),
_ => self.default_table.as_deref(),
}
}
pub fn table_region(&self, id: Option<&str>, active: &[&str]) -> Region {
let chain = self.chain(id);
let mut out = Region::default();
let mut rprs: Vec<&RunProps> = Vec::new();
let add = |r: &'_ Region, out: &mut Region| {
out.ppr.merge(&r.ppr);
out.tbl.merge(&r.tbl);
out.tc.merge(&r.tc);
out.tr.merge(&r.tr);
};
for s in &chain {
add(&s.base, &mut out);
rprs.push(&s.base.rpr);
}
for name in REGION_ORDER.iter().filter(|n| active.contains(n)) {
for s in &chain {
if let Some(r) = s.regions.get(*name) {
add(r, &mut out);
rprs.push(&r.rpr);
}
}
}
out.rpr = RunProps::fold_chain(rprs.into_iter());
out
}
pub fn table_defines(&self, id: Option<&str>, region: &str) -> bool {
self.chain(id).iter().any(|s| s.regions.contains_key(region))
}
}