use crate::namespaces::{A14, O, PT, R, VML, W, WP};
use crate::util::group_adjacent;
use crate::xmllinq::{Dom, NodeId, XName};
use super::WmlComparerSettings;
use super::tables::{
ATTRIBUTES_TO_TRIM_WHEN_CLONING, S_ELEMENTS_WITH_RELATIONSHIP_IDS,
S_RELATIONSHIP_ATTRIBUTE_NAMES,
};
pub type RelHashResolver<'a> = dyn Fn(&str) -> Option<String> + 'a;
pub fn null_rel_resolver(_r_id: &str) -> Option<String> {
Some("NULL Relationship".to_string())
}
fn is_rsid_attr(name: &XName) -> bool {
name.namespace_name() == W::URI
&& matches!(
name.local_name(),
"rsid"
| "rsidDel"
| "rsidP"
| "rsidR"
| "rsidRDefault"
| "rsidRPr"
| "rsidSect"
| "rsidTr"
)
}
fn is_pt(name: &XName) -> bool {
name.namespace_name() == PT::URI
}
fn is_volatile_para_attr(name: &XName) -> bool {
name.namespace_name() == crate::namespaces::W14::URI
&& matches!(name.local_name(), "paraId" | "textId")
}
fn whitespace_invariant_for_hash(text: &str) -> String {
text.chars().filter(|ch| !ch.is_whitespace()).collect()
}
fn apply_text_transform(text: &str, settings: &WmlComparerSettings) -> String {
let mut t = text.to_string();
if settings.case_insensitive {
t = t.to_uppercase();
}
if settings.conflate_breaking_and_nonbreaking_spaces {
t = t.replace(' ', "\u{00A0}"); }
t
}
fn strip_whitespace_in_clone_text(dom: &mut Dom, root: NodeId) {
let nodes: Vec<NodeId> = dom.descendant_nodes(root);
for n in nodes {
if !dom.is_text(n) {
continue;
}
let raw = dom.text_value(n).unwrap_or("").to_string();
let stripped = whitespace_invariant_for_hash(&raw);
if stripped != raw {
dom.set_text_value(n, &stripped);
}
}
}
pub fn clone_block_level_content_for_hashing(
dom: &mut Dom,
node: NodeId,
include_related_parts: bool,
settings: &WmlComparerSettings,
rel_hash: &RelHashResolver,
) -> NodeId {
let cloned = clone_internal(dom, node, include_related_parts, settings, rel_hash);
let root = cloned
.into_iter()
.next()
.unwrap_or_else(|| dom.new_element(dom.name(node).unwrap_or_else(|| W::name("p"))));
fn strip_nsdecls(dom: &mut Dom, id: NodeId) {
if dom.is_element(id) {
let n = dom.attr_count(id);
let mut to_drop = Vec::new();
for i in 0..n {
let (name, _) = dom.attr_at(id, i);
if dom.is_namespace_declaration(name) {
to_drop.push(name.clone());
}
}
for a in &to_drop {
dom.set_attribute_value(id, a, None);
}
let kids = dom.child_count(id);
for i in 0..kids {
let c = dom.child_at(id, i);
strip_nsdecls(dom, c);
}
}
}
strip_nsdecls(dom, root);
root
}
fn new_with_filtered_attrs(
dom: &mut Dom,
name: XName,
src: NodeId,
drop: impl Fn(&XName) -> bool,
children: Vec<NodeId>,
) -> NodeId {
let ne = dom.new_element(name);
let keep: Vec<(XName, String)> = (0..dom.attr_count(src))
.filter_map(|i| {
let (an, av) = dom.attr_at(src, i);
if drop(an) {
None
} else {
Some((an.clone(), av.to_string()))
}
})
.collect();
for (an, av) in &keep {
dom.set_attribute_value(ne, an, Some(av));
}
for c in children {
dom.add(ne, c);
}
ne
}
fn clone_children(
dom: &mut Dom,
node: NodeId,
include_related_parts: bool,
settings: &WmlComparerSettings,
rel_hash: &RelHashResolver,
) -> Vec<NodeId> {
let mut out = Vec::new();
let n = dom.child_count(node);
for i in 0..n {
let c = dom.child_at(node, i);
out.extend(clone_internal(
dom,
c,
include_related_parts,
settings,
rel_hash,
));
}
out
}
fn clone_internal(
dom: &mut Dom,
node: NodeId,
include_related_parts: bool,
settings: &WmlComparerSettings,
rel_hash: &RelHashResolver,
) -> Vec<NodeId> {
let out = clone_internal_unsalted(dom, node, include_related_parts, settings, rel_hash);
if dom.is_element(node)
&& dom.attribute(node, &PT::name("PreDelete")) == Some(super::PREDELETE_STAMP_ORIG)
{
for &c in &out {
if dom.is_element(c) {
dom.set_attribute_value(c, &PT::name("PreDeleteSalt"), Some("1"));
}
}
}
out
}
fn clone_internal_unsalted(
dom: &mut Dom,
node: NodeId,
include_related_parts: bool,
settings: &WmlComparerSettings,
rel_hash: &RelHashResolver,
) -> Vec<NodeId> {
if !dom.is_element(node) {
if dom.is_text(node) {
let raw = dom.text_value(node).unwrap_or("");
let t =
if settings.case_insensitive || settings.conflate_breaking_and_nonbreaking_spaces {
apply_text_transform(raw, settings)
} else {
raw.to_string()
};
return vec![dom.new_text(&t)];
}
return vec![dom.clone_subtree(node)];
}
let name = dom.name(node).unwrap();
if name == W::bookmark_start()
|| name == W::bookmark_end()
|| name == W::p_pr()
|| name == W::r_pr()
{
return vec![];
}
if name.namespace_name() == A14::URI {
return vec![];
}
if name == W::name("footnoteReference") || name == W::name("endnoteReference") {
return vec![dom.new_element(name)];
}
if name == W::p() {
let cloned_children = clone_children(dom, node, include_related_parts, settings, rel_hash);
let element_children: Vec<NodeId> = cloned_children
.into_iter()
.filter(|&c| dom.is_element(c))
.collect();
let salt_name = PT::name("PreDeleteSalt");
let grouped = group_adjacent(element_children.iter().copied(), |&e| {
if !is_single_t_run(dom, e) {
0u8
} else if dom.attribute(e, &salt_name).is_some() {
2
} else {
1
}
});
let new_p = new_with_filtered_attrs(
dom,
W::p(),
node,
|a| is_rsid_attr(a) || is_pt(a) || is_volatile_para_attr(a),
vec![],
);
for (kind, group) in grouped {
if kind != 0 {
let text: String = group
.iter()
.map(|&e| dom.value_str(e).into_owned())
.collect();
let text = apply_text_transform(&text, settings);
let r = dom.new_element(W::r());
if kind == 2 {
dom.set_attribute_value(r, &salt_name, Some("1"));
}
let t = dom.new_element(W::t());
dom.add_text(t, &text);
dom.add(r, t);
dom.add(new_p, r);
} else {
for e in group {
dom.add(new_p, e);
}
}
}
return vec![new_p];
}
if name == W::r() {
let mut runs = Vec::new();
let n = dom.child_count(node);
for i in 0..n {
let rc = dom.child_at(node, i);
if !dom.is_element(rc) {
continue;
}
if dom.name(rc).unwrap() == W::r_pr() {
continue;
}
let inner = clone_internal(dom, rc, include_related_parts, settings, rel_hash);
let r = dom.new_element(W::r());
for n in inner {
dom.add(r, n);
}
runs.push(r);
}
return runs;
}
if name == W::tbl() {
let tr_name = W::tr();
let children: Vec<NodeId> = element_children_named(dom, node, &tr_name)
.into_iter()
.flat_map(|tr| clone_internal(dom, tr, include_related_parts, settings, rel_hash))
.collect();
let tbl = dom.new_element(W::tbl());
for c in children {
dom.add(tbl, c);
}
return vec![tbl];
}
if name == W::tr() {
let tc_name = W::tc();
let children: Vec<NodeId> = element_children_named(dom, node, &tc_name)
.into_iter()
.flat_map(|tc| clone_internal(dom, tc, include_related_parts, settings, rel_hash))
.collect();
let tr = dom.new_element(W::tr());
for c in children {
dom.add(tr, c);
}
return vec![tr];
}
if name == W::tc() {
let children =
clone_children_elements(dom, node, include_related_parts, settings, rel_hash);
let tc = dom.new_element(W::tc());
for c in children {
dom.add(tc, c);
}
return vec![tc];
}
if name == W::tc_pr() {
let gs_name = W::grid_span();
let children: Vec<NodeId> = element_children_named(dom, node, &gs_name)
.into_iter()
.flat_map(|gs| clone_internal(dom, gs, include_related_parts, settings, rel_hash))
.collect();
let tcpr = dom.new_element(W::tc_pr());
for c in children {
dom.add(tcpr, c);
}
return vec![tcpr];
}
if name == W::grid_span() {
let val = dom.attribute(node, &W::val()).unwrap_or("").to_string();
let gs = dom.new_element(W::grid_span());
dom.set_attribute_value(gs, &XName::get("val", ""), Some(&val));
return vec![gs];
}
if name == W::txbx_content() {
let children =
clone_children_elements(dom, node, include_related_parts, settings, rel_hash);
let tb = dom.new_element(W::txbx_content());
for c in children {
dom.add(tb, c);
}
return vec![tb];
}
if include_related_parts && S_ELEMENTS_WITH_RELATIONSHIP_IDS.contains(&name) {
let ne = dom.new_element(name.clone());
let attrs: Vec<(XName, String)> = (0..dom.attr_count(node))
.map(|i| {
let (an, av) = dom.attr_at(node, i);
(an.clone(), av.to_string())
})
.collect();
for (an, av) in &attrs {
if is_pt(an) || ATTRIBUTES_TO_TRIM_WHEN_CLONING.contains(an) {
continue;
}
if S_RELATIONSHIP_ATTRIBUTE_NAMES.contains(an) {
match rel_hash(av) {
Some(v) => dom.set_attribute_value(ne, an, Some(&v)),
None => { }
}
} else {
dom.set_attribute_value(ne, an, Some(av));
}
}
for c in clone_children(dom, node, include_related_parts, settings, rel_hash) {
dom.add(ne, c);
}
return vec![ne];
}
if name == VML::name("shape") {
let children = clone_children(dom, node, include_related_parts, settings, rel_hash);
return vec![new_with_filtered_attrs(
dom,
name,
node,
|a| {
is_pt(a)
|| *a == XName::get("style", "")
|| *a == XName::get("id", "")
|| *a == XName::get("type", "")
},
children,
)];
}
if name == O::name("OLEObject") {
let children = clone_children(dom, node, include_related_parts, settings, rel_hash);
return vec![new_with_filtered_attrs(
dom,
name,
node,
|a| is_pt(a) || *a == XName::get("ObjectID", "") || *a == R::name("id"),
children,
)];
}
if name == W::object() {
let children = clone_children(dom, node, include_related_parts, settings, rel_hash);
return vec![new_with_filtered_attrs(dom, name, node, is_pt, children)];
}
if name == WP::name("docPr") {
let children = clone_children(dom, node, include_related_parts, settings, rel_hash);
return vec![new_with_filtered_attrs(
dom,
name,
node,
|a| is_pt(a) || *a == XName::get("id", ""),
children,
)];
}
let children = clone_children(dom, node, include_related_parts, settings, rel_hash);
vec![new_with_filtered_attrs(
dom,
name,
node,
|a| is_pt(a) || is_volatile_para_attr(a) || ATTRIBUTES_TO_TRIM_WHEN_CLONING.contains(a),
children,
)]
}
fn element_children(dom: &Dom, node: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
let n = dom.child_count(node);
for i in 0..n {
let c = dom.child_at(node, i);
if dom.is_element(c) {
out.push(c);
}
}
out
}
fn element_children_named(dom: &Dom, node: NodeId, filter: &XName) -> Vec<NodeId> {
let mut out = Vec::new();
let n = dom.child_count(node);
for i in 0..n {
let c = dom.child_at(node, i);
if dom.is_element(c) && dom.name(c).as_ref() == Some(filter) {
out.push(c);
}
}
out
}
fn clone_children_elements(
dom: &mut Dom,
node: NodeId,
include_related_parts: bool,
settings: &WmlComparerSettings,
rel_hash: &RelHashResolver,
) -> Vec<NodeId> {
element_children(dom, node)
.into_iter()
.flat_map(|c| clone_internal(dom, c, include_related_parts, settings, rel_hash))
.collect()
}
fn is_single_t_run(dom: &Dom, e: NodeId) -> bool {
if dom.name(e) != Some(W::r()) {
return false;
}
let mut el_count = 0usize;
let mut has_t = false;
let n = dom.child_count(e);
for i in 0..n {
let c = dom.child_at(e, i);
if !dom.is_element(c) {
continue;
}
el_count += 1;
if dom.name(c) == Some(W::t()) {
has_t = true;
}
}
el_count == 1 && has_t
}
const WML_DEFAULT_XMLNS: &str =
" xmlns=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"";
pub fn remove_existing_powertools_markup(dom: &mut Dom, root: NodeId) {
let unid = PT::unid();
let mut els = Vec::new();
dom.for_each_descendant_and_self(root, None, |el| els.push(el));
for el in els {
let mut pt_attrs = Vec::new();
let n = dom.attr_count(el);
for i in 0..n {
let (name, _) = dom.attr_at(el, i);
if name.namespace_name() == PT::URI && *name != unid {
pt_attrs.push(name.clone());
}
}
for a in &pt_attrs {
dom.set_attribute_value(el, a, None);
}
}
}
pub fn test_for_invalid_content(dom: &Dom, root: NodeId) -> Result<(), String> {
let invalid = [
W::name("altChunk"),
W::name("subDoc"),
W::name("contentPart"),
];
let mut bad: Option<String> = None;
dom.for_each_descendant_element(root, None, |d| {
if bad.is_some() {
return;
}
if let Some(name) = dom.name(d)
&& invalid.contains(&name)
{
bad = Some(format!("Document contains {}", name.local_name()));
}
});
match bad {
Some(e) => Err(e),
None => Ok(()),
}
}
pub fn block_hash_string(dom: &Dom, clone: NodeId) -> String {
let mut serialized = dom.serialize_element(clone);
if let Some(i) = serialized.find(WML_DEFAULT_XMLNS) {
serialized.drain(i..i + WML_DEFAULT_XMLNS.len());
}
serialized
}
pub fn block_sha1(dom: &Dom, clone: NodeId) -> String {
dom.serialize_element_sha1_hex(clone)
}
pub fn block_sha1_from_source(
dom: &mut Dom,
node: NodeId,
include_related_parts: bool,
settings: &WmlComparerSettings,
rel_hash: &RelHashResolver,
correlated_ws: bool,
) -> String {
if let Some(hex) = try_stream_hash_simple_paragraph(dom, node, settings, correlated_ws) {
return hex;
}
if let Some((content, _)) =
try_stream_hash_simple_table_or_tr(dom, node, settings, correlated_ws)
{
return content;
}
let clone =
clone_block_level_content_for_hashing(dom, node, include_related_parts, settings, rel_hash);
if correlated_ws {
strip_whitespace_in_clone_text(dom, clone);
}
block_sha1(dom, clone)
}
pub fn try_stream_hash_simple_paragraph(
dom: &Dom,
node: NodeId,
settings: &WmlComparerSettings,
correlated_ws: bool,
) -> Option<String> {
if dom.name(node) != Some(W::p()) {
return None;
}
if element_or_desc_has_predelete_orig(dom, node) {
return None;
}
let attr_xml = filtered_p_attr_xml(dom, node)?;
let frags = collect_simple_p_fragments(dom, node, settings, correlated_ws)?;
let mut xml = String::with_capacity(128);
xml.push_str("<w:p xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"");
xml.push_str(&attr_xml);
if frags.is_empty() {
xml.push_str(" />");
} else {
xml.push('>');
emit_merged_run_fragments(&frags, &mut xml, false);
xml.push_str("</w:p>");
}
if let Some(i) = xml.find(WML_DEFAULT_XMLNS) {
xml.drain(i..i + WML_DEFAULT_XMLNS.len());
}
Some(crate::util::sha1::sha1_hex(&xml))
}
enum RunFrag {
Text(String),
Leaf {
local: String,
attrs: String,
},
}
fn is_streamable_empty_run_leaf(local: &str) -> bool {
matches!(
local,
"br" | "tab" | "cr" | "noBreakHyphen" | "softHyphen" | "lastRenderedPageBreak"
)
}
fn filtered_p_attr_xml(dom: &Dom, node: NodeId) -> Option<String> {
let mut attr_xml = String::new();
for i in 0..dom.attr_count(node) {
let (an, av) = dom.attr_at(node, i);
if dom.is_namespace_declaration(an) {
continue;
}
if is_rsid_attr(an) || is_pt(an) || is_volatile_para_attr(an) {
continue;
}
let ns = an.namespace_name();
let local = an.local_name();
if ns == W::URI {
attr_xml.push(' ');
attr_xml.push_str("w:");
attr_xml.push_str(local);
attr_xml.push_str("=\"");
attr_xml.push_str(&escape_xml_attr(av));
attr_xml.push('"');
} else if ns.is_empty() {
attr_xml.push(' ');
attr_xml.push_str(local);
attr_xml.push_str("=\"");
attr_xml.push_str(&escape_xml_attr(av));
attr_xml.push('"');
} else {
return None;
}
}
Some(attr_xml)
}
fn collect_simple_p_fragments(
dom: &Dom,
node: NodeId,
settings: &WmlComparerSettings,
correlated_ws: bool,
) -> Option<Vec<RunFrag>> {
let mut frags: Vec<RunFrag> = Vec::new();
let n = dom.child_count(node);
for i in 0..n {
let c = dom.child_at(node, i);
if !dom.is_element(c) {
continue;
}
let name = dom.name(c)?;
if name == W::p_pr() || name == W::bookmark_start() || name == W::bookmark_end() {
continue;
}
if name != W::r() {
return None;
}
let mut saw = false;
for j in 0..dom.child_count(c) {
let cc = dom.child_at(c, j);
if !dom.is_element(cc) {
continue;
}
let cn = dom.name(cc)?;
if cn == W::r_pr() {
continue;
}
if cn == W::t() {
saw = true;
let mut t = apply_text_transform(&dom.value_str(cc), settings);
if correlated_ws {
t = whitespace_invariant_for_hash(&t);
}
frags.push(RunFrag::Text(t));
continue;
}
if cn.namespace_name() == W::URI
&& is_streamable_empty_run_leaf(cn.local_name())
&& !has_element_child(dom, cc)
{
let attrs = filtered_leaf_attr_xml(dom, cc)?;
saw = true;
frags.push(RunFrag::Leaf {
local: cn.local_name().to_string(),
attrs,
});
continue;
}
return None; }
let _ = saw; }
Some(frags)
}
fn has_element_child(dom: &Dom, node: NodeId) -> bool {
let n = dom.child_count(node);
for i in 0..n {
if dom.is_element(dom.child_at(node, i)) {
return true;
}
}
false
}
fn filtered_leaf_attr_xml(dom: &Dom, node: NodeId) -> Option<String> {
let mut attr_xml = String::new();
for i in 0..dom.attr_count(node) {
let (an, av) = dom.attr_at(node, i);
if dom.is_namespace_declaration(an) {
continue;
}
if is_pt(an) || is_volatile_para_attr(an) || ATTRIBUTES_TO_TRIM_WHEN_CLONING.contains(an) {
continue;
}
let ns = an.namespace_name();
let local = an.local_name();
if ns == W::URI {
attr_xml.push(' ');
attr_xml.push_str("w:");
attr_xml.push_str(local);
attr_xml.push_str("=\"");
attr_xml.push_str(&escape_xml_attr(av));
attr_xml.push('"');
} else if ns.is_empty() {
attr_xml.push(' ');
attr_xml.push_str(local);
attr_xml.push_str("=\"");
attr_xml.push_str(&escape_xml_attr(av));
attr_xml.push('"');
} else {
return None;
}
}
Some(attr_xml)
}
fn emit_merged_run_fragments(frags: &[RunFrag], out: &mut String, structure_only: bool) {
let mut i = 0;
while i < frags.len() {
match &frags[i] {
RunFrag::Text(t0) => {
let mut merged = t0.clone();
i += 1;
while i < frags.len() {
if let RunFrag::Text(t) = &frags[i] {
merged.push_str(t);
i += 1;
} else {
break;
}
}
if structure_only || merged.is_empty() {
out.push_str("<w:r><w:t /></w:r>");
} else {
out.push_str("<w:r><w:t>");
out.push_str(&escape_xml_text(&merged));
out.push_str("</w:t></w:r>");
}
}
RunFrag::Leaf { local, attrs } => {
out.push_str("<w:r><w:");
out.push_str(local);
out.push_str(attrs);
out.push_str(" /></w:r>");
i += 1;
}
}
}
}
pub fn try_stream_hash_simple_table_or_tr(
dom: &Dom,
node: NodeId,
settings: &WmlComparerSettings,
correlated_ws: bool,
) -> Option<(String, String)> {
let name = dom.name(node)?;
if name != W::tbl() && name != W::tr() && name != W::tc() {
return None;
}
if element_or_desc_has_predelete_orig(dom, node) {
return None;
}
let mut content = String::with_capacity(256);
let mut structure = String::with_capacity(256);
if name == W::tbl() {
emit_open_root(&mut content, "tbl");
emit_open_root(&mut structure, "tbl");
stream_tbl_body(
dom,
node,
settings,
correlated_ws,
&mut content,
&mut structure,
)?;
content.push_str("</w:tbl>");
structure.push_str("</w:tbl>");
} else if name == W::tr() {
emit_open_root(&mut content, "tr");
emit_open_root(&mut structure, "tr");
stream_tr_body(
dom,
node,
settings,
correlated_ws,
&mut content,
&mut structure,
)?;
content.push_str("</w:tr>");
structure.push_str("</w:tr>");
} else {
emit_open_root(&mut content, "tc");
emit_open_root(&mut structure, "tc");
stream_tc_body(
dom,
node,
settings,
correlated_ws,
&mut content,
&mut structure,
)?;
content.push_str("</w:tc>");
structure.push_str("</w:tc>");
}
if let Some(i) = content.find(WML_DEFAULT_XMLNS) {
content.drain(i..i + WML_DEFAULT_XMLNS.len());
}
if let Some(i) = structure.find(WML_DEFAULT_XMLNS) {
structure.drain(i..i + WML_DEFAULT_XMLNS.len());
}
Some((
crate::util::sha1::sha1_hex(&content),
crate::util::sha1::sha1_hex(&structure),
))
}
fn emit_open_root(out: &mut String, local: &str) {
out.push_str("<w:");
out.push_str(local);
out.push_str(" xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">");
}
fn element_or_desc_has_predelete_orig(dom: &Dom, node: NodeId) -> bool {
let pre = PT::name("PreDelete");
let mut stack = vec![node];
while let Some(n) = stack.pop() {
if !dom.is_element(n) {
continue;
}
if dom.attribute(n, &pre) == Some(super::PREDELETE_STAMP_ORIG) {
return true;
}
let c = dom.child_count(n);
for i in 0..c {
stack.push(dom.child_at(n, i));
}
}
false
}
fn stream_tbl_body(
dom: &Dom,
tbl: NodeId,
settings: &WmlComparerSettings,
correlated_ws: bool,
content: &mut String,
structure: &mut String,
) -> Option<()> {
let tr_name = W::tr();
let n = dom.child_count(tbl);
for i in 0..n {
let c = dom.child_at(tbl, i);
if !dom.is_element(c) || dom.name(c).as_ref() != Some(&tr_name) {
continue;
}
content.push_str("<w:tr>");
structure.push_str("<w:tr>");
stream_tr_body(dom, c, settings, correlated_ws, content, structure)?;
content.push_str("</w:tr>");
structure.push_str("</w:tr>");
}
Some(())
}
fn stream_tr_body(
dom: &Dom,
tr: NodeId,
settings: &WmlComparerSettings,
correlated_ws: bool,
content: &mut String,
structure: &mut String,
) -> Option<()> {
let tc_name = W::tc();
let n = dom.child_count(tr);
for i in 0..n {
let c = dom.child_at(tr, i);
if !dom.is_element(c) || dom.name(c).as_ref() != Some(&tc_name) {
continue;
}
content.push_str("<w:tc>");
structure.push_str("<w:tc>");
stream_tc_body(dom, c, settings, correlated_ws, content, structure)?;
content.push_str("</w:tc>");
structure.push_str("</w:tc>");
}
Some(())
}
fn stream_tc_body(
dom: &Dom,
tc: NodeId,
settings: &WmlComparerSettings,
correlated_ws: bool,
content: &mut String,
structure: &mut String,
) -> Option<()> {
let n = dom.child_count(tc);
for i in 0..n {
let c = dom.child_at(tc, i);
if !dom.is_element(c) {
continue;
}
let name = dom.name(c)?;
if name == W::bookmark_start()
|| name == W::bookmark_end()
|| name == W::p_pr()
|| name == W::r_pr()
{
continue;
}
if name.namespace_name() == A14::URI {
continue;
}
if name == W::tc_pr() {
stream_tc_pr(dom, c, content, structure);
continue;
}
if name == W::p() {
stream_simple_p_fragment(dom, c, settings, correlated_ws, content, structure)?;
continue;
}
if name == W::tbl() {
content.push_str("<w:tbl>");
structure.push_str("<w:tbl>");
stream_tbl_body(dom, c, settings, correlated_ws, content, structure)?;
content.push_str("</w:tbl>");
structure.push_str("</w:tbl>");
continue;
}
return None;
}
Some(())
}
fn stream_tc_pr(dom: &Dom, tc_pr: NodeId, content: &mut String, structure: &mut String) {
let gs_name = W::grid_span();
let mut spans: Vec<String> = Vec::new();
let n = dom.child_count(tc_pr);
for i in 0..n {
let c = dom.child_at(tc_pr, i);
if !dom.is_element(c) || dom.name(c).as_ref() != Some(&gs_name) {
continue;
}
let val = dom.attribute(c, &W::val()).unwrap_or("").to_string();
spans.push(val);
}
if spans.is_empty() {
content.push_str("<w:tcPr />");
structure.push_str("<w:tcPr />");
return;
}
content.push_str("<w:tcPr>");
structure.push_str("<w:tcPr>");
for val in &spans {
let frag = format!("<w:gridSpan val=\"{}\" />", escape_xml_attr(val));
content.push_str(&frag);
structure.push_str(&frag);
}
content.push_str("</w:tcPr>");
structure.push_str("</w:tcPr>");
}
fn stream_simple_p_fragment(
dom: &Dom,
node: NodeId,
settings: &WmlComparerSettings,
correlated_ws: bool,
content: &mut String,
structure: &mut String,
) -> Option<()> {
if dom.name(node) != Some(W::p()) {
return None;
}
let attr_xml = filtered_p_attr_xml(dom, node)?;
let frags = collect_simple_p_fragments(dom, node, settings, correlated_ws)?;
if frags.is_empty() {
content.push_str("<w:p");
content.push_str(&attr_xml);
content.push_str(" />");
structure.push_str("<w:p");
structure.push_str(&attr_xml);
structure.push_str(" />");
return Some(());
}
content.push_str("<w:p");
content.push_str(&attr_xml);
content.push('>');
emit_merged_run_fragments(&frags, content, false);
content.push_str("</w:p>");
structure.push_str("<w:p");
structure.push_str(&attr_xml);
structure.push('>');
emit_merged_run_fragments(&frags, structure, true);
structure.push_str("</w:p>");
Some(())
}
fn escape_xml_attr(s: &str) -> String {
if !s.bytes().any(|b| matches!(b, b'&' | b'<' | b'>' | b'"')) {
return s.to_string();
}
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
fn escape_xml_text(s: &str) -> String {
if !s.bytes().any(|b| matches!(b, b'&' | b'<' | b'>')) {
return s.to_string();
}
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
pub fn structure_sha1(dom: &Dom, node: NodeId) -> String {
dom.serialize_element_structure_sha1_hex(node)
}
pub fn clone_for_structure_hash(dom: &mut Dom, node: NodeId) -> Option<NodeId> {
if !dom.is_element(node) {
return None;
}
let name = dom.name(node).unwrap();
let ne = dom.new_element(name);
let attrs: Vec<(XName, String)> = (0..dom.attr_count(node))
.map(|i| {
let (an, av) = dom.attr_at(node, i);
(an.clone(), av.to_string())
})
.collect();
for (an, av) in &attrs {
dom.set_attribute_value(ne, an, Some(av));
}
let n = dom.child_count(node);
for i in 0..n {
let c = dom.child_at(node, i);
if let Some(child) = clone_for_structure_hash(dom, c) {
dom.add(ne, child);
}
}
Some(ne)
}
use super::tables::ELEMENTS_TO_HAVE_SHA1;
use std::collections::HashMap;
pub fn add_sha1_hash_to_block_level_content(
dom: &mut Dom,
content_parent: NodeId,
settings: &WmlComparerSettings,
rel_hash: &RelHashResolver,
) {
let mut targets = Vec::new();
dom.for_each_descendant_element(content_parent, None, |d| {
if dom
.name(d)
.is_some_and(|n| ELEMENTS_TO_HAVE_SHA1.contains(&n))
{
targets.push(d);
}
});
for d in targets {
let name = dom.name(d).unwrap();
if name == W::p() {
let sha = block_sha1_from_source(dom, d, true, settings, rel_hash, false);
dom.set_attribute_value(d, &PT::sha1_hash(), Some(&sha));
continue;
}
if (name == W::tbl() || name == W::tr() || name == W::tc())
&& let Some((sha, sha2)) = try_stream_hash_simple_table_or_tr(dom, d, settings, false)
{
dom.set_attribute_value(d, &PT::sha1_hash(), Some(&sha));
if name == W::tbl() || name == W::tr() {
dom.set_attribute_value(d, &PT::structure_sha1_hash(), Some(&sha2));
}
continue;
}
let clone = clone_block_level_content_for_hashing(dom, d, true, settings, rel_hash);
let sha = block_sha1(dom, clone);
dom.set_attribute_value(d, &PT::sha1_hash(), Some(&sha));
if name == W::tbl() || name == W::tr() {
let sha2 = structure_sha1(dom, clone);
dom.set_attribute_value(d, &PT::structure_sha1_hash(), Some(&sha2));
}
}
}
pub fn hash_block_level_content(
dom: &mut Dom,
source_root: NodeId,
after_proc_root: NodeId,
settings: &WmlComparerSettings,
rel_hash: &RelHashResolver,
) -> Result<(), String> {
let block = |n: &XName| *n == W::p() || *n == W::tbl() || *n == W::tr();
let unid = PT::unid();
let mut source_by_unid: HashMap<String, NodeId> = HashMap::new();
let mut dup: Option<String> = None;
dom.for_each_descendant_element(source_root, None, |d| {
if dup.is_some() {
return;
}
if dom.name(d).is_some_and(|n| block(&n))
&& let Some(u) = dom.attribute(d, &unid)
&& source_by_unid.insert(u.to_string(), d).is_some()
{
dup = Some(u.to_string());
}
});
if let Some(u) = dup {
return Err(format!("duplicate Unid in source: {u}"));
}
let mut after_blocks = Vec::new();
dom.for_each_descendant_element(after_proc_root, None, |d| {
if dom.name(d).is_some_and(|n| block(&n)) {
after_blocks.push(d);
}
});
for b in after_blocks {
let sha = block_sha1_from_source(
dom,
b,
true,
settings,
rel_hash,
settings.merge_replaced_paragraphs,
);
if let Some(u) = dom.attribute(b, &unid).map(|s| s.to_string())
&& let Some(&src) = source_by_unid.get(&u)
{
dom.set_attribute_value(src, &PT::correlated_sha1_hash(), Some(&sha));
}
}
Ok(())
}
#[cfg(test)]
mod escape_xml_tests {
use super::{escape_xml_attr, escape_xml_text};
#[test]
fn plain_text_roundtrips_without_entities() {
assert_eq!(escape_xml_text("hello world"), "hello world");
assert_eq!(escape_xml_attr("id-42"), "id-42");
}
#[test]
fn specials_are_escaped() {
assert_eq!(escape_xml_text("a&b<c>d"), "a&b<c>d");
assert_eq!(escape_xml_attr(r#"say "hi""#), "say "hi"");
}
}