use std::sync::atomic::{AtomicU64, Ordering};
use crate::namespaces::{PT, W};
use crate::util::group_adjacent;
use crate::xmllinq::{Dom, NodeId, XNamespace};
use super::lcs::TaggedAtom;
use super::{CorrelationStatus, WmlComparerSettings};
static REV_ID: AtomicU64 = AtomicU64::new(1);
fn delete_text_in_opaque(dom: &mut Dom, node: NodeId, status: CorrelationStatus) {
if matches!(status, CorrelationStatus::Deleted) {
let del_text = W::del_text();
for t in dom.descendants(node, Some(&W::t())) {
dom.set_name(t, del_text.clone());
}
}
}
fn next_rev_id() -> String {
REV_ID.fetch_add(1, Ordering::Relaxed).to_string()
}
pub fn produce_document(
dom: &mut Dom,
tagged: &[TaggedAtom],
settings: &WmlComparerSettings,
) -> NodeId {
let doc = dom.new_document();
let document = dom.new_element(W::document());
dom.set_attribute_value(document, &XNamespace::xmlns().name("w"), Some(W::URI));
dom.set_attribute_value(document, &XNamespace::xmlns().name("pt14"), Some(PT::URI));
let body = dom.new_element(W::body());
let mut para: Vec<TaggedAtom> = Vec::new();
for t in tagged {
let is_ppr = dom.name_is(t.atom.content_element, &W::p_pr());
if is_ppr {
let p = build_paragraph(dom, ¶, t, settings);
dom.add(body, p);
para.clear();
} else {
para.push(t.clone());
}
}
if !para.is_empty() {
let synthetic = TaggedAtom {
atom: para[0].atom.clone(),
status: CorrelationStatus::Equal,
};
let p = build_paragraph(dom, ¶, &synthetic, settings);
dom.add(body, p);
}
dom.add(document, body);
dom.add(doc, document);
doc
}
fn build_paragraph(
dom: &mut Dom,
run_atoms: &[TaggedAtom],
ppr_atom: &TaggedAtom,
settings: &WmlComparerSettings,
) -> NodeId {
let p = dom.new_element(W::p());
if dom.name_is(ppr_atom.atom.content_element, &W::p_pr())
&& dom.has_elements(ppr_atom.atom.content_element)
{
let ppr = dom.clone_subtree(ppr_atom.atom.content_element);
dom.add(p, ppr);
}
let groups = group_adjacent(run_atoms.iter().cloned(), |t| t.status);
for (status, group) in groups {
let text: String = group
.iter()
.filter(|t| {
let n = dom.name(t.atom.content_element);
n == Some(W::t()) || n == Some(W::del_text())
})
.map(|t| dom.value_str(t.atom.content_element).into_owned())
.collect();
if text.is_empty() {
continue;
}
match status {
CorrelationStatus::Inserted => {
let ins = wrap_run(dom, &text, false, settings, CorrelationStatus::Inserted);
dom.add(p, ins);
}
CorrelationStatus::Deleted => {
let del = wrap_run(dom, &text, true, settings, CorrelationStatus::Deleted);
dom.add(p, del);
}
_ => {
let r = build_text_run(dom, &text, false);
dom.add(p, r);
}
}
}
p
}
fn build_text_run(dom: &mut Dom, text: &str, deleted: bool) -> NodeId {
let r = dom.new_element(W::r());
let t = dom.new_element(if deleted { W::del_text() } else { W::t() });
if text.starts_with(' ') || text.ends_with(' ') {
dom.set_attribute_value(t, &XNamespace::xml().name("space"), Some("preserve"));
}
dom.add_text(t, text);
dom.add(r, t);
r
}
fn wrap_run(
dom: &mut Dom,
text: &str,
deleted: bool,
settings: &WmlComparerSettings,
status: CorrelationStatus,
) -> NodeId {
let wrapper_name = if matches!(status, CorrelationStatus::Deleted) {
W::del()
} else {
W::ins()
};
let wrapper = dom.new_element(wrapper_name);
dom.set_attribute_value(wrapper, &W::id(), Some(&next_rev_id()));
dom.set_attribute_value(wrapper, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(wrapper, &W::date(), Some(&settings.date_time_for_revisions));
let r = build_text_run(dom, text, deleted);
dom.add(wrapper, r);
wrapper
}
use super::atoms::{ComparisonUnit, ComparisonUnitAtom, CorrelatedSequence};
use crate::unid::generate_unid;
fn flatten_atoms(units: &[ComparisonUnit]) -> Vec<ComparisonUnitAtom> {
units
.iter()
.flat_map(|u| u.descendant_atoms().into_iter().cloned())
.collect()
}
fn atom_has_predelete_orig(dom: &Dom, atom: &ComparisonUnitAtom) -> bool {
let pre = PT::name("PreDelete");
if dom.attribute(atom.content_element, &pre) == Some(crate::comparer::PREDELETE_STAMP_ORIG) {
return true;
}
atom.ancestor_elements
.iter()
.any(|&a| dom.attribute(a, &pre) == Some(crate::comparer::PREDELETE_STAMP_ORIG))
}
pub fn flatten_to_comparison_unit_atom_list(
dom: &Dom,
seqs: &[CorrelatedSequence],
) -> Vec<ComparisonUnitAtom> {
let mut out = Vec::new();
for cs in seqs {
match cs.correlation_status {
CorrelationStatus::Equal => {
let before = flatten_atoms(cs.com_units_1.as_deref().unwrap_or(&[]));
let after = flatten_atoms(cs.com_units_2.as_deref().unwrap_or(&[]));
if before.iter().any(|b| atom_has_predelete_orig(dom, b)) {
for b in &before {
let mut del = b.clone();
del.correlation_status = CorrelationStatus::Deleted;
out.push(del);
}
for a in &after {
let mut ins = a.clone();
ins.correlation_status = CorrelationStatus::Inserted;
out.push(ins);
}
continue;
}
for (b, a) in before.iter().zip(after.iter()) {
let mut atom = a.clone();
atom.correlation_status = CorrelationStatus::Equal;
atom.content_element_before = Some(b.content_element);
atom.comparison_unit_atom_before = Some(std::sync::Arc::new(b.clone()));
out.push(atom);
}
}
CorrelationStatus::Deleted => {
for a in flatten_atoms(cs.com_units_1.as_deref().unwrap_or(&[])) {
let mut x = a;
x.correlation_status = CorrelationStatus::Deleted;
out.push(x);
}
}
CorrelationStatus::Inserted => {
for a in flatten_atoms(cs.com_units_2.as_deref().unwrap_or(&[])) {
let mut x = a;
x.correlation_status = CorrelationStatus::Inserted;
out.push(x);
}
}
other => panic!("Internal error: unexpected status in flatten: {other:?}"),
}
}
out
}
fn is_ppr_atom(dom: &Dom, atom: &ComparisonUnitAtom) -> bool {
dom.name_is(atom.content_element, &W::p_pr())
}
fn atom_in_textbox(dom: &Dom, atom: &ComparisonUnitAtom) -> bool {
let txbx = W::txbx_content();
atom.ancestor_elements
.iter()
.any(|&a| dom.name(a).as_ref() == Some(&txbx))
}
type UnidChainMemo = Option<(std::sync::Arc<[NodeId]>, std::sync::Arc<[String]>)>;
pub fn assemble_ancestor_unids(dom: &mut Dom, atoms: &mut [ComparisonUnitAtom]) {
let unid = PT::unid();
let footnote = W::footnote();
let endnote = W::endnote();
for atom in atoms.iter() {
let mut do_set = false;
if is_ppr_atom(dom, atom) {
if atom_in_textbox(dom, atom) {
do_set = true;
}
if atom.correlation_status == CorrelationStatus::Equal {
do_set = true;
}
}
if do_set && let Some(before) = &atom.comparison_unit_atom_before {
let after_anc = &atom.ancestor_elements;
let before_anc = &before.ancestor_elements;
if after_anc.len() == before_anc.len() {
let pairs: Vec<(NodeId, Option<String>)> = after_anc
.iter()
.zip(before_anc.iter())
.filter_map(|(&aft, &bef)| {
match (dom.attribute(aft, &unid), dom.attribute(bef, &unid)) {
(Some(_), Some(bv)) => Some((aft, Some(bv.to_string()))),
_ => None,
}
})
.collect();
for (aft, bv) in pairs {
dom.set_attribute_value(aft, &unid, bv.as_deref());
}
}
}
}
let deepest_unid: Option<String> = atoms.last().and_then(|last| {
last.ancestor_elements.first().and_then(|&outer| {
let nm = dom.name(outer);
if nm.as_ref() == Some(&footnote) || nm.as_ref() == Some(&endnote) {
dom.attribute(outer, &unid).map(|s| s.to_string())
} else {
None
}
})
});
let unid_or_mint = |dom: &mut Dom, ae: NodeId| -> String {
match dom.attribute(ae, &unid) {
Some(u) => u.to_string(),
None => {
let g = generate_unid();
dom.set_attribute_value(ae, &unid, Some(&g));
g
}
}
};
let mut current: Option<std::sync::Arc<[String]>> = None;
let mut current_elems: Option<std::sync::Arc<[NodeId]>> = None;
let mut memo: UnidChainMemo = None;
for atom in atoms.iter_mut().rev() {
if is_ppr_atom(dom, atom) && !atom_in_textbox(dom, atom) {
let mut cur: Vec<String> = atom
.ancestor_elements
.iter()
.map(|&ae| unid_or_mint(dom, ae))
.collect();
if let Some(d) = &deepest_unid
&& let Some(first) = cur.first_mut()
{
*first = d.clone();
}
let cur: std::sync::Arc<[String]> = cur.into();
atom.ancestor_unids = Some(std::sync::Arc::clone(&cur));
current = Some(cur);
current_elems = Some(std::sync::Arc::clone(&atom.ancestor_elements));
memo = None;
} else {
let prefix = current.clone().unwrap_or_default();
if let Some((elems, unids)) = &memo
&& std::sync::Arc::ptr_eq(elems, &atom.ancestor_elements)
{
atom.ancestor_unids = Some(std::sync::Arc::clone(unids));
continue;
}
let prev_elems = current_elems.clone().unwrap_or_default();
let mut share = 0usize;
while share < prefix.len()
&& share < atom.ancestor_elements.len()
&& share < prev_elems.len()
&& dom.name(atom.ancestor_elements[share]) == dom.name(prev_elems[share])
{
share += 1;
}
let mut full: Vec<String> = prefix[..share].to_vec();
for &ae in atom.ancestor_elements.iter().skip(share) {
full.push(unid_or_mint(dom, ae));
}
if let Some(d) = &deepest_unid
&& let Some(first) = full.first_mut()
{
*first = d.clone();
}
let full: std::sync::Arc<[String]> = full.into();
memo = Some((
std::sync::Arc::clone(&atom.ancestor_elements),
std::sync::Arc::clone(&full),
));
atom.ancestor_unids = Some(full);
}
}
let mut current: Option<std::sync::Arc<[String]>> = None;
let mut skip_until_ppr = false;
let mut memo: UnidChainMemo = None;
for atom in atoms.iter_mut().rev() {
if let Some(cur) = ¤t
&& atom.ancestor_elements.len() < cur.len()
{
skip_until_ppr = true;
current = None;
memo = None;
continue;
}
if is_ppr_atom(dom, atom) {
if !atom_in_textbox(dom, atom) {
skip_until_ppr = true;
current = None;
memo = None;
continue;
}
let cur: Vec<String> = atom
.ancestor_elements
.iter()
.map(|&ae| {
dom.attribute(ae, &unid)
.map(|s| s.to_string())
.expect("text-box pPr ancestor must have a Unid (Phase B)")
})
.collect();
let cur: std::sync::Arc<[String]> = cur.into();
atom.ancestor_unids = Some(std::sync::Arc::clone(&cur));
current = Some(cur);
skip_until_ppr = false;
memo = None;
continue;
}
if skip_until_ppr {
continue;
}
if let Some(cur) = ¤t {
if let Some((elems, unids)) = &memo
&& std::sync::Arc::ptr_eq(elems, &atom.ancestor_elements)
{
atom.ancestor_unids = Some(std::sync::Arc::clone(unids));
continue;
}
let extra: Vec<NodeId> = atom
.ancestor_elements
.iter()
.skip(cur.len())
.copied()
.collect();
let mut full: Vec<String> = cur.as_ref().to_vec();
for ae in extra {
full.push(unid_or_mint(dom, ae));
}
let full: std::sync::Arc<[String]> = full.into();
memo = Some((
std::sync::Arc::clone(&atom.ancestor_elements),
std::sync::Arc::clone(&full),
));
atom.ancestor_unids = Some(full);
}
}
}
fn xml_space_attr(text: &str) -> Option<&'static str> {
match (text.chars().next(), text.chars().last()) {
(Some(f), _) if f.is_whitespace() => Some("preserve"),
(_, Some(l)) if l.is_whitespace() => Some("preserve"),
_ => None,
}
}
fn status_str(s: CorrelationStatus) -> &'static str {
match s {
CorrelationStatus::Deleted => "Deleted",
CorrelationStatus::Inserted => "Inserted",
CorrelationStatus::MovedSource => "MovedSource",
CorrelationStatus::MovedDestination => "MovedDestination",
CorrelationStatus::FormatChanged => "FormatChanged",
CorrelationStatus::Equal => "Equal",
_ => "Nil",
}
}
fn group_by_key_stable<'a, K: Eq + std::hash::Hash + Clone>(
items: &[&'a ComparisonUnitAtom],
key: impl Fn(&ComparisonUnitAtom) -> K,
) -> Vec<(K, Vec<&'a ComparisonUnitAtom>)> {
let mut order: Vec<K> = Vec::new();
let mut map: std::collections::HashMap<K, Vec<&'a ComparisonUnitAtom>> =
std::collections::HashMap::new();
for it in items {
let k = key(it);
if !map.contains_key(&k) {
order.push(k.clone());
}
map.entry(k).or_default().push(*it);
}
order
.into_iter()
.map(|k| {
let v = map.remove(&k).unwrap();
(k, v)
})
.collect()
}
fn tag_status(dom: &mut Dom, node: NodeId, status: CorrelationStatus, atom: &ComparisonUnitAtom) {
match status {
CorrelationStatus::Deleted => {
dom.set_attribute_value(node, &PT::status(), Some("Deleted"));
}
CorrelationStatus::Inserted => {
dom.set_attribute_value(node, &PT::status(), Some("Inserted"));
}
CorrelationStatus::MovedSource | CorrelationStatus::MovedDestination => {
dom.set_attribute_value(node, &PT::status(), Some(status_str(status)));
if let Some(id) = atom.move_group_id {
dom.set_attribute_value(node, &PT::name("MoveGroupId"), Some(&id.to_string()));
dom.set_attribute_value(
node,
&PT::name("MoveName"),
Some(atom.move_name.as_deref().unwrap_or("")),
);
}
}
CorrelationStatus::FormatChanged => {
dom.set_attribute_value(node, &PT::status(), Some("FormatChanged"));
if let Some(fc) = &atom.format_change {
if let Some(old) = fc.old_run_properties {
let s = dom.serialize_element(old);
dom.set_attribute_value(node, &PT::name("OldRPr"), Some(&s));
}
if let Some(old) = fc.old_para_properties {
let s = dom.serialize_element(old);
dom.set_attribute_value(node, &PT::name("OldPPr"), Some(&s));
}
}
}
_ => {}
}
}
fn is_txbx_from_level(dom: &Dom, atom: &ComparisonUnitAtom, level: usize) -> bool {
let txbx = W::txbx_content();
atom.ancestor_elements
.iter()
.skip(level)
.any(|&a| dom.name(a).as_ref() == Some(&txbx))
}
pub fn convert_outer_math_wraps_to_internal(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
) {
let math_names = [
crate::namespaces::M::name("oMath"),
crate::namespaces::M::name("oMathPara"),
];
let mut max_id: u32 = 0;
for e in dom.descendants(root, None) {
if let Some(v) = dom.attribute(e, &W::id())
&& let Ok(n) = v.parse::<u32>()
{
max_id = max_id.max(n);
}
}
let mut id_gen = max_id + 1;
for rev_name in [W::ins(), W::del()] {
let wrappers: Vec<NodeId> = dom
.descendants(root, Some(&rev_name))
.into_iter()
.filter(|&w| {
let kids = dom.elements(w, None);
!kids.is_empty()
&& kids
.iter()
.all(|&k| dom.name(k).is_some_and(|n| math_names.contains(&n)))
})
.collect();
for w in wrappers {
let maths: Vec<NodeId> = dom.elements(w, None);
for m in &maths {
mark_math_revisions_internally(dom, *m, &rev_name, settings, &mut id_gen);
}
for m in maths {
dom.remove(m);
dom.add_before_self(w, m);
}
dom.remove(w);
}
}
}
fn mark_math_revisions_internally(
dom: &mut Dom,
math_root: NodeId,
rev_name: &crate::xmllinq::XName,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let m_r = crate::namespaces::M::name("r");
let m_ctrl_pr = crate::namespaces::M::name("ctrlPr");
let w_rpr = W::r_pr();
let cambria = |dom: &mut Dom| -> NodeId {
let rpr = dom.new_element(W::r_pr());
let fonts = dom.new_element(W::name("rFonts"));
dom.set_attribute_value(fonts, &W::name("ascii"), Some("Cambria Math"));
dom.set_attribute_value(fonts, &W::name("hAnsi"), Some("Cambria Math"));
dom.add(rpr, fonts);
rpr
};
let mut new_mark = |dom: &mut Dom| -> NodeId {
let w = dom.new_element(rev_name.clone());
dom.set_attribute_value(w, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(w, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(w, &W::date(), Some(&settings.date_time_for_revisions));
w
};
let targets: Vec<(NodeId, bool)> = dom
.descendants(math_root, Some(&m_r))
.into_iter()
.map(|n| (n, false))
.chain(
dom.descendants(math_root, Some(&m_ctrl_pr))
.into_iter()
.map(|n| (n, true)),
)
.collect();
for (node, is_ctrl_pr) in targets {
if dom
.elements(node, None)
.iter()
.any(|&c| dom.name(c).is_some_and(|n| n == W::ins() || n == W::del()))
{
continue;
}
let children: Vec<NodeId> = dom.elements(node, None);
let mark = new_mark(dom);
let mut saw_wrpr = false;
for c in children {
let is_m_rpr = dom.name(c) == Some(crate::namespaces::M::name("rPr"));
saw_wrpr |= dom.name(c).as_ref() == Some(&w_rpr);
dom.remove(c);
dom.add(mark, c);
let _ = is_m_rpr;
}
if !saw_wrpr {
let rpr = cambria(dom);
let anchor = dom
.elements(mark, None)
.into_iter()
.find(|&c| dom.name(c) == Some(crate::namespaces::M::name("rPr")));
match anchor {
Some(a) => dom.add_after_self(a, rpr),
None => match dom.elements(mark, None).first().copied() {
Some(first) => dom.add_before_self(first, rpr),
None => dom.add(mark, rpr),
},
}
}
let _ = is_ctrl_pr;
dom.add(node, mark);
}
}
pub fn coalesce_recurse(
dom: &mut Dom,
atoms: &[&ComparisonUnitAtom],
level: usize,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) -> Vec<NodeId> {
let dref: &Dom = dom;
let grouped = group_by_key_stable(atoms, |ca| {
if level >= ca.ancestor_elements.len() {
return (String::new(), String::new());
}
let u = ca
.ancestor_unids
.as_ref()
.and_then(|u| u.get(level).cloned())
.unwrap_or_default();
if u.is_empty() {
return (String::new(), String::new());
}
let nm = dref
.name(ca.ancestor_elements[level])
.map(|n| n.local_name().to_string())
.unwrap_or_default();
(u, nm)
});
let grouped: Vec<_> = grouped
.into_iter()
.filter(|(k, _)| !k.0.is_empty())
.collect();
if grouped.is_empty() {
return Vec::new();
}
let mut out = Vec::new();
for (gkey, g) in grouped {
let ancestor = g[0].ancestor_elements[level];
let aname = dom.name(ancestor).unwrap();
let groupedchildren = group_adjacent(g.iter().cloned(), |gc| {
let key = if level < gc.ancestor_elements.len() - 1 {
gc.ancestor_unids
.as_ref()
.and_then(|u| u.get(level + 1).cloned())
.unwrap_or_default()
} else {
String::new()
};
let st = if is_txbx_from_level(dom, gc, level) {
"Equal"
} else {
status_str(gc.correlation_status)
};
(key, st)
});
if aname == W::p() {
let p = dom.new_element(W::p());
for (an, av) in dom.attributes(ancestor) {
if an.namespace_name() != PT::URI {
dom.set_attribute_value(p, &an, Some(&av));
}
}
dom.set_attribute_value(p, &PT::unid(), Some(&gkey.0));
for (key, gc) in &groupedchildren {
if key.0.is_empty() {
for gcc in gc {
let dup = dom.clone_subtree(gcc.content_element);
tag_status(dom, dup, gcc.correlation_status, gcc);
dom.add(p, dup);
}
} else {
for child in coalesce_recurse(dom, gc, level + 1, settings, id_gen) {
dom.add(p, child);
}
}
}
out.push(p);
continue;
}
if aname == W::r() {
let r = dom.new_element(W::r());
for (an, av) in dom.attributes(ancestor) {
if an.namespace_name() != PT::URI
|| matches!(
an.local_name(),
"PreDelete"
| "PreDelAuthor"
| "PreDelDate"
| "PreIns"
| "PreInsAuthor"
| "PreInsDate"
)
{
dom.set_attribute_value(r, &an, Some(&av));
}
}
if let Some(rpr) = dom.element(ancestor, &W::r_pr()) {
let rpr_clone = dom.clone_subtree(rpr);
dom.add(r, rpr_clone);
}
for (key, gc) in &groupedchildren {
if key.0.is_empty() {
for gcc in gc {
let dup = dom.clone_subtree(gcc.content_element);
tag_status(dom, dup, gcc.correlation_status, gcc);
dom.add(r, dup);
}
} else {
for child in coalesce_recurse(dom, gc, level + 1, settings, id_gen) {
dom.add(r, child);
}
}
}
out.push(r);
continue;
}
if aname == W::t() {
for (_key, gc) in &groupedchildren {
let text: String = gc
.iter()
.map(|a| dom.value_str(a.content_element).into_owned())
.collect();
let first = &gc[0];
let elem_name = match first.correlation_status {
CorrelationStatus::Deleted => W::del_text(),
_ => W::t(),
};
let te = dom.new_element(elem_name);
tag_status(dom, te, first.correlation_status, first);
if let Some(sp) = xml_space_attr(&text) {
dom.set_attribute_value(te, &XNamespace::xml().name("space"), Some(sp));
}
dom.add_text(te, &text);
out.push(te);
}
continue;
}
if aname == W::drawing() {
for (_key, gc) in &groupedchildren {
for gcc in gc {
let d = dom.clone_subtree(gcc.content_element);
tag_status(dom, d, gcc.correlation_status, gcc);
delete_text_in_opaque(dom, d, gcc.correlation_status);
out.push(d);
}
}
continue;
}
if aname == W::pict() {
for (_key, gc) in &groupedchildren {
for gcc in gc {
let d = dom.clone_subtree(gcc.content_element);
tag_status(dom, d, gcc.correlation_status, gcc);
delete_text_in_opaque(dom, d, gcc.correlation_status);
out.push(d);
}
}
continue;
}
if aname == crate::namespaces::MC::name("AlternateContent") {
for (_key, gc) in &groupedchildren {
for gcc in gc {
let d = dom.clone_subtree(gcc.content_element);
tag_status(dom, d, gcc.correlation_status, gcc);
delete_text_in_opaque(dom, d, gcc.correlation_status);
out.push(d);
}
}
continue;
}
if aname == crate::namespaces::M::name("oMath")
|| aname == crate::namespaces::M::name("oMathPara")
{
for (_key, gc) in &groupedchildren {
for gcc in gc {
let rev = match gcc.correlation_status {
CorrelationStatus::Deleted => Some(W::del()),
CorrelationStatus::MovedSource => Some(W::move_from()),
CorrelationStatus::Inserted => Some(W::ins()),
CorrelationStatus::MovedDestination => Some(W::move_to()),
_ => None,
};
let content = dom.clone_subtree(gcc.content_element);
match rev {
Some(rname) => {
let w = dom.new_element(rname);
dom.set_attribute_value(
w,
&W::author(),
Some(&settings.author_for_revisions),
);
dom.set_attribute_value(w, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(
w,
&W::date(),
Some(&settings.date_time_for_revisions),
);
dom.add(w, content);
out.push(w);
}
None => out.push(content),
}
}
}
continue;
}
if super::tables::ALLOWABLE_RUN_CHILDREN.contains(&aname) {
for (_key, gc) in &groupedchildren {
let first = &gc[0];
match first.correlation_status {
CorrelationStatus::Deleted
| CorrelationStatus::Inserted
| CorrelationStatus::MovedSource
| CorrelationStatus::MovedDestination => {
for gcc in gc {
let dup = dom.new_element(aname.clone());
for (an, av) in dom.attributes(ancestor) {
if an.namespace_name() != PT::URI {
dom.set_attribute_value(dup, &an, Some(&av));
}
}
tag_status(dom, dup, gcc.correlation_status, gcc);
out.push(dup);
}
}
_ => {
for gcc in gc {
out.push(dom.clone_subtree(gcc.content_element));
}
}
}
}
continue;
}
let props: &[&str] = if aname == W::tbl() {
&["tblPr", "tblGrid"]
} else if aname == W::tr() {
&["trPr"]
} else if aname == W::tc() {
&["tcPr"]
} else if aname == W::sdt() {
&["sdtPr", "sdtEndPr"]
} else if aname == W::name("ruby") {
&["rubyPr"]
} else {
&[]
};
let pict_props = aname == W::pict();
let recon = reconstruct_element(
dom, &g, ancestor, props, pict_props, level, settings, id_gen,
);
out.push(recon);
}
out
}
#[allow(clippy::too_many_arguments)]
fn reconstruct_element(
dom: &mut Dom,
g: &[&ComparisonUnitAtom],
ancestor: NodeId,
props: &[&str],
pict_props: bool,
level: usize,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) -> NodeId {
let aname = dom.name(ancestor).unwrap();
let new_children = coalesce_recurse(dom, g, level + 1, settings, id_gen);
let ne = dom.new_element(aname.clone());
for (an, av) in dom.attributes(ancestor) {
dom.set_attribute_value(ne, &an, Some(&av));
}
if pict_props {
for p in dom.elements(ancestor, Some(&crate::namespaces::VML::name("shapetype"))) {
let c = dom.clone_subtree(p);
dom.add(ne, c);
}
}
for pname in props {
for p in dom.elements(ancestor, Some(&W::name(pname))) {
let c = dom.clone_subtree(p);
dom.add(ne, c);
}
}
if settings.merge_replaced_paragraphs && aname == W::tbl() {
let is_tbl = |anc: NodeId| dom.name(anc).is_some_and(|nm| *nm.local_name() == *"tbl");
let old_tbl = g.iter().find_map(|a| {
let direct = a
.ancestor_elements
.get(level)
.copied()
.filter(|&anc| anc != ancestor && is_tbl(anc));
direct.or_else(|| {
let before = a.comparison_unit_atom_before.as_ref()?;
before
.ancestor_elements
.get(level)
.copied()
.filter(|&anc| anc != ancestor && is_tbl(anc))
})
});
let ancestor_is_old = g.iter().any(|a| {
(a.correlation_status == CorrelationStatus::Deleted
&& a.ancestor_elements.get(level) == Some(&ancestor))
|| a.comparison_unit_atom_before
.as_ref()
.is_some_and(|b| b.ancestor_elements.get(level) == Some(&ancestor))
});
let old_tbl = match (old_tbl, ancestor_is_old) {
(Some(new_tbl), true) => {
for pname in ["tblPr", "tblGrid"] {
for hoisted in dom.elements(ne, Some(&W::name(pname))) {
dom.remove(hoisted);
}
for p in dom.elements(new_tbl, Some(&W::name(pname))) {
let c = dom.clone_subtree(p);
dom.add(ne, c);
}
}
Some(ancestor)
}
(found, _) => found,
};
if let Some(old_tbl) = old_tbl {
let strip_change = |dom: &mut Dom, el: NodeId, change: &str| {
for c in dom.elements(el, Some(&W::name(change))) {
dom.remove(c);
}
};
if let (Some(new_pr), Some(old_pr)) = (
dom.element(ne, &W::tbl_pr()),
dom.element(old_tbl, &W::tbl_pr()),
) {
let old_clone = dom.clone_subtree(old_pr);
strip_change(dom, old_clone, "tblPrChange");
if dom.serialize_element(old_clone) != dom.serialize_element(new_pr) {
let change = dom.new_element(W::name("tblPrChange"));
dom.set_attribute_value(change, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(
change,
&W::author(),
Some(&settings.author_for_revisions),
);
dom.set_attribute_value(
change,
&W::date(),
Some(&settings.date_time_for_revisions),
);
dom.add(change, old_clone);
dom.add(new_pr, change);
} else {
dom.remove(old_clone);
}
}
if let (Some(new_grid), Some(old_grid)) = (
dom.element(ne, &W::name("tblGrid")),
dom.element(old_tbl, &W::name("tblGrid")),
) {
let old_clone = dom.clone_subtree(old_grid);
strip_change(dom, old_clone, "tblGridChange");
if dom.serialize_element(old_clone) != dom.serialize_element(new_grid) {
let change = dom.new_element(W::name("tblGridChange"));
dom.set_attribute_value(change, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.add(change, old_clone);
dom.add(new_grid, change);
} else {
dom.remove(old_clone);
}
}
}
}
for c in new_children {
dom.add(ne, c);
}
if settings.merge_replaced_paragraphs && aname == W::tbl() {
rebuild_degenerate_grid(dom, ne, ancestor);
}
ne
}
const TBLPR_CHILD_ORDER: &[&str] = &[
"tblStyle",
"tblpPr",
"tblOverlap",
"bidiVisual",
"tblStyleRowBandSize",
"tblStyleColBandSize",
"tblW",
"jc",
"tblCellSpacing",
"tblInd",
"tblBorders",
"shd",
"tblLayout",
"tblCellMar",
"tblLook",
"tblCaption",
"tblDescription",
];
fn add_tblpr_child_in_order(dom: &mut Dom, tbl_pr: NodeId, child: NodeId, local: &str) {
let new_rank = TBLPR_CHILD_ORDER
.iter()
.position(|&n| n == local)
.unwrap_or(usize::MAX);
let anchor = dom.elements(tbl_pr, None).into_iter().rev().find(|&e| {
dom.name(e).is_some_and(|nm| {
TBLPR_CHILD_ORDER
.iter()
.position(|&n| n == nm.local_name())
.is_some_and(|rank| rank < new_rank)
})
});
match anchor {
Some(a) => dom.add_after_self(a, child),
None => dom.add_first(tbl_pr, child),
}
}
fn rebuild_degenerate_grid(dom: &mut Dom, tbl: NodeId, src_tbl: NodeId) {
let Some(grid) = dom.element(tbl, &W::name("tblGrid")) else {
return;
};
let grid_cols = dom.elements(grid, Some(&W::name("gridCol")));
let real_cols = dom
.elements(tbl, Some(&W::tr()))
.into_iter()
.map(|tr| {
dom.elements(tr, Some(&W::tc()))
.into_iter()
.map(|tc| {
dom.element(tc, &W::tc_pr())
.and_then(|pr| dom.element(pr, &W::grid_span()))
.and_then(|gs| dom.attribute(gs, &W::val()))
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(1)
})
.sum::<usize>()
})
.max()
.unwrap_or(0);
if real_cols < 2 || grid_cols.len() >= real_cols {
return;
}
let content_width = dom
.ancestors(src_tbl, None)
.last()
.map(|&root| dom.descendants(root, Some(&W::sect_pr())))
.and_then(|s| s.first().copied())
.and_then(|sect| {
let w: i64 = dom
.element(sect, &W::name("pgSz"))
.and_then(|e| dom.attribute(e, &W::name("w")))
.and_then(|v| v.parse().ok())?;
let mar = dom.element(sect, &W::name("pgMar"))?;
let l: i64 = dom
.attribute(mar, &W::name("left"))
.and_then(|v| v.parse().ok())?;
let r: i64 = dom
.attribute(mar, &W::name("right"))
.and_then(|v| v.parse().ok())?;
Some(w - l - r)
})
.filter(|&w| w > 0)
.unwrap_or_else(|| {
grid_cols
.iter()
.filter_map(|&c| dom.attribute(c, &W::name("w")))
.filter_map(|v| v.parse::<i64>().ok())
.sum()
});
if content_width <= 0 {
return;
}
for c in &grid_cols {
dom.remove(*c);
}
let each = content_width / real_cols as i64;
let mut new_cols = Vec::with_capacity(real_cols);
for i in 0..real_cols {
let w = if i + 1 == real_cols {
content_width - each * (real_cols as i64 - 1)
} else {
each
};
let col = dom.new_element(W::name("gridCol"));
dom.set_attribute_value(col, &W::name("w"), Some(&w.to_string()));
new_cols.push(col);
}
for col in new_cols.into_iter().rev() {
dom.add_first(grid, col);
}
if let Some(tbl_pr) = dom.element(tbl, &W::tbl_pr()) {
let tblw = match dom.element(tbl_pr, &W::name("tblW")) {
Some(e) => e,
None => {
let e = dom.new_element(W::name("tblW"));
add_tblpr_child_in_order(dom, tbl_pr, e, "tblW");
e
}
};
dom.set_attribute_value(tblw, &W::name("w"), Some("0"));
dom.set_attribute_value(tblw, &W::name("type"), Some("auto"));
}
}
pub fn produce_new_wml_markup_from_correlated_sequence(
dom: &mut Dom,
atoms: &[ComparisonUnitAtom],
settings: &WmlComparerSettings,
id_gen: &mut u32,
) -> Vec<NodeId> {
let refs: Vec<&ComparisonUnitAtom> = atoms.iter().collect();
coalesce_recurse(dom, &refs, 0, settings, id_gen)
}
#[cfg(test)]
mod opaque_text_tests {
use super::*;
fn opaque_with_text(d: &mut Dom, txt: &str) -> NodeId {
let drawing = d.new_element(W::drawing());
let r = d.new_element(W::r());
let t = d.new_element(W::t());
d.add_text(t, txt);
d.add(r, t);
d.add(drawing, r);
drawing
}
fn text_kind(d: &Dom, node: NodeId) -> String {
let leaf = d
.descendants(node, None)
.into_iter()
.find(|&c| {
matches!(
d.name(c).as_ref().map(|n| n.local_name()),
Some("t") | Some("delText")
)
})
.expect("a text leaf");
d.name(leaf).unwrap().local_name().to_string()
}
#[test]
fn deleted_opaque_text_becomes_deltext() {
let mut d = Dom::new();
let n = opaque_with_text(&mut d, "x");
delete_text_in_opaque(&mut d, n, CorrelationStatus::Deleted);
assert_eq!(text_kind(&d, n), "delText");
}
#[test]
fn moved_source_opaque_text_stays_t_like_word() {
let mut d = Dom::new();
let n = opaque_with_text(&mut d, "x");
delete_text_in_opaque(&mut d, n, CorrelationStatus::MovedSource);
assert_eq!(
text_kind(&d, n),
"t",
"Word Compare keeps w:t inside moveFrom (not delText)"
);
}
#[test]
fn non_deleted_opaque_text_stays_t() {
for status in [
CorrelationStatus::Inserted,
CorrelationStatus::MovedDestination,
CorrelationStatus::Equal,
] {
let mut d = Dom::new();
let n = opaque_with_text(&mut d, "x");
delete_text_in_opaque(&mut d, n, status);
assert_eq!(
text_kind(&d, n),
"t",
"non-deleted opaque text stays w:t for {status:?}"
);
}
}
#[test]
fn instr_text_is_untouched() {
let mut d = Dom::new();
let drawing = d.new_element(W::drawing());
let r = d.new_element(W::r());
let instr = d.new_element(W::instr_text());
d.add_text(instr, "FIELD");
d.add(r, instr);
d.add(drawing, r);
delete_text_in_opaque(&mut d, drawing, CorrelationStatus::Deleted);
assert_eq!(
d.descendants(drawing, Some(&W::instr_text())).len(),
1,
"instrText untouched"
);
assert!(
d.descendants(drawing, Some(&W::del_text())).is_empty(),
"no delText fabricated from instrText"
);
}
}
#[cfg(test)]
mod tblpr_order_tests {
use super::*;
#[test]
fn tblw_inserted_after_tblppr_and_bidivisual() {
let mut dom = Dom::new();
let xml = concat!(
"<w:tblPr xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">",
"<w:tblStyle w:val=\"TableGrid\"/>",
"<w:tblpPr w:leftFromText=\"0\"/>",
"<w:bidiVisual/>",
"</w:tblPr>"
);
let doc = dom.parse_xdocument(xml);
let tblpr = dom.root(doc).expect("root");
let tblw = dom.new_element(W::name("tblW"));
add_tblpr_child_in_order(&mut dom, tblpr, tblw, "tblW");
let order: Vec<String> = dom
.elements(tblpr, None)
.into_iter()
.map(|e| dom.name(e).unwrap().local_name().to_string())
.collect();
let pos = |n: &str| order.iter().position(|x| x == n).unwrap();
assert!(
pos("tblW") > pos("tblpPr") && pos("tblW") > pos("bidiVisual"),
"tblW must follow tblpPr and bidiVisual (CT_TblPrBase), got: {order:?}"
);
}
}