use std::cell::RefCell;
use std::collections::HashMap;
use crate::namespaces::{M, MC, PT, R, W, W14, WP14};
use crate::xmllinq::{Dom, NodeId, XName, XNamespace};
use super::WmlComparerSettings;
use super::tables::ALLOWABLE_RUN_CHILDREN;
thread_local! {
static PURE_DEL_CACHE: RefCell<Option<HashMap<NodeId, bool>>> = const { RefCell::new(None) };
static MIXED_CACHE: RefCell<Option<HashMap<NodeId, bool>>> = const { RefCell::new(None) };
}
pub fn begin_para_classification_cache() {
PURE_DEL_CACHE.with(|c| *c.borrow_mut() = Some(HashMap::new()));
MIXED_CACHE.with(|c| *c.borrow_mut() = Some(HashMap::new()));
}
pub fn end_para_classification_cache() {
PURE_DEL_CACHE.with(|c| *c.borrow_mut() = None);
MIXED_CACHE.with(|c| *c.borrow_mut() = None);
}
fn descendants_trimmed(dom: &Dom, node: NodeId, stop: &XName) -> Vec<NodeId> {
let mut out = Vec::new();
fn walk(dom: &Dom, node: NodeId, stop: &XName, out: &mut Vec<NodeId>) {
for c in dom.nodes(node) {
out.push(c);
if dom.is_element(c) && dom.name(c).as_ref() != Some(stop) {
walk(dom, c, stop, out);
}
}
}
walk(dom, node, stop, &mut out);
out
}
fn is_run_status_carrier(dom: &Dom, d: NodeId) -> bool {
match dom.name(d) {
Some(n) => {
n == W::t()
|| n == W::del_text()
|| ALLOWABLE_RUN_CHILDREN.contains(&n)
|| n == MC::name("AlternateContent")
|| n == W::pict()
|| n == W::name("object")
}
None => false,
}
}
fn convert_run_text_to_del_text(dom: &mut Dom, run: NodeId) {
fn walk(dom: &mut Dom, node: NodeId) {
for c in dom.nodes(node) {
if !dom.is_element(c) {
continue;
}
match dom.name(c).as_ref() {
Some(n)
if n == &W::ins()
|| n == &W::del()
|| n == &W::name("moveFrom")
|| n == &W::name("moveTo") =>
{
continue;
}
Some(n) if n == &W::t() => dom.set_name(c, W::del_text()),
Some(n) if n == &W::instr_text() => dom.set_name(c, W::name("delInstrText")),
_ => walk(dom, c),
}
}
}
walk(dom, run);
}
fn rev_el(dom: &mut Dom, name: XName, settings: &WmlComparerSettings, id_gen: &mut u32) -> NodeId {
let e = dom.new_element(name);
dom.set_attribute_value(e, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(e, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(e, &W::date(), Some(&settings.date_time_for_revisions));
e
}
fn copy_attrs(dom: &mut Dom, src: NodeId, dst: NodeId, skip_pt: bool) {
for (an, av) in dom.attributes(src) {
if skip_pt && an.namespace_name() == PT::URI {
continue;
}
dom.set_attribute_value(dst, &an, Some(&av));
}
}
fn copy_move_id_attrs(dom: &mut Dom, src: NodeId, dst: NodeId) {
for name in [W::author(), W::date(), W::id()] {
let av = dom.attribute(src, &name).map(str::to_string);
if let Some(av) = av {
dom.set_attribute_value(dst, &name, Some(&av));
}
}
}
fn rebuild_run(
dom: &mut Dom,
element: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
skip_pt: bool,
) -> NodeId {
let r = dom.new_element(W::r());
copy_attrs(dom, element, r, skip_pt);
for c in dom.nodes(element) {
for tn in mark_content_transform(dom, c, settings, id_gen) {
dom.add(r, tn);
}
}
r
}
pub fn mark_content_transform(
dom: &mut Dom,
node: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) -> Vec<NodeId> {
if !dom.is_element(node) {
return vec![dom.clone_subtree(node)];
}
let name = dom.name(node).unwrap();
if name == W::r() {
let txbx = W::name("txbxContent");
let carriers: Vec<NodeId> = descendants_trimmed(dom, node, &txbx)
.into_iter()
.filter(|&d| is_run_status_carrier(dom, d))
.collect();
let mut statuses: Vec<String> = Vec::new();
for c in &carriers {
if let Some(s) = dom.attribute(*c, &PT::status())
&& !statuses.iter().any(|x| x == s)
{
statuses.push(s.to_string());
}
}
if statuses.len() > 1 {
panic!("Internal error - both deleted and inserted text in the same run");
}
if statuses.is_empty() {
return vec![rebuild_run(dom, node, settings, id_gen, false)];
}
let status = statuses[0].as_str();
let move_name = |dom: &Dom| -> String {
carriers
.iter()
.find_map(|&c| {
dom.attribute(c, &PT::name("MoveName"))
.map(|s| s.to_string())
})
.unwrap_or_else(|| "move1".to_string())
};
match status {
"Deleted" | "Inserted" => {
let wrap = if status == "Deleted" {
W::del()
} else {
W::ins()
};
let w = rev_el(dom, wrap, settings, id_gen);
let r = rebuild_run(dom, node, settings, id_gen, false);
if status == "Deleted" {
convert_run_text_to_del_text(dom, r);
}
dom.add(w, r);
vec![w]
}
"MovedSource" | "MovedDestination" => {
let (range_start, mid, range_end) = if status == "MovedSource" {
(
W::name("moveFromRangeStart"),
W::name("moveFrom"),
W::move_from_range_end(),
)
} else {
(
W::name("moveToRangeStart"),
W::name("moveTo"),
W::move_to_range_end(),
)
};
let mname = move_name(dom);
let range_id = *id_gen;
*id_gen += 1;
let rs = dom.new_element(range_start);
dom.set_attribute_value(rs, &W::id(), Some(&range_id.to_string()));
dom.set_attribute_value(rs, &W::name("name"), Some(&mname));
dom.set_attribute_value(rs, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(rs, &W::date(), Some(&settings.date_time_for_revisions));
let mv = rev_el(dom, mid, settings, id_gen);
let r = rebuild_run(dom, node, settings, id_gen, false);
dom.add(mv, r);
let re = dom.new_element(range_end);
dom.set_attribute_value(re, &W::id(), Some(&range_id.to_string()));
vec![rs, mv, re]
}
"FormatChanged" => {
let old_rpr_str = carriers
.iter()
.find_map(|&c| dom.attribute(c, &PT::name("OldRPr")).map(|s| s.to_string()));
let r = rebuild_run(dom, node, settings, id_gen, true);
let rpr = match dom.element(r, &W::r_pr()) {
Some(p) => p,
None => {
let p = dom.new_element(W::r_pr());
dom.add_first(r, p);
p
}
};
let old_rpr = parse_rpr(dom, old_rpr_str.as_deref());
let chg = dom.new_element(W::name("rPrChange"));
dom.set_attribute_value(chg, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(chg, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(chg, &W::date(), Some(&settings.date_time_for_revisions));
dom.add(chg, old_rpr);
dom.add(rpr, chg);
vec![r]
}
other => panic!("Internal error - unknown run status: {other}"),
}
} else if name == W::p_pr() {
let status = dom.attribute(node, &PT::status()).map(|s| s.to_string());
let Some(status) = status else {
let ppr = dom.new_element(W::p_pr());
copy_attrs(dom, node, ppr, false);
for c in dom.nodes(node) {
for tn in mark_content_transform(dom, c, settings, id_gen) {
dom.add(ppr, tn);
}
}
return vec![ppr];
};
if status == "FormatChanged" {
let ppr = dom.clone_subtree(node);
dom.set_attribute_value(ppr, &PT::status(), None);
if let Some(old_rpr_s) = dom
.attribute(node, &PT::name("OldRPr"))
.map(|s| s.to_string())
{
dom.set_attribute_value(ppr, &PT::name("OldRPr"), None);
let rpr = match dom.element(ppr, &W::r_pr()) {
Some(p) => p,
None => {
let p = dom.new_element(W::r_pr());
dom.add_first(ppr, p);
p
}
};
if dom.element(rpr, &W::name("rPrChange")).is_none() {
let old_rpr = parse_rpr(dom, Some(&old_rpr_s));
let chg = dom.new_element(W::name("rPrChange"));
dom.set_attribute_value(chg, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(
chg,
&W::author(),
Some(&settings.author_for_revisions),
);
dom.set_attribute_value(
chg,
&W::date(),
Some(&settings.date_time_for_revisions),
);
dom.add(chg, old_rpr);
dom.add(rpr, chg);
}
}
if let Some(old_s) = dom
.attribute(node, &PT::name("OldPPr"))
.map(|s| s.to_string())
{
dom.set_attribute_value(ppr, &PT::name("OldPPr"), None);
let old_ppr = parse_ppr(dom, Some(&old_s));
if dom.element(ppr, &W::spacing_el()).is_none()
&& let Some(old_sp) = dom.element(old_ppr, &W::spacing_el())
{
let after = dom.attribute(old_sp, &W::name("after")).unwrap_or("");
let before = dom.attribute(old_sp, &W::name("before")).unwrap_or("");
let line = dom.attribute(old_sp, &W::name("line")).unwrap_or("");
let after_n: i64 = after.parse().unwrap_or(i64::MAX);
if before.is_empty() && line.is_empty() && after_n > 0 && after_n <= 40 {
let sp = dom.clone_subtree(old_sp);
dom.add_first(ppr, sp);
}
}
let chg = dom.new_element(W::p_pr_change());
dom.set_attribute_value(chg, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(chg, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(chg, &W::date(), Some(&settings.date_time_for_revisions));
let old_for_chg = parse_ppr(dom, Some(&old_s));
dom.add(chg, old_for_chg);
dom.add(ppr, chg); }
return vec![ppr];
}
let ppr = dom.clone_subtree(node);
let wrap = match status.as_str() {
"Deleted" | "MovedSource" => W::del(),
"Inserted" | "MovedDestination" => W::ins(),
other => panic!("Internal error - unknown pPr status: {other}"),
};
let rpr = match dom.element(ppr, &W::r_pr()) {
Some(p) => p,
None => {
let p = dom.new_element(W::r_pr());
dom.add_first(ppr, p);
p
}
};
let mark = rev_el(dom, wrap, settings, id_gen);
dom.add(rpr, mark);
vec![ppr]
} else {
let ne = dom.new_element(name);
copy_attrs(dom, node, ne, false);
for c in dom.nodes(node) {
for tn in mark_content_transform(dom, c, settings, id_gen) {
dom.add(ne, tn);
}
}
vec![ne]
}
}
fn parse_rpr(dom: &mut Dom, s: Option<&str>) -> NodeId {
if let Some(s) = s {
let doc = dom.parse_xdocument(s);
if let Some(root) = dom.root(doc) {
return dom.clone_subtree(root);
}
}
dom.new_element(W::r_pr())
}
fn parse_ppr(dom: &mut Dom, s: Option<&str>) -> NodeId {
if let Some(s) = s {
let doc = dom.parse_xdocument(s);
if let Some(root) = dom.root(doc) {
return dom.clone_subtree(root);
}
}
dom.new_element(W::p_pr())
}
pub fn mark_content_as_deleted_or_inserted(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) -> NodeId {
let v = mark_content_transform(dom, root, settings, id_gen);
v.into_iter()
.next()
.expect("root transform yields one node")
}
fn ppr_has_structural_props(dom: &Dom, ppr: NodeId) -> bool {
for c in dom.elements(ppr, None) {
let Some(n) = dom.name(c) else {
continue;
};
if n == W::r_pr()
|| n == W::sect_pr()
|| n == W::p_pr_change()
|| n.namespace_name() == PT::URI
{
continue;
}
return true;
}
false
}
fn ppr_is_jc_only(dom: &Dom, ppr: NodeId) -> bool {
let mut saw_jc = false;
for c in dom.elements(ppr, None) {
let Some(n) = dom.name(c) else {
continue;
};
if n == W::r_pr()
|| n == W::sect_pr()
|| n == W::p_pr_change()
|| n.namespace_name() == PT::URI
{
continue;
}
if n == W::jc_el() {
if saw_jc {
return false;
}
saw_jc = true;
} else {
return false;
}
}
saw_jc
}
fn conjoin_transform(dom: &mut Dom, node: NodeId, author: &str, date: &str) -> NodeId {
if !dom.is_element(node) {
return dom.clone_subtree(node);
}
let name = dom.name(node).unwrap();
if name == W::p() && dom.elements(node, Some(&W::p_pr())).len() >= 2 {
let pprs = dom.elements(node, Some(&W::p_pr()));
let ins_idx = pprs.iter().position(|&p| {
matches!(
dom.attribute(p, &PT::status()),
Some("Inserted") | Some("MovedDestination")
)
});
let del_idx = pprs.iter().position(|&p| {
matches!(
dom.attribute(p, &PT::status()),
Some("Deleted") | Some("MovedSource")
)
});
let live_idx = match (ins_idx, del_idx) {
(Some(i), Some(_d)) if ppr_has_structural_props(dom, pprs[i]) => i,
(Some(i), Some(d))
if !ppr_has_structural_props(dom, pprs[i])
&& ppr_has_structural_props(dom, pprs[d]) =>
{
d
}
(Some(i), _) => i,
(_, Some(d)) => d,
_ => pprs.len().saturating_sub(1),
};
let live_src = pprs[live_idx];
let live_is_inserted = matches!(
dom.attribute(live_src, &PT::status()),
Some("Inserted") | Some("MovedDestination")
);
let old_src = pprs.iter().copied().find(|&p| {
p != live_src
&& matches!(
dom.attribute(p, &PT::status()),
Some("Deleted") | Some("MovedSource")
)
});
let ppr = dom.clone_subtree(live_src);
if live_is_inserted {
for rpr in dom.elements(ppr, Some(&W::r_pr())) {
for child in dom.elements(rpr, None) {
let cn = dom.name(child).unwrap();
if cn == W::ins() || cn == W::del() {
dom.remove(child);
}
}
if dom.elements(rpr, None).is_empty() {
dom.remove(rpr);
}
}
}
dom.set_attribute_value(ppr, &PT::status(), None);
if live_is_inserted
&& let Some(old) = old_src
&& dom.element(ppr, &W::p_pr_change()).is_none()
{
let old_inner = dom.clone_subtree(old);
dom.set_attribute_value(old_inner, &PT::status(), None);
for rpr in dom.elements(old_inner, Some(&W::r_pr())) {
for child in dom.elements(rpr, None) {
let cn = dom.name(child).unwrap();
if cn == W::ins() || cn == W::del() {
dom.remove(child);
}
}
}
let chg = dom.new_element(W::p_pr_change());
dom.set_attribute_value(chg, &W::author(), Some(author));
dom.set_attribute_value(chg, &W::date(), Some(date));
dom.set_attribute_value(chg, &W::id(), Some("0"));
dom.add(chg, old_inner);
dom.add(ppr, chg);
}
let p = dom.new_element(W::p());
copy_attrs(dom, node, p, false);
dom.add(p, ppr);
for child in dom.elements(node, None) {
if dom.name(child).unwrap() != W::p_pr() {
let t = conjoin_transform(dom, child, author, date);
dom.add(p, t);
}
}
return p;
}
let ne = dom.new_element(name);
copy_attrs(dom, node, ne, false);
for c in dom.nodes(node) {
let t = conjoin_transform(dom, c, author, date);
dom.add(ne, t);
}
ne
}
pub fn conjoin_paragraph_marks(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
) -> NodeId {
conjoin_transform(
dom,
root,
&settings.author_for_revisions,
&settings.date_time_for_revisions,
)
}
pub fn fix_up_revision_ids(dom: &mut Dom, roots: &[NodeId]) {
let rev_names = [
W::ins(),
W::del(),
W::name("moveFrom"),
W::name("moveTo"),
W::name("moveFromRangeStart"),
W::move_from_range_end(),
W::name("moveToRangeStart"),
W::move_to_range_end(),
W::name("rPrChange"),
W::p_pr_change(),
W::name("tblPrChange"),
W::name("tblGridChange"),
W::name("trPrChange"),
W::name("tcPrChange"),
W::name("sectPrChange"),
W::name("numberingChange"),
W::name("cellMerge"),
];
let reserved_names = [
W::name("commentRangeStart"),
W::name("commentRangeEnd"),
W::name("commentReference"),
];
let mut reserved: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut all = Vec::new();
for &root in roots {
for d in dom.descendants(root, None) {
let Some(n) = dom.name(d) else { continue };
if rev_names.contains(&n) {
all.push(d);
} else if reserved_names.contains(&n)
&& let Some(id) = dom.attribute(d, &W::id()).and_then(|s| s.parse().ok())
{
reserved.insert(id);
}
}
}
let mut old_to_new: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
let mut next_id = 1u32;
let mut alloc = |old_to_new: &mut std::collections::HashMap<String, u32>,
old: &str,
remember: bool|
-> u32 {
while reserved.contains(&next_id) {
next_id += 1;
}
let v = next_id;
next_id += 1;
reserved.insert(v); if remember {
old_to_new.insert(old.to_string(), v);
}
v
};
let (mffe, mtre, mffs, mtrs) = (
W::move_from_range_end(),
W::move_to_range_end(),
W::name("moveFromRangeStart"),
W::name("moveToRangeStart"),
);
for rev in all {
let Some(old) = dom.attribute(rev, &W::id()).map(|s| s.to_string()) else {
continue;
};
let n = dom.name(rev).unwrap();
let new_id = if n == mffe || n == mtre {
match old_to_new.get(&old) {
Some(&v) => v,
None => alloc(&mut old_to_new, &old, true),
}
} else if n == mffs || n == mtrs {
alloc(&mut old_to_new, &old, true)
} else {
alloc(&mut old_to_new, &old, false)
};
dom.set_attribute_value(rev, &W::id(), Some(&new_id.to_string()));
}
}
pub fn ignore_pt14_namespace(dom: &mut Dom, root: NodeId) {
let pt14 = XNamespace::xmlns().name("pt14");
if dom.attribute(root, &pt14).is_none() {
dom.set_attribute_value(root, &pt14, Some(PT::URI));
}
let ignorable = MC::name("Ignorable");
let cur = dom.attribute(root, &ignorable).unwrap_or("").to_string();
let mut toks: Vec<&str> = cur.split_whitespace().collect();
if !toks.contains(&"pt14") {
toks.push("pt14");
dom.set_attribute_value(root, &ignorable, Some(&toks.join(" ")));
}
}
pub fn remove_powertools_scratch_markup(dom: &mut Dom, root: NodeId) {
for el in dom.descendants_and_self(root, None) {
let pts: Vec<XName> = dom
.attributes(el)
.into_iter()
.map(|(n, _)| n)
.filter(|n| n.namespace_name() == PT::URI)
.collect();
for a in pts {
dom.set_attribute_value(el, &a, None);
}
}
}
const DONT_CONSOLIDATE: &str = "DontConsolidate";
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 rpr_string(dom: &Dom, r: NodeId) -> String {
match dom.element(r, &W::r_pr()) {
Some(rpr) => dom.serialize_element(rpr),
None => String::new(),
}
}
fn coalesce_key(dom: &Dom, ce: NodeId) -> String {
let Some(name) = dom.name(ce) else {
return DONT_CONSOLIDATE.to_string();
};
if name == W::r() {
let non_rpr = dom
.elements(ce, None)
.into_iter()
.filter(|&e| !dom.name_is(e, &W::r_pr()))
.count();
if non_rpr != 1 {
return DONT_CONSOLIDATE.to_string();
}
if dom.attribute(ce, &PT::name("AbstractNumId")).is_some() {
return DONT_CONSOLIDATE.to_string();
}
let stamp: String = [
"PreIns",
"PreInsAuthor",
"PreInsDate",
"PreDelete",
"PreDelAuthor",
"PreDelDate",
]
.iter()
.map(|a| dom.attribute(ce, &PT::name(a)).unwrap_or("").to_string())
.collect::<Vec<_>>()
.join("\u{1}");
let rpr = rpr_string(dom, ce);
if dom.element(ce, &W::t()).is_some() {
return format!("Wt{rpr}\u{2}{stamp}");
}
if dom.element(ce, &W::instr_text()).is_some() {
return format!("WinstrText{rpr}\u{2}{stamp}");
}
return DONT_CONSOLIDATE.to_string();
}
if name == W::del() {
let non_rpr = dom
.elements(ce, Some(&W::r()))
.into_iter()
.flat_map(|r| dom.elements(r, None))
.filter(|&e| !dom.name_is(e, &W::r_pr()))
.count();
let has_del_text = dom
.elements(ce, None)
.into_iter()
.any(|c| dom.element(c, &W::del_text()).is_some());
if non_rpr != 1 || !has_del_text {
return DONT_CONSOLIDATE.to_string();
}
let author = dom.attribute(ce, &W::author()).unwrap_or("").to_string();
let date = dom.attribute(ce, &W::date()).unwrap_or("").to_string();
let rprs: String = dom
.elements(ce, Some(&W::r()))
.into_iter()
.filter_map(|r| {
dom.element(r, &W::r_pr())
.map(|rp| dom.serialize_element(rp))
})
.collect();
let stamp: String = dom
.elements(ce, Some(&W::r()))
.into_iter()
.flat_map(|r| {
[
"PreIns",
"PreInsAuthor",
"PreInsDate",
"PreDelete",
"PreDelAuthor",
"PreDelDate",
]
.iter()
.map(move |a| (r, *a))
})
.map(|(r, a)| dom.attribute(r, &PT::name(a)).unwrap_or("").to_string())
.collect::<Vec<_>>()
.join("\u{1}");
return format!("Wdel{author}{date}{rprs}\u{2}{stamp}");
}
DONT_CONSOLIDATE.to_string()
}
fn run_text_concat(dom: &Dom, r: NodeId) -> String {
let mut s = String::new();
for d in dom.descendants(r, None) {
let n = dom.name(d).unwrap();
if n == W::t() || n == W::del_text() || n == W::instr_text() {
s.push_str(&dom.value_str(d));
}
}
s
}
pub fn coalesce_adjacent_runs(dom: &mut Dom, container: NodeId) -> NodeId {
let cname = dom.name(container).unwrap();
let children = dom.elements(container, None);
let grouped = crate::util::group_adjacent(children, |&ce| coalesce_key(dom, ce));
let nc = dom.new_element(cname);
copy_attrs(dom, container, nc, false);
for (key, g) in grouped {
if key == DONT_CONSOLIDATE {
for e in g {
let c = dom.clone_subtree(e);
dom.add(nc, c);
}
continue;
}
let text: String = g.iter().map(|&r| run_text_concat(dom, r)).collect();
let first = g[0];
let fname = dom.name(first).unwrap();
if fname == W::r() {
let nr = dom.new_element(W::r());
copy_attrs(dom, first, nr, false);
if let Some(rpr) = dom.element(first, &W::r_pr()) {
let c = dom.clone_subtree(rpr);
dom.add(nr, c);
}
let leaf_name = if dom.element(first, &W::instr_text()).is_some() {
W::instr_text()
} else {
W::t()
};
let t = dom.new_element(leaf_name);
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(nr, t);
dom.add(nc, nr);
} else if fname == W::del() {
let nd = dom.new_element(W::del());
copy_attrs(dom, first, nd, false);
let nr = dom.new_element(W::r());
if let Some(fr) = dom.element(first, &W::r()) {
copy_attrs(dom, fr, nr, false);
if let Some(rpr) = dom.element(fr, &W::r_pr()) {
let c = dom.clone_subtree(rpr);
dom.add(nr, c);
}
}
let dt = dom.new_element(W::del_text());
if let Some(sp) = xml_space_attr(&text) {
dom.set_attribute_value(dt, &XNamespace::xml().name("space"), Some(sp));
}
dom.add_text(dt, &text);
dom.add(nr, dt);
dom.add(nd, nr);
dom.add(nc, nd);
} else {
for e in g {
let c = dom.clone_subtree(e);
dom.add(nc, c);
}
}
}
nc
}
pub fn resolve_alternate_content(dom: &mut Dom, root: NodeId) {
for ac in dom.descendants_and_self(root, Some(&MC::name("AlternateContent"))) {
if dom.parent(ac).is_none() {
continue;
}
let has_drawing = !dom.descendants(ac, Some(&W::drawing())).is_empty()
|| !dom.descendants(ac, Some(&W::pict())).is_empty();
if has_drawing {
continue;
}
let inside_drawing = dom
.ancestors_and_self(ac, None)
.into_iter()
.any(|a| dom.name_is(a, &W::drawing()));
if inside_drawing {
continue;
}
let src = dom
.elements(ac, Some(&MC::name("Choice")))
.into_iter()
.find(|&choice| choice_requires_understood(dom, choice))
.or_else(|| dom.element(ac, &MC::name("Fallback")));
if let Some(src) = src {
let kids = dom.nodes(src);
dom.replace_with(ac, &kids);
}
}
}
pub fn sanitize_sdt_properties(dom: &mut Dom, root: NodeId) {
const VALID: &[&str] = &[
"rPr",
"alias",
"lock",
"placeholder",
"showingPlcHdr",
"dataBinding",
"temporary",
"id",
"tag",
"group",
"comboBox",
"date",
"dropDownList",
"docPartObj",
"docPartList",
"equation",
"picture",
"richText",
"text",
"citation",
"bibliography",
];
for sdtpr in dom.descendants(root, Some(&W::name("sdtPr"))) {
for k in dom.elements(sdtpr, None) {
if let Some(n) = dom.name(k)
&& n.namespace_name() == W::URI
&& !VALID.contains(&n.local_name())
{
dom.remove(k);
}
}
}
}
pub fn unwrap_content_controls(dom: &mut Dom, root: NodeId) {
loop {
let sdts: Vec<NodeId> = dom.descendants(root, Some(&W::sdt()));
if sdts.is_empty() {
break;
}
let mut leaf: Vec<NodeId> = sdts
.iter()
.copied()
.filter(|&s| {
dom.descendants(s, Some(&W::sdt()))
.into_iter()
.all(|n| n == s)
})
.collect();
if leaf.is_empty() {
leaf = sdts;
}
let mut progressed = false;
for sdt in leaf {
if dom.parent(sdt).is_none() {
continue;
}
let kids: Vec<NodeId> = if let Some(content) = dom.element(sdt, &W::sdt_content()) {
dom.nodes(content)
} else {
Vec::new()
};
for k in kids {
if dom.parent(k).is_some() {
dom.remove(k);
dom.add_before_self(sdt, k);
}
}
dom.remove(sdt);
progressed = true;
}
if !progressed {
break;
}
}
}
pub fn unwrap_content_controls_in_pure_revisions(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let paras: Vec<NodeId> = dom.descendants(body, Some(&W::p()));
for p in paras {
let has_ins = !dom.descendants(p, Some(&W::ins())).is_empty()
|| para_mark_revision(dom, p, &W::ins());
let has_del = !dom.descendants(p, Some(&W::del())).is_empty()
|| para_mark_revision(dom, p, &W::del());
if !has_ins && !has_del {
continue;
}
if dom.descendants(p, Some(&W::sdt())).is_empty() {
continue;
}
unwrap_content_controls(dom, p);
}
}
fn choice_requires_understood(dom: &Dom, choice: NodeId) -> bool {
let Some(requires) = dom.attribute(choice, &XNamespace::none().name("Requires")) else {
return true;
};
requires
.split_whitespace()
.all(|prefix| prefix_in_scope(dom, choice, prefix))
}
fn prefix_in_scope(dom: &Dom, node: NodeId, prefix: &str) -> bool {
dom.ancestors_and_self(node, None).iter().any(|&anc| {
dom.attributes(anc)
.iter()
.any(|(name, _)| dom.is_namespace_declaration(name) && name.local_name() == prefix)
})
}
pub fn coalesce_all_paragraphs(dom: &mut Dom, root: NodeId) {
let paras = dom.descendants(root, Some(&W::p()));
for p in paras {
let np = coalesce_adjacent_runs(dom, p);
let children = dom.nodes(np);
dom.remove_nodes(p);
for c in children {
dom.add(p, c);
}
}
}
pub fn fix_paragraph_mark_revision_order(dom: &mut Dom, root: NodeId) {
let is_marker = |dom: &Dom, c: NodeId| {
matches!(
dom.name(c).as_ref().map(|n| n.local_name()),
Some("ins") | Some("del") | Some("moveFrom") | Some("moveTo")
)
};
for ppr in dom.descendants(root, Some(&W::p_pr())) {
let Some(rpr) = dom.element(ppr, &W::name("rPr")) else {
continue;
};
let markers: Vec<NodeId> = dom
.elements(rpr, None)
.into_iter()
.filter(|&c| is_marker(dom, c))
.collect();
for m in markers.into_iter().rev() {
dom.remove(m);
dom.add_first(rpr, m);
}
let anchor = dom
.element(ppr, &W::sect_pr())
.or_else(|| dom.element(ppr, &W::p_pr_change()));
dom.remove(rpr);
match anchor {
Some(a) => dom.add_before_self(a, rpr),
None => dom.add(ppr, rpr),
}
}
}
pub fn move_paragraph_properties_first(dom: &mut Dom, node: NodeId) {
if dom.name(node).as_ref() == Some(&W::p()) {
let kids = dom.nodes(node);
if let Some(pos) = kids
.iter()
.position(|&c| dom.name(c).as_ref() == Some(&W::p_pr()))
&& pos != 0
{
let ppr = kids[pos];
dom.remove(ppr);
dom.add_first(node, ppr);
}
}
for c in dom.nodes(node) {
if dom.is_element(c) {
move_paragraph_properties_first(dom, c);
}
}
}
pub fn unwrap_hyperlinks_to_styled_runs(dom: &mut Dom, root: NodeId) {
let hyperlinks: Vec<NodeId> = dom
.descendants(root, Some(&W::hyperlink()))
.into_iter()
.filter(|&hl| dom.attribute(hl, &R::name("id")).is_none())
.collect();
for hl in hyperlinks {
let runs: Vec<NodeId> = dom.descendants(hl, Some(&W::r())).into_iter().collect();
for r in runs {
let rpr = match dom.element(r, &W::r_pr()) {
Some(rp) => rp,
None => {
let rp = dom.new_element(W::r_pr());
dom.add_first(r, rp);
rp
}
};
if dom.element(rpr, &W::name("rStyle")).is_none() {
let rs = dom.new_element(W::name("rStyle"));
dom.set_attribute_value(rs, &W::val(), Some("Hyperlink"));
dom.add_first(rpr, rs);
}
}
wrap_revised_hyperlink_as_field(dom, hl);
let kids: Vec<NodeId> = dom.nodes(hl);
for k in kids {
dom.remove(k);
dom.add_before_self(hl, k);
}
dom.remove(hl);
}
}
fn wrap_revised_hyperlink_as_field(dom: &mut Dom, hl: NodeId) {
let Some(anchor) = dom.attribute(hl, &W::name("anchor")).map(str::to_string) else {
return;
};
let has_live_t = dom.descendants(hl, Some(&W::t())).iter().any(|&t| {
!dom.value_str(t).trim().is_empty()
&& !dom
.ancestors_and_self(t, None)
.iter()
.any(|&a| dom.name_is(a, &W::ins()))
});
let has_del_text = !dom.descendants(hl, Some(&W::del_text())).is_empty();
let has_ins = !dom.descendants(hl, Some(&W::ins())).is_empty();
let parent_rev = dom
.parent(hl)
.filter(|&p| dom.name_is(p, &W::del()) || dom.name_is(p, &W::ins()));
let (is_del, is_ins) = if let Some(p) = parent_rev {
(dom.name_is(p, &W::del()), dom.name_is(p, &W::ins()))
} else if has_del_text && !has_live_t {
(true, false)
} else if has_ins && !has_live_t && !has_del_text {
(false, true)
} else {
return; };
if !is_del && !is_ins {
return;
}
let instr_name = if is_del {
W::name("delInstrText")
} else {
W::name("instrText")
};
let mk_fld = |dom: &mut Dom, ty: &str| -> NodeId {
let r = dom.new_element(W::r());
let f = dom.new_element(W::name("fldChar"));
dom.set_attribute_value(f, &W::name("fldCharType"), Some(ty));
dom.add(r, f);
r
};
let begin = mk_fld(dom, "begin");
let instr_r = dom.new_element(W::r());
let instr = dom.new_element(instr_name);
let mut instr_text = format!("HYPERLINK \\l \"{anchor}\"");
if let Some(tip) = dom.attribute(hl, &W::name("tooltip")) {
let tip = tip.to_string();
instr_text.push_str(&format!(" \\o \"{tip}\""));
}
dom.add_text(instr, &instr_text);
dom.add(instr_r, instr);
let sep = mk_fld(dom, "separate");
let end = mk_fld(dom, "end");
if parent_rev.is_some() {
dom.add_before_self(hl, begin);
dom.add_before_self(hl, instr_r);
dom.add_before_self(hl, sep);
dom.add_after_self(hl, end);
return;
}
let rev_name = if is_del { W::del() } else { W::ins() };
let revs: Vec<NodeId> = dom.elements(hl, Some(&rev_name));
if let (Some(&first), Some(&last)) = (revs.first(), revs.last()) {
dom.add_first(first, sep);
dom.add_first(first, instr_r);
dom.add_first(first, begin);
dom.add(last, end);
} else {
match dom.elements(hl, None).first().copied() {
Some(firstc) => {
dom.add_before_self(firstc, sep);
dom.add_before_self(firstc, instr_r);
dom.add_before_self(firstc, begin);
}
None => {
dom.add(hl, begin);
dom.add(hl, instr_r);
dom.add(hl, sep);
}
}
dom.add(hl, end);
}
}
pub fn fold_short_list_label_into_empty_pure_del(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
loop {
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
let mut acted = false;
for i in 0..kids.len().saturating_sub(1) {
let ins_p = kids[i];
let del_p = kids[i + 1];
if !dom.name_is(ins_p, &W::p()) || !dom.name_is(del_p, &W::p()) {
continue;
}
if !para_is_pure_inserted(dom, ins_p) || !para_is_pure_deleted(dom, del_p) {
continue;
}
if para_has_real_del(dom, del_p) || !para_has_no_text(dom, del_p) {
continue;
}
if !para_mark_revision(dom, del_p, &W::del()) {
continue;
}
if !para_has_live_numpr(dom, ins_p) {
continue;
}
if !(1..=2).contains(¶_word_atom_count(dom, ins_p)) {
continue;
}
let has_content_del_after = kids[i + 2..].iter().any(|&k| {
dom.name_is(k, &W::p()) && para_is_pure_deleted(dom, k) && para_has_real_del(dom, k)
});
if !has_content_del_after {
continue;
}
if let Some(ippr) = dom.element(ins_p, &W::p_pr()) {
dom.remove(ippr);
}
if let Some(dppr) = dom.element(del_p, &W::p_pr()) {
let cloned = dom.clone_subtree(dppr);
if let Some(first) = dom.elements(ins_p, None).first().copied() {
dom.add_before_self(first, cloned);
} else {
dom.add(ins_p, cloned);
}
}
for c in dom.elements(del_p, None) {
if !dom.name_is(c, &W::p_pr()) {
dom.add(ins_p, c);
}
}
dom.remove(del_p);
acted = true;
break;
}
if !acted {
return;
}
}
}
pub fn strip_trailing_bare_empty_after_pure_i_dominant(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 12 {
return;
}
let last = kids[kids.len() - 1];
if !dom.name_is(last, &W::p()) {
return;
}
if !para_has_no_text(dom, last) {
return;
}
if !dom.descendants(last, Some(&W::ins())).is_empty()
|| !dom.descendants(last, Some(&W::del())).is_empty()
{
return;
}
let Some(ppr) = dom.element(last, &W::p_pr()) else {
return;
};
if dom.element(ppr, &W::spacing_el()).is_none() {
return;
}
let prev = kids[kids.len() - 2];
if !dom.name_is(prev, &W::p()) || !para_is_pure_deleted(dom, prev) {
return;
}
let mut pure_i = 0usize;
let mut pure_d = 0usize;
for &k in &kids {
if !dom.name_is(k, &W::p()) {
continue;
}
if para_is_pure_inserted(dom, k) {
pure_i += 1;
} else if para_is_pure_deleted(dom, k) {
pure_d += 1;
}
}
if pure_i < 10 || !(1..=4).contains(&pure_d) {
return;
}
dom.remove(last);
}
pub fn promote_live_numpr_on_pure_d_from_pprchange(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let mut source_num_id: Option<String> = None;
let mut short_label_pure_i = false;
for p in dom.elements(body, None) {
if !dom.name_is(p, &W::p()) || !para_is_pure_inserted(dom, p) {
continue;
}
if !para_has_live_numpr(dom, p) {
continue;
}
let words = para_word_atom_count(dom, p);
if (1..=2).contains(&words) {
short_label_pure_i = true;
}
if source_num_id.is_none()
&& let Some(ppr) = dom.element(p, &W::p_pr())
&& let Some(num) = dom.element(ppr, &W::num_pr())
&& let Some(nid) = dom.element(num, &W::name("numId"))
&& let Some(v) = dom.attribute(nid, &W::val())
{
source_num_id = Some(v.to_string());
}
if source_num_id.is_some() && short_label_pure_i {
break;
}
}
if !short_label_pure_i {
return;
}
let Some(src_id) = source_num_id else {
return;
};
for p in dom.elements(body, None) {
if !dom.name_is(p, &W::p()) {
continue;
}
if !para_is_pure_deleted(dom, p) || !para_has_real_del(dom, p) {
continue;
}
let body_txt = para_revision_body_text(dom, p);
if !body_looks_like_list_residual_label(&body_txt) {
continue;
}
let live_list_style = dom.element(p, &W::p_pr()).is_some_and(|ppr| {
dom.element(ppr, &W::p_style()).is_some_and(|ps| {
dom.attribute(ps, &W::val())
.unwrap_or("")
.to_ascii_lowercase()
.starts_with("list")
})
});
if live_list_style {
continue;
}
let Some(ppr) = dom.element(p, &W::p_pr()) else {
continue;
};
if dom.element(ppr, &W::num_pr()).is_some() {
continue;
}
let Some(chg) = dom.element(ppr, &W::p_pr_change()) else {
continue;
};
let Some(old) = dom.element(chg, &W::p_pr()) else {
continue;
};
let Some(old_num) = dom.element(old, &W::num_pr()) else {
continue;
};
let ilvl = dom
.element(old_num, &W::name("ilvl"))
.and_then(|il| dom.attribute(il, &W::val()))
.unwrap_or("0")
.to_string();
let num = dom.new_element(W::num_pr());
let il = dom.new_element(W::name("ilvl"));
dom.set_attribute_value(il, &W::val(), Some(&ilvl));
let nid = dom.new_element(W::name("numId"));
dom.set_attribute_value(nid, &W::val(), Some(&src_id));
dom.add(num, il);
dom.add(num, nid);
dom.add_first(ppr, num);
}
}
fn body_looks_like_section_number(text: &str) -> bool {
let s = text.trim_start();
let mut chars = s.chars().peekable();
let mut saw_digit = false;
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
saw_digit = true;
chars.next();
} else {
break;
}
}
if !saw_digit || chars.next() != Some('.') {
return false;
}
matches!(chars.peek(), Some(c) if c.is_ascii_digit())
}
fn body_looks_like_list_residual_label(text: &str) -> bool {
if body_looks_like_section_number(text) {
return true;
}
let t = text.trim().to_ascii_lowercase();
if let Some(rest) = t.strip_prefix("num ") {
let rest = rest.trim();
if rest.is_empty() {
return false;
}
let first = rest.chars().next().unwrap();
if !first.is_ascii_digit() {
return false;
}
return rest.split_whitespace().count() <= 2;
}
false
}
fn para_side_word_count(dom: &Dom, p: NodeId, ins_side: bool) -> usize {
let mut text = String::new();
let tag = if ins_side { W::ins() } else { W::del() };
for rev in dom.descendants(p, Some(&tag)) {
let child_tag = if ins_side { W::t() } else { W::del_text() };
for t in dom.descendants(rev, Some(&child_tag)) {
text.push_str(&dom.value_str(t));
text.push(' ');
}
if !ins_side {
for t in dom.descendants(rev, Some(&W::t())) {
text.push_str(&dom.value_str(t));
text.push(' ');
}
}
}
text.split_whitespace().filter(|w| !w.is_empty()).count()
}
pub fn ensure_pure_i_list_snug_spacing(dom: &mut Dom, root: NodeId) {
let mut token_counts: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
let mut candidates: Vec<(NodeId, String)> = Vec::new();
for p in dom.descendants(root, Some(&W::p())) {
if !para_is_pure_inserted(dom, p) {
continue;
}
let Some(ppr) = dom.element(p, &W::p_pr()) else {
continue;
};
if dom.element(ppr, &W::num_pr()).is_none() {
continue;
}
if dom.element(ppr, &W::spacing_el()).is_some() {
continue;
}
if dom.element(ppr, &W::p_style()).is_some_and(|ps| {
dom.attribute(ps, &W::val())
.unwrap_or("")
.eq_ignore_ascii_case("ListParagraph")
}) {
continue;
}
if para_word_atom_count(dom, p) != 1 {
continue;
}
let tok = para_revision_body_text(dom, p).trim().to_string();
if tok.is_empty() || tok.chars().count() > 12 {
continue;
}
*token_counts.entry(tok.clone()).or_insert(0) += 1;
candidates.push((p, tok));
}
for (p, tok) in candidates {
if token_counts.get(&tok).copied().unwrap_or(0) < 3 {
continue;
}
let Some(ppr) = dom.element(p, &W::p_pr()) else {
continue;
};
if dom.element(ppr, &W::spacing_el()).is_some() {
continue;
}
let sp = dom.new_element(W::spacing_el());
dom.set_attribute_value(sp, &W::name("before"), Some("0"));
dom.set_attribute_value(sp, &W::name("after"), Some("0"));
dom.set_attribute_value(sp, &W::name("line"), Some("240"));
dom.set_attribute_value(sp, &W::name("lineRule"), Some("auto"));
if let Some(rpr) = dom.element(ppr, &W::r_pr()) {
dom.add_before_self(rpr, sp);
} else if let Some(chg) = dom.element(ppr, &W::p_pr_change()) {
dom.add_before_self(chg, sp);
} else {
dom.add(ppr, sp);
}
}
}
pub fn normalize_incomplete_spacing(dom: &mut Dom, root: NodeId) {
let spacing_name = W::spacing_el();
let num_pr = W::num_pr();
let mut to_remove = Vec::new();
let mut to_rewrite: Vec<(NodeId, bool)> = Vec::new(); let mut need_rule: Vec<NodeId> = Vec::new();
for p in dom.descendants(root, Some(&W::p())) {
let Some(ppr) = dom.element(p, &W::p_pr()) else {
continue;
};
let has_num = dom.element(ppr, &num_pr).is_some();
let Some(sp) = dom.element(ppr, &spacing_name) else {
continue;
};
let line = dom.attribute(sp, &W::name("line")).unwrap_or("");
let after = dom.attribute(sp, &W::name("after")).unwrap_or("");
let before = dom.attribute(sp, &W::name("before")).unwrap_or("");
let rule = dom.attribute(sp, &W::name("lineRule")).unwrap_or("");
if line.is_empty() && rule == "auto" {
let zero_ba =
(before.is_empty() || before == "0") && (after.is_empty() || after == "0");
if zero_ba {
if has_num {
to_rewrite.push((sp, true));
} else {
to_remove.push(sp);
}
} else {
to_rewrite.push((sp, false));
}
} else if !line.is_empty() && rule.is_empty() {
need_rule.push(sp);
}
}
for sp in to_remove {
dom.remove(sp);
}
for (sp, list_shape) in to_rewrite {
if list_shape {
dom.set_attribute_value(sp, &W::name("before"), None);
dom.set_attribute_value(sp, &W::name("after"), Some("0"));
dom.set_attribute_value(sp, &W::name("line"), Some("240"));
dom.set_attribute_value(sp, &W::name("lineRule"), Some("auto"));
} else {
dom.set_attribute_value(sp, &W::name("line"), Some("240"));
}
}
for sp in need_rule {
dom.set_attribute_value(sp, &W::name("lineRule"), Some("auto"));
}
}
pub fn strip_redundant_demo_default_spacing(dom: &mut Dom, root: NodeId) {
let spacing_name = W::spacing_el();
let mut to_remove = Vec::new();
for p in dom.descendants(root, Some(&W::p())) {
let Some(ppr) = dom.element(p, &W::p_pr()) else {
continue;
};
let Some(sp) = dom.element(ppr, &spacing_name) else {
continue;
};
let line = dom.attribute(sp, &W::name("line")).unwrap_or("");
let after = dom.attribute(sp, &W::name("after")).unwrap_or("");
let before = dom.attribute(sp, &W::name("before")).unwrap_or("");
let rule = dom.attribute(sp, &W::name("lineRule")).unwrap_or("");
let line_ok = line == "276";
let after_ok = after.is_empty() || after == "200";
let before_ok = before.is_empty();
let rule_ok = rule.is_empty() || rule == "auto";
let has_pstyle = dom.element(ppr, &W::p_style()).is_some();
let has_ind = dom.element(ppr, &W::name("ind")).is_some();
let pure_i = para_is_pure_inserted(dom, p);
let keep = pure_i
&& !has_pstyle
&& after.is_empty()
&& (para_word_atom_count(dom, p) <= 1 || has_ind);
if line_ok && after_ok && before_ok && rule_ok && !keep {
to_remove.push(sp);
continue;
}
if let Ok(b) = before.parse::<i64>()
&& b >= 360
&& !after.is_empty()
&& !line.is_empty()
&& dom.element(ppr, &W::p_style()).is_none()
&& para_is_pure_deleted(dom, p)
{
to_remove.push(sp);
}
}
for sp in to_remove {
dom.remove(sp);
}
}
pub fn strip_redundant_normal_pstyle_and_bidi(dom: &mut Dom, root: NodeId) {
let mut drop: Vec<NodeId> = Vec::new();
for p in dom.descendants(root, Some(&W::p())) {
if !para_is_pure_inserted(dom, p) {
continue;
}
let Some(ppr) = dom.element(p, &W::p_pr()) else {
continue;
};
if let Some(ps) = dom.element(ppr, &W::p_style()) {
let v = dom.attribute(ps, &W::val()).unwrap_or("");
if v.eq_ignore_ascii_case("Normal") {
drop.push(ps);
}
}
if let Some(bidi) = dom.element(ppr, &W::name("bidi")) {
let v = dom.attribute(bidi, &W::val()).unwrap_or("1");
if v.is_empty() || v == "0" || v.eq_ignore_ascii_case("false") {
drop.push(bidi);
}
}
}
for n in drop {
if dom.parent(n).is_some() {
dom.remove(n);
}
}
let mut empty_ppr = Vec::new();
for p in dom.descendants(root, Some(&W::p())) {
if !para_is_pure_inserted(dom, p) {
continue;
}
if let Some(ppr) = dom.element(p, &W::p_pr())
&& dom.elements(ppr, None).is_empty()
{
empty_ppr.push(ppr);
}
}
for ppr in empty_ppr {
dom.remove(ppr);
}
}
pub fn strip_list_layout_from_mid_pure_del(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 2 {
return;
}
let last = kids[kids.len() - 1];
let mut has_list_shaped_pure_i = false;
for &k in &kids {
if !dom.name_is(k, &W::p()) || !para_is_pure_inserted(dom, k) {
continue;
}
if let Some(ppr) = dom.element(k, &W::p_pr())
&& spacing_is_list_single_line(dom, ppr)
{
has_list_shaped_pure_i = true;
break;
}
}
if !has_list_shaped_pure_i {
return;
}
let mut drop: Vec<NodeId> = Vec::new();
for &p in &kids {
if p == last || !dom.name_is(p, &W::p()) || !para_is_pure_deleted(dom, p) {
continue;
}
let Some(ppr) = dom.element(p, &W::p_pr()) else {
continue;
};
if dom.element(ppr, &W::num_pr()).is_some() {
continue;
}
if dom.element(ppr, &W::p_style()).is_some_and(|ps| {
dom.attribute(ps, &W::val())
.unwrap_or("")
.eq_ignore_ascii_case("ListParagraph")
}) {
continue;
}
if !spacing_is_list_single_line(dom, ppr) {
continue;
}
if let Some(sp) = dom.element(ppr, &W::spacing_el()) {
drop.push(sp);
}
if let Some(jc) = dom.element(ppr, &W::jc_el()) {
let v = dom.attribute(jc, &W::val()).unwrap_or("");
if v == "both" || v == "distribute" {
drop.push(jc);
}
}
if let Some(rpr) = dom.element(ppr, &W::r_pr()) {
for c in dom.elements(rpr, None) {
let Some(n) = dom.name(c) else {
continue;
};
if n == W::ins() || n == W::del() {
continue;
}
drop.push(c);
}
}
}
for n in drop {
if dom.parent(n).is_some() {
dom.remove(n);
}
}
}
fn spacing_is_list_single_line(dom: &Dom, ppr: NodeId) -> bool {
let Some(sp) = dom.element(ppr, &W::spacing_el()) else {
return false;
};
let line = dom.attribute(sp, &W::name("line")).unwrap_or("");
let after = dom.attribute(sp, &W::name("after")).unwrap_or("");
let before = dom.attribute(sp, &W::name("before")).unwrap_or("");
let rule = dom.attribute(sp, &W::name("lineRule")).unwrap_or("");
line == "240"
&& rule == "auto"
&& (after.is_empty() || after == "0")
&& (before.is_empty() || before == "0")
}
pub fn free_mesh_shared_title_token_in_mix(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
for &p in &kids {
if !dom.name_is(p, &W::p()) {
continue;
}
let has_ins = !dom.descendants(p, Some(&W::ins())).is_empty();
let has_del = !dom.descendants(p, Some(&W::del())).is_empty();
if !has_ins || !has_del {
continue;
}
let body_kids: Vec<NodeId> = dom
.elements(p, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
if body_kids.is_empty() {
continue;
}
let mut ins_nodes = Vec::new();
let mut del_nodes = Vec::new();
let mut other = false;
for &c in &body_kids {
let n = dom.name(c);
if n == Some(W::ins()) {
ins_nodes.push(c);
} else if n == Some(W::del()) {
del_nodes.push(c);
} else {
other = true;
break;
}
}
if other || ins_nodes.is_empty() || del_nodes.is_empty() {
continue;
}
if ins_nodes.len() > 2 || del_nodes.len() > 2 {
continue;
}
let author_of = |dom: &Dom, n: NodeId| dom.attribute(n, &W::author()).map(str::to_string);
let ins_author = ins_nodes.first().and_then(|&n| author_of(dom, n));
if ins_nodes.iter().any(|&n| author_of(dom, n) != ins_author)
|| del_nodes.iter().any(|&n| author_of(dom, n) != ins_author)
{
continue;
}
let mut ins_text = String::new();
for &ins in &ins_nodes {
for t in dom.descendants(ins, Some(&W::t())) {
ins_text.push_str(&dom.value_str(t));
}
}
let mut del_text = String::new();
for &del in &del_nodes {
for t in dom.descendants(del, Some(&W::del_text())) {
del_text.push_str(&dom.value_str(t));
}
}
if ins_text.is_empty() || del_text.is_empty() {
continue;
}
let ins_toks: Vec<String> = alnum_tokens(&ins_text);
let del_toks: Vec<String> = alnum_tokens(&del_text);
if ins_toks.len() > 6 || del_toks.len() > 6 || ins_toks.len() < 2 || del_toks.len() < 2 {
continue;
}
const BOILER: &[&str] = &[
"this", "that", "with", "from", "have", "will", "been", "were", "they", "them", "than",
"then", "when", "what", "which", "into", "over", "only", "also", "just", "more",
"most", "some", "such", "other", "about", "text", "page", "simple",
];
let del_set: std::collections::HashSet<&str> =
del_toks.iter().map(String::as_str).collect();
let shared: Vec<&str> = ins_toks
.iter()
.map(String::as_str)
.filter(|t| t.len() >= 5 && del_set.contains(t) && !BOILER.iter().any(|b| b == t))
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
if shared.len() != 1 {
continue;
}
let shared = shared[0];
if del_toks.last().map(String::as_str) != Some(shared) {
continue;
}
if ins_toks.iter().filter(|t| t.as_str() == shared).count() != 1
|| del_toks.iter().filter(|t| t.as_str() == shared).count() != 1
{
continue;
}
let Some(eq_label) = trailing_alnum_token(&del_text) else {
continue;
};
if !eq_label.eq_ignore_ascii_case(shared) {
continue;
}
let Some((ins_before, ins_after)) = split_around_token(&ins_text, &eq_label) else {
continue;
};
let Some(del_prefix) = strip_trailing_alnum_token(&del_text, &eq_label) else {
continue;
};
let (author, date, id) = {
let mut a = "Redline".to_string();
let mut d = "1970-01-01T00:00:00Z".to_string();
let mut id = "0".to_string();
if let Some(&ins) = ins_nodes.first() {
if let Some(v) = dom.attribute(ins, &W::author()) {
a = v.to_string();
}
if let Some(v) = dom.attribute(ins, &W::date()) {
d = v.to_string();
}
if let Some(v) = dom.attribute(ins, &W::id()) {
id = v.to_string();
}
}
(a, d, id)
};
let sample_rpr = ins_nodes
.first()
.and_then(|&ins| dom.element(ins, &W::r()))
.and_then(|r| dom.element(r, &W::r_pr()))
.map(|rpr| dom.clone_subtree(rpr));
for c in body_kids {
if dom.parent(c).is_some() {
dom.remove(c);
}
}
let _ = id;
rebuild_title_free_mesh(
dom,
p,
&ins_before,
&del_prefix,
&eq_label,
&ins_after,
&author,
&date,
sample_rpr,
);
}
}
fn alnum_tokens(text: &str) -> Vec<String> {
text.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_ascii_lowercase())
.collect()
}
fn split_around_token(text: &str, label: &str) -> Option<(String, String)> {
let chars: Vec<(usize, char)> = text.char_indices().collect();
let mut i = 0usize;
while i < chars.len() {
if chars[i].1.is_alphanumeric() {
let start_i = i;
let start_byte = chars[i].0;
while i < chars.len() && chars[i].1.is_alphanumeric() {
i += 1;
}
let end_byte = if i < chars.len() {
chars[i].0
} else {
text.len()
};
let tok = &text[start_byte..end_byte];
if tok.eq_ignore_ascii_case(label) {
let before = text[..start_byte].to_string();
let after = text[end_byte..].to_string();
if before.trim().is_empty() && after.trim().is_empty() {
return None;
}
let _ = start_i;
return Some((before, after));
}
} else {
i += 1;
}
}
None
}
#[allow(clippy::too_many_arguments)]
fn rebuild_title_free_mesh(
dom: &mut Dom,
p: NodeId,
ins_before: &str,
del_prefix: &str,
eq_label: &str,
ins_after: &str,
author: &str,
date: &str,
sample_rpr: Option<NodeId>,
) {
let mut next_id = 1u32;
let ib = ins_before.trim_end();
if !ib.is_empty() {
add_revision_text_run(
dom,
p,
W::ins(),
ib,
false,
author,
date,
&mut next_id,
sample_rpr,
);
}
let dp = del_prefix.trim_end();
if !dp.is_empty() {
add_revision_text_run(
dom,
p,
W::del(),
dp,
true,
author,
date,
&mut next_id,
None, );
}
let eq_r = dom.new_element(W::r());
if let Some(rpr) = sample_rpr {
let c = dom.clone_subtree(rpr);
dom.add(eq_r, c);
}
let eq_t = dom.new_element(W::t());
let eq_text = format!(" {eq_label}");
dom.set_attribute_value(eq_t, &XNamespace::xml().name("space"), Some("preserve"));
dom.add_text(eq_t, &eq_text);
dom.add(eq_r, eq_t);
dom.add(p, eq_r);
if !ins_after.is_empty() {
add_revision_text_run(
dom,
p,
W::ins(),
ins_after,
false,
author,
date,
&mut next_id,
sample_rpr,
);
}
}
#[allow(clippy::too_many_arguments)]
fn add_revision_text_run(
dom: &mut Dom,
p: NodeId,
wrapper: crate::xmllinq::XName,
text: &str,
deleted: bool,
author: &str,
date: &str,
next_id: &mut u32,
sample_rpr: Option<NodeId>,
) {
let w = dom.new_element(wrapper);
dom.set_attribute_value(w, &W::author(), Some(author));
dom.set_attribute_value(w, &W::date(), Some(date));
let id = next_id.to_string();
*next_id += 1;
dom.set_attribute_value(w, &W::id(), Some(&id));
let r = dom.new_element(W::r());
if let Some(rpr) = sample_rpr {
let c = dom.clone_subtree(rpr);
dom.add(r, c);
}
let te = dom.new_element(if deleted { W::del_text() } else { W::t() });
if text.starts_with(' ') || text.ends_with(' ') {
dom.set_attribute_value(te, &XNamespace::xml().name("space"), Some("preserve"));
}
dom.add_text(te, text);
dom.add(r, te);
dom.add(w, r);
dom.add(p, w);
}
fn para_is_pure_deleted(dom: &Dom, p: NodeId) -> bool {
if let Some(cached) =
PURE_DEL_CACHE.with(|c| c.borrow().as_ref().and_then(|m| m.get(&p).copied()))
{
return cached;
}
let v = para_is_pure_deleted_uncached(dom, p);
PURE_DEL_CACHE.with(|c| {
if let Some(m) = c.borrow_mut().as_mut() {
m.insert(p, v);
}
});
v
}
fn para_is_pure_deleted_uncached(dom: &Dom, p: NodeId) -> bool {
let has_del = !dom.descendants(p, Some(&W::del())).is_empty();
if !has_del {
return false;
}
if !dom.descendants(p, Some(&W::ins())).is_empty() {
return false;
}
for t in dom.descendants(p, Some(&W::t())) {
let mut in_del = false;
for a in dom.ancestors_and_self(t, None) {
if dom.name(a).as_ref() == Some(&W::del()) {
in_del = true;
break;
}
if a == p {
break;
}
}
if !in_del {
let v = dom.value(t);
if !v.trim().is_empty() {
return false;
}
}
}
true
}
pub fn strip_trailing_empty_pure_del_mark(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids = dom.elements(body, None);
let Some(&last) = kids.iter().rev().find(|&&k| !dom.name_is(k, &W::sect_pr())) else {
return;
};
if !dom.name_is(last, &W::p()) {
return;
}
let has_t = !dom.descendants(last, Some(&W::t())).is_empty()
|| !dom.descendants(last, Some(&W::del_text())).is_empty();
if has_t {
return;
}
if !para_is_pure_deleted(dom, last) && !para_mark_revision(dom, last, &W::del()) {
return;
}
if !dom.descendants(last, Some(&W::ins())).is_empty() {
return;
}
if let Some(ppr) = dom.element(last, &W::p_pr()) {
if let Some(rpr) = dom.element(ppr, &W::r_pr()) {
if let Some(d) = dom.element(rpr, &W::del()) {
dom.remove(d);
}
if dom.elements(rpr, None).is_empty() {
dom.remove(rpr);
}
}
if dom.elements(ppr, None).is_empty() {
dom.remove(ppr);
}
}
}
pub fn trailing_empty_spacing_to_pprchange(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids = dom.elements(body, None);
let Some(&last) = kids.iter().rev().find(|&&k| !dom.name_is(k, &W::sect_pr())) else {
return;
};
if !dom.name_is(last, &W::p()) {
return;
}
if !para_has_no_text(dom, last) {
return;
}
if !dom.descendants(last, Some(&W::ins())).is_empty()
|| !dom.descendants(last, Some(&W::del())).is_empty()
{
return;
}
let Some(ppr) = dom.element(last, &W::p_pr()) else {
return;
};
if dom.element(ppr, &W::p_pr_change()).is_some() {
return;
}
let Some(sp) = dom.element(ppr, &W::spacing_el()) else {
return;
};
for c in dom.elements(ppr, None) {
let Some(n) = dom.name(c) else {
continue;
};
if n == W::r_pr() {
continue;
}
if n != W::spacing_el() {
return;
}
}
let old_inner = dom.new_element(W::p_pr());
let sp_clone = dom.clone_subtree(sp);
dom.add(old_inner, sp_clone);
dom.remove(sp);
let chg = dom.new_element(W::p_pr_change());
dom.set_attribute_value(chg, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(chg, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(chg, &W::date(), Some(&settings.date_time_for_revisions));
dom.add(chg, old_inner);
dom.add(ppr, chg);
if let Some(rpr) = dom.element(ppr, &W::r_pr())
&& dom.elements(rpr, None).is_empty()
{
dom.remove(rpr);
}
}
fn para_has_no_text(dom: &Dom, p: NodeId) -> bool {
dom.descendants(p, Some(&W::t())).is_empty()
&& dom.descendants(p, Some(&W::del_text())).is_empty()
&& dom.descendants(p, Some(&W::name("br"))).is_empty()
&& dom.descendants(p, Some(&W::drawing())).is_empty()
&& dom.descendants(p, Some(&W::pict())).is_empty()
&& dom.descendants(p, Some(&W::name("object"))).is_empty()
&& dom.descendants(p, Some(&M::name("oMath"))).is_empty()
&& dom.descendants(p, Some(&M::name("oMathPara"))).is_empty()
}
fn para_has_omath(dom: &Dom, p: NodeId) -> bool {
!dom.descendants(p, Some(&M::name("oMath"))).is_empty()
|| !dom.descendants(p, Some(&M::name("oMathPara"))).is_empty()
}
fn para_is_visually_blank(dom: &Dom, p: NodeId) -> bool {
para_body_text_is_whitespace_only(dom, p)
&& !para_has_omath(dom, p)
&& dom.descendants(p, Some(&W::drawing())).is_empty()
&& dom.descendants(p, Some(&W::pict())).is_empty()
&& dom.descendants(p, Some(&W::name("object"))).is_empty()
&& dom.descendants(p, Some(&W::name("br"))).is_empty()
}
fn para_has_repeated_phrase(dom: &Dom, p: NodeId) -> bool {
let t = para_revision_body_text(dom, p).to_ascii_lowercase();
let words: Vec<&str> = t.split_whitespace().filter(|w| !w.is_empty()).collect();
if words.len() < 8 {
return false;
}
let mut seen = std::collections::HashSet::new();
for i in 0..=words.len().saturating_sub(4) {
let key = words[i..i + 4].join(" ");
if !seen.insert(key) {
return true;
}
}
false
}
fn para_is_empty_pure_ins(dom: &Dom, p: NodeId) -> bool {
if !dom.name_is(p, &W::p()) {
return false;
}
if !para_has_no_text(dom, p) {
return false;
}
let has_del =
!dom.descendants(p, Some(&W::del())).is_empty() || para_mark_revision(dom, p, &W::del());
if has_del {
return false;
}
let has_ins =
!dom.descendants(p, Some(&W::ins())).is_empty() || para_mark_revision(dom, p, &W::ins());
if !has_ins {
return false;
}
if !dom.descendants(p, Some(&W::drawing())).is_empty()
|| !dom.descendants(p, Some(&W::tbl())).is_empty()
{
return false;
}
true
}
pub fn strip_trailing_empty_pure_ins(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids = dom.elements(body, None);
let Some(&last) = kids.iter().rev().find(|&&k| !dom.name_is(k, &W::sect_pr())) else {
return;
};
if para_is_empty_pure_ins(dom, last) {
dom.remove(last);
}
}
pub fn relocate_title_page_last_empty_after_pure_dels(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 8 {
return;
}
let Some(di) = kids.iter().position(|&k| {
dom.name_is(k, &W::p())
&& para_is_pure_deleted(dom, k)
&& !para_body_text_is_whitespace_only(dom, k)
}) else {
return;
};
let mut dj = di;
while dj < kids.len() && dom.name_is(kids[dj], &W::p()) && para_is_pure_deleted(dom, kids[dj]) {
dj += 1;
}
let del_run = dj - di;
if del_run < 1 {
return;
}
if kids[dj..]
.iter()
.any(|&k| dom.name_is(k, &W::p()) && para_is_pure_inserted(dom, k))
{
return;
}
let mut empty_run = 0usize;
let mut j = di;
while j > 0 {
j -= 1;
let k = kids[j];
if dom.name_is(k, &W::p())
&& para_is_pure_inserted(dom, k)
&& para_body_text_is_whitespace_only(dom, k)
&& !para_has_omath(dom, k)
&& dom.descendants(k, Some(&W::name("br"))).is_empty()
&& dom.descendants(k, Some(&W::drawing())).is_empty()
{
empty_run += 1;
} else {
break;
}
}
if empty_run < 6 {
return;
}
let content_before = kids[..di.saturating_sub(empty_run)]
.iter()
.rev()
.find(|&&k| {
dom.name_is(k, &W::p())
&& para_is_pure_inserted(dom, k)
&& !para_body_text_is_whitespace_only(dom, k)
});
let Some(&anchor) = content_before else {
return;
};
let t = para_revision_body_text(dom, anchor).to_ascii_lowercase();
let title_page_tail = t.contains("prepared")
|| t.contains('@')
|| t.contains("2040")
|| t.contains("202")
|| t.contains("march ")
|| t.contains("january ")
|| t.contains("february ")
|| t.contains("april ")
|| t.contains("june ")
|| t.contains("july ")
|| t.contains("august ")
|| t.contains("september ")
|| t.contains("october ")
|| t.contains("november ")
|| t.contains("december ")
|| t.chars().filter(|c| c.is_ascii_digit()).count() >= 4;
if !title_page_tail {
return;
}
let last_empty = kids[di - 1];
if !para_is_pure_inserted(dom, last_empty)
|| !para_body_text_is_whitespace_only(dom, last_empty)
{
return;
}
strip_para_revision_marks(dom, last_empty);
let after_dels = if dj < kids.len() {
Some(kids[dj])
} else {
dom.elements(body, None)
.into_iter()
.find(|&k| dom.name_is(k, &W::sect_pr()))
};
dom.remove(last_empty);
match after_dels {
Some(anchor_node) if dom.parent(anchor_node).is_some() => {
dom.add_before_self(anchor_node, last_empty);
}
_ => {
dom.add(body, last_empty);
}
}
}
fn strip_para_revision_marks(dom: &mut Dom, p: NodeId) {
let mut wrappers: Vec<NodeId> = dom
.elements(p, None)
.into_iter()
.filter(|&c| matches!(dom.name(c), Some(n) if n == W::ins() || n == W::del()))
.collect();
wrappers.extend(
dom.descendants(p, Some(&W::ins()))
.into_iter()
.chain(dom.descendants(p, Some(&W::del()))),
);
wrappers.sort_by_key(|&n| std::cmp::Reverse(n.0));
wrappers.dedup();
for w in wrappers {
if dom.parent(w).is_none() {
continue;
}
let kids: Vec<NodeId> = dom.elements(w, None);
for c in kids {
dom.remove(c);
dom.add_before_self(w, c);
}
dom.remove(w);
}
if let Some(ppr) = dom.element(p, &W::p_pr())
&& let Some(rpr) = dom.element(ppr, &W::r_pr())
{
let marks: Vec<NodeId> = dom
.elements(rpr, None)
.into_iter()
.filter(|&c| matches!(dom.name(c), Some(n) if n == W::ins() || n == W::del()))
.collect();
for m in marks {
dom.remove(m);
}
if dom.elements(rpr, None).is_empty() {
dom.remove(rpr);
}
}
}
fn make_empty_pure_ins_para(
dom: &mut Dom,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) -> NodeId {
let p = dom.new_element(W::p());
let ppr = dom.new_element(W::p_pr());
let rpr = dom.new_element(W::r_pr());
let ins = dom.new_element(W::ins());
dom.set_attribute_value(ins, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(ins, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(ins, &W::date(), Some(&settings.date_time_for_revisions));
dom.add(rpr, ins);
dom.add(ppr, rpr);
dom.add(p, ppr);
p
}
pub fn ensure_empty_pure_i_before_short_title_del(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
loop {
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
let mut acted = false;
for i in 0..kids.len().saturating_sub(1) {
let ins_p = kids[i];
let del_p = kids[i + 1];
if !dom.name_is(ins_p, &W::p()) || !dom.name_is(del_p, &W::p()) {
continue;
}
if !para_is_pure_inserted(dom, ins_p) || !para_is_pure_deleted(dom, del_p) {
continue;
}
if para_body_text_is_whitespace_only(dom, ins_p) {
continue;
}
let n = para_word_atom_count(dom, del_p);
let short_title = (1..=4).contains(&n) && para_body_alnum_len(dom, del_p) <= 32;
if !(para_looks_like_demo_title(dom, del_p)
|| para_has_heading_or_title_style(dom, del_p)
|| short_title)
{
continue;
}
let after = kids.get(i + 2).copied();
let after2 = kids.get(i + 3).copied();
let empty_then_tbl = matches!(
(after, after2),
(Some(e), Some(t))
if dom.name_is(e, &W::p())
&& para_has_no_text(dom, e)
&& dom.name_is(t, &W::tbl())
);
if !empty_then_tbl {
continue;
}
for _ in 0..2 {
let spacer = make_empty_pure_ins_para(dom, settings, id_gen);
dom.add_before_self(del_p, spacer);
}
if let Some(e) = after
&& dom.name_is(e, &W::p())
&& para_has_no_text(dom, e)
&& !para_is_pure_deleted(dom, e)
&& !para_is_pure_inserted(dom, e)
{
let ppr = match dom.element(e, &W::p_pr()) {
Some(p) => p,
None => {
let p = dom.new_element(W::p_pr());
if let Some(first) = dom.elements(e, None).first().copied() {
dom.add_before_self(first, p);
} else {
dom.add(e, p);
}
p
}
};
let rpr = match dom.element(ppr, &W::r_pr()) {
Some(r) => r,
None => {
let r = dom.new_element(W::r_pr());
dom.add_first(ppr, r);
r
}
};
if dom.element(rpr, &W::del()).is_none() {
let del = dom.new_element(W::del());
dom.set_attribute_value(del, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(
del,
&W::author(),
Some(&settings.author_for_revisions),
);
dom.set_attribute_value(
del,
&W::date(),
Some(&settings.date_time_for_revisions),
);
dom.add_first(rpr, del);
}
}
acted = true;
break;
}
if !acted {
return;
}
}
}
pub fn strip_empty_pure_ins_before_trailing_pure_dels(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids = dom.elements(body, None);
let non_sect: Vec<NodeId> = kids
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if non_sect.len() < 2 {
return;
}
let mut run_start = non_sect.len();
while run_start > 0 {
let k = non_sect[run_start - 1];
if dom.name_is(k, &W::p()) && para_is_pure_deleted(dom, k) {
run_start -= 1;
} else {
break;
}
}
if run_start == non_sect.len() || run_start == 0 {
return;
}
let mut empty_run = 0usize;
let mut i = run_start;
while i > 0 && para_is_empty_pure_ins(dom, non_sect[i - 1]) {
empty_run += 1;
i -= 1;
}
if empty_run == 0 {
return;
}
if empty_run >= 3 {
return;
}
let first_del = non_sect[run_start];
let short_title_del = {
let n = para_word_atom_count(dom, first_del);
(1..=4).contains(&n) && para_body_alnum_len(dom, first_del) <= 32
};
let keep_title_spacers = (1..=2).contains(&empty_run)
&& (para_looks_like_demo_title(dom, first_del)
|| para_has_heading_or_title_style(dom, first_del)
|| short_title_del);
let table_after_del_run = non_sect
.get(run_start + 1..)
.into_iter()
.flatten()
.any(|&k| dom.name_is(k, &W::tbl()))
|| {
let after_run = run_start
+ non_sect[run_start..]
.iter()
.take_while(|&&k| dom.name_is(k, &W::p()) && para_is_pure_deleted(dom, k))
.count();
non_sect
.get(after_run)
.is_some_and(|&k| dom.name_is(k, &W::tbl()))
};
let strip_n = if keep_title_spacers {
if table_after_del_run {
0
} else {
empty_run.saturating_sub(1)
}
} else {
empty_run
};
for j in 0..strip_n {
let victim = non_sect[run_start - empty_run + j];
if para_is_empty_pure_ins(dom, victim) {
dom.remove(victim);
}
}
}
fn para_body_text_is_whitespace_only(dom: &Dom, p: NodeId) -> bool {
let mut saw = false;
for name in [W::t(), W::del_text()] {
for t in dom.descendants(p, Some(&name)) {
saw = true;
if !dom.value(t).trim().is_empty() {
return false;
}
}
}
let _ = saw;
true
}
fn para_is_pure_inserted(dom: &Dom, p: NodeId) -> bool {
if !dom.name_is(p, &W::p()) {
return false;
}
let has_del =
!dom.descendants(p, Some(&W::del())).is_empty() || para_mark_revision(dom, p, &W::del());
if has_del {
return false;
}
!dom.descendants(p, Some(&W::ins())).is_empty() || para_mark_revision(dom, p, &W::ins())
}
pub fn fold_whitespace_pure_ins_into_following_pure_del(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
{
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
let empty_pure_i = kids
.iter()
.filter(|&&k| {
dom.name_is(k, &W::p())
&& para_is_pure_inserted(dom, k)
&& para_body_text_is_whitespace_only(dom, k)
})
.count();
let content_pure_i = kids
.iter()
.filter(|&&k| {
dom.name_is(k, &W::p())
&& para_is_pure_inserted(dom, k)
&& !para_body_text_is_whitespace_only(dom, k)
})
.count();
if empty_pure_i >= 3 && content_pure_i == 0 {
return;
}
{
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
let first_del = kids.iter().position(|&k| {
dom.name_is(k, &W::p())
&& para_is_pure_deleted(dom, k)
&& !para_body_text_is_whitespace_only(dom, k)
});
if let Some(di) = first_del {
let mut empty_run = 0usize;
let mut j = di;
while j > 0 {
j -= 1;
let k = kids[j];
if dom.name_is(k, &W::p())
&& para_is_pure_inserted(dom, k)
&& para_is_visually_blank(dom, k)
{
empty_run += 1;
} else {
break;
}
}
let pure_i_after_del = kids[di + 1..]
.iter()
.any(|&k| dom.name_is(k, &W::p()) && para_is_pure_inserted(dom, k));
if empty_run >= 3 && !pure_i_after_del && content_pure_i >= 1 {
return;
}
}
}
}
loop {
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
let mut acted = false;
for i in 0..kids.len().saturating_sub(1) {
let ins_p = kids[i];
let del_p = kids[i + 1];
if !dom.name_is(del_p, &W::p()) {
continue;
}
if !para_is_pure_inserted(dom, ins_p) || !para_is_pure_deleted(dom, del_p) {
continue;
}
if !para_body_text_is_whitespace_only(dom, ins_p) {
continue;
}
if !dom.descendants(ins_p, Some(&W::name("br"))).is_empty()
|| !dom.descendants(ins_p, Some(&W::drawing())).is_empty()
|| !dom.descendants(ins_p, Some(&W::pict())).is_empty()
|| !dom.descendants(ins_p, Some(&W::name("object"))).is_empty()
{
continue;
}
if dom.descendants(ins_p, Some(&W::t())).is_empty() {
let mut later_ins = false;
let mut seen_del_p = false;
for &k2 in &kids {
if k2 == del_p {
seen_del_p = true;
continue;
}
if seen_del_p && !dom.descendants(k2, Some(&W::ins())).is_empty() {
later_ins = true;
break;
}
}
if later_ins {
continue;
}
}
if para_body_text_is_whitespace_only(dom, del_p) {
continue;
}
if dom.descendants(ins_p, Some(&W::t())).is_empty() {
let n = para_word_atom_count(dom, del_p);
let short_title = (1..=4).contains(&n) && para_body_alnum_len(dom, del_p) <= 32;
if para_looks_like_demo_title(dom, del_p)
|| para_has_heading_or_title_style(dom, del_p)
|| short_title
{
let mut following_pure_d = 0usize;
for &k in kids.iter().skip(i + 1) {
if dom.name_is(k, &W::p()) && para_is_pure_deleted(dom, k) {
following_pure_d += 1;
} else {
break;
}
}
if following_pure_d >= 1 {
continue;
}
}
}
let del_words = para_word_atom_count(dom, del_p);
let ins_has_drawing = !dom.descendants(ins_p, Some(&W::drawing())).is_empty()
|| !dom
.descendants(ins_p, Some(&W::name("AlternateContent")))
.is_empty();
if del_words > 12 || ins_has_drawing {
continue;
}
let del_structural = dom
.element(del_p, &W::p_pr())
.is_some_and(|dp| ppr_has_structural_props(dom, dp));
let ins_structural = dom
.element(ins_p, &W::p_pr())
.is_some_and(|ip| ppr_has_structural_props(dom, ip));
if !del_structural && ins_structural {
let mut following_pure_d = 0usize;
for &k in kids.iter().skip(i + 1) {
if dom.name_is(k, &W::p()) && para_is_pure_deleted(dom, k) {
following_pure_d += 1;
} else {
break;
}
}
let trailing_after_content_i = i > 0
&& kids[..i].iter().rev().any(|&k| {
dom.name_is(k, &W::p())
&& para_is_pure_inserted(dom, k)
&& !para_body_text_is_whitespace_only(dom, k)
});
if following_pure_d >= 3 && !trailing_after_content_i {
continue;
}
}
if del_structural {
let ins_has_fld = para_is_field_residue(dom, ins_p);
let del_heading_style = dom.element(del_p, &W::p_pr()).is_some_and(|dp| {
dom.element(dp, &W::p_style()).is_some_and(|ps| {
let v = dom
.attribute(ps, &W::val())
.unwrap_or("")
.to_ascii_lowercase();
v == "title" || v.starts_with("heading")
})
});
if !(ins_has_fld && del_heading_style) {
if let Some(ippr) = dom.element(ins_p, &W::p_pr()) {
dom.remove(ippr);
}
if let Some(dppr) = dom.element(del_p, &W::p_pr()) {
let cloned = dom.clone_subtree(dppr);
if let Some(first) = dom.elements(ins_p, None).first().copied() {
dom.add_before_self(first, cloned);
} else {
dom.add(ins_p, cloned);
}
}
for c in dom.elements(del_p, None) {
if !dom.name_is(c, &W::p_pr()) {
dom.add(ins_p, c);
}
}
dom.remove(del_p);
acted = true;
break;
}
}
if let Some(ppr) = dom.element(ins_p, &W::p_pr()) {
if let Some(rpr) = dom.element(ppr, &W::r_pr()) {
if let Some(ins_m) = dom.element(rpr, &W::ins()) {
dom.remove(ins_m);
}
if dom.element(rpr, &W::del()).is_none()
&& let Some(dppr) = dom.element(del_p, &W::p_pr())
&& let Some(drpr) = dom.element(dppr, &W::r_pr())
&& let Some(dmark) = dom.element(drpr, &W::del())
{
let cloned = dom.clone_subtree(dmark);
dom.add(rpr, cloned);
}
if dom.elements(rpr, None).is_empty() {
dom.remove(rpr);
}
} else if let Some(dppr) = dom.element(del_p, &W::p_pr()) {
if let Some(drpr) = dom.element(dppr, &W::r_pr())
&& dom.element(drpr, &W::del()).is_some()
{
let cloned = dom.clone_subtree(drpr);
dom.add(ppr, cloned);
}
}
} else if let Some(dppr) = dom.element(del_p, &W::p_pr()) {
if let Some(drpr) = dom.element(dppr, &W::r_pr())
&& dom.element(drpr, &W::del()).is_some()
{
let ins_has_fld = para_is_field_residue(dom, ins_p);
let del_heading_style = dom.element(dppr, &W::p_style()).is_some_and(|ps| {
let v = dom
.attribute(ps, &W::val())
.unwrap_or("")
.to_ascii_lowercase();
v == "title" || v.starts_with("heading")
});
let cloned = if ins_has_fld && del_heading_style {
let ppr = dom.new_element(W::p_pr());
let mark = dom.clone_subtree(drpr);
dom.add(ppr, mark);
ppr
} else {
dom.clone_subtree(dppr)
};
if let Some(first) = dom.elements(ins_p, None).first().copied() {
dom.add_before_self(first, cloned);
} else {
dom.add(ins_p, cloned);
}
}
}
for c in dom.elements(del_p, None) {
if !dom.name_is(c, &W::p_pr()) {
dom.add(ins_p, c);
}
}
dom.remove(del_p);
acted = true;
break;
}
if !acted {
return;
}
}
}
pub fn rotate_ins_mark_del_only_paragraph(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
loop {
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| dom.name_is(k, &W::p()))
.collect();
let mut acted = false;
for w in kids.windows(3) {
let (p0, p1, p2) = (w[0], w[1], w[2]);
if !para_mark_revision(dom, p0, &W::del()) {
continue;
}
let c0: Vec<NodeId> = dom
.elements(p0, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
let mut p0_ins = Vec::new();
let mut p0_del = Vec::new();
let mut bad = c0.is_empty();
for &c in &c0 {
if dom.name_is(c, &W::ins()) {
if !p0_del.is_empty() {
bad = true;
break;
}
p0_ins.push(c);
} else if dom.name_is(c, &W::del()) {
p0_del.push(c);
} else {
bad = true;
break;
}
}
if bad || p0_ins.is_empty() || p0_del.is_empty() {
continue;
}
if !para_mark_revision(dom, p1, &W::ins()) {
continue;
}
let c1: Vec<NodeId> = dom
.elements(p1, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
if c1.is_empty() || c1.iter().any(|&c| !dom.name_is(c, &W::del())) {
continue;
}
if para_mark_revision(dom, p2, &W::ins()) || para_mark_revision(dom, p2, &W::del()) {
continue;
}
let c2: Vec<NodeId> = dom
.elements(p2, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
let mut p2_lead_ins = Vec::new();
for &c in &c2 {
if dom.name_is(c, &W::ins()) {
p2_lead_ins.push(c);
} else {
break;
}
}
if p2_lead_ins.is_empty() || p2_lead_ins.len() == c2.len() {
continue;
}
let flip_mark = |dom: &mut Dom,
p: NodeId,
from: &crate::xmllinq::XName,
to: crate::xmllinq::XName| {
let Some(ppr) = dom.element(p, &W::p_pr()) else {
return;
};
let Some(rpr) = dom.element(ppr, &W::r_pr()) else {
return;
};
let Some(old) = dom.element(rpr, from) else {
return;
};
let attrs: Vec<_> = dom.attributes(old).into_iter().collect();
dom.remove(old);
let neu = dom.new_element(to);
for (n, v) in attrs {
dom.set_attribute_value(neu, &n, Some(&v));
}
dom.add_first(rpr, neu);
};
flip_mark(dom, p0, &W::del(), W::ins());
flip_mark(dom, p1, &W::ins(), W::del());
for &d in c1.iter().rev() {
dom.remove(d);
match dom
.elements(p2, None)
.iter()
.copied()
.find(|&c| !dom.name_is(c, &W::p_pr()))
{
Some(first) => dom.add_before_self(first, d),
None => dom.add(p2, d),
}
}
for &i in p2_lead_ins.iter().rev() {
dom.remove(i);
match dom
.elements(p1, None)
.iter()
.copied()
.find(|&c| !dom.name_is(c, &W::p_pr()))
{
Some(first) => dom.add_before_self(first, i),
None => dom.add(p1, i),
}
}
for &d in &p0_del {
dom.remove(d);
dom.add(p1, d);
}
acted = true;
break;
}
if !acted {
break;
}
}
}
pub fn restamp_stranded_del_mark_onto_del_only_paragraph(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
loop {
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| dom.name_is(k, &W::p()))
.collect();
let mut acted = false;
for w in kids.windows(2) {
let (p0, p1) = (w[0], w[1]);
if para_mark_revision(dom, p0, &W::del()) || para_mark_revision(dom, p0, &W::ins()) {
continue;
}
if let Some(ppr) = dom.element(p0, &W::p_pr())
&& !dom.elements(ppr, None).is_empty()
{
continue;
}
let c0: Vec<NodeId> = dom
.elements(p0, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
if c0.is_empty() || c0.iter().any(|&c| !dom.name_is(c, &W::del())) {
continue;
}
let has_del_text = c0.iter().any(|&d| {
dom.descendants(d, None)
.iter()
.any(|&n| dom.name_is(n, &W::del_text()))
});
if !has_del_text {
continue;
}
if !para_mark_revision(dom, p1, &W::del()) {
continue;
}
let c1: Vec<NodeId> = dom
.elements(p1, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
if !c1.is_empty() {
continue;
}
let Some(ppr1) = dom.element(p1, &W::p_pr()) else {
continue;
};
let ppr1_kids = dom.elements(ppr1, None);
if ppr1_kids.len() != 1 || !dom.name_is(ppr1_kids[0], &W::r_pr()) {
continue;
}
let rpr1_kids = dom.elements(ppr1_kids[0], None);
if rpr1_kids.len() != 1 || !dom.name_is(rpr1_kids[0], &W::del()) {
continue;
}
let cloned = dom.clone_subtree(ppr1);
match dom.element(p0, &W::p_pr()) {
Some(old) => {
dom.add_before_self(old, cloned);
dom.remove(old);
}
None => dom.add_first(p0, cloned),
}
acted = true;
break;
}
if !acted {
break;
}
}
}
pub fn split_head_short_title_long_del_mix(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let Some(&first) = dom
.elements(body, None)
.iter()
.find(|&&k| dom.name_is(k, &W::p()))
else {
return;
};
let kids: Vec<NodeId> = dom
.elements(first, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
if kids.is_empty()
|| para_mark_revision(dom, first, &W::ins())
|| para_mark_revision(dom, first, &W::del())
{
return;
}
let mut ins_run = Vec::new();
let mut del_run = Vec::new();
for &c in &kids {
if dom.name_is(c, &W::ins()) {
if !del_run.is_empty() {
return; }
ins_run.push(c);
} else if dom.name_is(c, &W::del()) {
del_run.push(c);
} else {
return; }
}
if ins_run.is_empty() || del_run.is_empty() {
return;
}
let text_of = |dom: &Dom, nodes: &[NodeId], del: bool| -> String {
let tag = if del { W::del_text() } else { W::t() };
let mut s = String::new();
for &n in nodes {
for t in dom.descendants(n, Some(&tag)) {
s.push_str(&dom.value_str(t));
}
}
s
};
let ins_text = text_of(dom, &ins_run, false);
let del_text = text_of(dom, &del_run, true);
let toks = |s: &str| -> Vec<String> {
s.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(str::to_lowercase)
.collect()
};
let it = toks(&ins_text);
let dt = toks(&del_text);
if it.is_empty() || it.len() > 4 || dt.len() < 10 {
return;
}
let dset: std::collections::HashSet<&str> = dt
.iter()
.map(String::as_str)
.filter(|t| t.len() >= 4)
.collect();
if it.iter().any(|t| t.len() >= 4 && dset.contains(t.as_str())) {
return;
}
let author = dom.attribute(del_run[0], &W::author()).map(str::to_string);
let date = dom.attribute(del_run[0], &W::date()).map(str::to_string);
let max_id: u32 = dom
.descendants(root, None)
.into_iter()
.filter_map(|e| {
dom.attribute(e, &W::id())
.and_then(|v| v.parse::<u32>().ok())
})
.max()
.unwrap_or(0);
let np = dom.new_element(W::p());
let ppr = dom.new_element(W::p_pr());
let rpr = dom.new_element(W::r_pr());
let mark = dom.new_element(W::del());
dom.set_attribute_value(mark, &W::id(), Some(&(max_id + 1).to_string()));
if let Some(a) = &author {
dom.set_attribute_value(mark, &W::author(), Some(a));
}
if let Some(d) = &date {
dom.set_attribute_value(mark, &W::date(), Some(d));
}
dom.add(rpr, mark);
dom.add(ppr, rpr);
dom.add(np, ppr);
for &d in &del_run {
dom.remove(d);
dom.add(np, d);
}
dom.add_after_self(first, np);
}
pub fn fold_leading_ins_from_mix_into_preceding_pure_del(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
loop {
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
let mut acted = false;
for i in 0..kids.len().saturating_sub(1) {
let del_p = kids[i];
let mix_p = kids[i + 1];
if !dom.name_is(del_p, &W::p()) || !dom.name_is(mix_p, &W::p()) {
continue;
}
if !para_is_pure_deleted(dom, del_p) {
continue;
}
let has_ins = !dom.descendants(mix_p, Some(&W::ins())).is_empty();
let has_del = !dom.descendants(mix_p, Some(&W::del())).is_empty()
|| para_mark_revision(dom, mix_p, &W::del());
if !has_ins || !has_del {
continue;
}
let del_text = para_revision_body_text(dom, del_p);
let del_tokens: Vec<&str> = del_text
.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.collect();
if del_tokens.is_empty() || del_tokens.len() > 8 {
continue;
}
let mix_kids: Vec<NodeId> = dom.elements(mix_p, None);
let mut leading_ins: Vec<NodeId> = Vec::new();
for &c in &mix_kids {
if dom.name_is(c, &W::p_pr()) {
continue;
}
if dom.name_is(c, &W::ins()) {
leading_ins.push(c);
} else {
break;
}
}
if leading_ins.is_empty() {
continue;
}
let mut ins_text = String::new();
for &ins_n in &leading_ins {
for t in dom.descendants(ins_n, Some(&W::t())) {
ins_text.push_str(&dom.value_str(t));
}
}
if ins_text.trim().is_empty() {
continue;
}
let del_sig: std::collections::HashSet<String> = del_tokens
.iter()
.filter(|t| t.chars().count() >= 4)
.map(|t| t.to_ascii_lowercase())
.collect();
let ins_sig: std::collections::HashSet<String> = ins_text
.split(|c: char| !c.is_alphanumeric())
.filter(|t| t.chars().count() >= 4)
.map(|t| t.to_ascii_lowercase())
.collect();
if del_sig.intersection(&ins_sig).count() > 0 {
continue;
}
let del_body_first = dom
.elements(del_p, None)
.into_iter()
.find(|&c| !dom.name_is(c, &W::p_pr()));
for &ins_n in &leading_ins {
if dom.parent(ins_n).is_none() {
continue;
}
dom.remove(ins_n);
if let Some(first) = del_body_first {
if dom.parent(first).is_some() {
dom.add_before_self(first, ins_n);
} else {
dom.add(del_p, ins_n);
}
} else {
dom.add(del_p, ins_n);
}
}
if dom.element(del_p, &W::p_pr()).is_none() {
} else if let Some(ppr) = dom.element(del_p, &W::p_pr())
&& let Some(rpr) = dom.element(ppr, &W::r_pr())
{
let _ = rpr;
}
acted = true;
break;
}
if !acted {
return;
}
}
}
pub fn strip_last_pure_del_mark_only_ppr(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids = dom.elements(body, None);
let Some(&last) = kids.iter().rev().find(|&&k| !dom.name_is(k, &W::sect_pr())) else {
return;
};
if !dom.name_is(last, &W::p()) || !para_is_pure_deleted(dom, last) {
return;
}
if para_has_no_text(dom, last) {
return;
}
let Some(ppr) = dom.element(last, &W::p_pr()) else {
return;
};
let ppr_kids = dom.elements(ppr, None);
if ppr_kids.len() != 1 {
return;
}
let rpr = ppr_kids[0];
if !dom.name_is(rpr, &W::r_pr()) {
return;
}
let rpr_kids = dom.elements(rpr, None);
if rpr_kids.len() != 1 || !dom.name_is(rpr_kids[0], &W::del()) {
return;
}
dom.remove(ppr);
}
fn para_is_mixed_revision(dom: &Dom, p: NodeId) -> bool {
if !dom.name_is(p, &W::p()) {
return false;
}
if let Some(cached) = MIXED_CACHE.with(|c| c.borrow().as_ref().and_then(|m| m.get(&p).copied()))
{
return cached;
}
let has_ins =
!dom.descendants(p, Some(&W::ins())).is_empty() || para_mark_revision(dom, p, &W::ins());
let has_del =
!dom.descendants(p, Some(&W::del())).is_empty() || para_mark_revision(dom, p, &W::del());
let v = has_ins && has_del;
MIXED_CACHE.with(|c| {
if let Some(m) = c.borrow_mut().as_mut() {
m.insert(p, v);
}
});
v
}
pub fn last_pure_del_spacing_to_pprchange(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids = dom.elements(body, None);
let Some(&last) = kids.iter().rev().find(|&&k| !dom.name_is(k, &W::sect_pr())) else {
return;
};
if !dom.name_is(last, &W::p()) {
return;
}
let is_mixed = para_is_mixed_revision(dom, last);
if !para_is_pure_deleted(dom, last) && !is_mixed {
return;
}
if is_mixed {
let ins_w = para_side_word_count(dom, last, true);
let del_w = para_side_word_count(dom, last, false);
if (1..=4).contains(&ins_w) && del_w >= 5 {
return;
}
if (1..=4).contains(&del_w) && ins_w >= 5 {
return;
}
}
let Some(ppr) = dom.element(last, &W::p_pr()) else {
return;
};
if dom.element(ppr, &W::p_pr_change()).is_some() {
return;
}
let movable: Vec<_> = if is_mixed {
vec![W::spacing_el()]
} else {
vec![
W::spacing_el(),
W::num_pr(),
W::name("ind"),
W::jc_el(),
W::p_style(),
]
};
let mut to_move: Vec<NodeId> = Vec::new();
for name in &movable {
if let Some(el) = dom.element(ppr, name) {
to_move.push(el);
}
}
if to_move.is_empty() {
return;
}
let old_inner = dom.new_element(W::p_pr());
for el in &to_move {
let cloned = dom.clone_subtree(*el);
dom.add(old_inner, cloned);
dom.remove(*el);
}
let chg = dom.new_element(W::p_pr_change());
dom.set_attribute_value(chg, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(chg, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(chg, &W::date(), Some(&settings.date_time_for_revisions));
dom.add(chg, old_inner);
dom.add(ppr, chg);
}
pub fn mixed_spacing_to_following_empty(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 2 {
return;
}
for i in 0..kids.len() - 1 {
let mixed = kids[i];
let empty = kids[i + 1];
if !dom.name_is(mixed, &W::p()) || !dom.name_is(empty, &W::p()) {
continue;
}
if !para_is_mixed_revision(dom, mixed) {
continue;
}
if !para_has_no_text(dom, empty) {
continue;
}
let is_trailing = i + 1 == kids.len() - 1;
let before_pure_d_table = i + 2 < kids.len()
&& dom.name_is(kids[i + 2], &W::tbl())
&& dom.descendants(kids[i + 2], Some(&W::ins())).is_empty()
&& !dom.descendants(kids[i + 2], Some(&W::del())).is_empty();
if !is_trailing && !before_pure_d_table {
continue;
}
let Some(mppr) = dom.element(mixed, &W::p_pr()) else {
continue;
};
let Some(spacing) = dom.element(mppr, &W::spacing_el()) else {
continue;
};
let eppr = match dom.element(empty, &W::p_pr()) {
Some(p) => p,
None => {
let p = dom.new_element(W::p_pr());
if let Some(first) = dom.elements(empty, None).first().copied() {
dom.add_before_self(first, p);
} else {
dom.add(empty, p);
}
p
}
};
if dom.element(eppr, &W::spacing_el()).is_none() {
let sp = dom.clone_subtree(spacing);
dom.add_first(eppr, sp);
}
dom.remove(spacing);
if dom.element(eppr, &W::p_pr_change()).is_none() {
let old_inner = dom.new_element(W::p_pr());
let chg = dom.new_element(W::p_pr_change());
dom.set_attribute_value(chg, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(chg, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(chg, &W::date(), Some(&settings.date_time_for_revisions));
dom.add(chg, old_inner);
dom.add(eppr, chg);
}
let rpr = match dom.element(mppr, &W::r_pr()) {
Some(r) => r,
None => {
let r = dom.new_element(W::r_pr());
dom.add(mppr, r);
r
}
};
if dom.element(rpr, &W::del()).is_none() && dom.element(rpr, &W::ins()).is_none() {
let mark = dom.new_element(W::del());
dom.set_attribute_value(mark, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(mark, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(mark, &W::date(), Some(&settings.date_time_for_revisions));
dom.add(rpr, mark);
}
if dom.elements(mppr, None).is_empty() {
dom.remove(mppr);
}
break; }
}
pub fn cleanup_spacing_and_default_jc(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.is_empty() {
return;
}
let last_i = kids.len() - 1;
let body_has_del = !dom.descendants(body, Some(&W::del())).is_empty();
for (i, &p) in kids.iter().enumerate() {
if !dom.name_is(p, &W::p()) {
continue;
}
let Some(ppr) = dom.element(p, &W::p_pr()) else {
continue;
};
if let Some(jc) = dom.element(ppr, &W::jc_el()) {
let val = dom.attribute(jc, &W::val()).unwrap_or("");
if val == "left" || val == "start" {
dom.remove(jc);
}
}
let ppc = dom.element(ppr, &W::p_pr_change());
if let Some(ppc) = ppc {
let old_ppr = dom
.elements(ppc, None)
.into_iter()
.find(|&c| dom.name_is(c, &W::p_pr()));
if let Some(old_ppr) = old_ppr {
let mut removed_left_jc = false;
if let Some(jc) = dom.element(old_ppr, &W::jc_el()) {
let val = dom.attribute(jc, &W::val()).unwrap_or("");
if val == "left" || val == "start" {
dom.remove(jc);
removed_left_jc = true;
}
}
if removed_left_jc {
let mut has_layout = false;
for c in dom.elements(old_ppr, None) {
let Some(n) = dom.name(c) else {
continue;
};
if n.local_name() == "rPr" {
continue;
}
has_layout = true;
break;
}
if !has_layout {
dom.remove(ppc);
continue; }
}
}
}
let Some(ppc) = dom.element(ppr, &W::p_pr_change()) else {
continue;
};
let old_ppr = dom
.elements(ppc, None)
.into_iter()
.find(|&c| dom.name_is(c, &W::p_pr()));
let Some(old_ppr) = old_ppr else {
continue;
};
if para_is_mixed_revision(dom, p) {
if let (Some(live_sp), Some(old_sp)) = (
dom.element(ppr, &W::spacing_el()),
dom.element(old_ppr, &W::spacing_el()),
) {
if dom.attribute(live_sp, &W::name("line")).is_none()
|| dom.attribute(old_sp, &W::name("line")).is_none()
{
continue;
}
let same_spacing = ["before", "after", "line", "lineRule"].iter().all(|&a| {
dom.attribute(live_sp, &W::name(a)).unwrap_or("")
== dom.attribute(old_sp, &W::name(a)).unwrap_or("")
});
if same_spacing {
let mut other_layout = false;
for c in dom.elements(old_ppr, None) {
let Some(n) = dom.name(c) else {
continue;
};
let local = n.local_name();
if local == "spacing" || local == "rPr" || local == "pStyle" {
continue;
}
other_layout = true;
break;
}
if !other_layout {
dom.remove(ppc);
continue;
}
}
}
continue;
}
if !body_has_del || dom.element(ppr, &W::spacing_el()).is_some() {
continue;
}
let Some(old_sp) = dom.element(old_ppr, &W::spacing_el()) else {
continue;
};
if !para_is_pure_deleted(dom, p) {
continue;
}
let mut other = false;
for c in dom.elements(old_ppr, None) {
let Some(n) = dom.name(c) else {
continue;
};
let local = n.local_name();
if local == "spacing" || local == "rPr" || local == "pStyle" {
continue;
}
other = true;
break;
}
if other {
continue;
}
let line = dom.attribute(old_sp, &W::name("line")).unwrap_or("");
let before = dom.attribute(old_sp, &W::name("before")).unwrap_or("");
let after = dom.attribute(old_sp, &W::name("after")).unwrap_or("");
let line_rule = dom.attribute(old_sp, &W::name("lineRule")).unwrap_or("");
let is_line276_noise =
line == "276" && before.is_empty() && after.is_empty() && line_rule.is_empty();
if is_line276_noise {
dom.remove(ppc);
continue;
}
if i == last_i {
continue;
}
let live = dom.clone_subtree(old_sp);
dom.add_before_self(ppc, live);
dom.remove(ppc);
}
}
pub fn promote_mid_pure_del_spacing_from_pprchange(dom: &mut Dom, root: NodeId) {
cleanup_spacing_and_default_jc(dom, root);
}
pub fn strip_default_left_jc(dom: &mut Dom, root: NodeId) {
cleanup_spacing_and_default_jc(dom, root);
}
pub fn strip_redundant_equal_spacing_pprchange(dom: &mut Dom, root: NodeId) {
cleanup_spacing_and_default_jc(dom, root);
}
pub fn park_mixed_spacing_onto_trailing_pure_del(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 3 {
return;
}
for i in 0..kids.len() {
let mixed = kids[i];
if !dom.name_is(mixed, &W::p()) || !para_is_mixed_revision(dom, mixed) {
continue;
}
let Some(mppr) = dom.element(mixed, &W::p_pr()) else {
continue;
};
let Some(spacing) = dom.element(mppr, &W::spacing_el()) else {
continue;
};
let mut j = i + 1;
while j < kids.len() && dom.name_is(kids[j], &W::p()) && para_is_pure_deleted(dom, kids[j])
{
j += 1;
}
let n_dels = j - (i + 1);
if !(2..=10).contains(&n_dels) {
continue;
}
let last_del = kids[j - 1];
if para_has_no_text(dom, last_del) {
continue;
}
let last_has_spacing = dom
.element(last_del, &W::p_pr())
.and_then(|p| dom.element(p, &W::spacing_el()))
.is_some();
if last_has_spacing {
continue;
}
let last_alnum = para_body_alnum_len(dom, last_del);
let last_toks = body_token_set(¶_revision_body_text(dom, last_del)).len();
if last_alnum > 80 || last_toks > 12 {
continue;
}
let has_before_after = {
let sp = spacing;
let before = dom
.attribute(sp, &W::name("before"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(0);
let after = dom
.attribute(sp, &W::name("after"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(0);
before >= 200 && after >= 80
};
if !has_before_after {
continue;
}
let lppr = match dom.element(last_del, &W::p_pr()) {
Some(p) => p,
None => {
let p = dom.new_element(W::p_pr());
if let Some(first) = dom.elements(last_del, None).first().copied() {
dom.add_before_self(first, p);
} else {
dom.add(last_del, p);
}
p
}
};
let sp = dom.clone_subtree(spacing);
dom.add_first(lppr, sp);
if dom.element(lppr, &W::p_pr_change()).is_none() {
let old_inner = dom.new_element(W::p_pr());
let chg = dom.new_element(W::p_pr_change());
dom.set_attribute_value(chg, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(chg, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(chg, &W::date(), Some(&settings.date_time_for_revisions));
dom.add(chg, old_inner);
dom.add(lppr, chg);
}
dom.remove(spacing);
let rpr = match dom.element(mppr, &W::r_pr()) {
Some(r) => r,
None => {
let r = dom.new_element(W::r_pr());
dom.add(mppr, r);
r
}
};
if dom.element(rpr, &W::del()).is_none() && dom.element(rpr, &W::ins()).is_none() {
let mark = dom.new_element(W::del());
dom.set_attribute_value(mark, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(mark, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(mark, &W::date(), Some(&settings.date_time_for_revisions));
dom.add(rpr, mark);
}
if dom.elements(mppr, None).is_empty() {
dom.remove(mppr);
}
break; }
}
pub fn park_mixed_numpr_onto_trailing_empty_pure_del(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 3 {
return;
}
for i in 0..kids.len() {
let mixed = kids[i];
if !dom.name_is(mixed, &W::p()) || !para_is_mixed_revision(dom, mixed) {
continue;
}
let Some(mppr) = dom.element(mixed, &W::p_pr()) else {
continue;
};
let Some(num) = dom.element(mppr, &W::num_pr()) else {
continue;
};
let mut j = i + 1;
while j < kids.len() && dom.name_is(kids[j], &W::p()) && para_is_pure_deleted(dom, kids[j])
{
j += 1;
}
let n_dels = j - (i + 1);
if !(2..=6).contains(&n_dels) {
continue;
}
let last_del = kids[j - 1];
let mut run_has_num = false;
for &d in &kids[i + 1..j] {
if dom
.element(d, &W::p_pr())
.and_then(|p| dom.element(p, &W::num_pr()))
.is_some()
{
run_has_num = true;
break;
}
}
if run_has_num {
continue;
}
let lppr = match dom.element(last_del, &W::p_pr()) {
Some(p) => p,
None => {
let p = dom.new_element(W::p_pr());
if let Some(first) = dom.elements(last_del, None).first().copied() {
dom.add_before_self(first, p);
} else {
dom.add(last_del, p);
}
p
}
};
if dom.element(lppr, &W::num_pr()).is_some() {
continue;
}
let num_clone = dom.clone_subtree(num);
if let Some(ppc) = dom.element(lppr, &W::p_pr_change()) {
dom.add_before_self(ppc, num_clone);
} else if let Some(rpr) = dom.element(lppr, &W::r_pr()) {
dom.add_before_self(rpr, num_clone);
} else {
dom.add_first(lppr, num_clone);
}
if dom.element(lppr, &W::p_pr_change()).is_none() {
let old_inner = dom.new_element(W::p_pr());
let chg = dom.new_element(W::p_pr_change());
dom.set_attribute_value(chg, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(chg, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(chg, &W::date(), Some(&settings.date_time_for_revisions));
dom.add(chg, old_inner);
dom.add(lppr, chg);
}
dom.remove(num);
break; }
}
pub fn last_pure_del_inherit_prev_jc(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 2 {
return;
}
let last = kids[kids.len() - 1];
if !dom.name_is(last, &W::p()) || !para_is_pure_deleted(dom, last) {
return;
}
let Some(lppr) = dom.element(last, &W::p_pr()) else {
return;
};
let Some(ppc) = dom.element(lppr, &W::p_pr_change()) else {
return;
};
let ppc_has_spacing = dom
.element(ppc, &W::p_pr())
.or_else(|| {
dom.elements(ppc, None)
.into_iter()
.find(|&c| dom.name_is(c, &W::p_pr()))
})
.is_some_and(|inner| dom.element(inner, &W::spacing_el()).is_some())
|| {
!dom.descendants(ppc, Some(&W::spacing_el())).is_empty()
};
if !ppc_has_spacing {
return;
}
if dom.element(lppr, &W::jc_el()).is_some() {
return;
}
let mut donor_jc = None;
for &prev in kids[..kids.len() - 1].iter().rev() {
if !dom.name_is(prev, &W::p()) {
continue;
}
let Some(pppr) = dom.element(prev, &W::p_pr()) else {
continue;
};
for c in dom.elements(pppr, None) {
if dom.name_is(c, &W::p_pr_change()) {
break;
}
if dom.name_is(c, &W::jc_el()) {
donor_jc = Some(c);
break;
}
}
if donor_jc.is_some() {
break;
}
}
let Some(jc) = donor_jc else {
return;
};
let cloned = dom.clone_subtree(jc);
if let Some(ppc) = dom.element(lppr, &W::p_pr_change()) {
dom.add_before_self(ppc, cloned);
} else {
dom.add_first(lppr, cloned);
}
}
pub fn strip_empty_pprchange_on_mix_with_live_jc(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 2 {
return;
}
let last = kids[kids.len() - 1];
for (i, &p) in kids.iter().enumerate() {
if p == last || !dom.name_is(p, &W::p()) || !para_is_mixed_revision(dom, p) {
continue;
}
let next_has_inserted_mark = kids[i + 1..]
.iter()
.find(|&&k| dom.name_is(k, &W::p()))
.is_some_and(|&n| {
dom.element(n, &W::p_pr())
.and_then(|np| dom.element(np, &W::r_pr()))
.is_some_and(|rpr| dom.element(rpr, &W::name("ins")).is_some())
});
if !next_has_inserted_mark {
continue;
}
let Some(ppr) = dom.element(p, &W::p_pr()) else {
continue;
};
if dom.element(ppr, &W::jc_el()).is_none() {
continue;
}
let Some(chg) = dom.element(ppr, &W::p_pr_change()) else {
continue;
};
let Some(old) = dom.element(chg, &W::p_pr()) else {
continue;
};
let mut has_layout = false;
for c in dom.elements(old, None) {
let Some(n) = dom.name(c) else {
continue;
};
if n == W::r_pr() {
continue;
}
has_layout = true;
break;
}
if has_layout {
continue;
}
dom.remove(chg);
}
}
pub fn promote_heading_spacing_from_pprchange_on_last_mix(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
let Some(&last) = kids.last() else {
return;
};
if !dom.name_is(last, &W::p()) || !para_is_mixed_revision(dom, last) {
return;
}
let Some(ppr) = dom.element(last, &W::p_pr()) else {
return;
};
if dom.element(ppr, &W::spacing_el()).is_some() {
return;
}
let Some(chg) = dom.element(ppr, &W::p_pr_change()) else {
return;
};
let Some(old) = dom.element(chg, &W::p_pr()) else {
return;
};
let Some(old_sp) = dom.element(old, &W::spacing_el()) else {
return;
};
let before = dom
.attribute(old_sp, &W::name("before"))
.and_then(|v| v.parse::<i64>().ok())
.unwrap_or(0);
let has_line = dom.attribute(old_sp, &W::name("line")).is_some();
if before < 200 || !has_line {
return;
}
let mut prior_live_heading = false;
for &k in &kids[..kids.len() - 1] {
if !dom.name_is(k, &W::p()) {
continue;
}
let Some(kppr) = dom.element(k, &W::p_pr()) else {
continue;
};
let Some(sp) = dom.element(kppr, &W::spacing_el()) else {
continue;
};
let b = dom
.attribute(sp, &W::name("before"))
.and_then(|v| v.parse::<i64>().ok())
.unwrap_or(0);
if b >= 200 && dom.attribute(sp, &W::name("line")).is_some() {
prior_live_heading = true;
break;
}
}
if !prior_live_heading {
return;
}
let live = dom.clone_subtree(old_sp);
dom.add_before_self(chg, live);
dom.remove(old_sp);
}
pub fn promote_live_jc_from_pprchange_on_body_mix(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 2 {
return;
}
let last = kids[kids.len() - 1];
for &p in &kids {
if p == last || !dom.name_is(p, &W::p()) || !para_is_mixed_revision(dom, p) {
continue;
}
let ins_w = para_side_word_count(dom, p, true);
let del_w = para_side_word_count(dom, p, false);
if ins_w + del_w < 6 {
continue;
}
let Some(ppr) = dom.element(p, &W::p_pr()) else {
continue;
};
if dom.element(ppr, &W::jc_el()).is_some() {
continue;
}
let Some(chg) = dom.element(ppr, &W::p_pr_change()) else {
continue;
};
let Some(old) = dom.element(chg, &W::p_pr()) else {
continue;
};
let Some(old_jc) = dom.element(old, &W::jc_el()) else {
continue;
};
let val = dom.attribute(old_jc, &W::val()).unwrap_or("").to_string();
if val.is_empty() {
continue;
}
let jc = dom.new_element(W::jc_el());
dom.set_attribute_value(jc, &W::val(), Some(&val));
dom.add_before_self(chg, jc);
let mut other_layout = false;
for c in dom.elements(old, None) {
let Some(n) = dom.name(c) else {
continue;
};
if n == W::jc_el() || n == W::r_pr() {
continue;
}
other_layout = true;
break;
}
if !other_layout {
dom.remove(chg);
}
}
}
pub fn strip_last_pure_del_mark_when_pprchange(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids = dom.elements(body, None);
let Some(&last) = kids.iter().rev().find(|&&k| !dom.name_is(k, &W::sect_pr())) else {
return;
};
if !dom.name_is(last, &W::p()) {
return;
}
if !para_is_pure_deleted(dom, last) && !para_is_mixed_revision(dom, last) {
return;
}
let Some(ppr) = dom.element(last, &W::p_pr()) else {
return;
};
if dom.element(ppr, &W::p_pr_change()).is_none() {
return;
}
if let Some(rpr) = dom.element(ppr, &W::r_pr())
&& let Some(d) = dom.element(rpr, &W::del())
{
let rpr_kids = dom.elements(rpr, None);
let only_del = rpr_kids.len() == 1 && rpr_kids[0] == d;
if only_del {
dom.remove(rpr);
} else {
dom.remove(d);
if dom.elements(rpr, None).is_empty() {
dom.remove(rpr);
}
}
}
}
pub fn reorder_replacements_ins_before_del(dom: &mut Dom, node: NodeId) {
let ins = W::ins();
let del = W::del();
let mut i = 0usize;
loop {
let kids = dom.nodes(node);
if i + 1 >= kids.len() {
break;
}
let (a, b) = (kids[i], kids[i + 1]);
let is_replacement = dom.is_element(a)
&& dom.is_element(b)
&& dom.name(a).as_ref() == Some(&del)
&& dom.name(b).as_ref() == Some(&ins)
&& dom.attribute(a, &W::author()).map(|s| s.to_string())
== dom.attribute(b, &W::author()).map(|s| s.to_string())
&& dom.attribute(a, &W::date()).map(|s| s.to_string())
== dom.attribute(b, &W::date()).map(|s| s.to_string());
if is_replacement {
dom.remove(b);
dom.add_before_self(a, b); i += 2;
} else {
i += 1;
}
}
for c in dom.nodes(node) {
if dom.is_element(c) {
reorder_replacements_ins_before_del(dom, c);
}
}
}
pub fn coalesce_adjacent_revisions(dom: &mut Dom, node: NodeId) {
let ins = W::ins();
let del = W::del();
let kids = dom.nodes(node);
let mut prev: Option<NodeId> = None;
for c in kids {
let is_rev =
dom.is_element(c) && matches!(dom.name(c), Some(ref n) if *n == ins || *n == del);
if is_rev {
if let Some(p) = prev {
let same = dom.name(p) == dom.name(c)
&& dom.attribute(p, &W::author()).map(|s| s.to_string())
== dom.attribute(c, &W::author()).map(|s| s.to_string())
&& dom.attribute(p, &W::date()).map(|s| s.to_string())
== dom.attribute(c, &W::date()).map(|s| s.to_string());
if same {
for gc in dom.nodes(c) {
dom.remove(gc);
dom.add(p, gc);
}
dom.remove(c);
continue; }
}
prev = Some(c);
} else {
prev = None; }
}
for c in dom.nodes(node) {
if dom.is_element(c) {
coalesce_adjacent_revisions(dom, c);
}
}
}
pub fn simplify_move_markup_to_del_ins(dom: &mut Dom, root: NodeId) -> NodeId {
simplify_move_transform(dom, root)
}
fn simplify_move_transform(dom: &mut Dom, node: NodeId) -> NodeId {
let name = dom.name(node).unwrap();
let ranges = [
W::name("moveFromRangeStart"),
W::move_from_range_end(),
W::name("moveToRangeStart"),
W::move_to_range_end(),
];
let new_name = if name == W::name("moveFrom") {
W::del()
} else if name == W::name("moveTo") {
W::ins()
} else {
name.clone()
};
let ne = dom.new_element(new_name);
if name == W::name("moveFrom") || name == W::name("moveTo") {
copy_move_id_attrs(dom, node, ne);
} else {
copy_attrs(dom, node, ne, false);
}
for c in dom.nodes(node) {
if dom.is_element(c) {
let cn = dom.name(c).unwrap();
if ranges.contains(&cn) {
continue; }
let t = simplify_move_transform(dom, c);
dom.add(ne, t);
} else {
let cc = dom.clone_subtree(c);
dom.add(ne, cc);
}
}
ne
}
pub fn merge_replaced_paragraphs(dom: &mut Dom, root: NodeId, comparer_author: &str) {
let mut containers: Vec<NodeId> = Vec::new();
if let Some(b) = dom.element(root, &W::body()) {
containers.push(b);
}
for name in [W::name("tc"), W::name("txbxContent"), W::sdt_content()] {
containers.extend(dom.descendants(root, Some(&name)));
}
for c in containers {
merge_replaced_in_container(dom, c, comparer_author);
}
}
pub fn residual_short_label_zip(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
loop {
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
let pure_i: Vec<NodeId> = kids
.iter()
.copied()
.filter(|&p| dom.name_is(p, &W::p()) && para_is_pure_inserted(dom, p))
.collect();
let pure_d: Vec<NodeId> = kids
.iter()
.copied()
.filter(|&p| dom.name_is(p, &W::p()) && para_is_pure_deleted(dom, p))
.collect();
if pure_i.is_empty() || pure_d.is_empty() {
return;
}
let mut acted = false;
let mut used_d: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
for &ip in &pure_i {
if !para_body_is_very_short(dom, ip) {
continue;
}
let it = para_revision_body_text(dom, ip);
let label = it
.chars()
.filter(|c| c.is_alphanumeric())
.collect::<String>()
.to_ascii_lowercase();
if label.is_empty() || label.len() > 2 {
continue;
}
let mut best: Option<NodeId> = None;
for &dp in &pure_d {
if used_d.contains(&dp) || dom.parent(dp).is_none() {
continue;
}
let dt = para_revision_body_text(dom, dp);
let toks: Vec<String> = dt
.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_ascii_lowercase())
.collect();
if toks.len() < 3 {
continue;
}
if toks.last().is_some_and(|t| t == &label) {
best = Some(dp);
break;
}
}
let Some(dp) = best else { continue };
if dom.parent(ip).is_none() || dom.parent(dp).is_none() {
continue;
}
fold_short_label_ins_into_del(dom, ip, dp);
used_d.insert(dp);
acted = true;
break; }
if !acted {
let short_i: Vec<NodeId> = pure_i
.iter()
.copied()
.filter(|&p| {
dom.parent(p).is_some()
&& para_body_is_very_short(dom, p)
&& para_body_alnum_len(dom, p) >= 1
})
.collect();
let lvl_d: Vec<NodeId> = pure_d
.iter()
.copied()
.filter(|&p| {
if used_d.contains(&p) || dom.parent(p).is_none() {
return false;
}
let t = para_revision_body_text(dom, p).to_ascii_lowercase();
t.contains("lvl") || t.contains("level")
})
.collect();
if !short_i.is_empty() && !lvl_d.is_empty() {
fold_short_label_ins_into_del(dom, short_i[0], lvl_d[0]);
acted = true;
}
}
if !acted {
return;
}
}
}
fn fold_short_label_ins_into_del(dom: &mut Dom, ip: NodeId, dp: NodeId) {
let empty_after = dom.parent(ip).and_then(|parent| {
let sibs: Vec<NodeId> = dom.elements(parent, None);
let pos = sibs.iter().position(|&s| s == ip)?;
let n = *sibs.get(pos + 1)?;
if dom.name_is(n, &W::p())
&& para_is_pure_inserted(dom, n)
&& (para_has_no_text(dom, n) || para_body_text_is_whitespace_only(dom, n))
{
Some(n)
} else {
None
}
});
if let Some(ippr) = dom.element(ip, &W::p_pr()) {
let dppr = match dom.element(dp, &W::p_pr()) {
Some(p) => p,
None => {
let p = dom.new_element(W::p_pr());
if let Some(first) = dom.elements(dp, None).first().copied() {
dom.add_before_self(first, p);
} else {
dom.add(dp, p);
}
p
}
};
if dom.element(dppr, &W::spacing_el()).is_none()
&& let Some(sp) = dom.element(ippr, &W::spacing_el())
{
let c = dom.clone_subtree(sp);
dom.add(dppr, c);
}
if dom.element(dppr, &W::name("ind")).is_none()
&& let Some(ind) = dom.element(ippr, &W::name("ind"))
{
let c = dom.clone_subtree(ind);
dom.add(dppr, c);
}
}
let ins_kids: Vec<NodeId> = dom
.elements(ip, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
let first_body = dom
.elements(dp, None)
.into_iter()
.find(|&c| !dom.name_is(c, &W::p_pr()));
for c in ins_kids {
if dom.parent(c).is_none() {
continue;
}
dom.remove(c);
if let Some(first) = first_body {
if dom.parent(first).is_some() {
dom.add_before_self(first, c);
} else {
dom.add(dp, c);
}
} else {
dom.add(dp, c);
}
}
if let Some(ippr) = dom.element(ip, &W::p_pr()) {
dom.remove(ippr);
}
if dom.parent(ip).is_some() {
dom.remove(ip);
}
mesh_short_label_shared_eq(dom, dp);
if let Some(emp) = empty_after
&& dom.parent(emp).is_some()
&& dom.parent(dp).is_some()
{
let Some(parent) = dom.parent(dp) else {
return;
};
let sibs: Vec<NodeId> = dom.elements(parent, None);
let after_dp = sibs
.iter()
.position(|&s| s == dp)
.and_then(|i| sibs.get(i + 1).copied());
dom.remove(emp);
if let Some(next) = after_dp {
if next != emp && dom.parent(next).is_some() {
dom.add_before_self(next, emp);
} else if let Some(n2) = dom
.elements(parent, None)
.into_iter()
.skip_while(|&s| s != dp)
.nth(1)
{
dom.add_before_self(n2, emp);
} else {
dom.add(parent, emp);
}
} else {
dom.add(parent, emp);
}
}
}
fn mesh_short_label_shared_eq(dom: &mut Dom, p: NodeId) {
let mut ins_text = String::new();
for ins in dom.descendants(p, Some(&W::ins())) {
for t in dom.descendants(ins, Some(&W::t())) {
ins_text.push_str(&dom.value_str(t));
}
}
let mut del_text = String::new();
for del in dom.descendants(p, Some(&W::del())) {
for t in dom.descendants(del, Some(&W::del_text())) {
del_text.push_str(&dom.value_str(t));
}
}
if ins_text.is_empty() || del_text.is_empty() {
return;
}
let label: String = ins_text
.chars()
.filter(|c| c.is_alphanumeric())
.collect::<String>()
.to_ascii_lowercase();
if label.is_empty() || label.len() > 2 {
return;
}
let del_toks: Vec<String> = del_text
.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_ascii_lowercase())
.collect();
if del_toks.len() < 3 || del_toks.last().is_none_or(|t| t != &label) {
return;
}
let Some(eq_label) = trailing_alnum_token(&del_text) else {
return;
};
if !eq_label.eq_ignore_ascii_case(&label) {
return;
}
let del_texts: Vec<NodeId> = dom
.descendants(p, Some(&W::del_text()))
.into_iter()
.collect();
let mut last_del_anchor: Option<NodeId> = None;
let mut stripped = false;
if let Some(&dt) = del_texts.last() {
let t = dom.value_str(dt).into_owned();
if let Some(prefix) = strip_trailing_alnum_token(&t, &eq_label) {
dom.set_value(dt, &prefix);
if prefix.starts_with(' ') || prefix.ends_with(' ') {
dom.set_attribute_value(dt, &XNamespace::xml().name("space"), Some("preserve"));
}
last_del_anchor = Some(dt);
stripped = true;
}
}
if !stripped {
let mut remain = eq_label.len();
for &dt in del_texts.iter().rev() {
if remain == 0 {
break;
}
let t = dom.value_str(dt).into_owned();
if t.len() <= remain {
remain -= t.len();
dom.set_value(dt, "");
last_del_anchor = Some(dt);
} else {
let keep = t.len() - remain;
let tail = &t[keep..];
if !tail.eq_ignore_ascii_case(&eq_label) {
return;
}
let new_t = t[..keep].to_string();
dom.set_value(dt, &new_t);
if new_t.starts_with(' ') || new_t.ends_with(' ') {
dom.set_attribute_value(dt, &XNamespace::xml().name("space"), Some("preserve"));
}
remain = 0;
last_del_anchor = Some(dt);
}
}
if remain != 0 {
return;
}
}
let mut strip_ins = label.len();
for ins in dom.descendants(p, Some(&W::ins())) {
for t in dom.descendants(ins, Some(&W::t())) {
if strip_ins == 0 {
break;
}
let v = dom.value_str(t).into_owned();
let mut chars: Vec<char> = v.chars().collect();
let mut i = 0usize;
while i < chars.len() && strip_ins > 0 {
if chars[i].is_alphanumeric() {
chars.remove(i);
strip_ins -= 1;
} else if chars[i].is_whitespace() {
i += 1;
} else {
i += 1;
}
}
let new_v: String = chars.into_iter().collect();
dom.set_value(t, &new_v);
if new_v.starts_with(' ') || new_v.ends_with(' ') {
dom.set_attribute_value(t, &XNamespace::xml().name("space"), Some("preserve"));
}
}
if strip_ins == 0 {
break;
}
}
let mut anchor = last_del_anchor.unwrap_or(p);
while let Some(par) = dom.parent(anchor) {
if dom.name_is(par, &W::del()) {
anchor = par;
break;
}
anchor = par;
if dom.name_is(par, &W::p()) {
break;
}
}
let eq_r = dom.new_element(W::r());
let eq_t = dom.new_element(W::t());
dom.add_text(eq_t, &eq_label);
dom.add(eq_r, eq_t);
if dom.name_is(anchor, &W::del()) {
dom.add_after_self(anchor, eq_r);
} else {
if let Some(ins) = dom
.elements(p, None)
.into_iter()
.find(|&c| dom.name_is(c, &W::ins()))
{
dom.add_before_self(ins, eq_r);
} else {
dom.add(p, eq_r);
}
}
let kids: Vec<NodeId> = dom.elements(p, None);
let mut leading_ins: Vec<NodeId> = Vec::new();
for &c in &kids {
if dom.name_is(c, &W::p_pr()) {
continue;
}
if dom.name_is(c, &W::ins()) {
leading_ins.push(c);
} else {
break; }
}
if !leading_ins.is_empty() && dom.parent(eq_r).is_some() {
let parent = dom.parent(eq_r).unwrap();
let sibs: Vec<NodeId> = dom.elements(parent, None);
let after_eq = sibs
.iter()
.position(|&s| s == eq_r)
.and_then(|i| sibs.get(i + 1).copied());
for ins in leading_ins {
if dom.parent(ins).is_none() {
continue;
}
let sibs_now: Vec<NodeId> = dom.elements(parent, None);
let ipos = sibs_now.iter().position(|&s| s == ins);
let epos = sibs_now.iter().position(|&s| s == eq_r);
if let (Some(i), Some(e)) = (ipos, epos)
&& i > e
{
continue;
}
dom.remove(ins);
if let Some(next) = after_eq.filter(|&n| dom.parent(n).is_some() && n != ins) {
dom.add_before_self(next, ins);
} else {
dom.add(parent, ins);
}
}
}
for t in dom.descendants(p, Some(&W::t())) {
if dom.parent(t).is_some() && dom.value_str(t).is_empty() {
dom.remove(t);
}
}
for r in dom.descendants(p, Some(&W::r())) {
if dom.parent(r).is_none() {
continue;
}
let has_content = dom.elements(r, None).into_iter().any(|c| {
let n = dom.name(c);
n != Some(W::r_pr()) && n.is_some()
});
if !has_content {
dom.remove(r);
}
}
for ins in dom.descendants(p, Some(&W::ins())) {
if dom.parent(ins).is_none() {
continue;
}
let mut any = false;
for t in dom.descendants(ins, Some(&W::t())) {
if !dom.value_str(t).is_empty() {
any = true;
break;
}
}
if !any {
dom.remove(ins);
}
}
}
fn trailing_alnum_token(text: &str) -> Option<String> {
let mut last = None;
let mut cur = String::new();
for c in text.chars() {
if c.is_alphanumeric() {
cur.push(c);
} else if !cur.is_empty() {
last = Some(std::mem::take(&mut cur));
}
}
if !cur.is_empty() {
last = Some(cur);
}
last
}
fn strip_trailing_alnum_token(text: &str, label: &str) -> Option<String> {
let tok = trailing_alnum_token(text)?;
if !tok.eq_ignore_ascii_case(label) {
return None;
}
let chars: Vec<(usize, char)> = text.char_indices().collect();
let mut i = chars.len();
while i > 0 && !chars[i - 1].1.is_alphanumeric() {
i -= 1;
}
while i > 0 && chars[i - 1].1.is_alphanumeric() {
i -= 1;
}
if i >= chars.len() {
return None;
}
Some(text[..chars[i].0].to_string())
}
fn para_mark_revision(dom: &Dom, p: NodeId, rev: &crate::xmllinq::XName) -> bool {
dom.element(p, &W::p_pr())
.and_then(|ppr| dom.element(ppr, &W::r_pr()))
.is_some_and(|rpr| dom.element(rpr, rev).is_some())
}
fn para_is_field_residue(dom: &Dom, p: NodeId) -> bool {
if dom.descendants(p, Some(&W::name("fldChar"))).is_empty() {
return false;
}
!dom.descendants(p, Some(&W::t()))
.iter()
.any(|&t| !dom.value_str(t).trim().is_empty())
}
fn accumulate_para_child_class(
dom: &Dom,
c: NodeId,
ins: &mut bool,
del: &mut bool,
plain: &mut bool,
) {
let Some(n) = dom.name(c) else {
*plain = true;
return;
};
if n == W::p_pr() || n == W::name("bookmarkStart") || n == W::name("bookmarkEnd") {
return;
}
if n == W::ins() {
*ins = true;
return;
}
if n == W::del() {
*del = true;
return;
}
if n == W::hyperlink() {
for gc in dom.elements(c, None) {
accumulate_para_child_class(dom, gc, ins, del, plain);
}
return;
}
*plain = true;
}
fn para_replacement_class(dom: &Dom, p: NodeId) -> Option<bool> {
if !dom.name_is(p, &W::p()) {
return None;
}
let (mut ins, mut del, mut plain) = (false, false, false);
for c in dom.elements(p, None) {
accumulate_para_child_class(dom, c, &mut ins, &mut del, &mut plain);
}
if !ins && !del && !plain {
if para_mark_revision(dom, p, &W::del()) {
del = true;
} else if para_mark_revision(dom, p, &W::ins()) {
ins = true;
}
}
match (ins, del, plain) {
(true, false, false) => Some(true),
(false, true, false) => Some(false),
_ => None,
}
}
fn para_class_carried_aware(dom: &Dom, p: NodeId, comparer_author: &str) -> Option<bool> {
if !dom.name_is(p, &W::p()) {
return None;
}
let (mut ins, mut del, mut plain) = (false, false, false);
for c in dom.elements(p, None) {
let Some(n) = dom.name(c) else {
plain = true;
continue;
};
if n == W::p_pr() || n == W::name("bookmarkStart") || n == W::name("bookmarkEnd") {
continue;
} else if n == W::ins() {
if dom.attribute(c, &W::author()).unwrap_or("") == comparer_author {
ins = true;
} else {
del = true; }
} else if n == W::del() {
del = true;
} else if n == W::hyperlink() {
for gc in dom.elements(c, None) {
let Some(gn) = dom.name(gc) else {
plain = true;
continue;
};
if gn == W::ins() {
if dom.attribute(gc, &W::author()).unwrap_or("") == comparer_author {
ins = true;
} else {
del = true;
}
} else if gn == W::del() {
del = true;
} else {
plain = true;
}
}
} else {
plain = true;
}
}
if !ins && !del && !plain {
if para_mark_revision(dom, p, &W::del()) {
del = true;
} else if para_mark_revision(dom, p, &W::ins()) {
ins = true;
}
}
match (ins, del, plain) {
(true, false, false) => Some(true),
(false, true, false) => Some(false),
_ => None,
}
}
fn para_has_real_del(dom: &Dom, p: NodeId) -> bool {
!dom.elements(p, Some(&W::del())).is_empty()
}
fn para_revision_body_text(dom: &Dom, p: NodeId) -> String {
let mut out = String::new();
for name in [W::name("t"), W::del_text()] {
for t in dom.descendants(p, Some(&name)) {
out.push_str(&dom.value_str(t));
out.push(' ');
}
}
out
}
fn body_token_set(text: &str) -> std::collections::HashSet<String> {
text.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_ascii_lowercase())
.collect()
}
fn para_body_is_digits_only(dom: &Dom, p: NodeId) -> bool {
let t = para_revision_body_text(dom, p);
let trimmed = t.trim();
!trimmed.is_empty()
&& trimmed
.chars()
.all(|c| c.is_ascii_digit() || c.is_whitespace())
}
fn para_body_alnum_len(dom: &Dom, p: NodeId) -> usize {
para_revision_body_text(dom, p)
.chars()
.filter(|c| c.is_alphanumeric())
.count()
}
fn para_body_is_very_short(dom: &Dom, p: NodeId) -> bool {
(1..=2).contains(¶_body_alnum_len(dom, p))
}
fn para_looks_like_demo_title(dom: &Dom, p: NodeId) -> bool {
let t = para_revision_body_text(dom, p);
let tokens: Vec<&str> = t
.split(|c: char| !c.is_alphanumeric())
.filter(|s| !s.is_empty())
.collect();
if tokens.is_empty() || tokens.len() > 8 {
return false;
}
tokens
.iter()
.rev()
.find(|s| s.chars().count() >= 4)
.is_some_and(|s| s.eq_ignore_ascii_case("demo"))
}
fn para_has_heading_or_title_style(dom: &Dom, p: NodeId) -> bool {
let Some(ppr) = dom.element(p, &W::p_pr()) else {
return false;
};
let Some(ps) = dom.element(ppr, &W::p_style()) else {
return false;
};
let val = dom.attribute(ps, &W::val()).unwrap_or("");
let v = val.to_ascii_lowercase();
v == "title" || v.starts_with("heading")
}
fn para_has_heading_style(dom: &Dom, p: NodeId) -> bool {
let Some(ppr) = dom.element(p, &W::p_pr()) else {
return false;
};
let Some(ps) = dom.element(ppr, &W::p_style()) else {
return false;
};
let val = dom.attribute(ps, &W::val()).unwrap_or("");
val.to_ascii_lowercase().starts_with("heading")
}
fn short_title_shares_sig_token(ins_text: &str, del_text: &str) -> bool {
const BOILER: &[&str] = &[
"this", "that", "with", "from", "have", "will", "been", "were", "they", "them", "than",
"then", "when", "what", "which", "into", "over", "only", "also", "just", "more", "most",
"some", "such", "other", "about",
];
let sig = |s: &str| -> std::collections::HashSet<String> {
s.split(|c: char| !c.is_alphanumeric())
.filter(|t| t.len() >= 4)
.map(|t| t.to_ascii_lowercase())
.filter(|t| !BOILER.iter().any(|b| b == t))
.collect()
};
let a = sig(ins_text);
let b = sig(del_text);
!a.is_empty() && a.intersection(&b).next().is_some()
}
fn body_text_jaccard(a: &str, b: &str) -> f64 {
let sa = body_token_set(a);
let sb = body_token_set(b);
if sa.is_empty() && sb.is_empty() {
return 1.0;
}
let inter = sa.intersection(&sb).count() as f64;
let uni = sa.union(&sb).count() as f64;
if uni == 0.0 { 0.0 } else { inter / uni }
}
const SOLE_DEL_FOLD_MIN_JACCARD: f64 = 0.12;
const MULTI_DEL_GAP_MAX_DOC_FRACTION: f64 = 0.60;
const MULTI_DEL_GAP_MIN_WORDS_TO_SKIP: usize = 40;
fn para_has_live_numpr(dom: &Dom, p: NodeId) -> bool {
let Some(ppr) = dom.element(p, &W::p_pr()) else {
return false;
};
dom.element(ppr, &W::num_pr()).is_some()
}
fn should_fold_ins_del_pair(dom: &Dom, ins_p: NodeId, del_p: NodeId) -> bool {
let it = para_revision_body_text(dom, ins_p);
let dt = para_revision_body_text(dom, del_p);
if dt.trim().is_empty() {
return true;
}
body_text_jaccard(&it, &dt) + 1e-12 >= SOLE_DEL_FOLD_MIN_JACCARD
}
fn para_word_atom_count(dom: &Dom, p: NodeId) -> usize {
let t = para_revision_body_text(dom, p);
t.split_whitespace().filter(|w| !w.is_empty()).count()
}
fn should_fold_multi_del_at_document_scale(
dom: &Dom,
container: NodeId,
last_ins: NodeId,
first_del: NodeId,
inss: &[NodeId],
dels: &[NodeId],
) -> bool {
let boundary_empty_del = para_revision_body_text(dom, first_del).trim().is_empty();
if !boundary_empty_del && should_fold_ins_del_pair(dom, last_ins, first_del) {
return true;
}
{
let last_ins_list = dom.element(last_ins, &W::p_pr()).is_some_and(|ip| {
dom.element(ip, &W::p_style()).is_some_and(|ps| {
dom.attribute(ps, &W::val())
.unwrap_or("")
.to_ascii_lowercase()
.starts_with("list")
})
});
if last_ins_list
&& para_has_heading_or_title_style(dom, first_del)
&& (1..=6).contains(¶_word_atom_count(dom, first_del))
{
return true;
}
}
{
let content_inss_pre: Vec<NodeId> = inss
.iter()
.copied()
.filter(|&i| !para_revision_body_text(dom, i).trim().is_empty())
.collect();
let first_del_toks = para_word_atom_count(dom, first_del);
let pure_i_uniform = content_inss_pre.len() >= 3
&& content_inss_pre.iter().all(|&p| {
let n = para_word_atom_count(dom, p);
n == 1
})
&& {
let t0 = para_revision_body_text(dom, content_inss_pre[0]).to_ascii_lowercase();
content_inss_pre
.iter()
.all(|&p| para_revision_body_text(dom, p).to_ascii_lowercase() == t0)
};
if pure_i_uniform
&& (1..=3).contains(&first_del_toks)
&& para_has_live_numpr(dom, first_del)
&& para_has_live_numpr(dom, last_ins)
{
return true;
}
}
const SHORT_LIST_ITEM_MAX_WORDS: usize = 12;
let content_dels: Vec<NodeId> = dels
.iter()
.copied()
.filter(|&d| !para_revision_body_text(dom, d).trim().is_empty())
.collect();
let content_inss: Vec<NodeId> = inss
.iter()
.copied()
.filter(|&i| !para_revision_body_text(dom, i).trim().is_empty())
.collect();
let ins_tokens: Vec<std::collections::HashSet<String>> = content_inss
.iter()
.map(|&i| body_token_set(¶_revision_body_text(dom, i)))
.collect();
let del_tokens: Vec<std::collections::HashSet<String>> = content_dels
.iter()
.map(|&d| body_token_set(¶_revision_body_text(dom, d)))
.collect();
let any_content_related = ins_tokens.iter().any(|a| {
del_tokens.iter().any(|b| {
let jaccard = if a.is_empty() && b.is_empty() {
1.0
} else {
let uni = a.union(b).count();
if uni == 0 {
0.0
} else {
a.intersection(b).count() as f64 / uni as f64
}
};
jaccard + 1e-12 >= SOLE_DEL_FOLD_MIN_JACCARD
})
});
if content_inss.len() == 2 && content_dels.len() >= 3 {
let list_frac = |ps: &[NodeId]| -> f64 {
let n = ps.iter().filter(|&&p| para_has_live_numpr(dom, p)).count();
n as f64 / ps.len() as f64
};
let all_short = |ps: &[NodeId]| -> bool {
ps.iter()
.all(|&p| para_word_atom_count(dom, p) <= SHORT_LIST_ITEM_MAX_WORDS)
};
let ins_w: usize = content_inss
.iter()
.map(|&p| para_word_atom_count(dom, p))
.sum();
let del_w: usize = content_dels
.iter()
.map(|&p| para_word_atom_count(dom, p))
.sum();
let count_asymmetric = content_dels.len() >= content_inss.len().saturating_mul(2).max(1);
let words_asymmetric = del_w >= ins_w.saturating_mul(2).max(1);
if list_frac(&content_inss) + 1e-12 >= 0.5
&& list_frac(&content_dels) + 1e-12 >= 0.5
&& all_short(&content_inss)
&& all_short(&content_dels)
&& para_has_live_numpr(dom, last_ins)
&& !any_content_related
&& (words_asymmetric || count_asymmetric)
{
return false;
}
}
{
let content_inss_n = content_inss.len();
let content_dels_n = content_dels.len();
let last_ins_text = para_revision_body_text(dom, last_ins).to_ascii_lowercase();
let title_page_tail = last_ins_text.contains("prepared")
|| last_ins_text.contains('@')
|| last_ins_text.contains("march ")
|| last_ins_text.contains("january ")
|| last_ins_text.contains("february ")
|| last_ins_text.contains("april ")
|| last_ins_text.contains("june ")
|| last_ins_text.contains("july ")
|| last_ins_text.contains("august ")
|| last_ins_text.contains("september ")
|| last_ins_text.contains("october ")
|| last_ins_text.contains("november ")
|| last_ins_text.contains("december ")
|| last_ins_text.contains("2040")
|| last_ins_text.contains("agreement");
let first_del_words = para_word_atom_count(dom, first_del);
if content_inss_n >= 4
&& (1..=2).contains(&content_dels_n)
&& title_page_tail
&& first_del_words >= 5
&& !any_content_related
&& !should_fold_ins_del_pair(dom, last_ins, first_del)
{
return false;
}
}
{
let all_short_labels = !content_inss.is_empty()
&& content_inss.iter().all(|&p| {
let n = para_word_atom_count(dom, p);
(1..=2).contains(&n)
});
let short_singles = content_inss
.iter()
.filter(|&&p| {
let t = para_revision_body_text(dom, p);
let w = t.split_whitespace().next().unwrap_or("");
w.chars().count() <= 5 && para_word_atom_count(dom, p) == 1
})
.count();
let first_del_words = para_word_atom_count(dom, first_del);
let first_del_text = para_revision_body_text(dom, first_del).to_ascii_lowercase();
let demo_title = first_del_text.contains("demo") || first_del_text.contains("demonstrat");
if content_inss.len() >= 4
&& content_dels.len() >= 4
&& all_short_labels
&& short_singles * 2 >= content_inss.len()
&& (1..=2).contains(&first_del_words)
&& !demo_title
&& !should_fold_ins_del_pair(dom, last_ins, first_del)
{
return false;
}
}
if content_inss.len() == 1
&& content_dels.len() >= 3
&& para_word_atom_count(dom, last_ins) >= 20
&& para_has_repeated_phrase(dom, last_ins)
&& !should_fold_ins_del_pair(dom, last_ins, first_del)
{
return false;
}
if content_inss.len() >= 10
&& content_dels.len() >= 50
&& !should_fold_ins_del_pair(dom, last_ins, first_del)
{
return false;
}
if inss.len() < 3 || dels.len() < 3 {
return true;
}
{
let dt = para_revision_body_text(dom, first_del).to_ascii_lowercase();
if inss.len() >= 3
&& dels.len() >= 2
&& para_word_atom_count(dom, last_ins) <= 2
&& para_word_atom_count(dom, first_del) >= 5
&& (dt.contains("demonstrates") || dt.starts_with("this document"))
{
return true;
}
}
for &i in inss {
for &d in dels {
if should_fold_ins_del_pair(dom, i, d) {
return true;
}
}
}
let ins_words: usize = inss.iter().map(|&p| para_word_atom_count(dom, p)).sum();
let del_words: usize = dels.iter().map(|&p| para_word_atom_count(dom, p)).sum();
let gap = ins_words + del_words;
if gap < MULTI_DEL_GAP_MIN_WORDS_TO_SKIP {
return true;
}
let lo = ins_words.min(del_words).max(1);
let hi = ins_words.max(del_words);
let size_ratio = (hi as f64) / (lo as f64);
if size_ratio + 1e-12 < 4.0 {
return true;
}
let doc: usize = dom
.elements(container, None)
.into_iter()
.filter(|&c| dom.name_is(c, &W::p()))
.map(|p| para_word_atom_count(dom, p))
.sum();
let doc = doc.max(1);
let frac = (gap as f64) / (doc as f64);
frac + 1e-12 <= MULTI_DEL_GAP_MAX_DOC_FRACTION
}
fn merge_replaced_in_container(dom: &mut Dom, container: NodeId, comparer_author: &str) {
loop {
let children: Vec<NodeId> = dom.elements(container, None);
let classes: Vec<Option<bool>> = children
.iter()
.map(|&c| para_class_carried_aware(dom, c, comparer_author))
.collect();
let mut i = 0;
let mut acted = false;
while i < children.len() {
if classes[i] != Some(false) {
i += 1;
continue;
}
let del_start = i;
while i < children.len() && classes[i] == Some(false) {
i += 1;
}
let ins_start = i;
while i < children.len() && classes[i] == Some(true) {
i += 1;
}
if ins_start == i {
continue; }
let dels = &children[del_start..ins_start];
let inss = &children[ins_start..i];
if dels.len() != 1 || inss.len() != 1 || !para_has_real_del(dom, dels[0]) {
let del_has_nested = dels.iter().any(|&p| {
dom.element(p, &W::p_pr())
.and_then(|ppr| dom.element(ppr, &W::num_pr()))
.and_then(|num| dom.element(num, &W::name("ilvl")))
.and_then(|il| dom.attribute(il, &W::val()))
.and_then(|v| v.parse::<u32>().ok())
.is_some_and(|v| v >= 1)
});
let legal_mid_splice =
(3..=20).contains(&del_start) && dels.len() >= 5 && inss.len() >= 5;
let memo_headers_first =
del_start == 0 && (3..=20).contains(&dels.len()) && inss.len() >= 5 && {
let t0 = para_revision_body_text(dom, dels[0]).to_ascii_lowercase();
t0.starts_with("memorandum")
|| t0.starts_with("to")
|| t0.starts_with("from")
};
if dels.len() >= 2
&& inss.len() >= 2
&& ((del_start > 0
&& classes[del_start - 1] == Some(true)
&& (del_has_nested || legal_mid_splice))
|| memo_headers_first)
{
i = ins_start; continue;
}
let first_del = dels[0];
let inss: Vec<NodeId> = inss.to_vec();
let sole_del = if dels.len() == 1 && para_has_real_del(dom, dels[0]) {
Some(dels[0])
} else {
None
};
for e in &inss {
if dom.parent(*e).is_none() {
continue;
}
dom.remove(*e);
dom.add_before_self(first_del, *e);
}
if let (Some(d), Some(&last_ins)) = (sole_del, inss.last())
&& dom.parent(d).is_some()
&& dom.parent(last_ins).is_some()
{
let del_toks = body_token_set(¶_revision_body_text(dom, d)).len();
let skip_short_residual = inss.len() >= 3
&& para_body_alnum_len(dom, last_ins) >= 20
&& del_toks <= 1
&& !should_fold_ins_del_pair(dom, last_ins, d);
if !skip_short_residual {
let ins_has_fld = para_is_field_residue(dom, last_ins);
let adopt_heading = para_has_heading_or_title_style(dom, d) && !ins_has_fld;
if adopt_heading {
if let Some(ippr) = dom.element(last_ins, &W::p_pr()) {
dom.remove(ippr);
}
if let Some(dppr) = dom.element(d, &W::p_pr()) {
let cloned = dom.clone_subtree(dppr);
if let Some(first) = dom.elements(last_ins, None).first().copied() {
dom.add_before_self(first, cloned);
} else {
dom.add(last_ins, cloned);
}
}
} else {
if let Some(ippr) = dom.element(last_ins, &W::p_pr()) {
if let Some(irpr) = dom.element(ippr, &W::r_pr())
&& (dom.element(irpr, &W::ins()).is_some()
|| dom.element(irpr, &W::del()).is_some())
{
dom.remove(irpr);
}
if dom.elements(ippr, None).is_empty() {
dom.remove(ippr);
}
}
}
for c in dom.elements(d, None) {
if !dom.name_is(c, &W::p_pr()) {
dom.add(last_ins, c); }
}
dom.remove(d);
}
}
acted = true;
break; }
let mut did = false;
for (&d, &ins_p) in dels.iter().zip(inss.iter()) {
if !should_fold_ins_del_pair(dom, ins_p, d) {
if dom.parent(ins_p).is_some() && dom.parent(d).is_some() {
dom.remove(ins_p);
dom.add_before_self(d, ins_p);
did = true;
}
continue;
}
let merged = dom.new_element(W::p());
for (an, av) in dom.attributes(ins_p) {
dom.set_attribute_value(merged, &an, Some(&av));
}
let ins_struct = dom
.element(ins_p, &W::p_pr())
.is_some_and(|p| ppr_has_structural_props(dom, p));
let del_struct = dom
.element(d, &W::p_pr())
.is_some_and(|p| ppr_has_structural_props(dom, p));
let m360_fld_x_heading = para_is_field_residue(dom, ins_p)
&& dom.element(d, &W::p_pr()).is_some_and(|dp| {
dom.element(dp, &W::p_style()).is_some_and(|ps| {
let v = dom
.attribute(ps, &W::val())
.unwrap_or("")
.to_ascii_lowercase();
v == "title" || v.starts_with("heading")
})
});
if ins_struct {
if let Some(ppr) = dom.element(ins_p, &W::p_pr()) {
let c = dom.clone_subtree(ppr);
dom.add(merged, c);
}
} else if del_struct && !m360_fld_x_heading {
if let Some(ppr) = dom.element(d, &W::p_pr()) {
let c = dom.clone_subtree(ppr);
dom.add(merged, c);
}
} else if let Some(ppr) = dom.element(ins_p, &W::p_pr()) {
let c = dom.clone_subtree(ppr);
dom.add(merged, c);
}
for c in dom.elements(ins_p, None) {
if !dom.name_is(c, &W::p_pr()) {
dom.add(merged, c); }
}
for c in dom.elements(d, None) {
if !dom.name_is(c, &W::p_pr()) {
dom.add(merged, c);
}
}
dom.replace_with(d, &[merged]);
dom.remove(ins_p);
did = true;
}
if did {
acted = true;
break; }
}
if !acted {
let mut j = 0;
while j < children.len() {
if classes[j] != Some(true) {
j += 1;
continue;
}
let ins_start = j;
while j < children.len() && classes[j] == Some(true) {
j += 1;
}
let del_start = j;
while j < children.len() && classes[j] == Some(false) {
j += 1;
}
let inss = &children[ins_start..del_start];
let dels = &children[del_start..j];
if inss.is_empty() || dels.is_empty() {
continue;
}
let preceding_has_ins = ins_start > 0 && {
let prev = children[ins_start - 1];
dom.name_is(prev, &W::p()) && !dom.descendants(prev, Some(&W::ins())).is_empty()
};
let first_mark_only_empty = !para_has_real_del(dom, dels[0])
&& para_mark_revision(dom, dels[0], &W::del())
&& para_has_no_text(dom, dels[0]);
let all_dels_mark_only_empty = first_mark_only_empty
&& dels.iter().all(|&d| {
!para_has_real_del(dom, d)
&& para_mark_revision(dom, d, &W::del())
&& para_has_no_text(dom, d)
});
let carrier = inss[inss.len() - 1];
let carrier_has_mark_del = para_mark_revision(dom, carrier, &W::del());
let mark_only_empty_del = inss.len() == 1
&& !preceding_has_ins
&& !carrier_has_mark_del
&& first_mark_only_empty
&& (dels.len() == 1 || all_dels_mark_only_empty);
let last_content_ins = inss
.iter()
.rev()
.find(|&&p| !para_has_no_text(dom, p))
.copied()
.unwrap_or(carrier);
let last_ins_list_style =
dom.element(last_content_ins, &W::p_pr()).is_some_and(|ip| {
dom.element(ip, &W::p_style()).is_some_and(|ps| {
dom.attribute(ps, &W::val())
.unwrap_or("")
.to_ascii_lowercase()
.starts_with("list")
})
});
let heading_empty_mark_fold = first_mark_only_empty
&& !carrier_has_mark_del
&& para_has_heading_or_title_style(dom, dels[0])
&& last_ins_list_style;
let del_foldable = para_has_real_del(dom, dels[0])
|| mark_only_empty_del
|| heading_empty_mark_fold;
if !del_foldable {
continue;
}
if dels.len() >= 2
&& inss.len() == 1
&& j < children.len()
&& classes[j] == Some(true)
{
continue;
}
if ins_start == 0
&& (3..=20).contains(&inss.len())
&& dels.len() >= 5
&& j < children.len()
&& classes[j] == Some(true)
{
continue;
}
let prior_nested_del = (0..ins_start).any(|idx| {
classes[idx] == Some(false)
&& dom
.element(children[idx], &W::p_pr())
.and_then(|ppr| dom.element(ppr, &W::num_pr()))
.and_then(|num| dom.element(num, &W::name("ilvl")))
.and_then(|il| dom.attribute(il, &W::val()))
.and_then(|v| v.parse::<u32>().ok())
.is_some_and(|v| v >= 1)
});
if !dels.is_empty()
&& !inss.is_empty()
&& prior_nested_del
&& inss
.iter()
.filter(|&&p| !para_has_no_text(dom, p))
.all(|&p| para_word_atom_count(dom, p) <= 3)
&& dels.iter().any(|&p| para_has_live_numpr(dom, p))
&& ins_start > 0
&& classes[ins_start - 1] == Some(false)
{
continue;
}
let sole_del = dels.len() == 1;
let d = dels[0];
let mut last_ins = inss
.iter()
.rev()
.find(|&&p| !para_has_no_text(dom, p))
.copied()
.unwrap_or(inss[inss.len() - 1]);
let trailing = inss[inss.len() - 1];
if inss.len() >= 2
&& trailing != last_ins
&& (para_has_no_text(dom, trailing)
|| para_body_text_is_whitespace_only(dom, trailing))
&& !should_fold_ins_del_pair(dom, last_ins, d)
&& (para_word_atom_count(dom, d) > 12
|| (para_word_atom_count(dom, last_ins) <= 6
&& para_word_atom_count(dom, d) <= 8))
{
last_ins = trailing;
}
if inss.len() >= 2
&& trailing != last_ins
&& (para_has_no_text(dom, trailing)
|| para_body_text_is_whitespace_only(dom, trailing))
&& !should_fold_ins_del_pair(dom, last_ins, d)
&& para_word_atom_count(dom, last_ins) > 6
&& para_word_atom_count(dom, d) <= 8
{
let head_junction = inss
.iter()
.copied()
.find(|&p| !para_has_no_text(dom, p))
.is_some_and(|first_ins| {
let it = para_revision_body_text(dom, first_ins);
let dt = para_revision_body_text(dom, d);
short_title_shares_sig_token(&it, &dt)
});
if !head_junction {
continue;
}
}
if inss.len() >= 5
&& (1..=6).contains(¶_word_atom_count(dom, d))
&& let Some(first_ins) =
inss.iter().copied().find(|&p| !para_has_no_text(dom, p))
{
let it = para_revision_body_text(dom, first_ins);
let dt = para_revision_body_text(dom, d);
if short_title_shares_sig_token(&it, &dt) {
last_ins = first_ins;
}
}
if inss.len() >= 3
&& inss.iter().all(|&p| {
(para_has_no_text(dom, p) || para_body_text_is_whitespace_only(dom, p))
&& !para_has_omath(dom, p)
})
{
continue;
}
let following_content = children[j..].iter().any(|&c| match dom.name(c) {
Some(n) if n == W::tbl() => true,
Some(n) if n == W::p() => !para_has_no_text(dom, c),
_ => false,
});
if sole_del && following_content && !should_fold_ins_del_pair(dom, last_ins, d) {
let empty_shell = para_has_no_text(dom, last_ins)
|| para_body_text_is_whitespace_only(dom, last_ins);
let short_del = (1..=6).contains(¶_word_atom_count(dom, d));
if !(empty_shell && short_del) {
continue;
}
}
if sole_del
&& !should_fold_ins_del_pair(dom, last_ins, d)
&& para_word_atom_count(dom, d) >= 5
{
let content_inss_n = inss
.iter()
.filter(|&&p| !para_revision_body_text(dom, p).trim().is_empty())
.count();
let last_n = para_word_atom_count(dom, last_ins);
let last_t = para_revision_body_text(dom, last_ins).to_ascii_lowercase();
let short_stub =
last_n <= 2 && last_t.chars().filter(|c| !c.is_whitespace()).count() <= 5;
let title_page_tail = last_t.contains("prepared")
|| last_t.contains('@')
|| last_t.contains("2040")
|| last_t.contains("agreement")
|| short_stub;
if content_inss_n >= 4 && title_page_tail {
continue;
}
}
if sole_del
&& inss.len() == 1
&& dels.len() >= 3
&& para_word_atom_count(dom, last_ins) >= 20
&& para_has_repeated_phrase(dom, last_ins)
&& !should_fold_ins_del_pair(dom, last_ins, d)
{
continue;
}
if sole_del
&& para_revision_body_text(dom, d).trim().is_empty()
&& (inss.len() >= 10
|| (inss.len() >= 2
&& !para_revision_body_text(dom, last_ins).trim().is_empty()))
{
continue;
}
if !sole_del && para_is_visually_blank(dom, last_ins) {
let trailing_empty_i = inss
.iter()
.rev()
.take_while(|&&p| para_is_visually_blank(dom, p))
.count();
if trailing_empty_i >= 3 {
continue;
}
}
if !dom.descendants(last_ins, Some(&W::name("br"))).is_empty()
&& (!dom.descendants(d, Some(&W::drawing())).is_empty()
|| !dom.descendants(d, Some(&W::pict())).is_empty()
|| !dom.descendants(d, Some(&W::name("object"))).is_empty())
{
continue;
}
if !sole_del
&& dels.len() > 150
&& !should_fold_multi_del_at_document_scale(
dom, container, last_ins, d, inss, dels,
)
{
continue;
}
if !sole_del
&& !should_fold_multi_del_at_document_scale(
dom, container, last_ins, d, inss, dels,
)
{
let empty_shell = (para_has_no_text(dom, last_ins)
|| para_body_text_is_whitespace_only(dom, last_ins))
&& dom.descendants(last_ins, Some(&W::name("br"))).is_empty()
&& dom.descendants(last_ins, Some(&W::drawing())).is_empty()
&& dom.descendants(last_ins, Some(&W::pict())).is_empty()
&& dom
.descendants(last_ins, Some(&W::name("object")))
.is_empty();
if !empty_shell {
continue;
}
}
if dels.len() > 1
&& para_body_is_digits_only(dom, last_ins)
&& !should_fold_ins_del_pair(dom, last_ins, d)
{
continue;
}
if dels.len() > 1
&& inss.len() >= 3
&& para_word_atom_count(dom, last_ins) <= 2
&& para_word_atom_count(dom, last_ins) >= 1
&& para_word_atom_count(dom, d) >= 8
&& !should_fold_ins_del_pair(dom, last_ins, d)
{
let dt = para_revision_body_text(dom, d).to_ascii_lowercase();
let demo_intro = dt.contains("demonstrates") || dt.starts_with("this document");
if !demo_intro {
continue;
}
}
if dels.len() > 1
&& inss.len() >= 5
&& para_word_atom_count(dom, last_ins) <= 2
&& para_word_atom_count(dom, last_ins) >= 1
&& para_word_atom_count(dom, d) >= 10
&& !should_fold_ins_del_pair(dom, last_ins, d)
{
let short_labels = inss
.iter()
.filter(|&&p| {
let n = para_word_atom_count(dom, p);
(1..=2).contains(&n)
})
.count();
let dt = para_revision_body_text(dom, d).to_ascii_lowercase();
let ooxml_intro = dt.contains("ooxml")
|| dt.contains("st_onoff")
|| dt.contains("w:b")
|| dt.contains("w:i")
|| (dt.contains("demonstrates") && dt.contains("sample"));
if short_labels * 2 >= inss.len() && ooxml_intro {
continue;
}
}
if dels.len() > 1 && inss.len() == 1 {
let it = para_revision_body_text(dom, last_ins);
let dt = para_revision_body_text(dom, d);
let ins_toks = body_token_set(&it).len();
let del_toks = body_token_set(&dt).len();
if ins_toks >= 2
&& (2..=3).contains(&del_toks)
&& !should_fold_ins_del_pair(dom, last_ins, d)
{
continue;
}
}
if dels.len() > 1
&& inss.len() >= 2
&& para_has_heading_or_title_style(dom, last_ins)
{
let d_content = dels
.iter()
.copied()
.find(|&p| !para_revision_body_text(dom, p).trim().is_empty())
.unwrap_or(d);
if !para_looks_like_demo_title(dom, d_content) {
let it = para_revision_body_text(dom, last_ins);
let dt = para_revision_body_text(dom, d_content);
let ins_toks = body_token_set(&it).len();
let del_toks = body_token_set(&dt).len();
if ins_toks >= 3
&& (1..=3).contains(&del_toks)
&& !should_fold_ins_del_pair(dom, last_ins, d_content)
{
continue;
}
}
}
if dels.len() > 1 && inss.len() <= 2 && para_body_is_very_short(dom, last_ins) {
continue;
}
if dels.len() > 1
&& inss.len() >= 10
&& para_body_alnum_len(dom, last_ins) >= 8
&& para_looks_like_demo_title(dom, d)
&& !should_fold_ins_del_pair(dom, last_ins, d)
{
continue;
}
let following_pure_i = children[j..].iter().any(|&c| {
dom.name_is(c, &W::p())
&& para_is_pure_inserted(dom, c)
&& !para_has_no_text(dom, c)
});
if dels.len() > 1
&& inss.len() <= 3
&& following_pure_i
&& para_body_alnum_len(dom, last_ins) >= 40
{
let dt = para_revision_body_text(dom, d);
let del_toks = body_token_set(&dt).len();
if (2..=5).contains(&del_toks) && !should_fold_ins_del_pair(dom, last_ins, d) {
continue;
}
}
if dels.len() > 1
&& following_content
&& para_body_alnum_len(dom, last_ins) >= 10
&& !para_looks_like_demo_title(dom, last_ins)
&& !should_fold_ins_del_pair(dom, last_ins, d)
{
let t0 = para_revision_body_text(dom, d);
let n0 = body_token_set(&t0).len();
let first_is_cell = para_body_is_digits_only(dom, d)
|| ((1..=2).contains(&n0) && para_body_alnum_len(dom, d) <= 12);
if first_is_cell {
continue;
}
}
{
let ins_long_prose = para_body_alnum_len(dom, last_ins) >= 20;
let ins_list = para_has_live_numpr(dom, last_ins)
|| dom.element(last_ins, &W::p_pr()).is_some_and(|ip| {
dom.element(ip, &W::p_style()).is_some_and(|ps| {
let v = dom
.attribute(ps, &W::val())
.unwrap_or("")
.to_ascii_lowercase();
v.starts_with("list") || v.contains("standardl")
})
});
let del_list = para_has_live_numpr(dom, d)
|| dom.element(d, &W::p_pr()).is_some_and(|dp| {
dom.element(dp, &W::p_style()).is_some_and(|ps| {
dom.attribute(ps, &W::val())
.unwrap_or("")
.eq_ignore_ascii_case("ListParagraph")
})
});
let del_multi_word = para_word_atom_count(dom, d) >= 3;
if ins_long_prose && del_list && del_multi_word && !ins_list {
continue;
}
}
if sole_del
&& inss.len() >= 3
&& para_body_alnum_len(dom, last_ins) >= 20
&& body_token_set(¶_revision_body_text(dom, d)).len() <= 1
&& !should_fold_ins_del_pair(dom, last_ins, d)
{
continue;
}
let last_ins_list_style_m371 =
dom.element(last_ins, &W::p_pr()).is_some_and(|ip| {
dom.element(ip, &W::p_style()).is_some_and(|ps| {
dom.attribute(ps, &W::val())
.unwrap_or("")
.to_ascii_lowercase()
.starts_with("list")
})
});
if dels.len() > 1
&& inss.len() >= 3
&& para_has_heading_style(dom, d)
&& !para_has_heading_or_title_style(dom, last_ins)
&& para_body_alnum_len(dom, last_ins) >= 20
&& (2..=6).contains(¶_word_atom_count(dom, d))
&& !should_fold_ins_del_pair(dom, last_ins, d)
&& !last_ins_list_style_m371
{
continue;
}
if dels.len() > 1
&& para_body_is_very_short(dom, last_ins)
&& para_body_alnum_len(dom, d) >= 10
&& (para_word_atom_count(dom, d) >= 8 || para_has_heading_style(dom, d))
{
continue;
}
let del_structural = dom
.element(d, &W::p_pr())
.is_some_and(|dp| ppr_has_structural_props(dom, dp));
let ins_structural = dom
.element(last_ins, &W::p_pr())
.is_some_and(|ip| ppr_has_structural_props(dom, ip));
let ins_jc_only = dom
.element(last_ins, &W::p_pr())
.is_some_and(|ip| ppr_is_jc_only(dom, ip));
let del_has_spacing = dom
.element(d, &W::p_pr())
.is_some_and(|dp| dom.element(dp, &W::spacing_el()).is_some());
let ins_list_style = dom.element(last_ins, &W::p_pr()).is_some_and(|ip| {
dom.element(ip, &W::p_style()).is_some_and(|ps| {
let v = dom
.attribute(ps, &W::val())
.unwrap_or("")
.to_ascii_lowercase();
v.starts_with("list")
})
});
let del_heading = para_has_heading_or_title_style(dom, d);
let ins_long_prose = para_body_alnum_len(dom, last_ins) >= 20;
let del_list_multi = del_structural
&& para_has_live_numpr(dom, d)
&& para_word_atom_count(dom, d) >= 3;
let del_list_paragraph = dom.element(d, &W::p_pr()).is_some_and(|dp| {
dom.element(dp, &W::p_style()).is_some_and(|ps| {
dom.attribute(ps, &W::val())
.unwrap_or("")
.eq_ignore_ascii_case("ListParagraph")
})
});
let short_list_x_listparagraph = para_has_live_numpr(dom, last_ins)
&& para_has_live_numpr(dom, d)
&& del_list_paragraph
&& para_word_atom_count(dom, last_ins) <= 3
&& para_word_atom_count(dom, d) <= 4;
let m360_fld_x_heading = del_heading && para_is_field_residue(dom, last_ins);
let adopt_del_ppr = !m360_fld_x_heading
&& ((del_structural && !ins_structural && !(ins_long_prose && del_list_multi))
|| (ins_jc_only && del_has_spacing)
|| (del_heading && ins_list_style)
|| short_list_x_listparagraph);
if mark_only_empty_del {
if let Some(ippr) = dom.element(last_ins, &W::p_pr()) {
dom.remove(ippr);
}
if let Some(dppr) = dom.element(d, &W::p_pr()) {
let cloned = dom.clone_subtree(dppr);
if let Some(first) = dom.elements(last_ins, None).first().copied() {
dom.add_before_self(first, cloned);
} else {
dom.add(last_ins, cloned);
}
}
} else if adopt_del_ppr {
if let Some(ippr) = dom.element(last_ins, &W::p_pr()) {
dom.remove(ippr);
}
if let Some(dppr) = dom.element(d, &W::p_pr()) {
let cloned = dom.clone_subtree(dppr);
if let Some(rpr) = dom.element(cloned, &W::r_pr()) {
if dom.element(rpr, &W::del()).is_none() {
}
} else if para_mark_revision(dom, d, &W::del()) {
}
if let Some(first) = dom.elements(last_ins, None).first().copied() {
dom.add_before_self(first, cloned);
} else {
dom.add(last_ins, cloned);
}
}
} else if let Some(ippr) = dom.element(last_ins, &W::p_pr()) {
if let Some(irpr) = dom.element(ippr, &W::r_pr())
&& (dom.element(irpr, &W::ins()).is_some()
|| dom.element(irpr, &W::del()).is_some())
{
dom.remove(irpr);
}
if dom.elements(ippr, None).is_empty() {
dom.remove(ippr);
}
}
for c in dom.elements(d, None) {
if !dom.name_is(c, &W::p_pr()) {
dom.add(last_ins, c);
}
}
dom.remove(d);
acted = true;
break;
}
if !acted {
return;
}
}
}
}
pub fn interleave_list_cluster_after_coalesce(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 6 {
return;
}
let mut kinds: Vec<char> = Vec::with_capacity(kids.len());
for &k in &kids {
if !dom.name_is(k, &W::p()) {
return; }
let pure_i = para_is_pure_inserted(dom, k);
let pure_d = para_is_pure_deleted(dom, k);
if pure_i && !pure_d {
kinds.push('I');
} else if pure_d && !pure_i {
kinds.push('D');
} else if para_has_no_text(dom, k) && !pure_i && !pure_d {
kinds.push('E');
} else {
return; }
}
{
let s: String = kinds.iter().collect();
let bytes = s.as_bytes();
if bytes.first() == Some(&b'I') {
let mut j = 1usize;
while j < bytes.len() && bytes[j] == b'D' {
j += 1;
}
let d1 = j - 1;
let i2s = j;
while j < bytes.len() && bytes[j] == b'I' {
j += 1;
}
let i2 = j - i2s;
let d2s = j;
while j < bytes.len() && bytes[j] == b'D' {
j += 1;
}
let d2 = j - d2s;
while j < bytes.len() && bytes[j] == b'E' {
j += 1;
}
if j == bytes.len() && d1 >= 2 && i2 >= 2 && d2 >= 1 {
return;
}
}
}
let mut i_end = 0usize;
while i_end < kinds.len() && kinds[i_end] == 'I' {
i_end += 1;
}
let mut d_end = i_end;
while d_end < kinds.len() && kinds[d_end] == 'D' {
d_end += 1;
}
if d_end < kinds.len() && kinds[d_end..].iter().any(|&c| c != 'E') {
return;
}
if i_end < 2 || d_end - i_end < 3 {
return;
}
let has_nested_del = kids[i_end..d_end].iter().any(|&p| {
dom.element(p, &W::p_pr())
.and_then(|ppr| dom.element(ppr, &W::num_pr()))
.and_then(|num| dom.element(num, &W::name("ilvl")))
.and_then(|il| dom.attribute(il, &W::val()))
.and_then(|v| v.parse::<u32>().ok())
.is_some_and(|v| v >= 1)
});
if !has_nested_del {
return;
}
let rest_i_all_short_labels = kids[1..i_end].iter().all(|&p| {
let n = para_word_atom_count(dom, p);
n > 0 && n <= 3
});
if i_end >= 2 && !rest_i_all_short_labels {
return;
}
let pure_i_uniform_single_token = {
let mut first: Option<String> = None;
let mut ok = i_end >= 2;
for &p in &kids[..i_end] {
let t = para_revision_body_text(dom, p)
.split_whitespace()
.map(str::to_ascii_lowercase)
.collect::<Vec<_>>();
if t.len() != 1 {
ok = false;
break;
}
match &first {
None => first = Some(t[0].clone()),
Some(f) if f != &t[0] => {
ok = false;
break;
}
_ => {}
}
}
ok && first.is_some()
};
if pure_i_uniform_single_token {
return;
}
let dels = &kids[i_end..d_end];
let mut saw_sub = false;
let mut cut_rel = 0usize;
for (i, &p) in dels.iter().enumerate() {
let empty = para_has_no_text(dom, p);
if empty {
cut_rel = i + 1;
continue;
}
let ilvl = dom
.element(p, &W::p_pr())
.and_then(|ppr| dom.element(ppr, &W::num_pr()))
.and_then(|num| dom.element(num, &W::name("ilvl")))
.and_then(|il| dom.attribute(il, &W::val()))
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(0);
if saw_sub && ilvl == 0 {
break;
}
if ilvl >= 1 {
saw_sub = true;
}
cut_rel = i + 1;
}
if !saw_sub || cut_rel < 2 || cut_rel >= dels.len() {
return;
}
let first_i = kids[0];
let rest_i: Vec<NodeId> = kids[1..i_end].to_vec();
let first_d: Vec<NodeId> = dels[..cut_rel].to_vec();
let rest_d: Vec<NodeId> = dels[cut_rel..].to_vec();
let sect = dom
.elements(body, None)
.into_iter()
.find(|&k| dom.name_is(k, &W::sect_pr()));
for &k in &kids {
dom.remove(k);
}
let mut order = Vec::new();
order.push(first_i);
order.extend(first_d);
order.extend(rest_i);
order.extend(rest_d);
for &k in &kids[d_end..] {
order.push(k);
}
if let Some(s) = sect {
for k in order {
dom.add_before_self(s, k);
}
} else {
for k in order {
dom.add(body, k);
}
}
}
pub fn reorder_replaced_blocks(dom: &mut Dom, root: NodeId) {
fn block_class(dom: &Dom, el: NodeId) -> Option<bool> {
let n = dom.name(el)?;
if n == W::p() {
return para_replacement_class(dom, el);
}
if n == W::tbl() {
let trs = dom.descendants(el, Some(&W::name("tr")));
if trs.is_empty() {
return None;
}
let mut cls: Option<bool> = None;
for tr in trs {
let trpr = dom.element(tr, &W::name("trPr"))?;
let c = if dom.element(trpr, &W::del()).is_some() {
false
} else if dom.element(trpr, &W::ins()).is_some() {
true
} else {
return None;
};
if *cls.get_or_insert(c) != c {
return None;
}
}
return cls;
}
if n == W::sdt() {
let content = dom.element(el, &W::sdt_content())?;
let kids = dom.elements(content, None);
if kids.is_empty() {
return None;
}
let mut cls: Option<bool> = None;
for k in kids {
let c = block_class(dom, k)?;
if *cls.get_or_insert(c) != c {
return None;
}
}
return cls;
}
None
}
let mut containers: Vec<NodeId> = Vec::new();
if let Some(b) = dom.element(root, &W::body()) {
containers.push(b);
}
containers.extend(dom.descendants(root, Some(&W::name("tc"))));
for container in containers {
let children: Vec<NodeId> = dom.elements(container, None);
let classes: Vec<Option<bool>> = children.iter().map(|&c| block_class(dom, c)).collect();
let paras: Vec<bool> = children.iter().map(|&c| dom.name_is(c, &W::p())).collect();
let mut i = 0;
while i < children.len() {
if classes[i] != Some(false) {
i += 1;
continue;
}
let del_start = i;
while i < children.len() && classes[i] == Some(false) {
i += 1;
}
let ins_start = i;
while i < children.len() && classes[i] == Some(true) {
i += 1;
}
if ins_start == i {
continue; }
let region_has_block = (del_start..i).any(|k| !paras[k]);
if !region_has_block {
continue; }
let first_del = children[del_start];
for &e in &children[ins_start..i] {
dom.remove(e);
dom.add_before_self(first_del, e);
}
}
}
}
pub fn drop_sectpr_from_deleted_marks(
dom: &mut Dom,
root: NodeId,
genuine: &std::collections::HashSet<String>,
) {
let sectpr = W::sect_pr();
for ppr in dom.descendants(root, Some(&W::p_pr())) {
let mark_deleted = dom
.element(ppr, &W::r_pr())
.is_some_and(|rpr| dom.element(rpr, &W::del()).is_some());
if !mark_deleted {
continue;
}
for sp in dom.elements(ppr, Some(§pr)) {
if genuine.contains(§pr_identity(dom, sp)) {
continue;
}
dom.remove(sp);
}
}
}
pub fn flatten_tracked_insertions_stamped(dom: &mut Dom, body: NodeId) {
let mut inss: Vec<NodeId> = dom.descendants(body, Some(&W::ins()));
inss.reverse();
for i in inss {
if dom.parent(i).is_none() {
continue;
}
let author = dom.attribute(i, &W::author()).map(|s| s.to_string());
let date = dom.attribute(i, &W::date()).map(|s| s.to_string());
let kids = dom.nodes(i);
let runs: Vec<NodeId> = kids
.iter()
.flat_map(|&k| {
if dom.name_is(k, &W::r()) {
vec![k]
} else {
dom.descendants(k, Some(&W::r()))
}
})
.collect();
for r in runs {
if dom.attribute(r, &PT::name("PreIns")).is_some() {
continue;
}
let under_nested_del = dom
.ancestors(r, None)
.iter()
.any(|&a| a != i && dom.name_is(a, &W::del()));
if under_nested_del {
continue;
}
dom.set_attribute_value(r, &PT::name("PreIns"), Some("1"));
if let Some(a) = &author {
dom.set_attribute_value(r, &PT::name("PreInsAuthor"), Some(a));
}
if let Some(dt) = &date {
dom.set_attribute_value(r, &PT::name("PreInsDate"), Some(dt));
}
}
dom.replace_with(i, &kids);
}
}
pub fn convert_stamped_preins(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let marker = PT::name("PreIns");
let runs: Vec<NodeId> = dom
.descendants(root, Some(&W::r()))
.into_iter()
.filter(|&r| dom.attribute(r, &marker).is_some())
.collect();
for r in runs {
dom.set_attribute_value(r, &marker, None);
let author = dom
.attribute(r, &PT::name("PreInsAuthor"))
.map(|s| s.to_string());
let date = dom
.attribute(r, &PT::name("PreInsDate"))
.map(|s| s.to_string());
dom.set_attribute_value(r, &PT::name("PreInsAuthor"), None);
dom.set_attribute_value(r, &PT::name("PreInsDate"), None);
let restamp = |dom: &mut Dom, wrapper: NodeId| {
if let Some(a) = &author {
dom.set_attribute_value(wrapper, &W::author(), Some(a));
}
if let Some(dt) = &date {
dom.set_attribute_value(wrapper, &W::date(), Some(dt));
}
};
let restore_text_kinds = |dom: &mut Dom, run: NodeId| {
for t in dom.descendants(run, Some(&W::del_text())) {
dom.set_name(t, W::t());
}
for t in dom.descendants(run, Some(&W::name("delInstrText"))) {
dom.set_name(t, W::instr_text());
}
};
let Some(parent) = dom.parent(r) else {
continue;
};
let pname = dom.name(parent);
if pname == Some(W::ins()) {
restamp(dom, parent);
continue;
}
if pname == Some(W::del()) {
let sibs = dom.elements(parent, None);
if sibs.len() == 1 {
dom.set_name(parent, W::ins());
dom.set_attribute_value(parent, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
restore_text_kinds(dom, r);
restamp(dom, parent);
} else {
let idx = sibs.iter().position(|&c| c == r).unwrap_or(0);
let d = rev_el(dom, W::ins(), settings, id_gen);
restore_text_kinds(dom, r);
restamp(dom, d);
dom.remove(r);
dom.add(d, r);
dom.add_after_self(parent, d);
let after: Vec<NodeId> = sibs[idx + 1..].to_vec();
if !after.is_empty() {
let del2 = dom.new_element(W::del());
for (an, av) in dom.attributes(parent) {
dom.set_attribute_value(del2, &an, Some(&av));
}
dom.set_attribute_value(del2, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
for a in after {
dom.remove(a);
dom.add(del2, a);
}
dom.add_after_self(d, del2);
}
if idx == 0 {
dom.remove(parent);
}
}
} else {
let w = rev_el(dom, W::ins(), settings, id_gen);
restore_text_kinds(dom, r);
restamp(dom, w);
dom.add_before_self(r, w);
dom.remove(r);
dom.add(w, r);
}
}
}
pub fn ensure_default_page_size(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let sectpr_name = W::sect_pr();
let sect = match dom.element(body, §pr_name) {
Some(s) => s,
None => {
let s = dom.new_element(sectpr_name);
dom.add(body, s);
s
}
};
if dom.element(sect, &W::name("pgSz")).is_none() {
let pg = dom.new_element(W::name("pgSz"));
dom.set_attribute_value(pg, &W::name("w"), Some("12240"));
dom.set_attribute_value(pg, &W::name("h"), Some("15840"));
let later = dom.elements(sect, None).into_iter().find(|&c| {
dom.name(c).is_some_and(|n| {
!matches!(
n.local_name(),
"headerReference" | "footerReference" | "footnotePr" | "endnotePr" | "type"
)
})
});
match later {
Some(l) => dom.add_before_self(l, pg),
None => dom.add(sect, pg),
}
}
}
const TBLPR_ORDER: [(&str, i32); 17] = [
("tblStyle", 10),
("tblpPr", 20),
("tblOverlap", 30),
("bidiVisual", 40),
("tblStyleRowBandSize", 50),
("tblStyleColBandSize", 60),
("tblW", 70),
("jc", 80),
("tblCellSpacing", 90),
("tblInd", 100),
("tblBorders", 110),
("shd", 120),
("tblLayout", 130),
("tblCellMar", 140),
("tblLook", 150),
("tblCaption", 160),
("tblDescription", 170),
];
pub fn wml_order_elements_per_standard(dom: &mut Dom, root: NodeId) {
fn rank(dom: &Dom, container: &str, e: NodeId) -> i32 {
let Some(n) = dom.name(e) else { return 999 };
let local = n.local_name();
let ns = n.namespace_name();
if ns == W14::URI {
if container == "rPr" {
return match local {
"wShadow" => 270,
"wTextOutline" => 280,
"wTextFill" => 290,
"wScene3d" => 300,
"wProps3d" => 310,
_ => 999,
};
}
return 999;
}
if ns != W::URI {
return 999;
}
let table: &[(&str, i32)] = match container {
"pPr" => &[
("pStyle", 10),
("keepNext", 20),
("keepLines", 30),
("pageBreakBefore", 40),
("framePr", 50),
("widowControl", 60),
("numPr", 70),
("suppressLineNumbers", 80),
("pBdr", 90),
("shd", 100),
("tabs", 120),
("suppressAutoHyphens", 130),
("kinsoku", 140),
("wordWrap", 150),
("overflowPunct", 160),
("topLinePunct", 170),
("autoSpaceDE", 180),
("autoSpaceDN", 190),
("bidi", 200),
("adjustRightInd", 210),
("snapToGrid", 220),
("spacing", 230),
("ind", 240),
("contextualSpacing", 250),
("mirrorIndents", 260),
("suppressOverlap", 270),
("jc", 280),
("textDirection", 290),
("textAlignment", 300),
("textboxTightWrap", 310),
("outlineLvl", 320),
("divId", 330),
("cnfStyle", 340),
("rPr", 350),
("sectPr", 360),
("pPrChange", 370),
],
"rPr" => &[
("moveFrom", 5),
("moveTo", 7),
("ins", 10),
("del", 20),
("rStyle", 30),
("rFonts", 40),
("b", 50),
("bCs", 60),
("i", 70),
("iCs", 80),
("caps", 90),
("smallCaps", 100),
("strike", 110),
("dstrike", 120),
("outline", 130),
("shadow", 140),
("emboss", 150),
("imprint", 160),
("noProof", 170),
("snapToGrid", 180),
("vanish", 190),
("webHidden", 200),
("color", 210),
("spacing", 220),
("w", 230),
("kern", 240),
("position", 250),
("sz", 260),
("szCs", 320),
("highlight", 330),
("u", 340),
("effect", 350),
("bdr", 360),
("shd", 370),
("fitText", 380),
("vertAlign", 390),
("rtl", 400),
("cs", 410),
("em", 420),
("lang", 430),
("eastAsianLayout", 440),
("specVanish", 450),
("oMath", 460),
],
"tblPr" => &TBLPR_ORDER,
"tcPr" => &[
("cnfStyle", 10),
("tcW", 20),
("gridSpan", 30),
("hMerge", 40),
("vMerge", 50),
("tcBorders", 60),
("shd", 70),
("noWrap", 80),
("tcMar", 90),
("textDirection", 100),
("tcFitText", 110),
("vAlign", 120),
("hideMark", 130),
("headers", 140),
],
"tcBorders" => &[
("top", 10),
("start", 20),
("left", 30),
("bottom", 40),
("right", 50),
("end", 60),
("insideH", 70),
("insideV", 80),
("tl2br", 90),
("tr2bl", 100),
],
"tblBorders" => &[
("top", 10),
("left", 20),
("start", 30),
("bottom", 40),
("right", 50),
("end", 60),
("insideH", 70),
("insideV", 80),
],
"pBdr" => &[
("top", 10),
("left", 20),
("bottom", 30),
("right", 40),
("between", 50),
("bar", 60),
],
"numPr" => crate::comparer::order_tables::NUMPR_ORDER,
_ => &[],
};
table
.iter()
.find(|(n2, _)| *n2 == local)
.map_or(999, |(_, r)| *r)
}
let sortable = [
"pPr",
"rPr",
"tblPr",
"tcPr",
"tcBorders",
"tblBorders",
"pBdr",
"numPr",
];
for el in dom.descendants_and_self(root, None) {
let Some(name) = dom.name(el) else { continue };
if name.namespace_name() != W::URI {
continue;
}
let local = name.local_name().to_string();
if sortable.contains(&local.as_str()) {
for n in dom.nodes(el) {
if !dom.is_element(n) {
dom.remove(n);
}
}
let mut kids: Vec<(i32, usize, NodeId)> = dom
.elements(el, None)
.into_iter()
.enumerate()
.map(|(i, c)| (rank(dom, &local, c), i, c))
.collect();
if kids.is_sorted_by_key(|(r, i, _)| (*r, *i)) {
continue;
}
kids.sort_by_key(|(r, i, _)| (*r, *i));
for (_, _, c) in kids {
dom.remove(c);
dom.add(el, c);
}
} else if local == "p" || local == "r" {
let props = if local == "p" { W::p_pr() } else { W::r_pr() };
for n in dom.nodes(el) {
if !dom.is_element(n) {
dom.remove(n);
}
}
let kids = dom.elements(el, None);
let needs_move = kids
.iter()
.position(|&c| dom.name_is(c, &props.clone()))
.is_some_and(|first_props| {
kids[..first_props]
.iter()
.any(|&c| !dom.name_is(c, &props.clone()))
});
if needs_move {
let (front, back): (Vec<NodeId>, Vec<NodeId>) = kids
.into_iter()
.partition(|&c| dom.name_is(c, &props.clone()));
for c in front.into_iter().chain(back) {
dom.remove(c);
dom.add(el, c);
}
}
}
}
}
pub fn wrap_bare_del_text_runs(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let del_text = W::del_text();
let move_from = W::name("moveFrom");
let runs: Vec<NodeId> = dom
.descendants(root, Some(&W::r()))
.into_iter()
.filter(|&r| {
if dom.element(r, &del_text).is_none() {
return false;
}
let Some(p) = dom.parent(r) else {
return true;
};
let n = dom.name(p);
n != Some(W::del()) && n != Some(move_from.clone())
})
.collect();
for r in runs {
let d = rev_el(dom, W::del(), settings, id_gen);
dom.add_before_self(r, d);
dom.remove(r);
dom.add(d, r);
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum FlattenSide {
Original,
Revised,
}
pub fn pending_deletion_texts(dom: &Dom, body: NodeId) -> std::collections::HashSet<String> {
let mut out = std::collections::HashSet::new();
for d in dom.descendants(body, Some(&W::del())) {
let text = pending_deletion_fingerprint(dom, d);
if !text.is_empty() {
out.insert(text);
}
}
out
}
fn pending_deletion_fingerprint(dom: &Dom, del: NodeId) -> String {
let del_text = W::del_text();
let del_instr = W::name("delInstrText");
dom.descendants(del, None)
.into_iter()
.filter(|&n| {
dom.name(n)
.is_some_and(|nm| nm == del_text || nm == del_instr)
})
.map(|t| dom.value(t))
.collect()
}
pub fn flatten_tracked_deletions(
dom: &mut Dom,
body: NodeId,
side: FlattenSide,
other_side_pending: Option<&std::collections::HashSet<String>>,
) {
if side == FlattenSide::Original {
for ppr in dom.descendants(body, Some(&W::p_pr())) {
if let Some(rpr) = dom.element(ppr, &W::r_pr())
&& let Some(d) = dom.element(rpr, &W::del())
{
dom.remove(d);
}
}
}
let rpr_name = W::r_pr();
let dels: Vec<NodeId> = dom.descendants(body, Some(&W::del()));
for d in dels {
if dom
.parent(d)
.is_some_and(|p| dom.name_is(p, &rpr_name.clone()))
{
continue;
}
let kids = dom.nodes(d);
if side == FlattenSide::Revised
&& kids
.iter()
.any(|&k| dom.is_element(k) && !dom.name_is(k, &W::r()))
{
continue;
}
let wrapper_text = pending_deletion_fingerprint(dom, d);
for t in dom.descendants(d, Some(&W::del_text())) {
dom.set_name(t, W::t());
}
for t in dom.descendants(d, Some(&W::name("delInstrText"))) {
dom.set_name(t, W::instr_text());
}
let author = dom.attribute(d, &W::author()).map(|s| s.to_string());
let date = dom.attribute(d, &W::date()).map(|s| s.to_string());
let side_stamp = if side == FlattenSide::Original
&& !other_side_pending.is_some_and(|set| set.contains(&wrapper_text))
{
super::PREDELETE_STAMP_ORIG
} else {
super::PREDELETE_STAMP_REV
};
for &k in &kids {
if dom.name_is(k, &W::r()) {
dom.set_attribute_value(k, &PT::name("PreDelete"), Some(side_stamp));
if let Some(a) = &author {
dom.set_attribute_value(k, &PT::name("PreDelAuthor"), Some(a));
}
if let Some(dt) = &date {
dom.set_attribute_value(k, &PT::name("PreDelDate"), Some(dt));
}
}
}
dom.replace_with(d, &kids);
}
}
pub fn convert_stamped_predeletes(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let marker = PT::name("PreDelete");
let runs: Vec<NodeId> = dom
.descendants(root, Some(&W::r()))
.into_iter()
.filter(|&r| dom.attribute(r, &marker).is_some())
.collect();
for r in runs {
dom.set_attribute_value(r, &marker, None);
let author = dom
.attribute(r, &PT::name("PreDelAuthor"))
.map(|s| s.to_string());
let date = dom
.attribute(r, &PT::name("PreDelDate"))
.map(|s| s.to_string());
dom.set_attribute_value(r, &PT::name("PreDelAuthor"), None);
dom.set_attribute_value(r, &PT::name("PreDelDate"), None);
let restamp = |dom: &mut Dom, wrapper: NodeId| {
if let Some(a) = &author {
dom.set_attribute_value(wrapper, &W::author(), Some(a));
}
if let Some(dt) = &date {
dom.set_attribute_value(wrapper, &W::date(), Some(dt));
}
};
let Some(parent) = dom.parent(r) else {
continue;
};
let pname = dom.name(parent);
if pname == Some(W::del()) {
if dom.elements(parent, None).len() == 1 {
restamp(dom, parent);
}
continue;
}
if pname == Some(W::ins()) {
let sibs = dom.elements(parent, None);
if sibs.len() == 1 {
dom.set_name(parent, W::del());
restamp(dom, parent);
} else {
let idx = sibs.iter().position(|&c| c == r).unwrap_or(0);
let d = rev_el(dom, W::del(), settings, id_gen);
restamp(dom, d);
dom.remove(r);
dom.add(d, r);
dom.add_after_self(parent, d);
let after: Vec<NodeId> = sibs[idx + 1..].to_vec();
if !after.is_empty() {
let ins2 = dom.new_element(W::ins());
for (an, av) in dom.attributes(parent) {
dom.set_attribute_value(ins2, &an, Some(&av));
}
dom.set_attribute_value(ins2, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
for a in after {
dom.remove(a);
dom.add(ins2, a);
}
dom.add_after_self(d, ins2);
}
if idx == 0 {
dom.remove(parent); }
}
} else {
let d = rev_el(dom, W::del(), settings, id_gen);
restamp(dom, d);
dom.add_before_self(r, d);
dom.remove(r);
dom.add(d, r);
}
for t in dom.descendants(r, Some(&W::t())) {
dom.set_name(t, W::del_text());
}
for t in dom.descendants(r, Some(&W::instr_text())) {
dom.set_name(t, W::name("delInstrText"));
}
}
}
pub fn synthesize_table_cell_margins(dom: &mut Dom, root: NodeId) {
fn dxa(dom: &mut Dom, name: &str, w: &str) -> NodeId {
let e = dom.new_element(W::name(name));
dom.set_attribute_value(e, &W::name("w"), Some(w));
dom.set_attribute_value(e, &W::name("type"), Some("dxa"));
e
}
fn insert_in_order(dom: &mut Dom, tblpr: NodeId, child: NodeId, rank: i32) {
let later = dom.elements(tblpr, None).into_iter().find(|&c| {
dom.name(c)
.map(|n| {
TBLPR_ORDER
.iter()
.find(|(l, _)| *l == n.local_name())
.map_or(999, |(_, r)| *r)
})
.unwrap_or(999)
> rank
});
match later {
Some(l) => dom.add_before_self(l, child),
None => dom.add(tblpr, child),
}
}
let tbls: Vec<NodeId> = dom.descendants(root, Some(&W::tbl()));
for tbl in tbls {
let Some(tblpr) = dom.element(tbl, &W::name("tblPr")) else {
continue;
};
if dom.element(tblpr, &W::name("tblBorders")).is_none() {
continue;
}
if dom.element(tblpr, &W::name("tblInd")).is_none() {
let ind = dxa(dom, "tblInd", "10");
insert_in_order(dom, tblpr, ind, 100);
}
if dom.element(tblpr, &W::name("tblCellMar")).is_none() {
let mar = dom.new_element(W::name("tblCellMar"));
let l = dxa(dom, "left", "10");
dom.add(mar, l);
let r = dxa(dom, "right", "10");
dom.add(mar, r);
insert_in_order(dom, tblpr, mar, 140);
}
}
}
pub fn fix_strict_validity_artifacts(dom: &mut Dom, root: NodeId) {
const CNF_FLAGS: [&str; 12] = [
"firstRow",
"lastRow",
"firstColumn",
"lastColumn",
"oddVBand",
"evenVBand",
"oddHBand",
"evenHBand",
"firstRowFirstColumn",
"firstRowLastColumn",
"lastRowFirstColumn",
"lastRowLastColumn",
];
for cnf in dom.descendants(root, Some(&W::name("cnfStyle"))) {
if dom.attribute(cnf, &W::val()).is_some() {
continue;
}
let mut mask = String::with_capacity(12);
for f in CNF_FLAGS {
let on = matches!(
dom.attribute(cnf, &W::name(f)),
Some("1") | Some("true") | Some("on")
);
mask.push(if on { '1' } else { '0' });
}
dom.set_attribute_value(cnf, &W::val(), Some(&mask));
}
for local in ["pctWidth", "pctHeight", "pctPosHOffset", "pctPosVOffset"] {
for e in dom.descendants(root, Some(&WP14::name(local))) {
let v = dom.value(e);
if let Some(num) = v.trim().strip_suffix('%')
&& let Ok(f) = num.trim().parse::<f64>()
{
let per_thousand = (f * 1000.0).round() as i64;
dom.set_value(e, &per_thousand.to_string());
}
}
}
for el in dom.descendants_and_self(root, None) {
for a in [W14::name("paraId"), W14::name("textId")] {
let out_of_range = dom.attribute(el, &a).is_some_and(|v| {
u32::from_str_radix(v.trim(), 16).map_or(true, |x| x >= 0x8000_0000)
});
if out_of_range {
dom.set_attribute_value(el, &a, None);
}
}
}
{
let graphic_data = crate::namespaces::A::name("graphicData");
let uri_attr = XNamespace::none().name("uri");
let drawing = W::drawing();
fn remap(dom: &mut Dom, node: NodeId, wp_uri: &str, target: &XNamespace, drawing: &XName) {
for c in dom.nodes(node) {
if !dom.is_element(c) {
continue;
}
let Some(n) = dom.name(c) else { continue };
if n == *drawing {
continue;
}
if n.namespace_name() == wp_uri {
dom.set_name(c, target.name(n.local_name()));
}
remap(dom, c, wp_uri, target, drawing);
}
}
let gds: Vec<NodeId> = dom.descendants(root, Some(&graphic_data));
for gd in gds {
let Some(uri) = dom.attribute(gd, &uri_attr).map(|s| s.to_string()) else {
continue;
};
if !uri.starts_with("http://schemas.microsoft.com/office/word/2010/wordprocessing") {
continue;
}
let target = XNamespace::get(&uri);
remap(dom, gd, crate::namespaces::WP::URI, &target, &drawing);
}
}
{
let wne_txbx = crate::namespaces::WNE::name("txbxContent");
for e in dom.descendants(root, Some(&wne_txbx)) {
dom.set_name(e, W::name("txbxContent"));
}
}
for local in ["jc", "lvlJc"] {
for e in dom.descendants(root, Some(&W::name(local))) {
match dom.attribute(e, &W::val()) {
Some("start") => dom.set_attribute_value(e, &W::val(), Some("left")),
Some("end") => dom.set_attribute_value(e, &W::val(), Some("right")),
_ => {}
}
}
}
const DRAWING_NS: [&str; 4] = [
"http://schemas.openxmlformats.org/drawingml/2006/main",
"http://schemas.openxmlformats.org/drawingml/2006/diagram",
"http://schemas.microsoft.com/office/drawing/2008/diagram",
"http://schemas.openxmlformats.org/drawingml/2006/chart",
];
for el in dom.descendants_and_self(root, None) {
let Some(n) = dom.name(el) else { continue };
if !DRAWING_NS.contains(&n.namespace_name()) {
continue;
}
let percents: Vec<(crate::xmllinq::XName, i64)> = dom
.attributes(el)
.into_iter()
.filter_map(|(an, av)| {
let t = av.trim();
let num = t.strip_suffix('%')?;
let f: f64 = num.trim().parse().ok()?;
Some((an, (f * 1000.0).round() as i64))
})
.collect();
for (an, v) in percents {
dom.set_attribute_value(el, &an, Some(&v.to_string()));
}
}
}
pub fn normalize_universal_measures(dom: &mut Dom, root: NodeId) {
const TWIPS_ATTRS: [(&str, &[&str]); 18] = [
("pgSz", &["w", "h"]),
("gridCol", &["w"]),
(
"pgMar",
&[
"top", "right", "bottom", "left", "header", "footer", "gutter",
],
),
(
"ind",
&["left", "right", "hanging", "firstLine", "start", "end"],
),
("spacing", &["before", "after", "line"]),
("tab", &["pos"]),
("defaultTabStop", &["val"]),
("tblW", &["w"]),
("tcW", &["w"]),
("tblInd", &["w"]),
("tblCellSpacing", &["w"]),
("trHeight", &["val"]),
("top", &["w"]),
("bottom", &["w"]),
("left", &["w"]),
("right", &["w"]),
("start", &["w"]),
("end", &["w"]),
];
fn to_twips(v: &str) -> Option<i64> {
let t = v.trim();
for (suf, factor) in [
("pt", 20.0),
("in", 1440.0),
("cm", 1440.0 / 2.54),
("mm", 1440.0 / 25.4), ("pc", 240.0),
("pi", 240.0),
] {
if let Some(n) = t.strip_suffix(suf) {
return n
.trim()
.parse::<f64>()
.ok()
.map(|f| (f * factor).round() as i64);
}
}
if t.contains('.') {
return t.parse::<f64>().ok().map(|f| f.round() as i64);
}
None
}
for el in dom.descendants_and_self(root, None) {
let Some(name) = dom.name(el) else { continue };
if name.namespace_name() != W::URI {
continue;
}
let Some(attrs) = TWIPS_ATTRS
.iter()
.find(|(ln, _)| name.local_name() == *ln)
.map(|(_, a)| *a)
else {
continue;
};
for a in attrs {
let an = W::name(a);
if let Some(tw) = dom.attribute(el, &an).and_then(to_twips) {
dom.set_attribute_value(el, &an, Some(&tw.to_string()));
}
}
}
}
pub fn sectpr_identity(dom: &mut Dom, sp: NodeId) -> String {
let c = dom.clone_subtree(sp);
remove_powertools_scratch_markup(dom, c);
normalize_universal_measures(dom, c);
for el in dom.descendants_and_self(c, None) {
let rsids: Vec<crate::xmllinq::XName> = dom
.attributes(el)
.into_iter()
.map(|(n, _)| n)
.filter(|n| n.local_name().starts_with("rsid"))
.collect();
for a in rsids {
dom.set_attribute_value(el, &a, None);
}
}
dom.serialize_element(c)
}
pub fn drop_hoisted_sectpr_artifacts(
dom: &mut Dom,
root: NodeId,
genuine: &std::collections::HashSet<String>,
) {
let sectpr = W::sect_pr();
for ppr in dom.descendants(root, Some(&W::p_pr())) {
for sp in dom.elements(ppr, Some(§pr)) {
if !genuine.contains(§pr_identity(dom, sp)) {
dom.remove(sp);
}
}
}
}
pub fn mark_fully_revised_rows(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let tr_name = W::name("tr");
let tc_name = W::name("tc");
let trs: Vec<NodeId> = dom.descendants(root, Some(&tr_name));
for tr in trs {
let mut class: Option<bool> = None; let mut any_content = false;
let mut mixed = false;
for tc in dom.elements(tr, Some(&tc_name)) {
for p in dom.descendants(tc, Some(&W::p())) {
match para_replacement_class(dom, p) {
Some(k) => {
any_content = true;
if class.get_or_insert(k) != &k {
mixed = true;
}
}
None => {
let content_bearing = |dom: &Dom, r: NodeId| {
!dom.value(r).trim().is_empty()
|| dom.descendants(r, None).iter().any(|&c| {
dom.name(c).is_some_and(|n| {
matches!(
n.local_name(),
"drawing" | "object" | "pict" | "sym"
)
})
})
};
if dom
.descendants(p, Some(&W::r()))
.iter()
.any(|&r| content_bearing(dom, r))
{
mixed = true;
}
}
}
}
}
if mixed || !any_content {
continue;
}
let Some(k) = class else { continue };
let trpr = match dom.element(tr, &W::name("trPr")) {
Some(p) => p,
None => {
let p = dom.new_element(W::name("trPr"));
dom.add_first(tr, p);
p
}
};
let rev_name = if k { W::ins() } else { W::del() };
if dom.element(trpr, &rev_name).is_some() {
continue; }
let rev = dom.new_element(rev_name);
dom.set_attribute_value(rev, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(rev, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(rev, &W::date(), Some(&settings.date_time_for_revisions));
dom.add(trpr, rev);
}
}
pub fn splice_trailing_short_pure_dels_midstream(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 10 {
return;
}
let del_end = kids.len();
let mut del_start = kids.len();
while del_start > 0 {
let k = kids[del_start - 1];
if !dom.name_is(k, &W::p()) || !para_is_pure_deleted(dom, k) {
break;
}
del_start -= 1;
}
let del_count = del_end - del_start;
if !(1..=6).contains(&del_count) {
return;
}
let prefix = &kids[..del_start];
if prefix.len() < 40 {
return;
}
let pure_ins = prefix
.iter()
.filter(|&&k| {
if dom.name_is(k, &W::p()) {
para_is_pure_inserted(dom, k)
} else if dom.name_is(k, &W::tbl()) {
let has_ins = !dom.descendants(k, Some(&W::ins())).is_empty();
let has_del = !dom.descendants(k, Some(&W::del())).is_empty();
has_ins && !has_del
} else {
false
}
})
.count();
if pure_ins * 2 < prefix.len() {
return;
}
let mut tip_toc: Option<usize> = None;
let mut first_numbered: Option<usize> = None;
for (i, &k) in prefix.iter().enumerate() {
if !dom.name_is(k, &W::p()) {
continue;
}
let mut text = String::new();
for t in dom.descendants(k, Some(&W::t())) {
text.push_str(&dom.value_str(t));
}
let lower = text.to_ascii_lowercase();
let trimmed = lower.trim_start();
if lower.contains("tip:") {
tip_toc = Some(i);
break;
}
if lower.contains("table of contents") {
tip_toc = Some(i);
}
if first_numbered.is_none()
&& (trimmed.starts_with("1.") || trimmed.starts_with("1 "))
&& trimmed.chars().count() >= 8
{
first_numbered = Some(i);
}
}
let Some(insert_after) = tip_toc.or(first_numbered) else {
return;
};
if insert_after >= del_start {
return;
}
let to_move: Vec<NodeId> = if tip_toc.is_some() {
kids[del_start..del_end].to_vec()
} else {
let first = kids[del_start];
if !para_is_pure_deleted(dom, first) {
return;
}
let first_alnum = para_body_alnum_len(dom, first);
if first_alnum == 0 || first_alnum > 40 {
return;
}
let rest = &kids[del_start + 1..del_end];
if rest.is_empty() {
vec![first]
} else {
let rest_all_longer = rest.iter().all(|&d| {
dom.name_is(d, &W::p())
&& para_is_pure_deleted(dom, d)
&& para_body_alnum_len(dom, d) > first_alnum + 10
});
if rest_all_longer {
vec![first]
} else {
return; }
}
};
let anchor = prefix[insert_after];
for &n in &to_move {
dom.remove(n);
}
let mut prev = anchor;
for &n in &to_move {
dom.add_after_self(prev, n);
prev = n;
}
}
pub fn split_digits_ins_from_mixed_title(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
for &mix in &kids {
if !dom.name_is(mix, &W::p()) {
continue;
}
let has_ins = !dom.descendants(mix, Some(&W::ins())).is_empty();
let has_del = !dom.descendants(mix, Some(&W::del())).is_empty();
if !has_ins || !has_del {
continue;
}
let mix_kids: Vec<NodeId> = dom.elements(mix, None);
let mut leading_ins: Vec<NodeId> = Vec::new();
let mut rest: Vec<NodeId> = Vec::new();
let mut in_leading = true;
for &c in &mix_kids {
if dom.name_is(c, &W::p_pr()) {
continue;
}
if in_leading && dom.name_is(c, &W::ins()) {
leading_ins.push(c);
} else {
in_leading = false;
rest.push(c);
}
}
if leading_ins.is_empty() || rest.is_empty() {
continue;
}
let mut ins_text = String::new();
for &ins_n in &leading_ins {
for t in dom.descendants(ins_n, Some(&W::t())) {
ins_text.push_str(&dom.value_str(t));
}
}
let trimmed = ins_text.trim();
if trimmed.is_empty()
|| !trimmed
.chars()
.all(|c| c.is_ascii_digit() || c.is_whitespace())
{
continue;
}
let rest_has_ins = rest
.iter()
.any(|&c| dom.name_is(c, &W::ins()) || !dom.descendants(c, Some(&W::ins())).is_empty());
let rest_has_del = rest
.iter()
.any(|&c| dom.name_is(c, &W::del()) || !dom.descendants(c, Some(&W::del())).is_empty());
if rest_has_ins || !rest_has_del {
continue;
}
let pure_i = dom.new_element(W::p());
for &ins_n in &leading_ins {
if dom.parent(ins_n).is_some() {
dom.remove(ins_n);
dom.add(pure_i, ins_n);
}
}
dom.add_before_self(mix, pure_i);
}
}
pub fn peel_trailing_ins_from_mix_into_following_pure_del(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
loop {
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
let mut acted = false;
for i in 0..kids.len().saturating_sub(1) {
let mix_p = kids[i];
let del_p = kids[i + 1];
if !dom.name_is(mix_p, &W::p()) || !dom.name_is(del_p, &W::p()) {
continue;
}
if !para_is_pure_deleted(dom, del_p) {
continue;
}
let has_ins = !dom.descendants(mix_p, Some(&W::ins())).is_empty();
let _has_del = !dom.descendants(mix_p, Some(&W::del())).is_empty()
|| para_mark_revision(dom, mix_p, &W::del());
if !has_ins {
continue;
}
let del_text = para_revision_body_text(dom, del_p);
let del_toks: Vec<String> = del_text
.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_ascii_lowercase())
.collect();
if del_toks.len() < 6 {
continue;
}
let mix_kids: Vec<NodeId> = dom.elements(mix_p, None);
let mut trailing_ins: Vec<NodeId> = Vec::new();
let mut saw_ins = false;
for &c in mix_kids.iter().rev() {
if dom.name_is(c, &W::p_pr()) {
continue;
}
if dom.name_is(c, &W::ins()) {
trailing_ins.push(c);
saw_ins = true;
continue;
}
if !saw_ins && dom.name_is(c, &W::r()) {
let mut t = String::new();
for tn in dom.descendants(c, Some(&W::t())) {
t.push_str(&dom.value_str(tn));
}
if t.chars().all(|ch| !ch.is_alphanumeric()) {
continue;
}
}
break;
}
trailing_ins.reverse();
if trailing_ins.is_empty() {
continue;
}
let non_ins = mix_kids.iter().any(|&c| {
let n = dom.name(c);
n != Some(W::p_pr()) && n != Some(W::ins())
});
if !non_ins {
continue;
}
let mut ins_text = String::new();
for &ins_n in &trailing_ins {
for t in dom.descendants(ins_n, Some(&W::t())) {
ins_text.push_str(&dom.value_str(t));
}
}
let ins_toks: Vec<String> = ins_text
.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_ascii_lowercase())
.collect();
if ins_toks.is_empty() || ins_toks.len() > 12 {
continue;
}
let del_set: std::collections::HashSet<&str> =
del_toks.iter().map(|s| s.as_str()).collect();
let shared = ins_toks
.iter()
.any(|t| t.chars().count() >= 3 && del_set.contains(t.as_str()));
if !shared {
continue;
}
let del_body_first = dom
.elements(del_p, None)
.into_iter()
.find(|&c| !dom.name_is(c, &W::p_pr()));
for &ins_n in &trailing_ins {
if dom.parent(ins_n).is_none() {
continue;
}
dom.remove(ins_n);
if let Some(first) = del_body_first {
dom.add_before_self(first, ins_n);
} else {
dom.add(del_p, ins_n);
}
}
acted = true;
break;
}
if !acted {
break;
}
}
}
pub fn restore_short_del_before_long_ins(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
loop {
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
let mut acted = false;
for i in 0..kids.len().saturating_sub(1) {
let ins_p = kids[i];
let del_p = kids[i + 1];
if !dom.name_is(ins_p, &W::p()) || !dom.name_is(del_p, &W::p()) {
continue;
}
if !para_is_pure_inserted(dom, ins_p) || !para_is_pure_deleted(dom, del_p) {
continue;
}
if i == 0 {
continue;
}
let prev = kids[i - 1];
if !dom.name_is(prev, &W::p()) {
continue;
}
{
let has_ins = !dom.descendants(prev, Some(&W::ins())).is_empty()
|| para_mark_revision(dom, prev, &W::ins());
let has_del = !dom.descendants(prev, Some(&W::del())).is_empty()
|| para_mark_revision(dom, prev, &W::del());
if !(has_ins && has_del) {
continue;
}
let prev_text = para_revision_body_text(dom, prev).to_ascii_lowercase();
if prev_text.contains("file_")
|| prev_text.contains(".docx")
|| prev_text.contains(".doc")
{
continue;
}
}
let d_len = para_body_alnum_len(dom, del_p);
let i_len = para_body_alnum_len(dom, ins_p);
if d_len == 0 || d_len >= i_len || d_len > 40 {
continue;
}
if dom.parent(del_p).is_none() || dom.parent(ins_p).is_none() {
continue;
}
dom.remove(del_p);
dom.add_before_self(ins_p, del_p);
acted = true;
break;
}
if !acted {
break;
}
}
}
pub fn peel_trailing_del_from_mix_into_following_pure_ins(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
loop {
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
let mut acted = false;
for i in 0..kids.len().saturating_sub(1) {
let mix_p = kids[i];
let ins_p = kids[i + 1];
if !dom.name_is(mix_p, &W::p()) || !dom.name_is(ins_p, &W::p()) {
continue;
}
if !para_is_pure_inserted(dom, ins_p) {
continue;
}
let has_del = !dom.descendants(mix_p, Some(&W::del())).is_empty()
|| para_mark_revision(dom, mix_p, &W::del());
let has_ins = !dom.descendants(mix_p, Some(&W::ins())).is_empty();
if !has_del || !has_ins {
continue;
}
let mix_kids: Vec<NodeId> = dom.elements(mix_p, None);
let mut trailing_del: Vec<NodeId> = Vec::new();
let mut saw_del = false;
for &c in mix_kids.iter().rev() {
if dom.name_is(c, &W::p_pr()) {
continue;
}
if dom.name_is(c, &W::del()) {
trailing_del.push(c);
saw_del = true;
continue;
}
if !saw_del && dom.name_is(c, &W::r()) {
let mut t = String::new();
for tn in dom.descendants(c, Some(&W::t())) {
t.push_str(&dom.value_str(tn));
}
if t.chars().all(|ch| !ch.is_alphanumeric()) {
continue;
}
}
break;
}
trailing_del.reverse();
if trailing_del.is_empty() {
continue;
}
let non_del = mix_kids.iter().any(|&c| {
let n = dom.name(c);
n != Some(W::p_pr()) && n != Some(W::del())
});
if !non_del {
continue;
}
let mut del_text = String::new();
for &d in &trailing_del {
for t in dom.descendants(d, Some(&W::del_text())) {
del_text.push_str(&dom.value_str(t));
}
for t in dom.descendants(d, Some(&W::t())) {
del_text.push_str(&dom.value_str(t));
}
}
let del_toks: Vec<String> = del_text
.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_ascii_lowercase())
.collect();
if del_toks.len() < 4 || del_toks.len() > 20 {
continue;
}
let ins_text = para_revision_body_text(dom, ins_p);
let ins_toks: Vec<String> = ins_text
.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_ascii_lowercase())
.collect();
if ins_toks.len() < 4 {
continue;
}
let ins_set: std::collections::HashSet<&str> =
ins_toks.iter().map(|s| s.as_str()).collect();
let shared = del_toks
.iter()
.any(|t| t.chars().count() >= 4 && ins_set.contains(t.as_str()));
let long_trail = del_toks.len() >= 6;
let next_looks_list = ins_toks.iter().any(|t| {
t == "bullet"
|| t == "item"
|| t == "point"
|| t == "first"
|| t == "second"
|| t == "third"
});
let short_copula = del_toks
.first()
.is_some_and(|t| t == "is" || t == "are" || t == "in");
let short_trail_long_ins = del_toks.len() >= 4
&& del_toks.len() <= 5
&& ins_toks.len() >= 6
&& !next_looks_list
&& short_copula;
if !shared && !long_trail && !short_trail_long_ins {
continue;
}
for &d in &trailing_del {
if dom.parent(d).is_none() {
continue;
}
dom.remove(d);
dom.add(ins_p, d);
}
acted = true;
break;
}
if !acted {
break;
}
}
}
pub fn fold_midstream_demo_title_into_numbered_heading(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 8 {
return;
}
if let Some(&first) = kids.iter().find(|&&k| dom.name_is(k, &W::p())) {
let has_ins_text = !dom.descendants(first, Some(&W::ins())).is_empty()
&& dom
.descendants(first, Some(&W::t()))
.iter()
.any(|&t| !dom.value_str(t).trim().is_empty());
let has_del_text = !dom.descendants(first, Some(&W::del_text())).is_empty();
let has_live_text = dom.descendants(first, Some(&W::t())).iter().any(|&t| {
!dom.value_str(t).trim().is_empty()
&& !dom
.ancestors_and_self(t, None)
.iter()
.any(|&a| dom.name_is(a, &W::ins()))
});
if has_ins_text && has_del_text && has_live_text {
return;
}
}
let mut targets: Vec<usize> = Vec::new();
for (i, &k) in kids.iter().enumerate() {
if !dom.name_is(k, &W::p()) || !para_is_pure_deleted(dom, k) {
continue;
}
if !para_looks_like_demo_title(dom, k) {
continue;
}
let following = kids[i + 1..].iter().any(|&c| match dom.name(c) {
Some(n) if n == W::p() => !para_has_no_text(dom, c),
Some(n) if n == W::tbl() => true,
_ => false,
});
if following {
targets.push(i);
}
}
for &di in targets.iter().rev() {
let d = kids[di];
if dom.parent(d).is_none() {
continue;
}
let mut heading: Option<NodeId> = None;
let start = di.saturating_sub(10);
for &k in kids[start..di].iter().rev() {
if !dom.name_is(k, &W::p()) {
continue;
}
if !para_is_pure_inserted(dom, k) {
if dom.name_is(k, &W::tbl()) {
break;
}
continue;
}
let mut text = String::new();
for t in dom.descendants(k, Some(&W::t())) {
text.push_str(&dom.value_str(t));
}
let trimmed = text.trim_start();
if (trimmed.starts_with("1.") || trimmed.starts_with("1 "))
&& trimmed.chars().count() >= 8
&& trimmed.chars().count() <= 80
{
heading = Some(k);
break;
}
}
let Some(h) = heading else {
continue;
};
if dom.parent(h).is_none() {
continue;
}
if let Some(ippr) = dom.element(h, &W::p_pr()) {
if let Some(irpr) = dom.element(ippr, &W::r_pr())
&& (dom.element(irpr, &W::ins()).is_some()
|| dom.element(irpr, &W::del()).is_some())
{
dom.remove(irpr);
}
if dom.elements(ippr, None).is_empty() {
dom.remove(ippr);
}
}
for c in dom.elements(d, None) {
if !dom.name_is(c, &W::p_pr()) {
dom.add(h, c);
}
}
dom.remove(d);
mesh_trailing_demo_eq_in_para(dom, h);
}
}
fn mesh_trailing_demo_eq_in_para(dom: &mut Dom, p: NodeId) {
let del_texts: Vec<NodeId> = dom
.descendants(p, Some(&W::del_text()))
.into_iter()
.collect();
if del_texts.is_empty() {
return;
}
let mut full = String::new();
for &dt in &del_texts {
full.push_str(&dom.value_str(dt));
}
let trimmed = full.trim_end();
let Some((head, demo_suffix)) = trimmed.rsplit_once(char::is_whitespace) else {
return;
};
if !demo_suffix.eq_ignore_ascii_case("demo") || head.trim().is_empty() {
return;
}
let strip_from = if full.ends_with(" Demo") {
full.len().saturating_sub(" Demo".len())
} else if full.ends_with("Demo") {
full.len().saturating_sub("Demo".len())
} else if full.to_ascii_lowercase().ends_with(" demo") {
full.len().saturating_sub(5)
} else {
return;
};
let mut remain = full.len() - strip_from;
let mut last_touched: Option<NodeId> = None;
for &dt in del_texts.iter().rev() {
if remain == 0 {
break;
}
let t = dom.value_str(dt);
if t.len() <= remain {
remain -= t.len();
dom.set_value(dt, "");
last_touched = Some(dt);
} else {
let keep = t.len() - remain;
let new_t = t[..keep].to_string();
dom.set_value(dt, &new_t);
remain = 0;
last_touched = Some(dt);
}
}
let Some(dt) = last_touched else {
return;
};
let mut anchor = dt;
while let Some(par) = dom.parent(anchor) {
if dom.name_is(par, &W::del()) {
anchor = par;
break;
}
anchor = par;
if dom.name_is(par, &W::p()) {
break;
}
}
let eq_r = dom.new_element(W::r());
let eq_t = dom.new_element(W::t());
dom.set_attribute_value(eq_t, &XNamespace::xml().name("space"), Some("preserve"));
dom.add_text(eq_t, " Demo");
dom.add(eq_r, eq_t);
if dom.name_is(anchor, &W::del()) {
dom.add_after_self(anchor, eq_r);
} else {
dom.add(p, eq_r);
}
}
pub fn park_jc_on_first_short_title_mix_from_body(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 2 {
return;
}
let mut title: Option<NodeId> = None;
for &k in &kids {
if !dom.name_is(k, &W::p()) {
continue;
}
if para_is_mixed_revision(dom, k) {
title = Some(k);
break;
}
}
let Some(p) = title else {
return;
};
let ins_w = para_side_word_count(dom, p, true);
let del_w = para_side_word_count(dom, p, false);
if ins_w + del_w == 0 || ins_w + del_w > 5 {
return;
}
let ppr = match dom.element(p, &W::p_pr()) {
Some(ppr) => ppr,
None => {
let ppr = dom.new_element(W::p_pr());
if let Some(first) = dom.elements(p, None).first().copied() {
dom.add_before_self(first, ppr);
} else {
dom.add(p, ppr);
}
ppr
}
};
if dom.element(ppr, &W::jc_el()).is_some() {
return;
}
if dom.element(ppr, &W::p_pr_change()).is_some() {
return;
}
let mut jc_val: Option<String> = None;
for &k in &kids {
if k == p || !dom.name_is(k, &W::p()) {
continue;
}
let Some(kppr) = dom.element(k, &W::p_pr()) else {
continue;
};
let Some(jc) = dom.element(kppr, &W::jc_el()) else {
continue;
};
if let Some(v) = dom.attribute(jc, &W::val())
&& !v.is_empty()
{
if v != "right" && v != "center" {
continue;
}
jc_val = Some(v.to_string());
break;
}
}
let Some(val) = jc_val else {
return;
};
let old_inner = dom.new_element(W::p_pr());
let old_jc = dom.new_element(W::jc_el());
dom.set_attribute_value(old_jc, &W::val(), Some(&val));
dom.add(old_inner, old_jc);
let chg = dom.new_element(W::p_pr_change());
dom.set_attribute_value(chg, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(chg, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(chg, &W::date(), Some(&settings.date_time_for_revisions));
dom.add(chg, old_inner);
dom.add(ppr, chg);
}
pub fn ensure_empty_pprchange_on_live_heading_spacing(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
if kids.len() < 2 {
return;
}
let last = kids[kids.len() - 1];
let last_has_empty_chg = (|| {
if !dom.name_is(last, &W::p()) {
return false;
}
let Some(lppr) = dom.element(last, &W::p_pr()) else {
return false;
};
let Some(chg) = dom.element(lppr, &W::p_pr_change()) else {
return false;
};
let Some(old) = dom.element(chg, &W::p_pr()) else {
return false;
};
for c in dom.elements(old, None) {
let Some(n) = dom.name(c) else {
continue;
};
if n == W::r_pr() {
continue;
}
return false;
}
true
})();
if !last_has_empty_chg {
return;
}
for &p in &kids {
if p == last || !dom.name_is(p, &W::p()) || !para_is_mixed_revision(dom, p) {
continue;
}
let Some(ppr) = dom.element(p, &W::p_pr()) else {
continue;
};
if dom.element(ppr, &W::p_pr_change()).is_some() {
continue;
}
if dom.element(ppr, &W::jc_el()).is_some() {
continue;
}
if dom.element(ppr, &W::r_pr()).is_some() {
continue;
}
let Some(sp) = dom.element(ppr, &W::spacing_el()) else {
continue;
};
let before = dom
.attribute(sp, &W::name("before"))
.and_then(|v| v.parse::<i64>().ok())
.unwrap_or(0);
if before < 200 || dom.attribute(sp, &W::name("line")).is_none() {
continue;
}
let old_inner = dom.new_element(W::p_pr());
let chg = dom.new_element(W::p_pr_change());
dom.set_attribute_value(chg, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(chg, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(chg, &W::date(), Some(&settings.date_time_for_revisions));
dom.add(chg, old_inner);
dom.add(ppr, chg);
}
}
pub fn ensure_empty_pprchange_on_eq_with_live_jc(
dom: &mut Dom,
root: NodeId,
settings: &WmlComparerSettings,
id_gen: &mut u32,
) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
for &p in &kids {
if !dom.name_is(p, &W::p()) {
continue;
}
if para_is_mixed_revision(dom, p)
|| para_is_pure_inserted(dom, p)
|| para_is_pure_deleted(dom, p)
{
continue;
}
if !dom.descendants(p, Some(&W::ins())).is_empty()
|| !dom.descendants(p, Some(&W::del())).is_empty()
|| para_mark_revision(dom, p, &W::ins())
|| para_mark_revision(dom, p, &W::del())
{
continue;
}
let Some(ppr) = dom.element(p, &W::p_pr()) else {
continue;
};
if dom.element(ppr, &W::jc_el()).is_none() {
continue;
}
if dom.element(ppr, &W::p_pr_change()).is_some() {
continue;
}
if dom.element(ppr, &W::r_pr()).is_some() {
continue;
}
let old_inner = dom.new_element(W::p_pr());
let chg = dom.new_element(W::p_pr_change());
dom.set_attribute_value(chg, &W::id(), Some(&id_gen.to_string()));
*id_gen += 1;
dom.set_attribute_value(chg, &W::author(), Some(&settings.author_for_revisions));
dom.set_attribute_value(chg, &W::date(), Some(&settings.date_time_for_revisions));
dom.add(chg, old_inner);
dom.add(ppr, chg);
}
}
pub fn strip_leading_del_echoing_prev_pure_i(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
for i in 1..kids.len() {
let prev = kids[i - 1];
let p = kids[i];
if !dom.name_is(prev, &W::p()) || !dom.name_is(p, &W::p()) {
continue;
}
if !para_is_pure_inserted(dom, prev) || !para_is_mixed_revision(dom, p) {
continue;
}
let prev_text = para_revision_body_text(dom, prev);
let prev_tok = prev_text
.split(|c: char| !c.is_alphanumeric())
.find(|t| !t.is_empty())
.map(|t| t.to_ascii_lowercase());
let Some(prev_tok) = prev_tok else {
continue;
};
let content: Vec<NodeId> = dom
.elements(p, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
if content.len() < 2 {
continue;
}
let mut target: Option<NodeId> = None;
for &c in content.iter().take(3) {
if !dom.name_is(c, &W::del()) {
continue;
}
let mut n_tok = 0usize;
let mut first_tok: Option<String> = None;
for t in dom.descendants(c, Some(&W::del_text())) {
for part in dom.value_str(t).split(|ch: char| !ch.is_alphanumeric()) {
if part.is_empty() {
continue;
}
n_tok += 1;
if first_tok.is_none() {
first_tok = Some(part.to_ascii_lowercase());
}
}
}
for t in dom.descendants(c, Some(&W::t())) {
for part in dom.value_str(t).split(|ch: char| !ch.is_alphanumeric()) {
if part.is_empty() {
continue;
}
n_tok += 1;
if first_tok.is_none() {
first_tok = Some(part.to_ascii_lowercase());
}
}
}
let Some(ft) = first_tok else {
continue;
};
if ft == prev_tok && (1..=2).contains(&n_tok) {
target = Some(c);
break;
}
}
let Some(del_node) = target else {
continue;
};
let other = content.iter().any(|&c| c != del_node);
if !other {
continue;
}
dom.remove(del_node);
}
}
pub fn free_mesh_wholesale_body_mix(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
for &p in &kids {
if !dom.name_is(p, &W::p()) || !para_is_mixed_revision(dom, p) {
continue;
}
let body_kids: Vec<NodeId> = dom
.elements(p, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
if body_kids.is_empty() {
continue;
}
let mut ins_nodes = Vec::new();
let mut del_nodes = Vec::new();
let mut other = false;
for &c in &body_kids {
let n = dom.name(c);
if n == Some(W::ins()) {
ins_nodes.push(c);
} else if n == Some(W::del()) {
del_nodes.push(c);
} else if n == Some(W::r()) {
let mut t = String::new();
for tn in dom.descendants(c, Some(&W::t())) {
t.push_str(&dom.value_str(tn));
}
if !t.chars().all(|ch| !ch.is_alphanumeric()) {
other = true;
break;
}
} else {
other = true;
break;
}
}
if other || ins_nodes.len() != 1 || del_nodes.len() != 1 {
continue;
}
let ins = ins_nodes[0];
let del = del_nodes[0];
let mut ins_text = String::new();
for t in dom.descendants(ins, Some(&W::t())) {
ins_text.push_str(&dom.value_str(t));
}
let mut del_text = String::new();
for t in dom.descendants(del, Some(&W::del_text())) {
del_text.push_str(&dom.value_str(t));
}
let trailing_period =
ins_text.trim_end().ends_with('.') || del_text.trim_end().ends_with('.');
let ins_toks = alnum_tokens(&ins_text);
let del_toks = alnum_tokens(&del_text);
if !(5..=40).contains(&ins_toks.len()) || !(5..=40).contains(&del_toks.len()) {
continue;
}
const BOILER: &[&str] = &[
"this", "that", "with", "from", "have", "will", "been", "were", "they", "them", "than",
"then", "when", "what", "which", "into", "over", "only", "also", "just", "more",
"most", "some", "such", "other", "about", "page", "text", "and", "the", "for", "are",
"was", "you", "all", "can", "her", "his", "its", "our", "out",
];
let is_sig = |t: &str| t.len() >= 4 && !BOILER.contains(&t);
let ins_sig: std::collections::HashSet<&str> = ins_toks
.iter()
.map(String::as_str)
.filter(|t| is_sig(t))
.collect();
let del_sig: std::collections::HashSet<&str> = del_toks
.iter()
.map(String::as_str)
.filter(|t| is_sig(t))
.collect();
let shared_sig = ins_sig.intersection(&del_sig).count();
let min_sig = ins_sig.len().min(del_sig.len());
if min_sig == 0 || (shared_sig as f64) / (min_sig as f64) < 0.35 {
continue;
}
let ins_words = split_words_preserve(&ins_text);
let del_words = split_words_preserve(&del_text);
let ins_keys: Vec<String> = ins_words.iter().map(|w| w.to_ascii_lowercase()).collect();
let del_keys: Vec<String> = del_words.iter().map(|w| w.to_ascii_lowercase()).collect();
let lcs = word_lcs_indices_eligible(&ins_keys, &del_keys, BOILER);
if lcs.len() < 2 {
continue;
}
let sig_lcs = lcs
.iter()
.filter(|&&(i, _)| is_sig(ins_keys[i].as_str()))
.count();
if sig_lcs < 1 {
continue;
}
let (author, date) = {
let mut a = "Redline".to_string();
let mut d = "1970-01-01T00:00:00Z".to_string();
if let Some(v) = dom.attribute(ins, &W::author()) {
a = v.to_string();
}
if let Some(v) = dom.attribute(ins, &W::date()) {
d = v.to_string();
}
(a, d)
};
let sample_rpr = dom
.element(ins, &W::r())
.and_then(|r| dom.element(r, &W::r_pr()))
.map(|rpr| dom.clone_subtree(rpr));
for c in body_kids {
if dom.parent(c).is_some() {
dom.remove(c);
}
}
rebuild_body_free_mesh_lcs(
dom,
p,
&ins_words,
&del_words,
&lcs,
&author,
&date,
sample_rpr,
trailing_period,
);
}
}
fn split_words_preserve(text: &str) -> Vec<String> {
text.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_string())
.collect()
}
fn word_lcs_indices(a: &[String], b: &[String]) -> Vec<(usize, usize)> {
let n = a.len();
let m = b.len();
let mut dp = vec![vec![0usize; m + 1]; n + 1];
for i in 0..n {
for j in 0..m {
if a[i] == b[j] {
dp[i + 1][j + 1] = dp[i][j] + 1;
} else {
dp[i + 1][j + 1] = dp[i + 1][j].max(dp[i][j + 1]);
}
}
}
let mut out = Vec::new();
let mut i = n;
let mut j = m;
while i > 0 && j > 0 {
if a[i - 1] == b[j - 1] {
out.push((i - 1, j - 1));
i -= 1;
j -= 1;
} else if dp[i - 1][j] >= dp[i][j - 1] {
i -= 1;
} else {
j -= 1;
}
}
out.reverse();
out
}
fn word_lcs_indices_eligible(a: &[String], b: &[String], boiler: &[&str]) -> Vec<(usize, usize)> {
const SHORT_OK: &[&str] = &["is", "of", "to", "be"];
let eligible = |t: &str| {
if t.len() >= 4 {
!boiler.contains(&t)
} else {
SHORT_OK.contains(&t)
}
};
let a_idx: Vec<usize> = (0..a.len()).filter(|&i| eligible(&a[i])).collect();
let b_idx: Vec<usize> = (0..b.len()).filter(|&j| eligible(&b[j])).collect();
if a_idx.is_empty() || b_idx.is_empty() {
return Vec::new();
}
let a_f: Vec<String> = a_idx.iter().map(|&i| a[i].clone()).collect();
let b_f: Vec<String> = b_idx.iter().map(|&j| b[j].clone()).collect();
word_lcs_indices(&a_f, &b_f)
.into_iter()
.map(|(i, j)| (a_idx[i], b_idx[j]))
.collect()
}
#[allow(clippy::too_many_arguments)]
fn rebuild_body_free_mesh_lcs(
dom: &mut Dom,
p: NodeId,
ins_words: &[String],
del_words: &[String],
lcs: &[(usize, usize)],
author: &str,
date: &str,
sample_rpr: Option<NodeId>,
trailing_period: bool,
) {
let mut next_id = 1u32;
let mut ii = 0usize;
let mut di = 0usize;
let mut li = 0usize;
let mut first_content = true;
let push_run = |dom: &mut Dom,
p: NodeId,
kind: &str,
words: &[String],
author: &str,
date: &str,
next_id: &mut u32,
sample_rpr: &Option<NodeId>,
first_content: &mut bool| {
if words.is_empty() {
return;
}
let mut text = words.join(" ");
if !*first_content {
text = format!(" {text}");
}
*first_content = false;
match kind {
"eq" => {
let r = dom.new_element(W::r());
if let Some(rpr) = sample_rpr {
let rpr_c = dom.clone_subtree(*rpr);
dom.add(r, rpr_c);
}
let t = dom.new_element(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);
dom.add(p, r);
}
"ins" => {
let ins = dom.new_element(W::ins());
dom.set_attribute_value(ins, &W::id(), Some(&next_id.to_string()));
*next_id += 1;
dom.set_attribute_value(ins, &W::author(), Some(author));
dom.set_attribute_value(ins, &W::date(), Some(date));
let r = dom.new_element(W::r());
if let Some(rpr) = sample_rpr {
let rpr_c = dom.clone_subtree(*rpr);
dom.add(r, rpr_c);
}
let t = dom.new_element(W::t());
if text.starts_with(' ') || text.contains(' ') {
dom.set_attribute_value(t, &XNamespace::xml().name("space"), Some("preserve"));
}
dom.add_text(t, &text);
dom.add(r, t);
dom.add(ins, r);
dom.add(p, ins);
}
"del" => {
let del = dom.new_element(W::del());
dom.set_attribute_value(del, &W::id(), Some(&next_id.to_string()));
*next_id += 1;
dom.set_attribute_value(del, &W::author(), Some(author));
dom.set_attribute_value(del, &W::date(), Some(date));
let r = dom.new_element(W::r());
if let Some(rpr) = sample_rpr {
let rpr_c = dom.clone_subtree(*rpr);
dom.add(r, rpr_c);
}
let t = dom.new_element(W::del_text());
if text.starts_with(' ') || text.contains(' ') {
dom.set_attribute_value(t, &XNamespace::xml().name("space"), Some("preserve"));
}
dom.add_text(t, &text);
dom.add(r, t);
dom.add(del, r);
dom.add(p, del);
}
_ => {}
}
};
while ii < ins_words.len() || di < del_words.len() {
if let Some(&(ei, ed)) = lcs.get(li) {
if ii == ei && di == ed {
let label = del_words[ed].clone(); push_run(
dom,
p,
"eq",
&[label],
author,
date,
&mut next_id,
&sample_rpr,
&mut first_content,
);
ii = ei + 1;
di = ed + 1;
li += 1;
continue;
}
if ii < ei {
let mut batch = Vec::new();
while ii < ei {
batch.push(ins_words[ii].clone());
ii += 1;
}
push_run(
dom,
p,
"ins",
&batch,
author,
date,
&mut next_id,
&sample_rpr,
&mut first_content,
);
continue;
}
if di < ed {
let mut batch = Vec::new();
while di < ed {
batch.push(del_words[di].clone());
di += 1;
}
push_run(
dom,
p,
"del",
&batch,
author,
date,
&mut next_id,
&sample_rpr,
&mut first_content,
);
continue;
}
li += 1;
} else {
if di < del_words.len() {
let batch: Vec<String> = del_words[di..].to_vec();
push_run(
dom,
p,
"del",
&batch,
author,
date,
&mut next_id,
&sample_rpr,
&mut first_content,
);
}
if ii < ins_words.len() {
let batch: Vec<String> = ins_words[ii..].to_vec();
push_run(
dom,
p,
"ins",
&batch,
author,
date,
&mut next_id,
&sample_rpr,
&mut first_content,
);
}
break;
}
}
if trailing_period {
let r = dom.new_element(W::r());
if let Some(rpr) = sample_rpr {
let rpr_c = dom.clone_subtree(rpr);
dom.add(r, rpr_c);
}
let t = dom.new_element(W::t());
dom.add_text(t, ".");
dom.add(r, t);
dom.add(p, r);
}
}
pub fn free_mesh_bookended_ins_del(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
for &p in &kids {
if !dom.name_is(p, &W::p()) || !para_is_mixed_revision(dom, p) {
continue;
}
let body_kids: Vec<NodeId> = dom
.elements(p, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
if body_kids.is_empty() {
continue;
}
let mut ins_nodes = Vec::new();
let mut del_nodes = Vec::new();
let mut bare_alnum = false;
let mut other = false;
for &c in &body_kids {
let n = dom.name(c);
if n == Some(W::ins()) {
ins_nodes.push(c);
} else if n == Some(W::del()) {
del_nodes.push(c);
} else if n == Some(W::r()) {
let mut t = String::new();
for tn in dom.descendants(c, Some(&W::t())) {
t.push_str(&dom.value_str(tn));
}
if t.chars().any(|ch| ch.is_alphanumeric()) {
bare_alnum = true;
}
} else {
other = true;
break;
}
}
if other || !bare_alnum || ins_nodes.len() != 1 || del_nodes.len() != 1 {
continue;
}
let ins = ins_nodes[0];
let del = del_nodes[0];
let mut ins_text = String::new();
for t in dom.descendants(ins, Some(&W::t())) {
ins_text.push_str(&dom.value_str(t));
}
let mut del_text = String::new();
for t in dom.descendants(del, Some(&W::del_text())) {
del_text.push_str(&dom.value_str(t));
}
let ins_toks = alnum_tokens(&ins_text);
let del_toks = alnum_tokens(&del_text);
if !(4..=40).contains(&ins_toks.len()) || !(4..=40).contains(&del_toks.len()) {
continue;
}
const BOILER: &[&str] = &[
"this", "that", "with", "from", "have", "will", "been", "were", "they", "them", "than",
"then", "when", "what", "which", "into", "over", "only", "also", "just", "more",
"most", "some", "such", "other", "about", "page", "text", "and", "the", "for", "are",
"was", "you", "all", "can", "her", "his", "its", "our", "out",
];
let mut ins_counts: std::collections::HashMap<&str, usize> =
std::collections::HashMap::new();
for t in &ins_toks {
*ins_counts.entry(t.as_str()).or_default() += 1;
}
let mut del_counts: std::collections::HashMap<&str, usize> =
std::collections::HashMap::new();
for t in &del_toks {
*del_counts.entry(t.as_str()).or_default() += 1;
}
let mut shared: Vec<&str> = ins_counts
.keys()
.filter(|t| {
t.len() >= 4
&& !BOILER.contains(t)
&& ins_counts.get(*t) == Some(&1)
&& del_counts.get(*t) == Some(&1)
})
.copied()
.collect();
shared.sort_by_key(|t| std::cmp::Reverse(t.len()));
let Some(&anchor) = shared.first() else {
continue;
};
if shared.len() != 1 {
continue;
}
let Some((ins_before, ins_after)) = split_around_whole_word(&ins_text, anchor) else {
continue;
};
let Some((del_before, del_after)) = split_around_whole_word(&del_text, anchor) else {
continue;
};
if ins_before.is_empty() && ins_after.is_empty() {
continue;
}
if del_before.is_empty() && del_after.is_empty() {
continue;
}
let (author, date) = {
let mut a = "Redline".to_string();
let mut d = "1970-01-01T00:00:00Z".to_string();
if let Some(v) = dom.attribute(ins, &W::author()) {
a = v.to_string();
}
if let Some(v) = dom.attribute(ins, &W::date()) {
d = v.to_string();
}
(a, d)
};
let eq_rpr = body_kids
.iter()
.find(|&&c| dom.name_is(c, &W::r()))
.and_then(|&r| dom.element(r, &W::r_pr()))
.map(|rpr| dom.clone_subtree(rpr));
let del_rpr = dom
.element(del, &W::r())
.and_then(|r| dom.element(r, &W::r_pr()))
.map(|rpr| dom.clone_subtree(rpr));
let mut prefix = Vec::new();
let mut suffix = Vec::new();
let mut seen_rev = false;
for &c in &body_kids {
let n = dom.name(c);
if n == Some(W::ins()) || n == Some(W::del()) {
seen_rev = true;
} else if n == Some(W::r()) {
if seen_rev {
suffix.push(c);
} else {
prefix.push(c);
}
}
}
let prefix_clones: Vec<NodeId> = prefix.iter().map(|&c| dom.clone_subtree(c)).collect();
let suffix_clones: Vec<NodeId> = suffix.iter().map(|&c| dom.clone_subtree(c)).collect();
for c in body_kids {
if dom.parent(c).is_some() {
dom.remove(c);
}
}
for c in prefix_clones {
dom.add(p, c);
}
let mut next_id = 1u32;
m460_push_rev_text(
dom,
p,
"ins",
&ins_before,
&author,
&date,
&mut next_id,
&None,
);
m460_push_rev_text(
dom,
p,
"del",
&del_before,
&author,
&date,
&mut next_id,
&del_rpr,
);
m460_push_eq_text(dom, p, anchor, &eq_rpr);
m460_push_rev_text(
dom,
p,
"ins",
&ins_after,
&author,
&date,
&mut next_id,
&None,
);
m460_push_rev_text(
dom,
p,
"del",
&del_after,
&author,
&date,
&mut next_id,
&del_rpr,
);
for c in suffix_clones {
dom.add(p, c);
}
}
}
fn split_around_whole_word(text: &str, word: &str) -> Option<(String, String)> {
let lower = text.to_ascii_lowercase();
let w = word.to_ascii_lowercase();
let mut start = 0usize;
while start < lower.len() {
let Some(rel) = lower[start..].find(&w) else {
break;
};
let i = start + rel;
let j = i + w.len();
let prev_ok = i == 0
|| !text
.get(i - 1..i)
.and_then(|s| s.chars().next())
.is_some_and(|c| c.is_alphanumeric());
let next_ok = j >= text.len()
|| !text
.get(j..j + 1)
.and_then(|s| s.chars().next())
.is_some_and(|c| c.is_alphanumeric());
if prev_ok && next_ok {
return Some((text[..i].to_string(), text[j..].to_string()));
}
start = i + 1;
}
None
}
fn m460_push_eq_text(dom: &mut Dom, p: NodeId, text: &str, sample_rpr: &Option<NodeId>) {
if text.is_empty() {
return;
}
let r = dom.new_element(W::r());
if let Some(rpr) = sample_rpr {
let rpr_c = dom.clone_subtree(*rpr);
dom.add(r, rpr_c);
}
let t = dom.new_element(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);
dom.add(p, r);
}
#[allow(clippy::too_many_arguments)]
fn m460_push_rev_text(
dom: &mut Dom,
p: NodeId,
kind: &str,
text: &str,
author: &str,
date: &str,
next_id: &mut u32,
sample_rpr: &Option<NodeId>,
) {
if text.is_empty() {
return;
}
match kind {
"ins" => {
let ins = dom.new_element(W::ins());
dom.set_attribute_value(ins, &W::id(), Some(&next_id.to_string()));
*next_id += 1;
dom.set_attribute_value(ins, &W::author(), Some(author));
dom.set_attribute_value(ins, &W::date(), Some(date));
let r = dom.new_element(W::r());
let t = dom.new_element(W::t());
if text.starts_with(' ') || text.ends_with(' ') || text.contains(' ') {
dom.set_attribute_value(t, &XNamespace::xml().name("space"), Some("preserve"));
}
dom.add_text(t, text);
dom.add(r, t);
dom.add(ins, r);
dom.add(p, ins);
}
"del" => {
let del = dom.new_element(W::del());
dom.set_attribute_value(del, &W::id(), Some(&next_id.to_string()));
*next_id += 1;
dom.set_attribute_value(del, &W::author(), Some(author));
dom.set_attribute_value(del, &W::date(), Some(date));
let r = dom.new_element(W::r());
if let Some(rpr) = sample_rpr {
let rpr_c = dom.clone_subtree(*rpr);
dom.add(r, rpr_c);
}
let t = dom.new_element(W::del_text());
if text.starts_with(' ') || text.ends_with(' ') || text.contains(' ') {
dom.set_attribute_value(t, &XNamespace::xml().name("space"), Some("preserve"));
}
dom.add_text(t, text);
dom.add(r, t);
dom.add(del, r);
dom.add(p, del);
}
_ => {}
}
}
pub fn free_mesh_pure_i_this_text(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
for i in 0..kids.len() {
let p = kids[i];
if !dom.name_is(p, &W::p()) || !para_is_pure_inserted(dom, p) {
continue;
}
let mut following_del = String::new();
let mut seen_p = 0usize;
for &k in kids.iter().skip(i + 1) {
if !dom.name_is(k, &W::p()) {
continue;
}
for t in dom.descendants(k, Some(&W::del_text())) {
following_del.push_str(&dom.value_str(t));
following_del.push(' ');
}
seen_p += 1;
if seen_p >= 2 {
break;
}
}
let following_del_l = following_del.to_ascii_lowercase();
if !following_del_l.contains("text") {
continue;
}
let mut ins_nodes = Vec::new();
let mut other = false;
for c in dom.elements(p, None) {
if dom.name_is(c, &W::p_pr()) {
continue;
}
if dom.name_is(c, &W::ins()) {
ins_nodes.push(c);
} else if dom.name_is(c, &W::r()) {
let mut t = String::new();
for tn in dom.descendants(c, Some(&W::t())) {
t.push_str(&dom.value_str(tn));
}
if t.chars().any(|ch| ch.is_alphanumeric()) {
other = true;
break;
}
} else {
other = true;
break;
}
}
if other || ins_nodes.is_empty() {
continue;
}
let mut text = String::new();
for &ins in &ins_nodes {
for t in dom.descendants(ins, Some(&W::t())) {
text.push_str(&dom.value_str(t));
}
}
let toks = alnum_tokens(&text);
if !(5..=20).contains(&toks.len()) {
continue;
}
let lower = text.to_ascii_lowercase();
if !lower.starts_with("this ") && !lower.starts_with("this\t") {
if !lower.starts_with("this") {
continue;
}
if lower.len() > 4 && lower.as_bytes()[4].is_ascii_alphanumeric() {
continue;
}
}
let Some((before_text, after_text)) = split_around_whole_word(&text, "text") else {
continue;
};
let Some((this_tok, mid)) = split_leading_this(&before_text) else {
continue;
};
if mid.trim().is_empty() && after_text.trim().is_empty() {
continue;
}
let (author, date) = {
let mut a = "Redline".to_string();
let mut d = "1970-01-01T00:00:00Z".to_string();
if let Some(v) = dom.attribute(ins_nodes[0], &W::author()) {
a = v.to_string();
}
if let Some(v) = dom.attribute(ins_nodes[0], &W::date()) {
d = v.to_string();
}
(a, d)
};
let sample_rpr = kids
.iter()
.take(i)
.rev()
.find(|&&k| dom.name_is(k, &W::p()))
.and_then(|&tp| {
dom.elements(tp, None)
.into_iter()
.find(|&c| dom.name_is(c, &W::r()))
.and_then(|r| dom.element(r, &W::r_pr()))
})
.map(|rpr| dom.clone_subtree(rpr));
let body_kids: Vec<NodeId> = dom
.elements(p, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
for c in body_kids {
if dom.parent(c).is_some() {
dom.remove(c);
}
}
let mut next_id = 1u32;
let this_eq = if this_tok.ends_with(' ') {
this_tok
} else {
format!("{this_tok} ")
};
m460_push_eq_text(dom, p, &this_eq, &sample_rpr);
m460_push_rev_text(dom, p, "ins", &mid, &author, &date, &mut next_id, &None);
let text_eq =
if after_text.starts_with(' ') || (!after_text.is_empty() && !mid.ends_with(' ')) {
if after_text.is_empty() {
"text".to_string()
} else {
"text ".to_string()
}
} else {
"text".to_string()
};
m460_push_eq_text(dom, p, &text_eq, &sample_rpr);
let after = after_text.trim_start();
m460_push_rev_text(dom, p, "ins", after, &author, &date, &mut next_id, &None);
}
}
fn split_leading_this(text: &str) -> Option<(String, String)> {
let lower = text.to_ascii_lowercase();
if !lower.starts_with("this") {
return None;
}
let rest_start = 4usize;
if text.len() > rest_start {
let next = text[rest_start..].chars().next()?;
if next.is_alphanumeric() {
return None;
}
}
let mut end = rest_start;
for (i, c) in text[rest_start..].char_indices() {
if c.is_whitespace() {
end = rest_start + i + c.len_utf8();
} else {
break;
}
}
if end == rest_start {
return Some((
text[..rest_start].to_string(),
text[rest_start..].to_string(),
));
}
Some((text[..end].to_string(), text[end..].to_string()))
}
pub fn peel_trailing_for_word_onto_next_mix(dom: &mut Dom, root: NodeId) {
let Some(body) = dom.element(root, &W::body()) else {
return;
};
let kids: Vec<NodeId> = dom
.elements(body, None)
.into_iter()
.filter(|&k| !dom.name_is(k, &W::sect_pr()))
.collect();
for i in 0..kids.len().saturating_sub(1) {
let p = kids[i];
let nxt = kids[i + 1];
if !dom.name_is(p, &W::p())
|| !dom.name_is(nxt, &W::p())
|| !para_is_mixed_revision(dom, p)
|| !para_is_mixed_revision(dom, nxt)
{
continue;
}
let p_body: Vec<NodeId> = dom
.elements(p, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
let n_body: Vec<NodeId> = dom
.elements(nxt, None)
.into_iter()
.filter(|&c| !dom.name_is(c, &W::p_pr()))
.collect();
let Some(&ins) = p_body.iter().rev().find(|&&c| dom.name_is(c, &W::ins())) else {
continue;
};
let Some(&del) = n_body.iter().find(|&&c| dom.name_is(c, &W::del())) else {
continue;
};
if n_body.first().copied() != Some(del) {
continue;
}
let mut ins_text = String::new();
for t in dom.descendants(ins, Some(&W::t())) {
ins_text.push_str(&dom.value_str(t));
}
let mut del_text = String::new();
for t in dom.descendants(del, Some(&W::del_text())) {
del_text.push_str(&dom.value_str(t));
}
let Some((ins_keep, for_word)) = split_trailing_for_word(&ins_text) else {
continue;
};
let del_trim = del_text.trim_end();
let del_lower = del_trim.to_ascii_lowercase();
if !del_lower.ends_with(" for") {
continue;
}
let for_start = del_trim.len() - 4; if for_start > 0 {
let before = del_trim.as_bytes()[for_start - 1];
let _ = before;
}
let del_keep = del_trim[..del_trim.len() - 4].to_string(); if ins_keep.trim().is_empty() || del_keep.trim().is_empty() {
continue;
}
let (author, date) = {
let mut a = "Redline".to_string();
let mut d = "1970-01-01T00:00:00Z".to_string();
if let Some(v) = dom.attribute(ins, &W::author()) {
a = v.to_string();
}
if let Some(v) = dom.attribute(ins, &W::date()) {
d = v.to_string();
}
(a, d)
};
let sample_rpr = n_body
.iter()
.find(|&&c| dom.name_is(c, &W::r()))
.and_then(|&r| dom.element(r, &W::r_pr()))
.or_else(|| {
dom.element(ins, &W::r())
.and_then(|r| dom.element(r, &W::r_pr()))
})
.map(|rpr| dom.clone_subtree(rpr));
let ins_rpr = dom
.element(ins, &W::r())
.and_then(|r| dom.element(r, &W::r_pr()))
.map(|rpr| dom.clone_subtree(rpr));
rewrite_rev_text(dom, ins, &ins_keep, false);
rewrite_rev_text(dom, del, &del_keep, true);
let eq_text = " for ";
let r_eq = dom.new_element(W::r());
if let Some(rpr) = sample_rpr {
let rpr_c = dom.clone_subtree(rpr);
dom.add(r_eq, rpr_c);
}
let t_eq = dom.new_element(W::t());
dom.set_attribute_value(t_eq, &XNamespace::xml().name("space"), Some("preserve"));
dom.add_text(t_eq, eq_text);
dom.add(r_eq, t_eq);
let new_ins = dom.new_element(W::ins());
dom.set_attribute_value(new_ins, &W::id(), Some("1"));
dom.set_attribute_value(new_ins, &W::author(), Some(&author));
dom.set_attribute_value(new_ins, &W::date(), Some(&date));
let r_ins = dom.new_element(W::r());
if let Some(rpr) = ins_rpr {
let rpr_c = dom.clone_subtree(rpr);
dom.add(r_ins, rpr_c);
}
let t_ins = dom.new_element(W::t());
let word_sp = format!("{for_word} ");
dom.set_attribute_value(t_ins, &XNamespace::xml().name("space"), Some("preserve"));
dom.add_text(t_ins, &word_sp);
dom.add(r_ins, t_ins);
dom.add(new_ins, r_ins);
dom.add_after_self(del, r_eq);
dom.add_after_self(r_eq, new_ins);
}
}
fn split_trailing_for_word(text: &str) -> Option<(String, String)> {
let trimmed = text.trim_end();
let lower = trimmed.to_ascii_lowercase();
let Some(idx) = lower.rfind(" for ") else {
return None;
};
let after = &trimmed[idx + 5..];
let word = after.trim();
if word.is_empty() || !word.chars().all(|c| c.is_alphanumeric()) {
return None;
}
if word.chars().any(|c| c.is_whitespace()) {
return None;
}
let before = trimmed[..idx].to_string();
if before.trim().is_empty() {
return None;
}
Some((before, word.to_string()))
}
fn rewrite_rev_text(dom: &mut Dom, rev: NodeId, new_text: &str, is_del: bool) {
let runs: Vec<NodeId> = dom.elements(rev, Some(&W::r())).to_vec();
let r0 = if let Some(&r) = runs.first() {
let tags: Vec<NodeId> = if is_del {
dom.elements(r, Some(&W::del_text())).to_vec()
} else {
dom.elements(r, Some(&W::t())).to_vec()
};
for t in tags {
if dom.parent(t).is_some() {
dom.remove(t);
}
}
for r in runs.iter().skip(1) {
if dom.parent(*r).is_some() {
dom.remove(*r);
}
}
r
} else {
let r = dom.new_element(W::r());
dom.add(rev, r);
r
};
let t = if is_del {
dom.new_element(W::del_text())
} else {
dom.new_element(W::t())
};
if new_text.starts_with(' ') || new_text.ends_with(' ') || new_text.contains(' ') {
dom.set_attribute_value(t, &XNamespace::xml().name("space"), Some("preserve"));
}
dom.add_text(t, new_text);
dom.add(r0, t);
}
pub fn hoist_hyperlinks_out_of_revisions(dom: &mut Dom, root: NodeId) {
let rev_names = [W::name("ins"), W::name("del")];
let hyperlink = W::hyperlink();
let mut next_id = dom
.descendants_and_self(root, None)
.into_iter()
.filter_map(|n| dom.attribute(n, &W::id()).and_then(|v| v.parse::<u32>().ok()))
.max()
.unwrap_or(0)
+ 1;
for _ in 0..16 {
let targets: Vec<NodeId> = rev_names
.iter()
.flat_map(|n| dom.descendants(root, Some(n)))
.filter(|&rev| !dom.elements(rev, Some(&hyperlink)).is_empty())
.collect();
if targets.is_empty() {
return;
}
for rev in targets {
split_revision_around_hyperlinks(dom, rev, &mut next_id);
}
}
}
fn split_revision_around_hyperlinks(dom: &mut Dom, rev: NodeId, next_id: &mut u32) {
let Some(rev_name) = dom.name(rev) else { return };
let hyperlink = W::hyperlink();
let attrs = dom.attributes(rev);
let children = dom.nodes(rev);
let w_id = W::id();
let like_rev = |dom: &mut Dom, next_id: &mut u32| -> NodeId {
let e = dom.new_element(rev_name.clone());
for (name, value) in &attrs {
dom.set_attribute_value(e, name, Some(value));
}
dom.set_attribute_value(e, &w_id, Some(&next_id.to_string()));
*next_id += 1;
e
};
let mut pending: Option<NodeId> = None; for child in children {
dom.remove(child);
if dom.name(child).as_ref() == Some(&hyperlink) {
pending = None;
let inner_children = dom.nodes(child);
if !inner_children.is_empty() {
let inner = like_rev(dom, next_id);
for ic in inner_children {
dom.remove(ic);
dom.add(inner, ic);
}
dom.add(child, inner);
}
dom.add_before_self(rev, child);
continue;
}
let target = match pending {
Some(p) => p,
None => {
let p = like_rev(dom, next_id);
dom.add_before_self(rev, p);
pending = Some(p);
p
}
};
dom.add(target, child);
}
dom.remove(rev);
}
pub fn repair_inherited_invalidity(dom: &mut Dom, root: NodeId) {
for el in dom.descendants_and_self(root, None) {
let Some(name) = dom.name(el) else { continue };
if name.namespace_name() != W::URI {
continue;
}
for (attr, _) in dom.attributes(el) {
if attr.namespace_name().is_empty()
&& !dom.is_namespace_declaration(&attr)
&& attr.local_name() != "xmlns"
{
dom.set_attribute_value(el, &attr, None);
}
}
}
for shd in dom.descendants_and_self(root, Some(&W::name("shd"))) {
if dom.attribute(shd, &W::val()).is_none() {
dom.set_attribute_value(shd, &W::val(), Some("clear"));
}
}
let lvl = W::name("lvl");
let r_pr = W::r_pr();
for hl in dom.descendants(root, Some(&W::name("highlight"))) {
let in_numbering_symbol = dom
.parent(hl)
.filter(|&p| dom.name(p).as_ref() == Some(&r_pr))
.and_then(|p| dom.parent(p))
.is_some_and(|g| dom.name(g).as_ref() == Some(&lvl));
if in_numbering_symbol {
dom.remove(hl);
}
}
}
pub fn enforce_deleted_text_kinds(dom: &mut Dom, root: NodeId) {
fn walk(dom: &mut Dom, node: NodeId) {
for c in dom.nodes(node) {
if !dom.is_element(c) {
continue;
}
match dom.name(c).as_ref() {
Some(n)
if n == &W::ins() || n == &W::name("moveFrom") || n == &W::name("moveTo") =>
{
continue;
}
Some(n) if n == &W::t() => dom.set_name(c, W::del_text()),
Some(n) if n == &W::instr_text() => dom.set_name(c, W::name("delInstrText")),
_ => walk(dom, c),
}
}
}
for del in dom.descendants(root, Some(&W::del())) {
walk(dom, del);
}
}