use std::sync::Arc;
use crate::namespaces::{MC, PT, W};
use crate::unid::assign_to_all_elements;
use crate::util::group_adjacent;
use crate::xmllinq::{Dom, NodeId, XName, XNamespace};
use super::atoms::{AtomHash, ComparisonUnitAtom};
use super::tables::{
ALLOWABLE_RUN_CHILDREN, ELEMENTS_TO_THROW_AWAY, INVALID_ELEMENTS, recursion_info,
};
use super::{CorrelationStatus, WmlComparerSettings};
pub fn verify_no_invalid_content(dom: &Dom, content_parent: NodeId) -> Result<(), String> {
for d in dom.descendants(content_parent, None) {
if let Some(name) = dom.name(d)
&& INVALID_ELEMENTS.contains(&name)
{
return Err(format!("Document contains {}", name.local_name()));
}
}
Ok(())
}
pub fn move_last_sectpr_into_last_paragraph(
dom: &mut Dom,
content_parent: NodeId,
) -> Result<(), String> {
let sectprs = dom.elements(content_parent, Some(&W::sect_pr()));
if sectprs.len() > 1 {
return Err("Invalid document: multiple body-level sectPr".to_string());
}
let Some(last_sectpr) = sectprs.first().copied() else {
return Ok(());
};
let last_para = dom
.elements(content_parent, Some(&W::p()))
.last()
.copied()
.or_else(|| {
dom.descendants(content_parent, Some(&W::p()))
.last()
.copied()
});
let Some(last_para) = last_para else {
return Ok(());
};
let ppr = match dom.element(last_para, &W::p_pr()) {
Some(pp) => pp,
None => {
let pp = dom.new_element(W::p_pr());
dom.add_first(last_para, pp);
pp
}
};
let moved = dom.clone_subtree(last_sectpr);
dom.add(ppr, moved);
for sp in dom.elements(content_parent, Some(&W::sect_pr())) {
dom.remove(sp);
}
Ok(())
}
fn revision_tracking_element_from_ancestors(
dom: &Dom,
content: NodeId,
ancestors: &[NodeId],
) -> Option<NodeId> {
if dom.name_is(content, &W::p_pr()) {
for rpr in dom.elements(content, Some(&W::r_pr())) {
for e in dom.elements(rpr, None) {
let n = dom.name(e).unwrap();
if n == W::del() || n == W::ins() {
return Some(e);
}
}
}
return None;
}
ancestors.iter().copied().find(|&a| {
let n = dom.name(a).unwrap();
n == W::del() || n == W::ins() || n == W::move_from() || n == W::move_to()
})
}
fn status_from_rev_track_element(dom: &Dom, rte: Option<NodeId>) -> CorrelationStatus {
let Some(rte) = rte else {
return CorrelationStatus::Equal;
};
let n = dom.name(rte).unwrap();
if n == W::del() {
CorrelationStatus::Deleted
} else if n == W::ins() {
CorrelationStatus::Inserted
} else if n == W::move_from() {
CorrelationStatus::MovedSource
} else if n == W::move_to() {
CorrelationStatus::MovedDestination
} else {
CorrelationStatus::Equal
}
}
fn atom_hash(dom: &Dom, content: NodeId, settings: &WmlComparerSettings) -> AtomHash {
let mut text = dom.value(content);
if settings.case_insensitive {
text = text.to_uppercase();
}
if settings.conflate_breaking_and_nonbreaking_spaces {
text = text.replace(' ', "\u{00A0}");
}
let local = dom
.name(content)
.map(|n| n.local_name().to_string())
.unwrap_or_default();
if let Some(h) = dom.attribute(content, &PT::sha1_hash()) {
return AtomHash::from_hex(h);
}
AtomHash::of_bytes(format!("{local}{text}").as_bytes())
}
pub fn create_comparison_unit_atom_list(
dom: &mut Dom,
content_parent: NodeId,
settings: &WmlComparerSettings,
) -> Vec<ComparisonUnitAtom> {
verify_no_invalid_content(dom, content_parent).expect("invalid content in comparer input");
assign_to_all_elements(dom, content_parent);
move_last_sectpr_into_last_paragraph(dom, content_parent)
.expect("invalid document: multiple body sectPr");
let mut list = Vec::new();
let mut path = Vec::new();
recurse(dom, content_parent, &mut list, settings, &mut path);
list
}
fn annotate_element_with_props(
dom: &mut Dom,
element: NodeId,
list: &mut Vec<ComparisonUnitAtom>,
child_property_names: Option<&[XName]>,
settings: &WmlComparerSettings,
path: &mut Vec<NodeId>,
) {
for item in dom.elements(element, None) {
let skip = match (child_property_names, dom.name(item)) {
(Some(props), Some(n)) => props.contains(&n),
_ => false,
};
if !skip {
recurse(dom, item, list, settings, path);
}
}
}
fn push_atom(
dom: &Dom,
content: NodeId,
ancestors: &Arc<[NodeId]>,
list: &mut Vec<ComparisonUnitAtom>,
settings: &WmlComparerSettings,
) {
let mut hash = atom_hash(dom, content, settings);
let predel = PT::name("PreDelete");
if dom.attribute(content, &predel) == Some(super::PREDELETE_STAMP_ORIG)
|| ancestors
.iter()
.any(|&a| dom.attribute(a, &predel) == Some(super::PREDELETE_STAMP_ORIG))
{
hash = AtomHash::of_bytes(format!("PREDEL|{}", hash.to_hex_string()).as_bytes());
}
let mut atom = ComparisonUnitAtom::new(content, Arc::clone(ancestors), hash);
atom.rev_track_element =
revision_tracking_element_from_ancestors(dom, content, ancestors.as_ref());
atom.correlation_status = status_from_rev_track_element(dom, atom.rev_track_element);
list.push(atom);
}
fn chain_with(path: &[NodeId], element: NodeId) -> Arc<[NodeId]> {
let mut c = Vec::with_capacity(path.len() + 1);
c.extend_from_slice(path);
c.push(element);
Arc::from(c)
}
fn recurse(
dom: &mut Dom,
element: NodeId,
list: &mut Vec<ComparisonUnitAtom>,
settings: &WmlComparerSettings,
path: &mut Vec<NodeId>,
) {
let Some(name) = dom.name(element) else {
return;
};
if name == W::body() || name == W::name("hdr") || name == W::name("ftr") {
let mut i = 0;
while i < dom.child_count(element) {
let item = dom.child_at(element, i);
i += 1;
if dom.name(item).is_some() {
recurse(dom, item, list, settings, path);
}
}
return;
}
if name == W::footnote() || name == W::endnote() {
path.push(element);
let mut i = 0;
while i < dom.child_count(element) {
let item = dom.child_at(element, i);
i += 1;
if dom.name(item).is_some() {
recurse(dom, item, list, settings, path);
}
}
path.pop();
return;
}
if name == W::name("footnotes") || name == W::name("endnotes") {
let mut i = 0;
while i < dom.child_count(element) {
let item = dom.child_at(element, i);
i += 1;
if dom.name(item).is_some() {
recurse(dom, item, list, settings, path);
}
}
return;
}
if name == W::p() {
path.push(element);
let mut i = 0;
while i < dom.child_count(element) {
let item = dom.child_at(element, i);
i += 1;
match dom.name(item) {
Some(n) if n != W::p_pr() => recurse(dom, item, list, settings, path),
_ => {}
}
}
path.pop();
let para_props = dom.element(element, &W::p_pr());
let content = match para_props {
Some(pp) => pp,
None => dom.new_element(W::p_pr()),
};
let chain = chain_with(path, element);
push_atom(dom, content, &chain, list, settings);
return;
}
if name == W::r() {
path.push(element);
let mut i = 0;
while i < dom.child_count(element) {
let item = dom.child_at(element, i);
i += 1;
match dom.name(item) {
Some(n) if n != W::r_pr() => recurse(dom, item, list, settings, path),
_ => {}
}
}
path.pop();
return;
}
if name == W::t() || name == W::del_text() {
let val = dom.value(element);
let chain = chain_with(path, element);
for ch in val.chars() {
let content = dom.new_element(name.clone());
dom.add_text(content, &ch.to_string());
push_atom(dom, content, &chain, list, settings);
}
return;
}
if name == MC::name("AlternateContent") {
let chain = chain_with(path, element);
push_atom(dom, element, &chain, list, settings);
return;
}
if name == W::pict() {
let chain = chain_with(path, element);
push_atom(dom, element, &chain, list, settings);
return;
}
if ALLOWABLE_RUN_CHILDREN.contains(&name) || name == W::object() {
let chain = chain_with(path, element);
push_atom(dom, element, &chain, list, settings);
return;
}
if name == W::name("fldSimple") && dom.elements(element, None).is_empty() {
let chain = chain_with(path, element);
push_atom(dom, element, &chain, list, settings);
return;
}
if let Some(ri) = recursion_info(&name) {
path.push(element);
annotate_element_with_props(
dom,
element,
list,
ri.child_property_names.as_deref(),
settings,
path,
);
path.pop();
return;
}
if ELEMENTS_TO_THROW_AWAY.contains(&name) {
return;
}
path.push(element);
annotate_element_with_props(dom, element, list, None, settings, path);
path.pop();
}
pub fn coalesce(dom: &mut Dom, atoms: &[ComparisonUnitAtom]) -> 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 children = coalesce_recurse(dom, atoms, 0);
for c in children {
dom.add(body, c);
}
dom.add(document, body);
dom.add(doc, document);
doc
}
fn coalesce_recurse(dom: &mut Dom, atoms: &[ComparisonUnitAtom], level: usize) -> Vec<NodeId> {
let groups = group_by_ancestor_unid(dom, atoms, level);
let mut out = Vec::new();
for group in groups {
let ancestor = group[0].ancestor_elements[level];
let aname = dom.name(ancestor).unwrap();
if aname == W::p() {
let by_name = group_adjacent(group.iter().cloned(), |a| {
dom.name(a.content_element).unwrap()
});
let p = dom.new_element(W::p());
for (an, av) in dom.attributes(ancestor) {
dom.set_attribute_value(p, &an, Some(&av));
}
for (cname, gc) in &by_name {
if *cname == W::p_pr() {
for atom in gc {
let cloned = dom.clone_subtree(atom.content_element);
dom.add(p, cloned);
}
}
}
for (cname, gc) in &by_name {
if *cname != W::p_pr() {
let children = coalesce_recurse(dom, gc, level + 1);
for c in children {
dom.add(p, c);
}
}
}
out.push(p);
continue;
}
if aname == W::r() {
let by_name = group_adjacent(group.iter().cloned(), |a| {
dom.name(a.content_element).unwrap()
});
let r = dom.new_element(W::r());
for rpr in dom.elements(ancestor, Some(&W::r_pr())) {
let cloned = dom.clone_subtree(rpr);
dom.add(r, cloned);
}
for (cname, gc) in &by_name {
if *cname == W::t() || *cname == W::del_text() {
let text: String = gc
.iter()
.map(|a| dom.value_str(a.content_element))
.collect();
let t = dom.new_element(cname.clone());
if let Some(sp) = xml_space_attr(&text) {
dom.set_attribute_value(t, &XNamespace::xml().name("space"), Some(sp));
}
dom.add_text(t, &text);
dom.add(r, t);
} else {
for atom in gc {
let cloned = dom.clone_subtree(atom.content_element);
dom.add(r, cloned);
}
}
}
out.push(r);
continue;
}
let ne = dom.new_element(aname);
for (an, av) in dom.attributes(ancestor) {
dom.set_attribute_value(ne, &an, Some(&av));
}
let children = coalesce_recurse(dom, &group, level + 1);
for c in children {
dom.add(ne, c);
}
out.push(ne);
}
out
}
fn xml_space_attr(text: &str) -> Option<&'static str> {
if text.starts_with(' ') || text.ends_with(' ') {
Some("preserve")
} else {
None
}
}
fn group_by_ancestor_unid(
dom: &Dom,
atoms: &[ComparisonUnitAtom],
level: usize,
) -> Vec<Vec<ComparisonUnitAtom>> {
let unid_name = PT::unid();
let mut order: Vec<String> = Vec::new();
let mut map: std::collections::HashMap<String, Vec<ComparisonUnitAtom>> =
std::collections::HashMap::new();
for atom in atoms {
let ancestor = atom.ancestor_elements[level];
let key = dom
.attribute(ancestor, &unid_name)
.unwrap_or("")
.to_string();
if !map.contains_key(&key) {
order.push(key.clone());
}
map.entry(key).or_default().push(atom.clone());
}
order.into_iter().map(|k| map.remove(&k).unwrap()).collect()
}