use crate::docx::{Block, Document, VMerge};
use crate::layout::{ParaId, TableId};
use super::index::DocIndex;
use super::position::{Place, Position};
use super::selection::Selection;
use super::step::para_len;
use super::table::Grid;
#[derive(Clone, Debug, PartialEq)]
pub struct Violation {
pub invariant: &'static str,
pub detail: String,
}
pub fn check(doc: &Document, idx: &DocIndex, selection: &Selection) -> Vec<Violation> {
let mut out = Vec::new();
for s in &doc.sections {
blocks(&s.blocks, &mut out, true);
}
for b in doc.stories.values().chain(doc.footnotes.values()).chain(doc.endnotes.values()) {
blocks(b, &mut out, false);
}
if let Some(Block::Table(t)) = doc.sections.last().and_then(|s| s.blocks.last()) {
out.push(Violation { invariant: "8 final paragraph", detail: format!("the body ends with table {}", t.id) });
}
for id in duplicate_ids(doc) {
out.push(Violation { invariant: "4 unique ids", detail: format!("id {id} appears twice") });
}
fn anchors(blocks: &[Block], idx: &DocIndex, out: &mut Vec<Violation>) {
for b in blocks {
match b {
Block::Para(p) => {
for i in &p.inlines {
if let crate::docx::Inline::Float(f, _) = i {
let id = crate::layout::FloatId(f.id);
if idx.float_anchor(id) != Some(crate::layout::ParaId(p.id)) {
out.push(Violation {
invariant: "5 float anchors",
detail: format!("float {} in paragraph {} is indexed at {:?}", f.id, p.id, idx.float_anchor(id)),
});
}
if let Some(tb) = &f.text_box {
anchors(&tb.blocks, idx, out);
}
}
}
}
Block::Table(t) => t.rows.iter().flat_map(|r| r.cells.iter()).for_each(|c| anchors(&c.blocks, idx, out)),
}
}
}
for b in doc.sections.iter().map(|s| &s.blocks).chain(doc.stories.values()).chain(doc.footnotes.values()).chain(doc.endnotes.values()) {
anchors(b, idx, &mut out);
}
let mut position = |p: Option<Position>, what: &str| {
let Some(p) = p else { return };
if let Some(e) = invalid(doc, idx, &p) {
out.push(Violation { invariant: "7 valid positions", detail: format!("{what}: {e}") });
}
};
position(selection.anchor(), "anchor");
position(selection.focus(), "focus");
elements(doc, idx, &mut out);
out
}
fn elements(doc: &Document, idx: &DocIndex, out: &mut Vec<Violation>) {
use crate::docx::{ControlId, ControlKind, ElementId, Inline};
use std::collections::{HashMap, HashSet};
let mut seen_fields: HashSet<crate::docx::FieldId> = HashSet::new();
let mut used: HashSet<ControlId> = HashSet::new();
let mut members: HashMap<ControlId, (String, Vec<usize>)> = HashMap::new();
fn walk(
blocks: &[Block],
container: &str,
doc: &Document,
seen: &mut HashSet<crate::docx::FieldId>,
used: &mut HashSet<ControlId>,
members: &mut HashMap<ControlId, (String, Vec<usize>)>,
out: &mut Vec<Violation>,
) {
let lists = |list: &[ControlId], what: String, out: &mut Vec<Violation>| {
for (k, c) in list.iter().enumerate() {
let kind = doc.controls.get(c).map(|c| &c.props.kind);
let next = list.get(k + 1).and_then(|n| doc.controls.get(n)).map(|c| &c.props.kind);
let prev = k.checked_sub(1).and_then(|p| doc.controls.get(&list[p])).map(|c| &c.props.kind);
if kind == Some(&ControlKind::RepeatingItem) && prev != Some(&ControlKind::RepeatingSection) {
out.push(Violation { invariant: "C3 item in a section", detail: format!("{what}: item {c:?} not directly in a section") });
}
if kind == Some(&ControlKind::RepeatingSection) && next != Some(&ControlKind::RepeatingItem) {
out.push(Violation { invariant: "C3 item in a section", detail: format!("{what}: section {c:?} without an item") });
}
}
};
for (bi, b) in blocks.iter().enumerate() {
let (ids, what) = match b {
Block::Para(p) => (&p.controls, format!("paragraph {}", p.id)),
Block::Table(t) => (&t.controls, format!("table {}", t.id)),
};
lists(ids, what.clone(), out);
for c in ids {
used.insert(*c);
let e = members.entry(*c).or_insert_with(|| (container.to_string(), Vec::new()));
if e.0 != container {
out.push(Violation { invariant: "C4 one container", detail: format!("control {c:?} in {} and {container}", e.0) });
}
e.1.push(bi);
}
match b {
Block::Para(p) => {
let mut stack: Vec<ElementId> = Vec::new();
let mut sep: HashSet<crate::docx::FieldId> = HashSet::new();
for i in &p.inlines {
match i {
Inline::FieldBegin { id, .. } => {
if !seen.insert(*id) {
out.push(Violation {
invariant: "F1 one field",
detail: format!("field {id:?} begins twice (paragraph {})", p.id),
});
}
stack.push(ElementId::Field(*id));
}
Inline::FieldSep { id, .. } => {
if stack.last() != Some(&ElementId::Field(*id)) || !sep.insert(*id) {
out.push(Violation {
invariant: "F2 fields nest",
detail: format!("separator of {id:?} out of place (paragraph {})", p.id),
});
}
}
Inline::FieldEnd { id, .. } => {
if stack.pop() != Some(ElementId::Field(*id)) {
out.push(Violation {
invariant: "F2 fields nest",
detail: format!("end of {id:?} out of place (paragraph {})", p.id),
});
}
}
Inline::ControlStart(c) => {
used.insert(*c);
stack.push(ElementId::Control(*c));
}
Inline::ControlEnd(c) => {
if stack.pop() != Some(ElementId::Control(*c)) {
out.push(Violation {
invariant: "C2 controls nest",
detail: format!("end of {c:?} out of place (paragraph {})", p.id),
});
}
}
Inline::Sym { control: Some(c), .. } => {
used.insert(*c);
}
Inline::Float(f, _) => {
if let Some(tb) = &f.text_box {
walk(&tb.blocks, &format!("text box {}", f.id), doc, seen, used, members, out);
}
}
_ => {}
}
}
if !stack.is_empty() {
out.push(Violation { invariant: "F1 one field", detail: format!("{stack:?} not closed in paragraph {}", p.id) });
}
for s in crate::docx::fields::spans(&p.inlines) {
if let Inline::FieldBegin { kind, .. } = &p.inlines[s.begin]
&& *kind != crate::docx::fields::kind_at(&p.inlines, s)
{
out.push(Violation { invariant: "F3 kind from code", detail: format!("field in paragraph {} has {kind:?}", p.id) });
}
}
}
Block::Table(t) => {
let mut rows: HashMap<ControlId, Vec<usize>> = HashMap::new();
for (ri, r) in t.rows.iter().enumerate() {
lists(&r.controls, format!("table {} row {ri}", t.id), out);
for c in &r.controls {
used.insert(*c);
rows.entry(*c).or_default().push(ri);
}
for (ci, c) in r.cells.iter().enumerate() {
lists(&c.controls, format!("table {} row {ri} cell {ci}", t.id), out);
used.extend(c.controls.iter().copied());
walk(&c.blocks, &format!("table {} row {ri} cell {ci}", t.id), doc, seen, used, members, out);
}
}
for (c, v) in rows {
if v.windows(2).any(|w| w[1] != w[0] + 1) {
out.push(Violation { invariant: "C2 contiguous", detail: format!("control {c:?} wraps rows {v:?} of table {}", t.id) });
}
}
}
}
}
}
for (k, s) in doc.sections.iter().enumerate() {
walk(&s.blocks, &format!("section {k}"), doc, &mut seen_fields, &mut used, &mut members, out);
}
let mut stories: Vec<(&String, &Vec<Block>)> = doc.stories.iter().collect();
stories.sort_by_key(|(k, _)| *k);
for (name, b) in stories {
walk(b, name, doc, &mut seen_fields, &mut used, &mut members, out);
}
for (kind, notes) in [("footnote", &doc.footnotes), ("endnote", &doc.endnotes)] {
for (id, b) in notes {
walk(b, &format!("{kind} {id}"), doc, &mut seen_fields, &mut used, &mut members, out);
}
}
for (c, (container, v)) in &members {
if v.windows(2).any(|w| w[1] != w[0] + 1) {
out.push(Violation { invariant: "C2 contiguous", detail: format!("control {c:?} wraps blocks {v:?} of {container}") });
}
}
for c in &used {
if !doc.controls.contains_key(c) {
out.push(Violation { invariant: "C1 controls resolve", detail: format!("control {c:?} is used but not in the document's controls") });
}
}
for c in doc.controls.keys() {
if !used.contains(c) {
out.push(Violation { invariant: "C1 controls resolve", detail: format!("control {c:?} has no content") });
}
if used.contains(c) != idx.element(ElementId::Control(*c)).is_some() {
out.push(Violation {
invariant: "C1 controls resolve",
detail: format!("control {c:?} indexed {:?}", idx.element(ElementId::Control(*c))),
});
}
}
let mut items = HashSet::new();
for p in &doc.data_parts {
if !items.insert(&p.item_id) {
out.push(Violation { invariant: "D1 unique item ids", detail: format!("item {} twice", p.item_id) });
}
if roxmltree::Document::parse(&p.xml).is_err() {
out.push(Violation { invariant: "D2 well-formed", detail: format!("item {} isn't well-formed", p.item_id) });
}
}
}
fn blocks(blocks: &[Block], out: &mut Vec<Violation>, _body: bool) {
for b in blocks {
let Block::Table(t) = b else { continue };
let grid = Grid::of(t);
for (r, row) in t.rows.iter().enumerate() {
let width = row.grid_before + row.cells.iter().map(|c| c.span.max(1)).sum::<usize>() + row.grid_after;
if !t.grid.is_empty() && width > t.grid.len() {
out.push(Violation {
invariant: "2 grid spans",
detail: format!("table {} row {r} spans {width} of {} columns", t.id, t.grid.len()),
});
}
for (c, cell) in row.cells.iter().enumerate() {
if !matches!(cell.blocks.last(), Some(Block::Para(_))) {
out.push(Violation { invariant: "1 cell ends with a paragraph", detail: format!("table {} row {r} cell {c}", t.id) });
}
if cell.v_merge == VMerge::Continue {
let (sr, _) = grid.merge_start(r, c);
if sr == r {
out.push(Violation { invariant: "3 merge has a start", detail: format!("table {} row {r} cell {c}", t.id) });
}
}
self::blocks(&cell.blocks, out, false);
}
}
}
}
pub fn invalid(doc: &Document, idx: &DocIndex, p: &Position) -> Option<String> {
match p.place {
Place::Text { para, offset, .. } => {
let Some(par) = idx.para(doc, para) else { return Some(format!("paragraph {para:?} isn't in the document")) };
if offset > para_len(par) {
return Some(format!("offset {offset:?} past the end of paragraph {para:?}"));
}
let slot = idx.para_slot(para)?;
if idx.story(slot.container) != p.story {
return Some(format!("paragraph {para:?} isn't in story {:?}", p.story));
}
if !super::text::UnitText::of(par).is_boundary(offset) {
return Some(format!("offset {offset:?} of {para:?} splits a grapheme"));
}
None
}
Place::RowEnd { table, row } => match idx.table(doc, table) {
Some(t) if row < t.rows.len() => None,
_ => Some(format!("row {row} of table {table:?} isn't in the document")),
},
}
}
pub fn duplicate_ids(doc: &Document) -> Vec<u64> {
let mut seen = std::collections::HashSet::new();
let mut dup = Vec::new();
fn walk(blocks: &[Block], seen: &mut std::collections::HashSet<u64>, dup: &mut Vec<u64>) {
for b in blocks {
let id = match b {
Block::Para(p) => ParaId(p.id).0,
Block::Table(t) => TableId(t.id).0,
};
if !seen.insert(id) {
dup.push(id);
}
if let Block::Para(p) = b {
for i in &p.inlines {
if let crate::docx::Inline::Float(f, _) = i {
if !seen.insert(f.id) {
dup.push(f.id);
}
if let Some(tb) = &f.text_box {
walk(&tb.blocks, seen, dup);
}
}
}
}
if let Block::Table(t) = b {
t.rows.iter().flat_map(|r| r.cells.iter()).for_each(|c| walk(&c.blocks, seen, dup));
}
}
}
for s in &doc.sections {
walk(&s.blocks, &mut seen, &mut dup);
}
for b in doc.stories.values().chain(doc.footnotes.values()).chain(doc.endnotes.values()) {
walk(b, &mut seen, &mut dup);
}
dup
}