use crate::namespaces::{PT, W};
use crate::util::group_adjacent;
use crate::xmllinq::{Dom, NodeId, XName};
use super::atoms::{ComparisonUnit, ComparisonUnitAtom, ComparisonUnitGroup, ComparisonUnitWord};
use super::tables::{COMPARISON_GROUPING_ELEMENTS, WORD_BREAK_ELEMENTS};
use super::{ComparisonUnitGroupType, CorrelationStatus, WmlComparerSettings};
pub fn hierarchical_grouping_key(dom: &Dom, element: NodeId) -> String {
let local = dom
.name(element)
.map(|n| n.local_name().to_string())
.unwrap_or_default();
let unid = dom.attribute(element, &PT::unid()).unwrap_or("");
format!("{local}:{unid}")
}
fn is_grouping_element(name: &XName) -> bool {
COMPARISON_GROUPING_ELEMENTS.contains(name)
}
fn is_word_break_element(name: &XName) -> bool {
WORD_BREAK_ELEMENTS.contains(name)
}
fn is_digit_char(c: char) -> bool {
c.is_ascii_digit()
}
fn is_cjk(c: char) -> bool {
('\u{4e00}'..='\u{9fff}').contains(&c)
}
fn is_word_letter(c: char) -> bool {
c.is_alphabetic() || c == '_'
}
pub fn get_comparison_unit_list(
dom: &Dom,
atoms: &[ComparisonUnitAtom],
settings: &WmlComparerSettings,
) -> Vec<ComparisonUnit> {
let word_mode = settings.merge_replaced_paragraphs;
let mut next_index: i64 = 0;
let mut keyed: Vec<(i64, ComparisonUnitAtom)> = Vec::with_capacity(atoms.len());
let mut prev_t_char: Option<char> = None;
for (i, atom) in atoms.iter().enumerate() {
let key: i64;
let cname = dom.name(atom.content_element).unwrap();
if cname == W::t() {
let val = dom.value_str(atom.content_element);
let ch = val.chars().next().unwrap_or('\0');
if ch == '.' || ch == ',' {
let before_is_digit = i > 0 && {
let prev = &atoms[i - 1];
dom.name(prev.content_element).unwrap() == W::t()
&& dom
.value_str(prev.content_element)
.chars()
.next()
.is_some_and(is_digit_char)
};
let after_is_digit = i + 1 < atoms.len() && {
let next = &atoms[i + 1];
dom.name(next.content_element).unwrap() == W::t()
&& dom
.value_str(next.content_element)
.chars()
.next()
.is_some_and(is_digit_char)
};
let after_is_letter = i + 1 < atoms.len() && {
let next = &atoms[i + 1];
dom.name(next.content_element).unwrap() == W::t()
&& dom
.value_str(next.content_element)
.chars()
.next()
.is_some_and(is_word_letter)
};
if before_is_digit && after_is_digit {
key = next_index;
} else if word_mode && before_is_digit && after_is_letter {
next_index += 1;
key = next_index;
} else if before_is_digit || after_is_digit {
key = next_index;
} else {
next_index += 1;
key = next_index;
next_index += 1;
}
prev_t_char = Some(ch);
} else if word_mode && ch == '-' && prev_t_char.is_some_and(is_word_letter) {
key = next_index;
prev_t_char = Some('-');
} else if is_cjk(ch) || settings.word_separators.contains(&ch) {
next_index += 1;
key = next_index;
next_index += 1;
prev_t_char = Some(ch);
} else if word_mode {
let break_boundary = matches!(
prev_t_char,
Some(p) if is_digit_char(p) != is_digit_char(ch)
&& (is_digit_char(p) || is_word_letter(p))
&& (is_digit_char(ch) || is_word_letter(ch))
) || matches!(prev_t_char, Some('-') if is_word_letter(ch));
if break_boundary {
next_index += 1;
}
key = next_index;
prev_t_char = Some(ch);
} else {
key = next_index;
prev_t_char = Some(ch);
}
} else if is_word_break_element(&cname) {
next_index += 1;
key = next_index;
next_index += 1;
prev_t_char = None;
} else {
key = next_index;
}
keyed.push((key, atom.clone()));
}
let grouped = group_adjacent(keyed, |(k, _)| *k);
let words: Vec<ComparisonUnitWord> = grouped
.into_iter()
.map(|(_, items)| ComparisonUnitWord::new(items.into_iter().map(|(_, a)| a).collect()))
.collect();
let with_keys: Vec<(Vec<String>, Vec<NodeId>, ComparisonUnitWord)> = words
.into_iter()
.map(|word| {
let first = &word.contents[0];
let group_ancestors: Vec<NodeId> = first
.ancestor_elements
.iter()
.copied()
.filter(|&a| is_grouping_element(&dom.name(a).unwrap()))
.collect();
let arr: Vec<String> = group_ancestors
.iter()
.map(|&a| hierarchical_grouping_key(dom, a))
.collect();
(arr, group_ancestors, word)
})
.collect();
get_hierarchical_comparison_units(dom, with_keys, 0)
}
type WordWithKeys = (Vec<String>, Vec<NodeId>, ComparisonUnitWord);
fn get_hierarchical_comparison_units(
dom: &Dom,
input: Vec<WordWithKeys>,
level: usize,
) -> Vec<ComparisonUnit> {
let grouped = group_adjacent(input, |(arr, _, _)| {
if level >= arr.len() {
String::new()
} else {
arr[level].clone()
}
});
let mut out = Vec::new();
for (key, group) in grouped {
if key.is_empty() {
for (_, _, word) in group {
out.push(ComparisonUnit::Word(word));
}
} else {
let group_type = match key.split(':').next().unwrap_or("") {
"p" => ComparisonUnitGroupType::Paragraph,
"tbl" => ComparisonUnitGroupType::Table,
"tr" => ComparisonUnitGroupType::Row,
"tc" => ComparisonUnitGroupType::Cell,
"txbxContent" => ComparisonUnitGroupType::Textbox,
_ => ComparisonUnitGroupType::Paragraph,
};
let ancestor_for_hash = group[0].1.get(level).copied();
let children = get_hierarchical_comparison_units(dom, group, level + 1);
let sha1 = ancestor_for_hash
.and_then(|a| dom.attribute(a, &PT::sha1_hash()).map(|s| s.to_string()))
.unwrap_or_default();
let correlated = ancestor_for_hash.and_then(|a| {
dom.attribute(a, &PT::correlated_sha1_hash())
.map(|s| s.to_string())
});
let structure = ancestor_for_hash.and_then(|a| {
dom.attribute(a, &PT::structure_sha1_hash())
.map(|s| s.to_string())
});
out.push(ComparisonUnit::Group(ComparisonUnitGroup {
correlation_status: CorrelationStatus::Nil,
group_type,
contents: children,
level,
sha1_key: crate::util::sha1::sha1_fingerprint(&sha1),
sha1_key128: crate::util::sha1::sha1_fingerprint128(&sha1),
sha1_hash: sha1,
correlated_sha1_hash: correlated,
structure_sha1_hash: structure,
atom_count_memo: std::cell::Cell::new(usize::MAX),
}));
}
}
out
}