use std::collections::HashMap;
use std::rc::Rc;
use super::model::*;
use super::numbering::{Level, Numbering};
use super::props::*;
use super::styles::{Region, Styles, ThemeFonts};
use super::write::props::Cx;
#[derive(Clone)]
pub struct Resolver {
pub styles: Styles,
pub numbering: Numbering,
pub theme: ThemeFonts,
pub style_links: HashMap<(i64, i64), String>,
}
impl std::fmt::Debug for Resolver {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Resolver({} styles)", self.styles.by_id.len())
}
}
pub struct ParaResolved {
pub style: ParaStyle,
pub base: RunProps,
pub level: Option<Level>,
pub num_id: Option<i64>,
pub ilvl: i64,
pub from_style: bool,
pub style_id: Option<String>,
}
impl Resolver {
pub fn new(styles: Styles, numbering: Numbering, theme: ThemeFonts) -> Resolver {
let mut r = Resolver { styles, numbering, theme, style_links: HashMap::new() };
r.compute_style_links();
r
}
fn compute_style_links(&mut self) {
for (a, abs) in &self.numbering.abstracts {
for (i, l) in abs.levels.iter().enumerate() {
if let Some(ps) = l.as_ref().and_then(|l| l.pstyle.clone()) {
self.style_links.insert((*a, i as i64), ps);
}
}
}
for id in &self.styles.paragraph_order {
let ppr = &self.styles.by_id[id].base.ppr;
let Some(num) = ppr.num_id.filter(|n| *n != 0) else { continue };
let Some(a) = self.numbering.abstract_of(num, &self.styles) else { continue };
let ilvl = ppr.ilvl.unwrap_or(0);
self.style_links.entry((a, ilvl)).or_insert_with(|| id.clone());
}
}
pub fn font(&self, f: &Option<FontRef>) -> Option<String> {
match f {
Some(FontRef::Theme(t)) => Some(self.theme.resolve(t)),
Some(FontRef::Name(n)) => Some(n.clone()),
None => None,
}
}
pub fn run_style(&self, p: &RunProps) -> Run {
let ascii = self.font(&p.ascii);
let h_ansi = self.font(&p.h_ansi);
let fallback = || ascii.clone().or_else(|| h_ansi.clone()).unwrap_or_else(|| self.theme.minor.clone());
Rc::new(RunStyle {
ascii: ascii.clone().or_else(|| h_ansi.clone()).unwrap_or_else(fallback),
h_ansi: h_ansi.clone().or_else(|| ascii.clone()).unwrap_or_else(fallback),
east_asia: match (&p.east_asia, &p.east_asia_below) {
(Some(FontRef::Theme(t)), Some(below)) if self.theme.ea_unset(t) => self.font(&Some(below.clone())),
(e, _) => self.font(e),
}
.unwrap_or_else(fallback),
cs: self.font(&p.cs).unwrap_or_else(fallback),
east_asia_unset: p.east_asia_below.is_none() && matches!(&p.east_asia, Some(FontRef::Theme(t)) if self.theme.ea_unset(t)),
hint_east_asia: p.hint.as_deref() == Some("eastAsia"),
size: p.size.unwrap_or(10.0),
bold: p.toggle(toggle::B),
italic: p.toggle(toggle::I),
caps: p.toggle(toggle::CAPS),
small_caps: p.toggle(toggle::SMALL_CAPS),
strike: p.toggle(toggle::STRIKE),
dstrike: p.dstrike.unwrap_or(false),
hidden: p.toggle(toggle::VANISH),
underline: p.underline.clone().filter(|u| u != "none"),
color: p.color.flatten(),
highlight: p.highlight.flatten(),
em: p.em.clone().filter(|e| e != "none"),
toc: false,
shading: p.shading.flatten(),
vert_align: p.vert_align.unwrap_or(VertAlign::Baseline),
position: p.position.unwrap_or(0.0),
spacing: p.spacing.unwrap_or(0.0),
scale: p.scale.unwrap_or(100.0),
kern: p.kern.unwrap_or(0.0),
ligatures: p.ligatures.unwrap_or(0),
lang: p.lang.clone().unwrap_or_default(),
style_id: p.style.clone(),
direct: Raw::default(),
})
}
pub fn para_style(&self, p: &ParaProps) -> ParaStyle {
let line_rule = p.line_rule.unwrap_or(LineRule::Auto);
let line = match (p.line, line_rule) {
(Some(l), LineRule::Auto) => l / 240.0,
(Some(l), _) => l / 20.0,
(None, _) => 1.0,
};
let bdr = |b: &Option<Border>| b.clone().filter(|b| b.visible());
ParaStyle {
style_id: p.style.clone(),
jc: p.jc.unwrap_or(Jc::Left),
ind_left: p.ind_left.unwrap_or(0.0),
ind_right: p.ind_right.unwrap_or(0.0),
ind_first: p.ind_first.unwrap_or(0.0),
space_before: p.space_before.unwrap_or(0.0),
space_after: p.space_after.unwrap_or(0.0),
before_auto: p.before_auto.unwrap_or(false),
after_auto: p.after_auto.unwrap_or(false),
line,
line_rule: if p.line.is_some() { line_rule } else { LineRule::Auto },
contextual: p.contextual.unwrap_or(false),
keep_next: p.keep_next.unwrap_or(false),
keep_lines: p.keep_lines.unwrap_or(false),
page_break_before: p.page_break_before.unwrap_or(false),
widow_control: p.widow_control.unwrap_or(true),
suppress_auto_hyphens: p.suppress_auto_hyphens.unwrap_or(false),
suppress_line_numbers: p.suppress_line_numbers.unwrap_or(false),
tabs: p.resolved_tabs(),
border_top: bdr(&p.border_top),
border_bottom: bdr(&p.border_bottom),
border_left: bdr(&p.border_left),
border_right: bdr(&p.border_right),
border_between: bdr(&p.border_between),
shading: p.shading.flatten(),
frame: p.frame.clone(),
text_alignment: p.text_alignment.clone().unwrap_or_else(|| "auto".into()),
outline_lvl: p.outline_lvl,
list_jc: "left".into(),
num: None,
list_fmt: None,
direct: Raw::default(),
}
}
pub fn run_props(&self, base: &RunProps, direct: &RunProps) -> RunProps {
let mut p = base.clone();
if let Some(cs) = direct.style.as_deref() {
p.merge_level(&self.styles.chain_rpr(Some(cs)));
}
p.merge(direct);
p
}
pub fn paragraph(&self, direct: &ParaProps, region: Option<&Region>) -> ParaResolved {
let style_id = self.styles.paragraph_style(direct.style.as_deref()).map(str::to_string);
let style_ppr = self.styles.chain_ppr(style_id.as_deref());
let (num_id, ilvl, from_style) = match direct.num_id {
Some(id) => (Some(id), direct.ilvl.or(style_ppr.ilvl).unwrap_or(0), false),
None => match style_ppr.num_id {
Some(id) => (Some(id), direct.ilvl.or(style_ppr.ilvl).unwrap_or(0), true),
None => (None, 0, false),
},
};
let num_id = num_id.filter(|&id| id != 0);
let level = num_id.and_then(|id| self.numbering.level(id, ilvl, &self.styles)).cloned();
let mut ppr = self.styles.doc_ppr.clone();
if let Some(t) = region {
ppr.merge(&t.ppr);
}
if let (Some(l), true) = (&level, from_style) {
ppr.merge(&l.ppr);
ppr.merge(&style_ppr);
} else {
ppr.merge(&style_ppr);
if let Some(l) = &level {
ppr.merge(&l.ppr);
}
}
ppr.merge(direct);
ppr.style = style_id.clone();
ppr.num_id = num_id;
let mut style = self.para_style(&ppr);
if let Some(l) = &level {
style.list_jc = l.jc.clone();
style.num = num_id.map(|id| (id, ilvl));
style.list_fmt = Some(l.fmt.clone());
}
let mut base = self.styles.doc_rpr.clone();
if let Some(t) = region {
base.merge_level(&t.rpr);
}
base.merge_level(&self.styles.chain_rpr(style_id.as_deref()));
ParaResolved { style, base, level, num_id, ilvl, from_style, style_id }
}
pub fn label_shows(&self, num_id: i64, ilvl: i64, from_style: bool, style_id: Option<&str>) -> bool {
!from_style || {
let a = self.numbering.abstract_of(num_id, &self.styles).unwrap_or(-1);
self.style_links.get(&(a, ilvl)).map(|s| Some(s.as_str()) == style_id).unwrap_or(true)
}
}
pub fn region_of(&self, t: &Table, row: usize, cell: usize) -> Region {
let look = t.look.unwrap_or(Look { first_row: true, last_row: false, first_col: true, last_col: false, no_h_band: false, no_v_band: true });
let ncols = t.rows.get(row).map_or(0, |r| r.cells.len());
self.cell_region(t.style_id.as_deref(), look, t.bands, (row, cell), (t.rows.len(), ncols))
}
pub fn run_from_xml(&self, base: &RunProps, rpr: Option<&str>, cx: &Cx) -> Run {
let direct = rpr.and_then(|x| cx.parse(x, |_, el| RunProps::parse(Some(el)))).unwrap_or_default();
let mut s = (*self.run_style(&self.run_props(base, &direct))).clone();
s.direct = Raw(rpr.map(Rc::from));
Rc::new(s)
}
pub fn rerun(&self, base: &RunProps, run: &RunStyle, cx: &Cx) -> Run {
let rpr = run.direct.0.as_deref();
let mut direct = rpr.and_then(|x| cx.parse(x, |_, el| RunProps::parse(Some(el)))).unwrap_or_default();
if run.toc && direct.style.as_deref() == Some("Hyperlink") {
direct.style = None;
}
let mut s = (*self.run_style(&self.run_props(base, &direct))).clone();
s.direct = Raw(rpr.map(Rc::from));
s.toc = run.toc;
Rc::new(s)
}
pub fn para_from_xml(&self, ppr: Option<&str>, region: Option<&Region>, cx: &Cx) -> ParaResolved {
let direct = ppr.and_then(|x| cx.parse(x, |_, el| ParaProps::parse(Some(el)))).unwrap_or_default();
let mut r = self.paragraph(&direct, region);
r.style.direct = Raw(ppr.map(Rc::from));
r
}
pub fn cell_region(
&self,
style_id: Option<&str>,
look: Look,
bands: (i64, i64),
(ri, ci): (usize, usize),
(nrows, ncols): (usize, usize),
) -> Region {
let defines = |r: &str| self.styles.table_defines(style_id, r);
let first_row = look.first_row && defines("firstRow");
let last_row = look.last_row && defines("lastRow");
let first_col = look.first_col && defines("firstCol");
let last_col = look.last_col && defines("lastCol");
let (row_band, col_band) = bands;
let is_first_row = ri == 0 && first_row;
let is_last_row = ri + 1 == nrows && last_row;
let is_first_col = ci == 0 && first_col;
let is_last_col = ci + 1 == ncols && last_col;
let mut active: Vec<&str> = Vec::new();
if row_band > 0 && !look.no_h_band && !is_first_row && !is_last_row {
let r = ri as i64 - first_row as i64;
active.push(if (r / row_band) % 2 == 0 { "band1Horz" } else { "band2Horz" });
}
if col_band > 0 && !look.no_v_band && !is_first_col && !is_last_col {
let c = ci as i64 - first_col as i64;
active.push(if (c / col_band) % 2 == 0 { "band1Vert" } else { "band2Vert" });
}
if is_first_col {
active.push("firstCol");
}
if is_last_col {
active.push("lastCol");
}
if is_first_row {
active.push("firstRow");
}
if is_last_row {
active.push("lastRow");
}
if ri == 0 && ci == 0 && look.first_row && look.first_col {
active.push("nwCell");
}
if ri == 0 && ci + 1 == ncols && look.first_row && look.last_col {
active.push("neCell");
}
if ri + 1 == nrows && ci == 0 && look.last_row && look.first_col {
active.push("swCell");
}
if ri + 1 == nrows && ci + 1 == ncols && look.last_row && look.last_col {
active.push("seCell");
}
self.styles.table_region(style_id, &active)
}
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use super::*;
fn docs() -> Vec<PathBuf> {
fn walk(d: &Path, out: &mut Vec<PathBuf>) {
for e in std::fs::read_dir(d).into_iter().flatten().flatten() {
let p = e.path();
if p.is_dir() {
walk(&p, out);
} else if p.extension().is_some_and(|x| x == "docx") {
out.push(p);
}
}
}
let mut out = Vec::new();
walk(&Path::new(env!("CARGO_MANIFEST_DIR")).join("tests"), &mut out);
out
}
fn check(res: &Resolver, cx: &Cx, blocks: &[Block], region: Option<&Region>, what: &str, checked: &mut usize) {
for b in blocks {
match b {
Block::Para(p) => {
if p.src.as_ref().is_some_and(|s| s.ranges.len() > 1) {
continue;
}
let r = res.para_from_xml(p.style.direct.0.as_deref(), region, cx);
assert_eq!(*p.style, r.style, "{what}: paragraph {:?}", p.id);
assert_eq!(*p.mark, *res.run_from_xml(&r.base, p.mark.direct.0.as_deref(), cx), "{what}: the mark of {:?}", p.id);
for i in &p.inlines {
if let Inline::Text(t, run) = i {
assert_eq!(**run, *res.rerun(&r.base, run, cx), "{what}: run {t:?} of {:?}", p.id);
*checked += 1;
}
}
}
Block::Table(t) => {
for (ri, row) in t.rows.iter().enumerate() {
for (ci, c) in row.cells.iter().enumerate() {
let region = res.region_of(t, ri, ci);
check(res, cx, &c.blocks, Some(®ion), what, checked);
}
}
}
}
}
}
#[test]
fn re_resolving_gives_what_reading_resolved() {
let mut checked = 0;
for f in docs() {
let Ok(doc) = super::super::read_editable(&std::fs::read(&f).expect("readable")) else { continue };
let (Some(res), Some(src)) = (&doc.resolver, &doc.source) else { panic!("{}: no resolver", f.display()) };
let cx = Cx::of_xml(&src.xml);
for s in &doc.sections {
check(res, &cx, &s.blocks, None, &f.display().to_string(), &mut checked);
}
}
assert!(checked > 10_000, "only {checked} runs checked");
}
}