use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
use crate::markup_simplifier::remove_rsid_transform;
use crate::namespaces::{M, PT, W};
use crate::xmllinq::{Dom, NodeId, XName};
const TRACKED_REVISION_LOCALS: &[&str] = &[
"cellDel",
"cellIns",
"cellMerge",
"customXmlDelRangeEnd",
"customXmlDelRangeStart",
"customXmlInsRangeEnd",
"customXmlInsRangeStart",
"customXmlMoveFromRangeEnd",
"customXmlMoveFromRangeStart",
"customXmlMoveToRangeEnd",
"customXmlMoveToRangeStart",
"del",
"delInstrText",
"delText",
"ins",
"moveFrom",
"moveFromRangeEnd",
"moveFromRangeStart",
"moveTo",
"moveToRangeEnd",
"moveToRangeStart",
"numberingChange",
"pPrChange",
"rPrChange",
"sectPrChange",
"tblGridChange",
"tblPrChange",
"tblPrExChange",
"tcPrChange",
"trPrChange",
];
static TRACKED_REVISION_LOCAL_SET: LazyLock<HashSet<&'static str>> =
LazyLock::new(|| TRACKED_REVISION_LOCALS.iter().copied().collect());
pub fn tracked_revisions_elements() -> Vec<XName> {
TRACKED_REVISION_LOCALS.iter().map(|l| W::name(l)).collect()
}
pub fn element_has_tracked_revisions(dom: &Dom, root: NodeId) -> bool {
fn walk(dom: &Dom, id: NodeId) -> bool {
if let Some(name) = dom.name(id)
&& name.namespace_name() == W::URI
&& TRACKED_REVISION_LOCAL_SET.contains(name.local_name())
{
return true;
}
for i in 0..dom.child_count(id) {
if walk(dom, dom.child_at(id, i)) {
return true;
}
}
false
}
walk(dom, root)
}
fn has_child(dom: &Dom, el: NodeId, name: &XName) -> bool {
dom.element(el, name).is_some()
}
fn has_path(dom: &Dom, el: NodeId, a: &XName, b: &XName) -> bool {
dom.elements(el, Some(a))
.into_iter()
.any(|ae| has_child(dom, ae, b))
}
pub fn accept_move_from_move_to_transform(dom: &mut Dom, node: NodeId) -> Vec<NodeId> {
if !element_has_tracked_revisions(dom, node) {
return take_subtree(dom, node);
}
if !dom.is_element(node) {
return take_subtree(dom, node);
}
let name = dom.name(node).unwrap();
if name == W::move_to() {
let mut out = Vec::new();
for c in dom.nodes(node) {
out.extend(accept_move_from_move_to_transform(dom, c));
}
return out;
}
if name == W::move_from() {
return vec![];
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
for tn in accept_move_from_move_to_transform(dom, c) {
dom.add(ne, tn);
}
}
vec![ne]
}
fn take_subtree(dom: &mut Dom, node: NodeId) -> Vec<NodeId> {
if dom.parent(node).is_some() {
dom.remove(node);
}
vec![node]
}
fn element_or_desc_has_name(dom: &Dom, root: NodeId, name: &XName) -> bool {
fn walk(dom: &Dom, id: NodeId, name: &XName) -> bool {
if dom.name(id).as_ref() == Some(name) {
return true;
}
let n = dom.child_count(id);
for i in 0..n {
if walk(dom, dom.child_at(id, i), name) {
return true;
}
}
false
}
walk(dom, root, name)
}
fn has_run_content(dom: &Dom, element: NodeId) -> bool {
let content_names = [
W::r(),
W::smart_tag(),
W::ins(),
W::del(),
W::hyperlink(),
W::fld_simple(),
W::sdt(),
];
dom.elements(element, None)
.into_iter()
.any(|e| dom.name(e).is_some_and(|n| content_names.contains(&n)))
}
pub fn accept_all_other_revisions_transform(dom: &mut Dom, node: NodeId) -> Vec<NodeId> {
if !element_has_tracked_revisions(dom, node) {
return take_subtree(dom, node);
}
if !dom.is_element(node) {
return take_subtree(dom, node);
}
let name = dom.name(node).unwrap();
if name == W::ins() {
let mut out = Vec::new();
for c in dom.nodes(node) {
out.extend(accept_all_other_revisions_transform(dom, c));
}
return out;
}
let drop_markers = [
"customXmlDelRangeStart",
"customXmlDelRangeEnd",
"customXmlInsRangeStart",
"customXmlInsRangeEnd",
"customXmlMoveFromRangeStart",
"customXmlMoveFromRangeEnd",
"customXmlMoveToRangeStart",
"customXmlMoveToRangeEnd",
"moveFromRangeStart",
"moveFromRangeEnd",
"moveToRangeStart",
"moveToRangeEnd",
];
if name.namespace_name() == W::URI && drop_markers.contains(&name.local_name()) {
return vec![];
}
let drop_format = [
"pPrChange",
"rPrChange",
"tblPrChange",
"tblGridChange",
"tcPrChange",
"trPrChange",
"tblPrExChange",
"sectPrChange",
"numberingChange",
"delInstrText",
"delText",
"cellIns",
];
if name.namespace_name() == W::URI && drop_format.contains(&name.local_name()) {
return vec![];
}
if name == M::name("f") {
let removed = dom
.elements(node, Some(&M::name("fPr")))
.into_iter()
.any(|fpr| has_path(dom, fpr, &M::name("ctrlPr"), &W::del()));
if removed {
return vec![];
}
}
if name == W::tr() && has_path(dom, node, &W::tr_pr(), &W::del()) {
return vec![];
}
if name == W::tbl() {
let rows = dom.elements(node, Some(&W::tr()));
if !rows.is_empty()
&& rows
.iter()
.all(|&tr| has_path(dom, tr, &W::tr_pr(), &W::del()))
{
return vec![];
}
}
if name == W::del() {
return hoist_comment_markers_from(dom, node);
}
if name == W::cell_merge() {
let parent_is_tcpr = dom
.parent(node)
.and_then(|p| dom.name(p))
.is_some_and(|pn| pn == W::tc_pr());
if parent_is_tcpr {
let vmerge = dom.attribute(node, &W::v_merge()).map(|s| s.to_string());
if vmerge.as_deref() == Some("rest") {
let v = dom.new_element(W::v_merge());
dom.set_attribute_value(v, &W::val(), Some("restart"));
return vec![v];
}
if vmerge.as_deref() == Some("cont") {
let v = dom.new_element(W::v_merge());
dom.set_attribute_value(v, &W::val(), Some("continue"));
return vec![v];
}
}
}
if name == W::hyperlink() {
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
for tn in accept_all_other_revisions_transform(dom, c) {
dom.add(ne, tn);
}
}
return if has_run_content(dom, ne) {
vec![ne]
} else {
vec![]
};
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
for tn in accept_all_other_revisions_transform(dom, c) {
dom.add(ne, tn);
}
}
vec![ne]
}
pub fn accept_revisions_for_element(dom: &mut Dom, element: NodeId) -> NodeId {
let has_rev = element_has_tracked_revisions(dom, element);
let e = remove_rsid_transform(dom, element).expect("root not dropped by rsid removal");
let e = if has_rev {
let v = accept_move_from_move_to_transform(dom, e);
debug_assert_eq!(v.len(), 1);
let e = v[0];
let v = accept_all_other_revisions_transform(dom, e);
debug_assert_eq!(v.len(), 1);
v[0]
} else {
e
};
let unique_id = PT::unique_id();
let run_ids = PT::run_ids();
for d in dom.descendants_and_self(e, None) {
dom.set_attribute_value(d, &unique_id, None);
dom.set_attribute_value(d, &run_ids, None);
}
let num_pr = W::num_pr();
for np in dom.descendants(e, Some(&num_pr)) {
if !dom.has_elements(np) {
dom.remove(np);
}
}
e
}
pub fn accept_revisions_document(dom: &mut Dom, root: NodeId) -> NodeId {
accept_revisions_for_part_content(dom, root)
}
fn effective_parent(dom: &Dom, node: NodeId) -> Option<NodeId> {
let skip = [W::sdt_content(), W::sdt(), W::smart_tag()];
dom.ancestors(node, None)
.into_iter()
.find(|&a| dom.name(a).is_some_and(|n| !skip.contains(&n)))
}
fn reverse_revisions_transform(dom: &mut Dom, node: NodeId) -> NodeId {
if !dom.is_element(node) {
return dom.clone_subtree(node);
}
let name = dom.name(node).unwrap();
let parent = effective_parent(dom, node);
let parent_name = parent.and_then(|p| dom.name(p));
let grandparent_is_ppr = parent
.and_then(|p| dom.parent(p))
.and_then(|gp| dom.name(gp))
.is_some_and(|n| n == W::p_pr());
if name == W::del() && parent_name.as_ref() == Some(&W::r_pr()) && grandparent_is_ppr {
return dom.new_element(W::ins());
}
if name == W::ins() && parent_name.as_ref() == Some(&W::r_pr()) && grandparent_is_ppr {
return dom.new_element(W::del());
}
if name == W::del() && parent_name.as_ref() == Some(&W::tr_pr()) {
return dom.new_element(W::ins());
}
if name == W::ins() && parent_name.as_ref() == Some(&W::tr_pr()) {
return dom.new_element(W::del());
}
if name == W::del() {
return rebuild_named(dom, W::ins(), node, false);
}
if name == W::ins() {
return rebuild_named(dom, W::del(), node, false);
}
let swap_pairs: &[(&str, &str)] = &[
("moveFrom", "moveTo"),
("moveFromRangeStart", "moveToRangeStart"),
("moveFromRangeEnd", "moveToRangeEnd"),
("moveTo", "moveFrom"),
("moveToRangeStart", "moveFromRangeStart"),
("moveToRangeEnd", "moveFromRangeEnd"),
("customXmlDelRangeStart", "customXmlInsRangeStart"),
("customXmlDelRangeEnd", "customXmlInsRangeEnd"),
("customXmlInsRangeStart", "customXmlDelRangeStart"),
("customXmlInsRangeEnd", "customXmlDelRangeEnd"),
("customXmlMoveFromRangeStart", "customXmlMoveToRangeStart"),
("customXmlMoveFromRangeEnd", "customXmlMoveToRangeEnd"),
("customXmlMoveToRangeStart", "customXmlMoveFromRangeStart"),
("customXmlMoveToRangeEnd", "customXmlMoveFromRangeEnd"),
("delInstrText", "instrText"),
("delText", "t"),
];
if name.namespace_name() == W::URI
&& let Some((_, to)) = swap_pairs
.iter()
.find(|(from, _)| *from == name.local_name())
{
return rebuild_named(dom, W::name(to), node, true);
}
rebuild_named(dom, name, node, true)
}
fn rebuild_named(dom: &mut Dom, new_name: XName, src: NodeId, keep_attrs: bool) -> NodeId {
let ne = dom.new_element(new_name);
if keep_attrs {
for (an, av) in dom.attributes(src) {
dom.set_attribute_value(ne, &an, Some(&av));
}
}
for c in dom.nodes(src) {
let t = reverse_revisions_transform(dom, c);
dom.add(ne, t);
}
ne
}
fn reject_revisions_for_part_transform(dom: &mut Dom, node: NodeId) -> Option<NodeId> {
if !dom.is_element(node) {
return Some(dom.clone_subtree(node));
}
let name = dom.name(node).unwrap();
if name == W::num_pr() && dom.element(node, &W::ins()).is_some() {
return None;
}
let change_reverts: &[(&str, &str, &str)] = &[
("pPr", "pPrChange", "pPr"),
("sectPr", "sectPrChange", "sectPr"),
("tblGrid", "tblGridChange", "tblGrid"),
("tcPr", "tcPrChange", "tcPr"),
("trPr", "trPrChange", "trPr"),
("tblPrEx", "tblPrExChange", "tblPrEx"),
("tblPr", "tblPrChange", "tblPr"),
];
if name.namespace_name() == W::URI
&& let Some((_, change, saved)) = change_reverts
.iter()
.find(|(prop, _, _)| *prop == name.local_name())
&& let Some(chg) = dom.element(node, &W::name(change))
{
let new_prop = match dom.element(chg, &W::name(saved)) {
Some(sp) => dom.clone_subtree(sp),
None => dom.new_element(W::name(saved)),
};
if name == W::p_pr()
&& let Some(rpr) = dom.element(node, &W::r_pr())
{
let rpr_clone = dom.clone_subtree(rpr);
dom.add(new_prop, rpr_clone);
}
return reject_revisions_for_part_transform(dom, new_prop);
}
if name == W::r_pr()
&& let Some(chg) = dom.element(node, &W::r_pr_change())
{
let saved = dom.element(chg, &W::r_pr());
let new_rpr = match saved {
Some(sp) => dom.clone_subtree(sp),
None => dom.new_element(W::r_pr()),
};
return reject_revisions_for_part_transform(dom, new_rpr);
}
if name == W::numbering_change() || name == W::cell_del() || name == W::cell_merge() {
return None;
}
if name == W::tc() {
let has_cell_ins = dom
.elements(node, Some(&W::tc_pr()))
.into_iter()
.any(|tcpr| dom.element(tcpr, &W::cell_ins()).is_some());
if has_cell_ins {
return None;
}
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
if let Some(t) = reject_revisions_for_part_transform(dom, c) {
dom.add(ne, t);
}
}
Some(ne)
}
pub fn reject_revisions_document(dom: &mut Dom, root: NodeId) -> NodeId {
if !element_has_tracked_revisions(dom, root) {
return remove_rsid_transform(dom, root).expect("reject clean: root not dropped by rsid");
}
let reverted =
reject_revisions_for_part_transform(dom, root).expect("reject: root must not be dropped");
let reversed = reverse_revisions_transform(dom, reverted);
let derssid = remove_rsid_transform(dom, reversed).expect("reject: root not dropped by rsid");
accept_revisions_for_part_content(dom, derssid)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TagType {
Element,
EmptyElement,
EndElement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Tag {
pub element: NodeId,
pub tag_type: TagType,
}
pub fn descendant_and_self_tags(dom: &Dom, element: NodeId) -> Vec<Tag> {
let mut out = vec![Tag {
element,
tag_type: TagType::Element,
}];
let mut stack: Vec<(Vec<NodeId>, usize)> = vec![(element_children_vec(dom, element), 0)];
while let Some(top) = stack.last_mut() {
if top.1 < top.0.len() {
let current = top.0[top.1];
top.1 += 1;
if dom.child_count(current) == 0 {
out.push(Tag {
element: current,
tag_type: TagType::EmptyElement,
});
continue;
}
out.push(Tag {
element: current,
tag_type: TagType::Element,
});
stack.push((element_children_vec(dom, current), 0));
continue;
}
stack.pop();
if let Some(parent_frame) = stack.last() {
out.push(Tag {
element: parent_frame.0[parent_frame.1 - 1],
tag_type: TagType::EndElement,
});
}
}
out.push(Tag {
element,
tag_type: TagType::EndElement,
});
out
}
fn element_children_vec(dom: &Dom, id: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
let n = dom.child_count(id);
for i in 0..n {
let c = dom.child_at(id, i);
if dom.is_element(c) {
out.push(c);
}
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlockContentInfo {
pub previous_block_content_element: Option<NodeId>,
pub this_block_content_element: Option<NodeId>,
pub next_block_content_element: Option<NodeId>,
}
fn first_block_content(dom: &Dom, roots: &[NodeId]) -> Option<NodeId> {
let (p, tbl) = (W::p(), W::tbl());
let is_block = |e: NodeId| dom.name(e).is_some_and(|n| n == p || n == tbl);
for &r in roots {
if is_block(r) {
return Some(r);
}
let mut stack: Vec<(NodeId, usize)> = vec![(r, 0)];
while let Some((node, i)) = stack.last_mut() {
let n = dom.child_count(*node);
if *i >= n {
stack.pop();
continue;
}
let c = dom.child_at(*node, *i);
*i += 1;
if !dom.is_element(c) {
continue;
}
if is_block(c) {
return Some(c);
}
stack.push((c, 0));
}
}
None
}
fn elements_after_self(dom: &Dom, id: NodeId) -> Vec<NodeId> {
dom.nodes_after_self(id)
.into_iter()
.filter(|&n| dom.is_element(n))
.collect()
}
pub fn iterate_block_content_elements(dom: &Dom, element: NodeId) -> Vec<BlockContentInfo> {
let kids = element_children_vec(dom, element);
if kids.is_empty() {
return Vec::new();
}
let Some(first) = first_block_content(dom, &kids) else {
return Vec::new();
};
let mut chain: Vec<NodeId> = vec![first];
'outer: loop {
let mut current = *chain.last().unwrap();
loop {
if let Some(next) = first_block_content(dom, &elements_after_self(dom, current)) {
chain.push(next);
break;
}
let Some(parent) = dom.parent(current) else {
break 'outer;
};
current = parent;
if current == element {
break 'outer;
}
}
}
(0..chain.len())
.map(|i| BlockContentInfo {
previous_block_content_element: (i > 0).then(|| chain[i - 1]),
this_block_content_element: Some(chain[i]),
next_block_content_element: chain.get(i + 1).copied(),
})
.collect()
}
fn block_level_content_containers() -> [XName; 7] {
[
W::body(),
W::tc(),
W::txbx_content(),
W::hdr(),
W::ftr(),
W::endnote(),
W::footnote(),
]
}
pub fn get_paragraph_info(dom: &Dom, content_element: NodeId) -> BlockContentInfo {
let parent = dom
.parent(content_element)
.expect("GetParagraphInfo called for element without parent");
let parent_name = dom.name(parent);
assert!(
parent_name.is_some_and(|n| block_level_content_containers().contains(&n)),
"GetParagraphInfo called for element that is not child of content container"
);
let (p, tc, txbx) = (W::p(), W::tc(), W::txbx_content());
let mut paragraph = dom
.descendants_and_self(content_element, None)
.into_iter()
.find(|&e| dom.name(e).is_some_and(|n| n == p || n == tc || n == txbx));
if let Some(hit) = paragraph
&& dom.name(hit).is_some_and(|n| n == tc || n == txbx)
{
paragraph = None;
}
let previous = dom
.nodes_before_self(content_element)
.into_iter()
.rev()
.find(|&n| dom.is_element(n));
BlockContentInfo {
previous_block_content_element: previous,
this_block_content_element: paragraph,
next_block_content_element: None,
}
}
pub fn content_elements_before_self(dom: &Dom, element: NodeId) -> Vec<NodeId> {
dom.nodes_before_self(element)
.into_iter()
.rev()
.filter(|&n| dom.is_element(n))
.collect()
}
fn transform_instr_text_to_del_instr_text(dom: &mut Dom, node: NodeId) -> NodeId {
if !dom.is_element(node) {
return dom.clone_subtree(node);
}
let name = dom.name(node).unwrap();
if name == W::instr_text() {
let ne = dom.new_element(W::del_instr_text());
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
let cc = dom.clone_subtree(c);
dom.add(ne, cc);
}
return ne;
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
let tc = transform_instr_text_to_del_instr_text(dom, c);
dom.add(ne, tc);
}
ne
}
pub fn fix_up_deleted_or_inserted_field_codes_transform(dom: &mut Dom, node: NodeId) -> NodeId {
let fld = W::fld_char();
let instr = W::instr_text();
if !element_or_desc_has_name(dom, node, &fld) && !element_or_desc_has_name(dom, node, &instr) {
return node;
}
if !dom.is_element(node) {
return node;
}
let name = dom.name(node).unwrap();
if name == W::p() {
let key_of = |dom: &Dom, e: NodeId| -> u8 {
let n = dom.name(e).unwrap();
let holds_fld_char = |d: &Dom| {
d.elements(e, Some(&W::r()))
.into_iter()
.any(|r| d.element(r, &W::fld_char()).is_some())
};
if n == W::del() && holds_fld_char(dom) {
2
} else if n == W::ins() && holds_fld_char(dom) {
3
} else if n == W::r() && dom.element(e, &W::instr_text()).is_some() {
4
} else {
1
}
};
let children = dom.elements(node, None);
let grouped = crate::util::group_adjacent(children, |&e| key_of(dom, e));
let g_len = grouped.len();
let new_paragraph = dom.new_element(W::p());
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(new_paragraph, &an, Some(&av));
}
for (i, (key, group)) in grouped.iter().enumerate() {
match key {
1..=3 => {
for &e in group {
let t = fix_up_deleted_or_inserted_field_codes_transform(dom, e);
dom.add(new_paragraph, t);
}
}
4 => {
let flanked_by = |k: u8| {
i != 0 && i != g_len - 1 && grouped[i - 1].0 == k && grouped[i + 1].0 == k
};
if flanked_by(2) {
let del = dom.new_element(W::del());
for &e in group {
let t = transform_instr_text_to_del_instr_text(dom, e);
dom.add(del, t);
}
dom.add(new_paragraph, del);
} else if flanked_by(3) {
let ins = dom.new_element(W::ins());
for &e in group {
let t = fix_up_deleted_or_inserted_field_codes_transform(dom, e);
dom.add(ins, t);
}
dom.add(new_paragraph, ins);
} else {
for &e in group {
let t = fix_up_deleted_or_inserted_field_codes_transform(dom, e);
dom.add(new_paragraph, t);
}
}
}
_ => unreachable!("Internal error"),
}
}
return new_paragraph;
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
let tc = fix_up_deleted_or_inserted_field_codes_transform(dom, c);
dom.add(ne, tc);
}
ne
}
pub fn accept_move_from_ranges(dom: &mut Dom, document: NodeId) -> NodeId {
use std::collections::{HashMap, HashSet};
let mfrs = W::move_from_range_start();
let mfre = W::move_from_range_end();
let mut start_tags_in_range: Vec<NodeId> = Vec::new();
let mut end_tags_in_range: Vec<NodeId> = Vec::new();
let mut potential: HashMap<String, (Vec<NodeId>, Vec<NodeId>)> = HashMap::new();
for tag in descendant_and_self_tags(dom, document) {
let name = dom.name(tag.element).unwrap();
if name == mfrs {
let id = dom
.attribute(tag.element, &W::id())
.unwrap_or("")
.to_string();
potential.insert(id, (Vec::new(), Vec::new()));
continue;
}
if name == mfre {
let id = dom.attribute(tag.element, &W::id()).unwrap_or("");
if let Some((starts, ends)) = potential.remove(id) {
start_tags_in_range.extend(starts);
end_tags_in_range.extend(ends);
}
continue;
}
if potential.is_empty() {
continue;
}
match tag.tag_type {
TagType::Element => {
for (starts, _) in potential.values_mut() {
starts.push(tag.element);
}
}
TagType::EmptyElement => {
for (starts, ends) in potential.values_mut() {
starts.push(tag.element);
ends.push(tag.element);
}
}
TagType::EndElement => {
for (_, ends) in potential.values_mut() {
ends.push(tag.element);
}
}
}
}
let end_set: HashSet<NodeId> = end_tags_in_range.into_iter().collect();
let to_delete: HashSet<NodeId> = start_tags_in_range
.into_iter()
.filter(|e| end_set.contains(e))
.collect();
if to_delete.is_empty() {
return document;
}
accept_move_from_ranges_transform(dom, document, &to_delete)
.expect("the document root is never in a moveFrom range")
}
fn accept_move_from_ranges_transform(
dom: &mut Dom,
node: NodeId,
to_delete: &std::collections::HashSet<NodeId>,
) -> Option<NodeId> {
if !dom.is_element(node) {
return Some(dom.clone_subtree(node));
}
if to_delete.contains(&node) {
return None;
}
let ne = dom.new_element(dom.name(node).unwrap());
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
if let Some(tc) = accept_move_from_ranges_transform(dom, c, to_delete) {
dom.add(ne, tc);
}
}
Some(ne)
}
pub fn collapse_paragraph_transform(dom: &mut Dom, node: NodeId) -> Vec<NodeId> {
if !dom.is_element(node) {
return vec![dom.clone_subtree(node)];
}
let name = dom.name(node).unwrap();
if name == W::p() {
let keep: Vec<NodeId> = dom
.elements(node, None)
.into_iter()
.filter(|&e| dom.name(e) != Some(W::p_pr()))
.collect();
return keep.into_iter().map(|e| dom.clone_subtree(e)).collect();
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
for tc in collapse_paragraph_transform(dom, c) {
dom.add(ne, tc);
}
}
vec![ne]
}
pub fn coalesque_paragraph_end_tags_in_move_from_transform(
dom: &mut Dom,
node: NodeId,
group: &[NodeId],
) -> NodeId {
if !dom.is_element(node) {
return dom.clone_subtree(node);
}
let name = dom.name(node).unwrap();
if name == W::p() {
let np = dom.new_element(W::p());
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(np, &an, Some(&av));
}
for e in dom.elements(node, None) {
let ce = dom.clone_subtree(e);
dom.add(np, ce);
}
for &member in group.iter().skip(1) {
for collapsed in collapse_paragraph_transform(dom, member) {
dom.add(np, collapsed);
}
}
return np;
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
let tc = coalesque_paragraph_end_tags_in_move_from_transform(dom, c, group);
dom.add(ne, tc);
}
ne
}
pub fn accept_paragraph_end_tags_in_move_from_transform(dom: &mut Dom, node: NodeId) -> NodeId {
if !element_or_desc_has_name(dom, node, &W::move_from_range_start()) {
return node;
}
if !dom.is_element(node) {
return dom.clone_subtree(node);
}
let name = dom.name(node).unwrap();
if block_level_content_containers().contains(&name) {
let mfrs = W::move_from_range_start();
let mfre = W::move_from_range_end();
let mark_in_open_range = |dom: &Dom, p: NodeId| {
!dom.elements(p, Some(&mfrs)).is_empty() && dom.elements(p, Some(&mfre)).is_empty()
};
let key_of = |dom: &Dom, c: NodeId| -> bool {
let pi = get_paragraph_info(dom, c);
if let Some(this) = pi.this_block_content_element
&& mark_in_open_range(dom, this)
{
return true;
}
let previous = content_elements_before_self(dom, c).into_iter().find(|&e| {
get_paragraph_info(dom, e)
.this_block_content_element
.is_some()
});
if let Some(prev) = previous {
let pi2 = get_paragraph_info(dom, prev);
if dom.name(c) == Some(W::p())
&& mark_in_open_range(dom, pi2.this_block_content_element.unwrap())
{
return true;
}
}
false
};
let children = dom.elements(node, None);
let grouped = crate::util::group_adjacent(children, |&c| key_of(dom, c));
if grouped.len() == 1 && !grouped[0].0 {
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for (key, group) in grouped {
if !key {
for e in group {
let ce = dom.clone_subtree(e);
dom.add(ne, ce);
}
} else {
let first = group[0];
let t = coalesque_paragraph_end_tags_in_move_from_transform(dom, first, &group);
dom.add(ne, t);
}
}
return ne;
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
let tc = accept_paragraph_end_tags_in_move_from_transform(dom, c);
dom.add(ne, tc);
}
return ne;
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
let tc = accept_paragraph_end_tags_in_move_from_transform(dom, c);
dom.add(ne, tc);
}
ne
}
pub fn accept_deleted_and_moved_from_content_controls(dom: &mut Dom, root: NodeId) -> NodeId {
use std::collections::{HashMap, HashSet};
let cxdel_s = W::name("customXmlDelRangeStart");
let cxdel_e = W::name("customXmlDelRangeEnd");
let cxmf_s = W::name("customXmlMoveFromRangeStart");
let cxmf_e = W::name("customXmlMoveFromRangeEnd");
let mfrs = W::move_from_range_start();
let mfre = W::move_from_range_end();
let sdt = W::sdt();
let mut del_starts: Vec<NodeId> = Vec::new();
let mut del_ends: Vec<NodeId> = Vec::new();
let mut mf_starts: Vec<NodeId> = Vec::new();
let mut mf_ends: Vec<NodeId> = Vec::new();
let mut potential_del: HashMap<String, (Vec<NodeId>, Vec<NodeId>)> = HashMap::new();
let mut potential_mf: HashMap<String, (Vec<NodeId>, Vec<NodeId>)> = HashMap::new();
for tag in descendant_and_self_tags(dom, root) {
let name = dom.name(tag.element).unwrap();
if name == cxdel_s {
let id = dom
.attribute(tag.element, &W::id())
.unwrap_or("")
.to_string();
potential_del.insert(id, (Vec::new(), Vec::new()));
continue;
}
if name == cxdel_e {
let id = dom.attribute(tag.element, &W::id()).unwrap_or("");
if let Some((starts, ends)) = potential_del.remove(id) {
del_starts.extend(starts);
del_ends.extend(ends);
}
continue;
}
if name == cxmf_s {
let id = dom
.attribute(tag.element, &W::id())
.unwrap_or("")
.to_string();
potential_mf.insert(id, (Vec::new(), Vec::new()));
continue;
}
if name == cxmf_e {
let id = dom.attribute(tag.element, &W::id()).unwrap_or("");
if let Some((starts, ends)) = potential_mf.remove(id) {
mf_starts.extend(starts);
mf_ends.extend(ends);
}
continue;
}
if name == sdt {
match tag.tag_type {
TagType::Element => {
for (starts, _) in potential_del.values_mut().chain(potential_mf.values_mut()) {
starts.push(tag.element);
}
}
TagType::EmptyElement => {
for (starts, ends) in
potential_del.values_mut().chain(potential_mf.values_mut())
{
starts.push(tag.element);
ends.push(tag.element);
}
}
TagType::EndElement => {
for (_, ends) in potential_del.values_mut().chain(potential_mf.values_mut()) {
ends.push(tag.element);
}
}
}
continue;
}
if !potential_mf.is_empty() && name != mfrs && name != mfre {
match tag.tag_type {
TagType::Element => {
for (starts, _) in potential_mf.values_mut() {
starts.push(tag.element);
}
}
TagType::EmptyElement => {
for (starts, ends) in potential_mf.values_mut() {
starts.push(tag.element);
ends.push(tag.element);
}
}
TagType::EndElement => {
for (_, ends) in potential_mf.values_mut() {
ends.push(tag.element);
}
}
}
}
}
let del_end_set: HashSet<NodeId> = del_ends.into_iter().collect();
let to_collapse: HashSet<NodeId> = del_starts
.into_iter()
.filter(|e| del_end_set.contains(e))
.collect();
let mf_end_set: HashSet<NodeId> = mf_ends.into_iter().collect();
let to_delete: HashSet<NodeId> = mf_starts
.into_iter()
.filter(|e| mf_end_set.contains(e))
.collect();
if to_collapse.is_empty() && to_delete.is_empty() {
return root;
}
let out: Vec<NodeId> = accept_deleted_and_moved_from_content_controls_transform(
dom,
root,
&to_collapse,
&to_delete,
);
debug_assert_eq!(out.len(), 1, "the root is neither collapsed nor deleted");
out[0]
}
fn accept_deleted_and_moved_from_content_controls_transform(
dom: &mut Dom,
node: NodeId,
to_collapse: &std::collections::HashSet<NodeId>,
to_delete: &std::collections::HashSet<NodeId>,
) -> Vec<NodeId> {
if !dom.is_element(node) {
return vec![dom.clone_subtree(node)];
}
let name = dom.name(node).unwrap();
if name == W::sdt() && to_collapse.contains(&node) {
let content = dom
.element(node, &W::sdt_content())
.expect("collapsed w:sdt must carry sdtContent (C# NREs otherwise)");
let mut out = Vec::new();
for c in dom.nodes(content) {
out.extend(accept_deleted_and_moved_from_content_controls_transform(
dom,
c,
to_collapse,
to_delete,
));
}
return out;
}
if to_delete.contains(&node) {
return vec![];
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
for tc in
accept_deleted_and_moved_from_content_controls_transform(dom, c, to_collapse, to_delete)
{
dom.add(ne, tc);
}
}
vec![ne]
}
fn is_run_content(name: &XName) -> Option<bool> {
if name.namespace_name() == crate::namespaces::M::URI {
return Some(true);
}
if name.namespace_name() != W::URI {
return None;
}
match name.local_name() {
"r" | "fldSimple" | "hyperlink" | "subDoc" | "smartTag" | "smartTagPr" => Some(true),
"bookmarkStart"
| "bookmarkEnd"
| "commentRangeStart"
| "commentRangeEnd"
| "customXmlDelRangeStart"
| "customXmlDelRangeEnd"
| "customXmlInsRangeStart"
| "customXmlInsRangeEnd"
| "customXmlMoveFromRangeStart"
| "customXmlMoveFromRangeEnd"
| "customXmlMoveToRangeStart"
| "customXmlMoveToRangeEnd"
| "del"
| "moveFrom"
| "moveFromRangeStart"
| "moveFromRangeEnd"
| "moveToRangeStart"
| "moveToRangeEnd"
| "permStart"
| "permEnd"
| "proofErr" => Some(false),
_ => None,
}
}
fn collapse_transform(dom: &mut Dom, node: NodeId) -> Vec<NodeId> {
if !dom.is_element(node) {
return vec![dom.clone_subtree(node)];
}
let name = dom.name(node).unwrap();
if name.namespace_name() == W::URI
&& matches!(
name.local_name(),
"dir" | "bdr" | "ins" | "moveTo" | "smartTag"
)
{
let kids = dom.elements(node, None);
return kids.into_iter().map(|e| dom.clone_subtree(e)).collect();
}
if name == W::sdt() {
let mut out = Vec::new();
for sc in dom.elements(node, Some(&W::sdt_content())) {
for e in dom.elements(sc, None) {
out.push(dom.clone_subtree(e));
}
}
return out;
}
if name == W::p_pr() {
return vec![];
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
for tc in collapse_transform(dom, c) {
dom.add(ne, tc);
}
}
vec![ne]
}
fn all_para_content_is_deleted(dom: &mut Dom, p: NodeId) -> bool {
let collapsed = collapse_transform(dom, p);
debug_assert_eq!(collapsed.len(), 1, "w:p rebuilds to a single element");
let test_p = collapsed[0];
!dom.elements(test_p, None).into_iter().any(|ce| {
let n = dom.name(ce).unwrap();
is_run_content(&n)
.unwrap_or_else(|| panic!("Internal error 20, found element {}", n.clark()))
})
}
fn paragraph_mark_is_deleted_or_moved_from(dom: &Dom, p: NodeId) -> bool {
dom.element(p, &W::p_pr())
.and_then(|ppr| dom.element(ppr, &W::r_pr()))
.is_some_and(|rpr| {
dom.elements(rpr, None).into_iter().any(|e| {
dom.name(e)
.is_some_and(|n| n == W::del() || n == W::move_from())
})
})
}
fn has_deleted_or_moved_from_paragraph_mark(dom: &Dom, root: NodeId) -> bool {
fn walk(dom: &Dom, id: NodeId) -> bool {
if dom.name(id).as_ref() == Some(&W::p())
&& paragraph_mark_is_deleted_or_moved_from(dom, id)
{
return true;
}
let n = dom.child_count(id);
for i in 0..n {
if walk(dom, dom.child_at(id, i)) {
return true;
}
}
false
}
walk(dom, root)
}
pub fn accept_deleted_and_move_from_paragraph_marks_transform(
dom: &mut Dom,
node: NodeId,
) -> NodeId {
if !dom.is_element(node) {
return dom.clone_subtree(node);
}
let name = dom.name(node).unwrap();
if !block_level_content_containers().contains(&name) {
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
let tc = accept_deleted_and_move_from_paragraph_marks_transform(dom, c);
dom.add(ne, tc);
}
return ne;
}
let body_sect_pr = if name == W::body() {
dom.element(node, &W::sect_pr())
} else {
None
};
let chain = iterate_block_content_elements(dom, node);
let mut infos: Vec<(bool, i32)> = Vec::with_capacity(chain.len());
let mut current_key = 0i32;
let mut state = 0u8; for c in &chain {
let this = c.this_block_content_element.unwrap();
let tn = dom.name(this).unwrap();
if tn == W::p() {
if paragraph_mark_is_deleted_or_moved_from(dom, this) {
match state {
0 | 2 => {
state = 1;
current_key += 1;
infos.push((true, current_key));
}
_ => infos.push((true, current_key)),
}
} else {
match state {
0 => {
current_key += 1;
infos.push((false, current_key));
}
1 => {
state = 2;
infos.push((true, current_key));
}
_ => {
state = 0;
current_key += 1;
infos.push((false, current_key));
}
}
}
} else if tn == W::tbl() || tn.namespace_name() == M::URI {
current_key += 1;
infos.push((false, current_key));
state = 0;
} else {
infos.push((false, current_key));
}
}
let zipped: Vec<(BlockContentInfo, (bool, i32))> = chain.into_iter().zip(infos).collect();
let grouped = crate::util::group_adjacent(zipped, |z| z.1.1);
let mut rebuilt: Vec<(Vec<NodeId>, Option<NodeId>, Vec<NodeId>)> = Vec::new();
for (_key, group) in &grouped {
if group[0].1.0 {
let last_this = group.last().unwrap().0.this_block_content_element.unwrap();
let np = dom.new_element(W::p());
for ppr in dom.elements(last_this, Some(&W::p_pr())) {
let c = dom.clone_subtree(ppr);
dom.add(np, c);
}
let mut orig_ids = Vec::new();
for z in group {
let this = z.0.this_block_content_element.unwrap();
orig_ids.push(this);
for collapsed in collapse_paragraph_transform(dom, this) {
dom.add(np, collapsed);
}
}
let last_mark_is_del = dom
.element(last_this, &W::p_pr())
.and_then(|ppr| dom.element(ppr, &W::r_pr()))
.is_some_and(|rpr| dom.element(rpr, &W::del()).is_some());
let next = group.last().unwrap().0.next_block_content_element;
let next_is_none_or_tbl =
next.is_none() || next.is_some_and(|n| dom.name(n) == Some(W::tbl()));
if all_para_content_is_deleted(dom, np) && last_mark_is_del && next_is_none_or_tbl {
let markers = hoist_comment_markers_from(dom, np);
rebuilt.push((orig_ids, None, markers));
} else {
rebuilt.push((orig_ids, Some(np), Vec::new()));
}
} else {
for z in group {
let this = z.0.this_block_content_element.unwrap();
let rebuilt_name = dom.name(this).unwrap();
let re = dom.new_element(rebuilt_name);
for (an, av) in dom.attributes(this) {
dom.set_attribute_value(re, &an, Some(&av));
}
for c in dom.nodes(this) {
let tc = accept_deleted_and_move_from_paragraph_marks_transform(dom, c);
dom.add(re, tc);
}
rebuilt.push((vec![this], Some(re), Vec::new()));
}
}
}
let ne = dom.new_element(name.clone());
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for e in dom.elements(node, Some(&W::tc_pr())) {
let ce = dom.clone_subtree(e);
dom.add(ne, ce);
}
let mut emitted: HashSet<usize> = HashSet::new();
for c in dom.elements(node, None) {
let Some(cn) = dom.name(c) else {
continue;
};
if cn == W::tc_pr() || cn == W::sect_pr() {
continue;
}
if is_body_level_range_marker(&cn) {
let clone = dom.clone_subtree(c);
dom.add(ne, clone);
continue;
}
if let Some(ri) = rebuilt.iter().position(|(ids, _, _)| ids.contains(&c))
&& emitted.insert(ri)
{
let (_ids, rebuilt_node, markers) = &rebuilt[ri];
if let Some(rebuilt_node) = rebuilt_node {
dom.add(ne, *rebuilt_node);
}
for &m in markers {
dom.add(ne, m);
}
continue;
}
for (ri, (ids, rebuilt_node, markers)) in rebuilt.iter().enumerate() {
if emitted.contains(&ri) {
continue;
}
let nested = ids
.iter()
.any(|&id| id == c || dom.ancestors(id, None).into_iter().any(|a| a == c));
if !nested {
continue;
}
emitted.insert(ri);
if let Some(rebuilt_node) = rebuilt_node {
dom.add(ne, *rebuilt_node);
}
for &m in markers {
dom.add(ne, m);
}
}
}
if let Some(sp) = body_sect_pr {
let c = dom.clone_subtree(sp);
dom.add(ne, c);
}
ne
}
fn is_body_level_range_marker(name: &XName) -> bool {
name.namespace_name() == W::URI
&& matches!(
name.local_name(),
"commentRangeStart"
| "commentRangeEnd"
| "bookmarkStart"
| "bookmarkEnd"
| "permStart"
| "permEnd"
)
}
fn hoist_comment_markers_from(dom: &mut Dom, node: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
for e in dom.descendants(node, None) {
let Some(n) = dom.name(e) else {
continue;
};
if n.namespace_name() == W::URI
&& matches!(n.local_name(), "commentRangeStart" | "commentRangeEnd")
{
out.push(dom.clone_subtree(e));
}
}
out
}
pub fn annotate_run_elements_with_id(dom: &mut Dom, element: NodeId) {
let unique_id = PT::unique_id();
for (run_id, r) in (0..).zip(dom.descendants(element, Some(&W::r()))) {
dom.set_attribute_value(r, &unique_id, Some(&run_id.to_string()));
}
}
fn descendants_trimmed(dom: &Dom, element: NodeId, trim: &XName) -> Vec<NodeId> {
let mut out = Vec::new();
let mut stack: Vec<NodeId> = dom.elements(element, None).into_iter().rev().collect();
while let Some(e) = stack.pop() {
out.push(e);
if dom.name(e).as_ref() != Some(trim) {
for c in dom.elements(e, None).into_iter().rev() {
stack.push(c);
}
}
}
out
}
pub fn annotate_content_controls_with_run_ids(dom: &mut Dom, element: NodeId) {
let unique_id = PT::unique_id();
let run_ids_name = PT::run_ids();
let txbx = W::txbx_content();
for (sdt_id, e) in (0..).zip(dom.descendants(element, Some(&W::sdt()))) {
let ids: Vec<String> = descendants_trimmed(dom, e, &txbx)
.into_iter()
.filter(|&d2| dom.name(d2) == Some(W::r()))
.filter_map(|r| dom.attribute(r, &unique_id).map(str::to_string))
.collect();
dom.set_attribute_value(e, &run_ids_name, Some(&ids.join(",")));
dom.set_attribute_value(e, &unique_id, Some(&sdt_id.to_string()));
}
}
fn order_sdt(name: &XName) -> i32 {
if name.namespace_name() != W::URI {
return 999;
}
match name.local_name() {
"sdtPr" => 10,
"sdtEndPr" => 20,
"sdtContent" => 30,
"bookmarkStart" => 40,
"bookmarkEnd" => 50,
_ => 999,
}
}
pub fn add_block_level_content_controls(
dom: &mut Dom,
new_document: NodeId,
original: NodeId,
) -> NodeId {
use std::collections::HashSet;
let sdt = W::sdt();
let unique_id = PT::unique_id();
let run_ids_name = PT::run_ids();
let original_ccs = dom.descendants(original, Some(&sdt));
let existing_ids: HashSet<String> = dom
.descendants(new_document, Some(&sdt))
.into_iter()
.filter_map(|e| dom.attribute(e, &unique_id).map(str::to_string))
.collect();
let mut run_by_id: HashMap<String, NodeId> = HashMap::new();
for r in dom.descendants(new_document, Some(&W::r())) {
if let Some(id) = dom.attribute(r, &unique_id) {
run_by_id.entry(id.to_string()).or_insert(r);
}
}
for cc in original_ccs {
let cc_id = dom.attribute(cc, &unique_id).unwrap_or("").to_string();
if existing_ids.contains(&cc_id) {
continue;
}
let run_ids: Vec<String> = dom
.attribute(cc, &run_ids_name)
.unwrap_or("")
.split(',')
.map(str::to_string)
.collect();
let runs: Vec<String> = dom
.descendants(cc, Some(&W::r()))
.into_iter()
.filter_map(|r| dom.attribute(r, &unique_id).map(str::to_string))
.filter(|id| run_ids.contains(id))
.collect();
let runs_in_new_document: Vec<NodeId> = runs
.iter()
.filter_map(|id| run_by_id.get(id).copied())
.collect();
let Some(first_run) = runs_in_new_document.first().copied() else {
continue;
};
let mut intersection: Vec<NodeId> = dom.ancestors(first_run, None);
for &run in &runs_in_new_document[1..] {
let anc: HashSet<NodeId> = dom.ancestors(run, None).into_iter().collect();
intersection.retain(|a| anc.contains(a));
}
let Some(&common_ancestor) = intersection.first() else {
continue;
};
let child_containing = |dom: &Dom, run: NodeId| -> NodeId {
dom.elements(common_ancestor, None)
.into_iter()
.find(|&c| {
dom.descendants_and_self(c, Some(&W::r()))
.into_iter()
.any(|z| z == run)
})
.expect("common ancestor child containing the run")
};
let first_run_child = child_containing(dom, *runs_in_new_document.first().unwrap());
let last_run_child = child_containing(dom, *runs_in_new_document.last().unwrap());
let significant: Vec<NodeId> = dom
.elements(common_ancestor, None)
.into_iter()
.filter(|&e| {
let n = dom.name(e).unwrap();
n != W::p_pr()
&& n != W::name("commentRangeStart")
&& n != W::name("commentRangeEnd")
})
.collect();
if dom.name(common_ancestor) == Some(W::p())
&& significant.first() == Some(&first_run_child)
&& significant.last() == Some(&last_run_child)
{
let new_cc = dom.new_element(dom.name(cc).unwrap());
for (an, av) in dom.attributes(cc) {
dom.set_attribute_value(new_cc, &an, Some(&av));
}
let mut cc_children = dom.elements(cc, None);
cc_children.sort_by_key(|&e| order_sdt(&dom.name(e).unwrap()));
for e in cc_children {
let clone = dom.clone_subtree(e);
dom.add(new_cc, clone);
}
dom.add_before_self(common_ancestor, new_cc);
dom.remove(common_ancestor);
continue;
}
let children = dom.elements(common_ancestor, None);
let first_idx = children.iter().position(|&c| c == first_run_child).unwrap();
let last_idx = children.iter().position(|&c| c == last_run_child).unwrap();
let before: Vec<NodeId> = children[..first_idx].to_vec();
let in_range: Vec<NodeId> = children[first_idx..=last_idx].to_vec();
let after: Vec<NodeId> = children[last_idx + 1..].to_vec();
for &c in &children {
dom.remove(c);
}
let new_cc = dom.new_element(dom.name(cc).unwrap());
for (an, av) in dom.attributes(cc) {
dom.set_attribute_value(new_cc, &an, Some(&av));
}
let sdt_content = dom.new_element(W::sdt_content());
for &e in &in_range {
dom.add(sdt_content, e); }
let cc_props: Vec<NodeId> = dom
.elements(cc, None)
.into_iter()
.filter(|&e| dom.name(e) != Some(W::sdt_content()))
.collect();
let mut cc_kids: Vec<NodeId> = cc_props.into_iter().map(|e| dom.clone_subtree(e)).collect();
cc_kids.push(sdt_content);
cc_kids.sort_by_key(|&e| order_sdt(&dom.name(e).unwrap()));
for e in cc_kids {
dom.add(new_cc, e);
}
for &e in &before {
dom.add(common_ancestor, e);
}
dom.add(common_ancestor, new_cc);
for &e in &after {
dom.add(common_ancestor, e);
}
}
new_document
}
pub fn accept_deleted_and_move_from_paragraph_marks(dom: &mut Dom, element: NodeId) -> NodeId {
if !has_deleted_or_moved_from_paragraph_mark(dom, element) {
return element;
}
annotate_run_elements_with_id(dom, element);
annotate_content_controls_with_run_ids(dom, element);
let new_element = accept_deleted_and_move_from_paragraph_marks_transform(dom, element);
add_block_level_content_controls(dom, new_element, element)
}
fn a6_block_level_elements() -> [XName; 8] {
[
W::p(),
W::tbl(),
W::sdt(),
W::del(),
W::ins(),
M::name("oMath"),
M::name("oMathPara"),
W::move_to(),
]
}
pub fn remove_rows_left_empty_by_move_from(dom: &mut Dom, node: NodeId) -> NodeId {
remove_rows_left_empty_by_move_from_inner(dom, node).expect("the root element is not a w:tr")
}
fn remove_rows_left_empty_by_move_from_inner(dom: &mut Dom, node: NodeId) -> Option<NodeId> {
if !dom.is_element(node) {
return Some(dom.clone_subtree(node));
}
let name = dom.name(node).unwrap();
if name == W::tr() {
let block = a6_block_level_elements();
let non_empty_cells = dom.elements(node, Some(&W::tc())).into_iter().any(|tc| {
dom.elements(tc, None)
.into_iter()
.any(|tcc| dom.name(tcc).is_some_and(|n| block.contains(&n)))
});
if !non_empty_cells {
return None;
}
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
if let Some(tc) = remove_rows_left_empty_by_move_from_inner(dom, c) {
dom.add(ne, tc);
}
}
Some(ne)
}
fn order_tc_pr(name: &XName) -> i32 {
if name.namespace_name() != W::URI {
return 999;
}
match name.local_name() {
"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,
_ => 999,
}
}
pub fn accept_deleted_cells_transform(dom: &mut Dom, node: NodeId) -> NodeId {
let cell_del = W::cell_del();
if !element_or_desc_has_name(dom, node, &cell_del) {
return node;
}
if !dom.is_element(node) {
return node;
}
let name = dom.name(node).unwrap();
if name != W::tr() {
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
let tc = accept_deleted_cells_transform(dom, c);
dom.add(ne, tc);
}
return ne;
}
let tc_name = W::tc();
let cell_del = W::cell_del();
let has_cell_del = |dom: &Dom, e: NodeId| !dom.descendants(e, Some(&cell_del)).is_empty();
let children = dom.elements(node, None);
let key_of = |dom: &Dom, e: NodeId| -> (bool, Option<NodeId>) {
let cell_after = dom
.nodes_after_self(e)
.into_iter()
.find(|&s| dom.name(s) == Some(tc_name.clone()));
let cell_after_is_deleted = cell_after.is_some_and(|ca| has_cell_del(dom, ca));
if dom.name(e) == Some(tc_name.clone()) && (cell_after_is_deleted || has_cell_del(dom, e)) {
let anchor = std::iter::once(e)
.chain(content_elements_before_self(dom, e))
.find(|&z| dom.name(z) == Some(tc_name.clone()) && !has_cell_del(dom, z));
return (true, anchor);
}
(false, Some(e))
};
let grouped = crate::util::group_adjacent(children, |&e| key_of(dom, e));
let tr = dom.new_element(W::tr());
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(tr, &an, Some(&av));
}
for ((is_deleted, _anchor), group) in grouped {
if !is_deleted {
for e in group {
let c = dom.clone_subtree(e);
dom.add(tr, c);
}
continue;
}
let first = group[0];
if has_cell_del(dom, first) {
continue; }
let tcpr_name = W::tc_pr();
let grid_span_name = W::grid_span();
let current_tc_pr = dom
.element(first, &tcpr_name)
.expect("anchor cell must carry w:tcPr (C# NREs on currentTcPr.Elements())");
let grid_span: i32 = dom
.element(current_tc_pr, &grid_span_name)
.and_then(|g| dom.attribute(g, &W::val()))
.and_then(|v| v.parse().ok())
.unwrap_or(1);
let new_grid_span = grid_span + group.len() as i32 - 1;
let gs = dom.new_element(grid_span_name.clone());
dom.set_attribute_value(gs, &W::val(), Some(&new_grid_span.to_string()));
let mut tcpr_kids: Vec<NodeId> = vec![gs];
let rest: Vec<NodeId> = dom
.elements(current_tc_pr, None)
.into_iter()
.filter(|&e| dom.name(e) != Some(grid_span_name.clone()))
.collect();
for e in rest {
tcpr_kids.push(dom.clone_subtree(e));
}
tcpr_kids.sort_by_key(|&e| order_tc_pr(&dom.name(e).unwrap()));
let ordered_tc_pr = dom.new_element(tcpr_name.clone());
for e in tcpr_kids {
dom.add(ordered_tc_pr, e);
}
let new_tc = dom.new_element(tc_name.clone());
dom.add(new_tc, ordered_tc_pr);
let body_kids: Vec<NodeId> = dom
.elements(first, None)
.into_iter()
.filter(|&e| dom.name(e) != Some(tcpr_name.clone()))
.collect();
for e in body_kids {
let c = dom.clone_subtree(e);
dom.add(new_tc, c);
}
dom.add(tr, new_tc);
}
tr
}
fn fix_widths(dom: &mut Dom, tbl: NodeId) -> NodeId {
let grid_lines: Vec<i64> = dom
.elements(tbl, Some(&W::name("tblGrid")))
.into_iter()
.flat_map(|g| dom.elements(g, Some(&W::name("gridCol"))))
.map(|gc| {
dom.attribute(gc, &W::name("w"))
.and_then(|v| v.parse().ok())
.expect("gridCol w:w must be an integer (C# casts)")
})
.collect();
let new_tbl = dom.clone_subtree(tbl);
for tr in dom.elements(new_tbl, Some(&W::tr())) {
let mut last_used: i64 = -1;
for tc in dom.elements(tr, Some(&W::tc())) {
let tc_w = dom
.element(tc, &W::tc_pr())
.and_then(|p| dom.element(p, &W::name("tcW")))
.filter(|&w| dom.attribute(w, &W::name("w")).is_some());
let Some(tc_w) = tc_w else { continue };
let grid_span: i64 = dom
.element(tc, &W::tc_pr())
.and_then(|p| dom.element(p, &W::grid_span()))
.and_then(|g| dom.attribute(g, &W::val()))
.and_then(|v| v.parse().ok())
.unwrap_or(1);
let z = std::cmp::min(grid_lines.len() as i64 - 1, last_used + grid_span);
let w: i64 = grid_lines
.iter()
.enumerate()
.filter(|(i, _)| (*i as i64) > last_used && (*i as i64) <= z)
.map(|(_, g)| g)
.sum();
dom.set_attribute_value(tc_w, &W::name("w"), Some(&w.to_string()));
last_used += grid_span;
}
}
new_tbl
}
fn table_has_revision_marks(dom: &Dom, tbl: NodeId) -> bool {
for tag in ["ins", "del", "moveFrom", "moveTo", "cellIns", "cellDel"] {
if element_or_desc_has_name(dom, tbl, &W::name(tag)) {
return true;
}
}
false
}
fn subtree_needs_adjacent_table_merge(dom: &Dom, root: NodeId) -> bool {
let tbl_name = W::tbl();
fn walk(dom: &Dom, id: NodeId, tbl_name: &XName) -> bool {
if !dom.is_element(id) {
return false;
}
let kids = dom.elements(id, None);
let mut run = 0usize;
let mut run_has_rev = false;
for &k in &kids {
if dom.name(k).as_ref() == Some(tbl_name) {
run += 1;
if table_has_revision_marks(dom, k) {
run_has_rev = true;
}
if run >= 2 && run_has_rev {
return true;
}
} else {
run = 0;
run_has_rev = false;
}
}
for &k in &kids {
if walk(dom, k, tbl_name) {
return true;
}
}
false
}
walk(dom, root, &tbl_name)
}
pub fn merge_adjacent_tables_transform(dom: &mut Dom, node: NodeId) -> NodeId {
if !subtree_needs_adjacent_table_merge(dom, node) {
return node;
}
if !dom.is_element(node) {
return node;
}
let name = dom.name(node).unwrap();
let tbl_name = W::tbl();
if dom.element(node, &tbl_name).is_none() {
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
let tc = merge_adjacent_tables_transform(dom, c);
dom.add(ne, tc);
}
return ne;
}
let children = dom.elements(node, None);
let grouped = crate::util::group_adjacent(children, |&e| {
if dom.name(e) != Some(tbl_name.clone()) {
return String::new();
}
let bidi = dom
.elements(e, Some(&W::tbl_pr()))
.into_iter()
.any(|p| dom.element(p, &W::name("bidiVisual")).is_some());
if bidi {
"tbl|bidiVisual".to_string()
} else {
"tbl".to_string()
}
});
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for (key, group) in grouped {
if key.is_empty() || group.len() == 1 {
for e in group {
let c = dom.clone_subtree(e);
dom.add(ne, c);
}
continue;
}
if !group.iter().any(|&t| table_has_revision_marks(dom, t)) {
for e in group {
let c = dom.clone_subtree(e);
dom.add(ne, c);
}
continue;
}
let mut rolled: Vec<i64> = Vec::new();
for &tbl in &group {
let mut sum = 0i64;
for g in dom.elements(tbl, Some(&W::name("tblGrid"))) {
for gc in dom.elements(g, Some(&W::name("gridCol"))) {
let v: i64 = dom
.attribute(gc, &W::name("w"))
.and_then(|v| v.parse().ok())
.expect("gridCol w:w must be an integer (C# casts)");
sum += v;
rolled.push(sum);
}
}
}
rolled.sort_unstable();
rolled.dedup();
let new_table = dom.new_element(tbl_name.clone());
for pr in dom.elements(group[0], Some(&W::tbl_pr())) {
let c = dom.clone_subtree(pr);
dom.add(new_table, c);
}
let new_grid = dom.new_element(W::name("tblGrid"));
for (i, &r) in rolled.iter().enumerate() {
let v = if i == 0 { r } else { r - rolled[i - 1] };
let gc = dom.new_element(W::name("gridCol"));
dom.set_attribute_value(gc, &W::name("w"), Some(&v.to_string()));
dom.add(new_grid, gc);
}
dom.add(new_table, new_grid);
for &tbl in &group {
let fixed = fix_widths(dom, tbl);
for tr in dom.elements(fixed, Some(&W::tr())) {
let new_row = dom.new_element(W::tr());
for (an, av) in dom.attributes(tr) {
dom.set_attribute_value(new_row, &an, Some(&av));
}
let non_cells: Vec<NodeId> = dom
.elements(tr, None)
.into_iter()
.filter(|&e| dom.name(e) != Some(W::tc()))
.collect();
for e in non_cells {
let c = dom.clone_subtree(e);
dom.add(new_row, c);
}
for tc in dom.elements(tr, Some(&W::tc())) {
let w: Option<i64> = dom
.element(tc, &W::tc_pr())
.and_then(|p| dom.element(p, &W::name("tcW")))
.and_then(|t| dom.attribute(t, &W::name("w")))
.and_then(|v| v.parse().ok());
let Some(w) = w else {
let c = dom.clone_subtree(tc);
dom.add(new_row, c);
continue;
};
let mut width_to_left = 0i64;
for btc in dom.elements(tr, Some(&W::tc())) {
if btc == tc {
break;
}
width_to_left += dom
.element(btc, &W::tc_pr())
.and_then(|p| dom.element(p, &W::name("tcW")))
.and_then(|t| dom.attribute(t, &W::name("w")))
.and_then(|v| v.parse::<i64>().ok())
.unwrap_or(0);
}
let rolled_pairs: Vec<i64> =
std::iter::once(0).chain(rolled.iter().copied()).collect();
let start = rolled_pairs.iter().position(|&gv| gv >= width_to_left);
let Some(start_idx) = start else {
let c = dom.clone_subtree(tc);
dom.add(new_row, c);
continue;
};
let start_value = rolled_pairs[start_idx];
let grids_required = rolled_pairs[start_idx..]
.iter()
.take_while(|&&gv| gv - start_value < w)
.count() as i64;
let mut tcpr_kids: Vec<NodeId> = Vec::new();
let props: Vec<NodeId> = dom
.elements(tc, Some(&W::tc_pr()))
.into_iter()
.flat_map(|p| dom.elements(p, None))
.filter(|&e| dom.name(e) != Some(W::grid_span()))
.collect();
for e in props {
tcpr_kids.push(dom.clone_subtree(e));
}
if grids_required != 1 {
let gs = dom.new_element(W::grid_span());
dom.set_attribute_value(gs, &W::val(), Some(&grids_required.to_string()));
tcpr_kids.push(gs);
}
tcpr_kids.sort_by_key(|&e| order_tc_pr(&dom.name(e).unwrap()));
let ordered_tc_pr = dom.new_element(W::tc_pr());
for e in tcpr_kids {
dom.add(ordered_tc_pr, e);
}
let new_cell = dom.new_element(W::tc());
dom.add(new_cell, ordered_tc_pr);
let body_kids: Vec<NodeId> = dom
.elements(tc, None)
.into_iter()
.filter(|&e| dom.name(e) != Some(W::tc_pr()))
.collect();
for e in body_kids {
let c = dom.clone_subtree(e);
dom.add(new_cell, c);
}
dom.add(new_row, new_cell);
}
dom.add(new_table, new_row);
}
}
dom.add(ne, new_table);
}
ne
}
fn has_empty_table_cell(dom: &Dom, root: NodeId) -> bool {
let tc = W::tc();
let tcpr = W::tc_pr();
fn walk(dom: &Dom, id: NodeId, tc: &XName, tcpr: &XName) -> bool {
if let Some(name) = dom.name(id)
&& name == *tc
{
let mut only_tcpr = true;
for i in 0..dom.child_count(id) {
let c = dom.child_at(id, i);
if dom.is_element(c) && dom.name(c).is_some_and(|n| n != *tcpr) {
only_tcpr = false;
break;
}
}
if only_tcpr {
return true;
}
}
for i in 0..dom.child_count(id) {
if walk(dom, dom.child_at(id, i), tc, tcpr) {
return true;
}
}
false
}
walk(dom, root, &tc, &tcpr)
}
pub fn add_empty_paragraph_to_any_empty_cells(dom: &mut Dom, node: NodeId) -> NodeId {
if !dom.is_element(node) {
return node;
}
let tc = W::tc();
let tcpr = W::tc_pr();
let mut empty: Vec<NodeId> = Vec::new();
collect_empty_table_cells(dom, node, &tc, &tcpr, &mut empty);
for cell in empty {
let p = dom.new_element(W::p());
dom.add(cell, p);
}
node
}
fn collect_empty_table_cells(
dom: &Dom,
id: NodeId,
tc: &XName,
tcpr: &XName,
out: &mut Vec<NodeId>,
) {
if let Some(name) = dom.name(id)
&& name == *tc
{
let mut only_tcpr = true;
for i in 0..dom.child_count(id) {
let c = dom.child_at(id, i);
if dom.is_element(c) && dom.name(c).is_some_and(|n| n != *tcpr) {
only_tcpr = false;
break;
}
}
if only_tcpr {
out.push(id);
}
}
for i in 0..dom.child_count(id) {
let c = dom.child_at(id, i);
if dom.is_element(c) {
collect_empty_table_cells(dom, c, tc, tcpr, out);
}
}
}
pub fn accept_revisions_for_part_content(dom: &mut Dom, root: NodeId) -> NodeId {
let has_rev = element_has_tracked_revisions(dom, root);
let e = remove_rsid_transform(dom, root).expect("root not dropped by rsid removal");
let e = if has_rev {
let e = fix_up_deleted_or_inserted_field_codes_transform(dom, e);
let contains_move_from = !dom.descendants(e, Some(&W::move_from())).is_empty();
let e = {
let v = accept_move_from_move_to_transform(dom, e);
debug_assert_eq!(v.len(), 1);
v[0]
};
let e = accept_move_from_ranges(dom, e);
let e = accept_paragraph_end_tags_in_move_from_transform(dom, e);
let e = accept_deleted_and_moved_from_content_controls(dom, e);
let e = accept_deleted_and_move_from_paragraph_marks(dom, e);
let e = if contains_move_from {
remove_rows_left_empty_by_move_from(dom, e)
} else {
e
};
let e = {
let v = accept_all_other_revisions_transform(dom, e);
debug_assert_eq!(v.len(), 1);
v[0]
};
let e = accept_deleted_cells_transform(dom, e);
merge_adjacent_tables_transform(dom, e)
} else {
e
};
let e = if has_empty_table_cell(dom, e) {
add_empty_paragraph_to_any_empty_cells(dom, e)
} else {
e
};
let unique_id = PT::unique_id();
let run_ids = PT::run_ids();
for d in dom.descendants_and_self(e, None) {
dom.set_attribute_value(d, &unique_id, None);
dom.set_attribute_value(d, &run_ids, None);
}
let num_pr = W::num_pr();
for np in dom.descendants(e, Some(&num_pr)) {
if !dom.has_elements(np) {
dom.remove(np);
}
}
e
}
fn accept_revisions_for_styles_transform(dom: &mut Dom, node: NodeId) -> Option<NodeId> {
if !dom.is_element(node) {
return Some(dom.clone_subtree(node));
}
let name = dom.name(node).unwrap();
if name == W::p_pr_change() || name == W::r_pr_change() {
return None;
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
if let Some(tc) = accept_revisions_for_styles_transform(dom, c) {
dom.add(ne, tc);
}
}
Some(ne)
}
fn reject_revisions_for_styles_transform(dom: &mut Dom, node: NodeId) -> Option<NodeId> {
if !dom.is_element(node) {
return Some(dom.clone_subtree(node));
}
let name = dom.name(node).unwrap();
if name == W::p_pr()
&& let Some(chg) = dom.element(node, &W::p_pr_change())
{
let inner = dom.element(chg, &W::p_pr());
return inner.and_then(|i| reject_revisions_for_styles_transform(dom, i));
}
if name == W::r_pr()
&& let Some(chg) = dom.element(node, &W::r_pr_change())
{
let inner = dom.element(chg, &W::r_pr());
return inner.and_then(|i| reject_revisions_for_styles_transform(dom, i));
}
let ne = dom.new_element(name);
for (an, av) in dom.attributes(node) {
dom.set_attribute_value(ne, &an, Some(&av));
}
for c in dom.nodes(node) {
if let Some(tc) = reject_revisions_for_styles_transform(dom, c) {
dom.add(ne, tc);
}
}
Some(ne)
}
fn revision_bearing_parts(pkg: &crate::opc::PartFs) -> Vec<(String, bool)> {
let main = pkg
.main_document_part()
.unwrap_or_else(|| "word/document.xml".to_string());
let mut headers = Vec::new();
let mut footers = Vec::new();
let mut endnotes = Vec::new();
let mut footnotes = Vec::new();
let mut styles = Vec::new();
if let Some(rels) = pkg.read_rels_for(&main) {
for r in &rels.items {
if r.target_mode.as_deref() == Some("External") {
continue;
}
let bucket = match r.rel_type.rsplit('/').next().unwrap_or("") {
"header" => &mut headers,
"footer" => &mut footers,
"endnotes" => &mut endnotes,
"footnotes" => &mut footnotes,
"styles" => &mut styles,
_ => continue,
};
bucket.push(pkg.resolve_rel_target(&main, &r.target));
}
}
let mut out = vec![(main, false)];
for p in headers
.into_iter()
.chain(footers)
.chain(endnotes)
.chain(footnotes)
{
out.push((p, false));
}
for p in styles {
out.push((p, true));
}
out
}
fn process_part<F>(pkg: &mut crate::opc::PartFs, part: &str, f: F)
where
F: FnOnce(&mut Dom, NodeId) -> Option<NodeId>,
{
let Some(xml) = pkg.part_string(part) else {
return;
};
let mut dom = Dom::new();
let doc = dom.parse_xdocument(&xml);
let Some(root) = dom.root(doc) else {
return;
};
let Some(new_root) = f(&mut dom, root) else {
return;
};
dom.replace_with(root, &[new_root]);
pkg.set_part(part, dom.serialize_document(doc).into_bytes());
}
pub fn accept_revisions_package(pkg: &mut crate::opc::PartFs) {
for (part, is_styles) in revision_bearing_parts(pkg) {
if is_styles {
process_part(pkg, &part, |dom, root| {
accept_revisions_for_styles_transform(dom, root)
});
} else {
process_part(pkg, &part, |dom, root| {
Some(accept_revisions_for_part_content(dom, root))
});
}
}
}
pub fn reject_revisions_package(pkg: &mut crate::opc::PartFs) {
for (part, is_styles) in revision_bearing_parts(pkg) {
if is_styles {
process_part(pkg, &part, |dom, root| {
let rejected = reject_revisions_for_styles_transform(dom, root)?;
accept_revisions_for_styles_transform(dom, rejected)
});
} else {
process_part(pkg, &part, |dom, root| {
Some(reject_revisions_document(dom, root))
});
}
}
}