use std::collections::HashMap;
use crate::docx::{Block, Document, Inline, Paragraph};
use crate::layout::ids::{ParaId, Unit};
use crate::layout::traverse::semantic_chars;
use crate::layout::{Snapshot, layout};
use super::{fonts, gate_set};
fn paragraphs(d: &Document) -> HashMap<ParaId, (&Paragraph, bool)> {
fn walk<'a>(blocks: &'a [Block], in_box: bool, out: &mut HashMap<ParaId, (&'a Paragraph, bool)>) {
for b in blocks {
match b {
Block::Para(p) => {
out.insert(ParaId(p.id), (p, in_box));
for i in &p.inlines {
if let Inline::Float(f, _) = i
&& let Some(tb) = &f.text_box
{
walk(&tb.blocks, true, out);
}
}
}
Block::Table(t) => {
for r in &t.rows {
for c in &r.cells {
walk(&c.blocks, in_box, out);
}
}
}
}
}
}
let mut out = HashMap::new();
for s in &d.sections {
walk(&s.blocks, false, &mut out);
}
for blocks in d.stories.values().chain(d.footnotes.values()).chain(d.endnotes.values()).chain(d.separators.values()) {
walk(blocks, false, &mut out);
}
out
}
fn text_units(p: &Paragraph) -> HashMap<u32, (char, bool)> {
let mut out = HashMap::new();
let mut u = 0u32;
let shown = crate::docx::fields::layout_shown(&p.inlines);
for (i, shown) in p.inlines.iter().zip(shown) {
if let Inline::Text(s, run) = i {
for c in s.chars() {
out.insert(u, (c, run.hidden || shown == crate::docx::fields::Shown::Not));
u += 1;
}
} else {
u += i.units() as u32;
}
}
out
}
fn shows(drawn: char, c: char) -> bool {
drawn == c
|| drawn.to_uppercase().eq(c.to_uppercase())
|| (0xF020..=0xF0FF).contains(&(c as u32))
|| (matches!(c, '\u{AD}' | '\u{2011}') && drawn == '-')
}
fn check(name: &str, d: &Document, s: &Snapshot) -> Vec<String> {
let paras = paragraphs(d);
let mut problems = Vec::new();
for (pi, page) in s.pages().iter().enumerate() {
for it in crate::layout::traverse::paint_items(page) {
let crate::layout::fragment::Item::Glyphs(r) = it else { continue };
let Some(para) = r.para else { continue };
let Some(&(p, _)) = paras.get(¶) else {
problems.push(format!("{name} page {}: glyphs of unknown paragraph {para:?}", pi + 1));
continue;
};
let units = text_units(p);
for &(c, _, u) in r.chars.iter() {
let Some(Unit(u)) = u else { continue };
if let Some(&(want, _)) = units.get(&u)
&& !shows(c, want)
{
problems.push(format!("{name} page {}: paragraph {para:?} unit {u} draws {c:?}, the document has {want:?}", pi + 1));
}
}
}
}
let mut seen: HashMap<ParaId, Vec<(u32, char)>> = HashMap::new();
for page in s.pages() {
let mut chars = Vec::new();
semantic_chars(page, &mut chars);
for (para, Unit(u), c) in chars {
seen.entry(para).or_default().push((u, c));
}
}
let excluded: Vec<(ParaId, std::ops::Range<Unit>)> = s.excluded().iter().map(|(_, p, r)| (*p, r.clone())).collect();
for (para, mut got) in seen {
let Some(&(p, in_box)) = paras.get(¶) else { continue };
let units = text_units(p);
got.retain(|(u, _)| units.contains_key(u));
got.sort_by_key(|x| x.0);
if let Some(w) = got.windows(2).find(|w| w[0].0 == w[1].0) {
problems.push(format!("{name}: paragraph {para:?} unit {} shown twice", w[0].0));
continue;
}
let excluded_here = |u: u32| excluded.iter().any(|(q, r)| *q == para && r.contains(&Unit(u)));
let mut want: Vec<u32> = units.iter().filter(|(u, (_, hidden))| !hidden && !excluded_here(**u)).map(|(u, _)| *u).collect();
want.sort();
let got_units: Vec<u32> = got.iter().map(|x| x.0).collect();
if in_box {
continue;
}
let missing: Vec<u32> =
want.iter().copied().filter(|u| got_units.binary_search(u).is_err() && !units[u].0.is_whitespace() && units[u].0 != '\u{AD}').collect();
if let Some(u) = missing.first() {
problems.push(format!("{name}: paragraph {para:?} doesn't show unit {u} ({:?}) ({} missing)", units[u].0, missing.len()));
}
}
problems
}
fn run(every: usize) {
let fonts = fonts();
let mut problems = Vec::new();
let docs = gate_set(crate::features::ALL);
if every == 1 && !docs.is_empty() {
assert!(docs.len() > 1000, "the whole baseline set: {} documents", docs.len());
}
for (k, (path, d)) in docs.into_iter().enumerate() {
if k % every != 0 {
continue;
}
let s = layout(&d, &fonts);
problems.extend(check(&path.display().to_string(), &d, &s));
}
assert!(problems.is_empty(), "{} problems:\n{}", problems.len(), problems.iter().take(30).cloned().collect::<Vec<_>>().join("\n"));
}
#[test]
fn source_mapping_round_trip_and_semantic_sample() {
run(25);
}
#[test]
#[ignore]
fn source_mapping_all() {
run(1);
}