xml_3dm/merge/
mod.rs

1//! 3-way merge algorithm.
2//!
3//! This module implements the core 3-way merge algorithm that combines
4//! two branch trees relative to a common base tree. The algorithm handles
5//! insert, delete, update, move, and copy operations on subtrees.
6//!
7//! # Algorithm Overview
8//!
9//! The merge proceeds recursively through the matched trees:
10//! 1. Create MergeLists for each branch's children
11//! 2. Remove deleted/moved nodes and resolve conflicts
12//! 3. Merge the lists into MergePairs
13//! 4. For each pair, merge node content and recurse for children
14
15mod conflict_log;
16mod edit_log;
17mod merge_list;
18mod merge_pair;
19
20pub use conflict_log::{ConflictLog, ConflictType};
21pub use edit_log::{EditLog, EditType};
22pub use merge_list::{MergeEntry, MergeList};
23pub use merge_pair::{MergePair, MergePairList};
24
25use std::io::Write;
26
27use crate::matching::TriMatching;
28use crate::node::base::BaseNode;
29use crate::node::branch::BranchNode;
30use crate::node::{MatchType, NodeRef, XmlContent, XmlElement};
31
32/// Operation codes for merge list processing.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
34pub enum Operation {
35    /// No operation - node unchanged.
36    Nop,
37    /// Move inside child list.
38    MoveI,
39    /// Move far (to different parent).
40    MoveF,
41    /// Node deleted.
42    Delete,
43}
44
45/// The main merge struct that orchestrates the 3-way merge.
46pub struct Merge {
47    /// The tri-matching with base, left, and right trees.
48    matching: TriMatching,
49    /// Log of conflicts encountered during merge.
50    pub conflict_log: ConflictLog,
51    /// Log of edit operations performed.
52    pub edit_log: EditLog,
53    /// Current indentation level for formatted output.
54    indent: usize,
55}
56
57impl Merge {
58    /// Creates a new Merge from a TriMatching.
59    pub fn new(matching: TriMatching) -> Self {
60        Merge {
61            matching,
62            conflict_log: ConflictLog::new(),
63            edit_log: EditLog::new(),
64            indent: 0,
65        }
66    }
67
68    /// Returns a reference to the tri-matching.
69    pub fn matching(&self) -> &TriMatching {
70        &self.matching
71    }
72
73    /// Runs the 3-way merge and writes the result.
74    ///
75    /// The output is written as XML to the provided writer.
76    pub fn merge<W: Write>(&mut self, writer: &mut W) -> std::io::Result<()> {
77        // Start document
78        writeln!(writer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
79
80        // Run the tree merge
81        self.tree_merge(
82            Some(self.matching.left_root().clone()),
83            Some(self.matching.right_root().clone()),
84            writer,
85        )?;
86
87        Ok(())
88    }
89
90    /// Main recursive merge function.
91    ///
92    /// Merges the child lists of a and b, then recurses for each child.
93    fn tree_merge<W: Write>(
94        &mut self,
95        a: Option<NodeRef>,
96        b: Option<NodeRef>,
97        writer: &mut W,
98    ) -> std::io::Result<()> {
99        // Create merge lists for each branch
100        let mlist_a = a.as_ref().map(|node| self.make_merge_list(node));
101        let mlist_b = b.as_ref().map(|node| self.make_merge_list(node));
102
103        // Generate merge pair list
104        let merged = match (&mlist_a, &mlist_b) {
105            (Some(ma), Some(mb)) => self.make_merge_pair_list(ma.clone(), mb.clone()),
106            (Some(ma), None) => self.merge_list_to_pair_list(ma, None),
107            (None, Some(mb)) => self.merge_list_to_pair_list(mb, None),
108            (None, None) => MergePairList::new(),
109        };
110
111        // Process each merge pair
112        for pair in merged.pairs() {
113            let merged_node = self.merge_node_content(pair);
114
115            if let Some(content) = &merged_node {
116                match content {
117                    XmlContent::Text(text) => {
118                        // Output text content (with indentation only if on new line)
119                        let text_str: String = text.text().iter().collect();
120                        write!(
121                            writer,
122                            "{}{}",
123                            Self::indent_str_for(self.indent),
124                            escape_xml_text(&text_str)
125                        )?;
126                    }
127                    XmlContent::Comment(comment) => {
128                        // Output comment
129                        let comment_text: String = comment.text().iter().collect();
130                        writeln!(
131                            writer,
132                            "{}<!-- {} -->",
133                            Self::indent_str_for(self.indent),
134                            comment_text
135                        )?;
136                    }
137                    XmlContent::Element(elem) => {
138                        // Skip synthetic root
139                        if elem.qname() == "$ROOT$" {
140                            let partners = self.get_recursion_partners(pair);
141                            self.tree_merge(partners.first, partners.second, writer)?;
142                        } else {
143                            // Output element start tag
144                            self.write_start_element(writer, elem, self.indent)?;
145
146                            // Increment indent for children
147                            self.indent += 1;
148
149                            // Get recursion partners and recurse
150                            let partners = self.get_recursion_partners(pair);
151                            self.tree_merge(partners.first, partners.second, writer)?;
152
153                            // Decrement indent
154                            self.indent -= 1;
155
156                            // Output end tag
157                            writeln!(
158                                writer,
159                                "{}</{}>",
160                                Self::indent_str_for(self.indent),
161                                elem.qname()
162                            )?;
163                        }
164                    }
165                }
166            }
167        }
168
169        Ok(())
170    }
171
172    /// Creates a merge list from the children of a branch node.
173    fn make_merge_list(&mut self, parent: &NodeRef) -> MergeList {
174        MergeList::from_branch(parent, &mut self.edit_log)
175    }
176
177    /// Merges two merge lists into a merge pair list.
178    fn make_merge_pair_list(&mut self, mlist_a: MergeList, mlist_b: MergeList) -> MergePairList {
179        let mut mlist_a = mlist_a;
180        let mut mlist_b = mlist_b;
181
182        // Remove deleted or far-moved nodes
183        self.remove_deleted_or_moved(&mut mlist_a, &mut mlist_b);
184
185        // Checkpoint for potential rollback
186        self.edit_log.checkpoint();
187
188        // Lists should now have the same entries
189        if mlist_a.entry_count() != mlist_b.entry_count() {
190            // Fallback to simpler merge
191            self.edit_log.rewind();
192            let is_left = mlist_a
193                .entry_parent()
194                .map(|p| BranchNode::is_left_tree(&p))
195                .unwrap_or(true);
196            return if is_left {
197                self.merge_list_to_pair_list(&mlist_a, Some(&mlist_b))
198            } else {
199                self.merge_list_to_pair_list(&mlist_b, Some(&mlist_a))
200            };
201        }
202
203        let mut merged = MergePairList::new();
204        let mut pos_a = 0usize;
205        let mut pos_b = 0usize;
206
207        loop {
208            let ea = mlist_a.entry(pos_a);
209            let eb = mlist_b.entry(pos_b);
210
211            if ea.is_none() || eb.is_none() {
212                break;
213            }
214
215            let ea = ea.unwrap();
216            let eb = eb.unwrap();
217
218            // Add hangons from ea
219            for hangon in ea.hangons() {
220                let partner = hangon.borrow().first_partner(MatchType::FULL);
221                merged.append(Some(hangon.clone()), partner);
222                self.log_hangon_struct_ops(hangon);
223            }
224
225            // Add hangons from eb (if not equal to ea's hangons)
226            if !self.check_hangon_combine(ea, eb) {
227                for hangon in eb.hangons() {
228                    let partner = hangon.borrow().first_partner(MatchType::FULL);
229                    merged.append(Some(hangon.clone()), partner);
230                    self.log_hangon_struct_ops(hangon);
231                }
232            }
233
234            // Determine next positions
235            let next_a: Option<usize>;
236            let next_b: Option<usize>;
237
238            let ea_locked = ea.locked && mlist_a.entry(pos_a + 1).is_some_and(|e| e.locked);
239            let eb_locked = eb.locked && mlist_b.entry(pos_b + 1).is_some_and(|e| e.locked);
240
241            if !ea_locked && !eb_locked {
242                // No locking, both advance
243                next_a = Some(pos_a + 1);
244                next_b = Some(pos_b + 1);
245            } else if ea_locked && !eb_locked {
246                next_a = Some(pos_a + 1);
247                next_b = mlist_b.find_partner_index(mlist_a.entry(pos_a + 1));
248            } else if !ea_locked && eb_locked {
249                next_b = Some(pos_b + 1);
250                next_a = mlist_a.find_partner_index(mlist_b.entry(pos_b + 1));
251            } else {
252                // Both locked - check for conflict
253                let partner_b = mlist_b.find_partner_index(mlist_a.entry(pos_a + 1));
254                if partner_b != Some(pos_b + 1) {
255                    // Conflict - fallback
256                    self.conflict_log.add_list_conflict(
257                        ConflictType::Move,
258                        "Conflicting moves inside child list",
259                        ea.node().and_then(|n| BranchNode::base_match(&n)),
260                        ea.node(),
261                        eb.node(),
262                    );
263                    self.edit_log.rewind();
264                    let is_left = mlist_a
265                        .entry_parent()
266                        .map(|p| BranchNode::is_left_tree(&p))
267                        .unwrap_or(true);
268                    return if is_left {
269                        self.merge_list_to_pair_list(&mlist_a, Some(&mlist_b))
270                    } else {
271                        self.merge_list_to_pair_list(&mlist_b, Some(&mlist_a))
272                    };
273                }
274                next_a = Some(pos_a + 1);
275                next_b = partner_b;
276            }
277
278            pos_a = next_a.unwrap_or(pos_a + 1);
279            pos_b = next_b.unwrap_or(pos_b + 1);
280
281            // Check if done (at END marker)
282            if mlist_a.is_end(pos_a) || mlist_b.is_end(pos_b) {
283                break;
284            }
285
286            // Add the main entry pair
287            let ea = mlist_a.entry(pos_a);
288            let eb = mlist_b.entry(pos_b);
289            if let (Some(ea), Some(eb)) = (ea, eb) {
290                merged.append(ea.node(), eb.node());
291                self.log_entry_struct_ops(ea, eb);
292            }
293        }
294
295        self.edit_log.commit();
296        merged
297    }
298
299    /// Converts a single merge list to a pair list.
300    fn merge_list_to_pair_list(
301        &mut self,
302        mlist_a: &MergeList,
303        mlist_b: Option<&MergeList>,
304    ) -> MergePairList {
305        let mut merged = MergePairList::new();
306
307        for (i, entry) in mlist_a.entries().iter().enumerate() {
308            // Skip START marker but process its hangons
309            if i > 0 && !mlist_a.is_end(i) {
310                let node = entry.node();
311                let partner = node
312                    .as_ref()
313                    .and_then(|n| n.borrow().first_partner(MatchType::FULL));
314                merged.append(node, partner);
315                if let Some(n) = entry.node() {
316                    self.log_hangon_struct_ops(&n);
317                }
318            }
319
320            // Add hangons
321            for hangon in entry.hangons() {
322                let partner = hangon.borrow().first_partner(MatchType::FULL);
323                merged.append(Some(hangon.clone()), partner);
324                self.log_hangon_struct_ops(hangon);
325            }
326
327            // Add hangons from mlist_b if present
328            if let Some(mb) = mlist_b {
329                if let Some(partner_entry) = mb.find_partner(entry) {
330                    if !self.check_hangon_combine(entry, partner_entry) {
331                        for hangon in partner_entry.hangons() {
332                            let partner = hangon.borrow().first_partner(MatchType::FULL);
333                            merged.append(Some(hangon.clone()), partner);
334                            self.log_hangon_struct_ops(hangon);
335                        }
336                    }
337                }
338            }
339        }
340
341        merged
342    }
343
344    /// Removes deleted or far-moved nodes from merge lists.
345    fn remove_deleted_or_moved(&mut self, mlist_a: &mut MergeList, mlist_b: &mut MergeList) {
346        let base_parent = mlist_a
347            .entry_parent()
348            .and_then(|p| BranchNode::base_match(&p));
349
350        if base_parent.is_none() {
351            return;
352        }
353
354        let base_parent = base_parent.unwrap();
355        let child_count = base_parent.borrow().child_count();
356
357        for i in 0..child_count {
358            let bn = base_parent.borrow().child(i).cloned();
359            if bn.is_none() {
360                continue;
361            }
362            let bn = bn.unwrap();
363
364            let op1 = self.get_operation(&bn, mlist_a);
365            let op2 = self.get_operation(&bn, mlist_b);
366
367            // Handle based on which operation is smaller
368            // The table is symmetric in terms of which list we apply actions to
369            if op1 <= op2 {
370                self.handle_operation_pair(op1, op2, &bn, mlist_a, mlist_b);
371            } else {
372                self.handle_operation_pair(op2, op1, &bn, mlist_b, mlist_a);
373            }
374        }
375    }
376
377    /// Handles a pair of operations on a base node.
378    fn handle_operation_pair(
379        &mut self,
380        op1: Operation,
381        op2: Operation,
382        bn: &NodeRef,
383        ml1: &mut MergeList,
384        _ml2: &mut MergeList,
385    ) {
386        use Operation::*;
387
388        match (op1, op2) {
389            (Nop, Nop) | (Nop, MoveI) | (MoveI, MoveI) | (Delete, Delete) => {
390                // OK cases - no action needed
391            }
392            (Nop, MoveF) | (Nop, Delete) => {
393                // Delete the node from ml1
394                if let Some(ix) = ml1.match_in_list(bn) {
395                    if op2 == Delete {
396                        // Check for modifications in deleted subtree
397                        if let Some(entry) = ml1.entry(ix) {
398                            if let Some(node) = entry.node() {
399                                if self.is_deletia_modified(&node, ml1) {
400                                    self.conflict_log.add_list_warning(
401                                        ConflictType::Delete,
402                                        "Modifications in deleted subtree",
403                                        Some(bn.clone()),
404                                        entry.node(),
405                                        None,
406                                    );
407                                }
408                            }
409                        }
410                        self.edit_log.delete(Some(bn.clone()));
411                    }
412
413                    // Move hangons to predecessor
414                    ml1.move_hangons_to_predecessor(ix);
415                    ml1.remove_entry_at(ix);
416                }
417            }
418            (MoveI, MoveF) => {
419                // Conflict: node moved to different locations
420                if let Some(ix) = ml1.match_in_list(bn) {
421                    let node = ml1.entry(ix).and_then(|e| e.node());
422                    let partner = node
423                        .as_ref()
424                        .and_then(|n| n.borrow().first_partner(MatchType::FULL));
425                    self.conflict_log.add_list_conflict(
426                        ConflictType::Move,
427                        "Node moved to different locations",
428                        Some(bn.clone()),
429                        node,
430                        partner,
431                    );
432                    ml1.remove_entry_at(ix);
433                }
434            }
435            (MoveI, Delete) => {
436                // Conflict: node moved and deleted
437                if let Some(ix) = ml1.match_in_list(bn) {
438                    let node = ml1.entry(ix).and_then(|e| e.node());
439                    self.conflict_log.add_list_conflict(
440                        ConflictType::Move,
441                        "Node moved and deleted",
442                        Some(bn.clone()),
443                        node,
444                        None,
445                    );
446                    self.edit_log.delete(Some(bn.clone()));
447                    ml1.remove_entry_at(ix);
448                }
449            }
450            (MoveF, MoveF) => {
451                // Check for movef-movef conflict
452                if self.is_movef_movef_conflict(bn) {
453                    self.conflict_log.add_list_conflict(
454                        ConflictType::Move,
455                        "Node moved to different locations",
456                        Some(bn.clone()),
457                        BaseNode::left(bn).borrow().full_match(),
458                        BaseNode::right(bn).borrow().full_match(),
459                    );
460                }
461            }
462            (MoveF, Delete) => {
463                // Conflict: moved and deleted - keep the move
464                self.conflict_log.add_list_conflict(
465                    ConflictType::Move,
466                    "Node moved and deleted",
467                    Some(bn.clone()),
468                    BaseNode::left(bn).borrow().full_match(),
469                    BaseNode::right(bn).borrow().full_match(),
470                );
471            }
472            _ => {}
473        }
474    }
475
476    /// Gets the operation on a base node in a merge list.
477    fn get_operation(&self, bn: &NodeRef, ml: &MergeList) -> Operation {
478        if let Some(pos) = ml.match_in_list(bn) {
479            if ml.entry(pos).is_some_and(|e| e.moved) {
480                Operation::MoveI
481            } else {
482                Operation::Nop
483            }
484        } else {
485            // Check if moved far or deleted
486            let is_left = ml
487                .entry_parent()
488                .map(|p| BranchNode::is_left_tree(&p))
489                .unwrap_or(true);
490
491            let copies = if is_left {
492                BaseNode::left(bn)
493            } else {
494                BaseNode::right(bn)
495            };
496
497            if copies.borrow().match_count() == 0 {
498                Operation::Delete
499            } else {
500                Operation::MoveF
501            }
502        }
503    }
504
505    /// Checks if a deleted subtree has modifications.
506    fn is_deletia_modified(&self, n: &NodeRef, ml: &MergeList) -> bool {
507        let base_match = BranchNode::base_match(n);
508        if base_match.is_none() {
509            return true; // Inserted node
510        }
511
512        let base_match = base_match.unwrap();
513        if self.get_operation(&base_match, ml) != Operation::Nop {
514            return true; // Node was moved
515        }
516
517        if BranchNode::base_match_type(n) != MatchType::FULL {
518            return true; // Structural or content modification
519        }
520
521        // Check if deleted in other branch
522        if let Some(partners) = BranchNode::partners(n) {
523            if partners.borrow().match_count() == 0 {
524                if !self.matches(n, &base_match) {
525                    return true; // Updated
526                }
527                // Check children recursively
528                for i in 0..n.borrow().child_count() {
529                    if let Some(child) = n.borrow().child(i).cloned() {
530                        // Would need child's merge list - simplify for now
531                        if BranchNode::base_match_type(&child) != MatchType::FULL {
532                            return true;
533                        }
534                    }
535                }
536            }
537        }
538
539        false
540    }
541
542    /// Checks for movef-movef conflict.
543    fn is_movef_movef_conflict(&self, _bn: &NodeRef) -> bool {
544        // Simplified implementation - check if moved under non-matching parents
545        // Full implementation would check if parents match structurally
546        false
547    }
548
549    /// Merges the content of nodes in a merge pair.
550    fn merge_node_content(&mut self, pair: &MergePair) -> Option<XmlContent> {
551        let n1 = pair.first.as_ref();
552        let n2 = pair.second.as_ref();
553
554        match (n1, n2) {
555            (None, None) => None,
556            (Some(n), None) | (None, Some(n)) => n.borrow().content().cloned(),
557            (Some(n1), Some(n2)) => {
558                let n1_content = n1.borrow().is_match(MatchType::CONTENT);
559                let n2_content = n2.borrow().is_match(MatchType::CONTENT);
560
561                if n1_content {
562                    if !n2_content {
563                        self.edit_log.update(Some(n2.clone()));
564                        n2.borrow().content().cloned()
565                    } else {
566                        self.content_merge(n1, n2)
567                    }
568                } else if n2_content {
569                    self.edit_log.update(Some(n1.clone()));
570                    n1.borrow().content().cloned()
571                } else {
572                    // Neither matches content - forced merge
573                    self.content_merge(n1, n2)
574                }
575            }
576        }
577    }
578
579    /// Merges content of two nodes.
580    fn content_merge(&mut self, a: &NodeRef, b: &NodeRef) -> Option<XmlContent> {
581        let base_a = BranchNode::base_match(a);
582        let base_b = BranchNode::base_match(b);
583
584        let a_updated = base_a.as_ref().is_none_or(|ba| !self.matches(a, ba));
585        let b_updated = base_b.as_ref().is_none_or(|bb| !self.matches(b, bb));
586
587        if a_updated && b_updated {
588            // Both updated - check if same update
589            if self.matches(a, b) {
590                self.conflict_log.add_node_warning(
591                    ConflictType::Update,
592                    "Node updated in both branches, but updates are equal",
593                    base_a.clone(),
594                    Some(a.clone()),
595                    Some(b.clone()),
596                );
597                self.edit_log.update(Some(a.clone()));
598                return a.borrow().content().cloned();
599            }
600
601            // Try element merge
602            if let Some(merged) = self.merge_elements(base_a.as_ref(), a, b) {
603                return Some(merged);
604            }
605
606            // Conflict - use first branch
607            self.conflict_log.add_node_conflict(
608                ConflictType::Update,
609                "Node updated in both branches, using branch 1",
610                base_a,
611                Some(a.clone()),
612                Some(b.clone()),
613            );
614            self.edit_log.update(Some(a.clone()));
615            a.borrow().content().cloned()
616        } else if b_updated {
617            self.edit_log.update(Some(b.clone()));
618            b.borrow().content().cloned()
619        } else if a_updated {
620            self.edit_log.update(Some(a.clone()));
621            a.borrow().content().cloned()
622        } else {
623            a.borrow().content().cloned()
624        }
625    }
626
627    /// Attempts to merge two element nodes.
628    fn merge_elements(
629        &self,
630        base: Option<&NodeRef>,
631        a: &NodeRef,
632        b: &NodeRef,
633    ) -> Option<XmlContent> {
634        let base_content = base.and_then(|n| n.borrow().content().cloned());
635        let a_content = a.borrow().content().cloned();
636        let b_content = b.borrow().content().cloned();
637
638        match (&base_content, &a_content, &b_content) {
639            (
640                Some(XmlContent::Element(base_elem)),
641                Some(XmlContent::Element(a_elem)),
642                Some(XmlContent::Element(b_elem)),
643            ) => {
644                // Determine tag name
645                let tag_name = if base_elem.qname() == b_elem.qname() {
646                    a_elem.qname().to_string()
647                } else if base_elem.qname() == a_elem.qname() {
648                    b_elem.qname().to_string()
649                } else {
650                    return None; // Both changed tag name
651                };
652
653                // Merge attributes
654                self.merge_attributes(
655                    base_elem.attributes(),
656                    a_elem.attributes(),
657                    b_elem.attributes(),
658                )
659                .map(|merged_attrs| XmlContent::Element(XmlElement::new(tag_name, merged_attrs)))
660            }
661            _ => None,
662        }
663    }
664
665    /// Merges attributes from base, a, and b.
666    fn merge_attributes(
667        &self,
668        base: &std::collections::HashMap<String, String>,
669        a: &std::collections::HashMap<String, String>,
670        b: &std::collections::HashMap<String, String>,
671    ) -> Option<std::collections::HashMap<String, String>> {
672        let mut merged = std::collections::HashMap::new();
673        let mut deletia = std::collections::HashSet::new();
674
675        // Check for deleted attributes with conflicts
676        for (key, base_val) in base {
677            let in_a = a.get(key);
678            let in_b = b.get(key);
679
680            match (in_a, in_b) {
681                (None, Some(b_val)) if base_val != b_val => return None, // Deleted and changed
682                (Some(a_val), None) if base_val != a_val => return None, // Deleted and changed
683                (None, _) | (_, None) => {
684                    deletia.insert(key.clone());
685                }
686                _ => {}
687            }
688        }
689
690        // Build merged attributes from a
691        for (key, a_val) in a {
692            if deletia.contains(key) {
693                continue;
694            }
695
696            if let Some(b_val) = b.get(key) {
697                let base_val = base.get(key);
698                if base_val == Some(b_val) {
699                    // A possibly updated
700                    merged.insert(key.clone(), a_val.clone());
701                } else if base_val == Some(a_val) {
702                    // B possibly updated
703                    merged.insert(key.clone(), b_val.clone());
704                } else if a_val != b_val {
705                    // Both changed differently
706                    return None;
707                } else {
708                    merged.insert(key.clone(), a_val.clone());
709                }
710            } else {
711                // Insert from a
712                merged.insert(key.clone(), a_val.clone());
713            }
714        }
715
716        // Add inserts from b
717        for (key, b_val) in b {
718            if !deletia.contains(key) && !a.contains_key(key) {
719                merged.insert(key.clone(), b_val.clone());
720            }
721        }
722
723        Some(merged)
724    }
725
726    /// Gets partners for recursion.
727    fn get_recursion_partners(&self, pair: &MergePair) -> RecursionPartners {
728        let n1 = pair.first.as_ref();
729        let n2 = pair.second.as_ref();
730
731        match (n1, n2) {
732            (None, _) | (_, None) => RecursionPartners {
733                first: pair.first.clone(),
734                second: pair.second.clone(),
735            },
736            (Some(n1), Some(n2)) => {
737                let n1_children = n1.borrow().is_match(MatchType::CHILDREN);
738                let n2_children = n2.borrow().is_match(MatchType::CHILDREN);
739
740                if n1_children && n2_children {
741                    RecursionPartners {
742                        first: Some(n1.clone()),
743                        second: Some(n2.clone()),
744                    }
745                } else if n1_children && n2.borrow().is_match(MatchType::CONTENT) {
746                    RecursionPartners {
747                        first: None,
748                        second: Some(n2.clone()),
749                    }
750                } else if n1.borrow().is_match(MatchType::CONTENT) && n2_children {
751                    RecursionPartners {
752                        first: Some(n1.clone()),
753                        second: None,
754                    }
755                } else {
756                    RecursionPartners {
757                        first: Some(n1.clone()),
758                        second: Some(n2.clone()),
759                    }
760                }
761            }
762        }
763    }
764
765    /// Checks if two nodes have equal content.
766    fn matches(&self, a: &NodeRef, b: &NodeRef) -> bool {
767        let a_borrowed = a.borrow();
768        let b_borrowed = b.borrow();
769        let a_content = a_borrowed.content();
770        let b_content = b_borrowed.content();
771
772        match (a_content, b_content) {
773            (Some(ac), Some(bc)) => ac.content_equals(bc),
774            _ => false,
775        }
776    }
777
778    /// Logs structural operations for a hangon node.
779    fn log_hangon_struct_ops(&mut self, n: &NodeRef) {
780        if !BranchNode::has_base_match(n) {
781            self.edit_log.insert(Some(n.clone()));
782        } else {
783            let base = BranchNode::base_match(n).unwrap();
784            let is_left = BranchNode::is_left_tree(n);
785
786            let copies = if is_left {
787                BaseNode::left(&base)
788            } else {
789                BaseNode::right(&base)
790            };
791
792            if copies.borrow().match_count() > 1 {
793                self.edit_log.copy(Some(n.clone()));
794            } else {
795                self.edit_log.r#move(Some(n.clone()));
796            }
797        }
798    }
799
800    /// Logs structural operations for merge entries.
801    fn log_entry_struct_ops(&mut self, e1: &MergeEntry, e2: &MergeEntry) {
802        if e1.moved {
803            if let Some(n) = e1.node() {
804                self.edit_log.r#move(Some(n));
805            }
806        } else if e2.moved {
807            if let Some(n) = e2.node() {
808                self.edit_log.r#move(Some(n));
809            }
810        }
811    }
812
813    /// Checks if hangons from two entries are equal.
814    /// Returns true only if hangons match exactly (same count and content).
815    /// When false, the caller should add eb's hangons to the merge list.
816    fn check_hangon_combine(&mut self, ea: &MergeEntry, eb: &MergeEntry) -> bool {
817        // If ea has no hangons, return false so eb's hangons are added
818        if ea.hangons().is_empty() {
819            return false;
820        }
821
822        if ea.hangons().len() != eb.hangons().len() {
823            if !ea.hangons().is_empty() && !eb.hangons().is_empty() {
824                self.conflict_log.add_list_warning(
825                    ConflictType::Insert,
826                    "Insertions/copies in both branches",
827                    ea.node().and_then(|n| BranchNode::base_match(&n)),
828                    ea.node(),
829                    eb.node(),
830                );
831            }
832            return false;
833        }
834
835        // Check if hangons match exactly
836        let mut equal = true;
837        for (ha, hb) in ea.hangons().iter().zip(eb.hangons().iter()) {
838            if !self.tree_matches(ha, hb) {
839                equal = false;
840                break;
841            }
842        }
843
844        if equal {
845            self.conflict_log.add_list_warning(
846                ConflictType::Insert,
847                "Equal insertions/copies in both branches",
848                ea.node().and_then(|n| BranchNode::base_match(&n)),
849                ea.node(),
850                eb.node(),
851            );
852        } else if !ea.hangons().is_empty() {
853            self.conflict_log.add_list_warning(
854                ConflictType::Insert,
855                "Insertions/copies in both branches, sequencing",
856                ea.node().and_then(|n| BranchNode::base_match(&n)),
857                ea.node(),
858                eb.node(),
859            );
860        }
861
862        equal
863    }
864
865    /// Checks if entire subtrees match exactly.
866    fn tree_matches(&self, a: &NodeRef, b: &NodeRef) -> bool {
867        if !self.matches(a, b) {
868            return false;
869        }
870
871        let a_count = a.borrow().child_count();
872        let b_count = b.borrow().child_count();
873
874        if a_count != b_count {
875            return false;
876        }
877
878        for i in 0..a_count {
879            let a_child = a.borrow().child(i).cloned();
880            let b_child = b.borrow().child(i).cloned();
881
882            match (a_child, b_child) {
883                (Some(ac), Some(bc)) => {
884                    if !self.tree_matches(&ac, &bc) {
885                        return false;
886                    }
887                }
888                _ => return false,
889            }
890        }
891
892        true
893    }
894
895    /// Writes an element start tag.
896    fn write_start_element<W: Write>(
897        &self,
898        writer: &mut W,
899        elem: &XmlElement,
900        indent: usize,
901    ) -> std::io::Result<()> {
902        write!(writer, "{}<{}", Self::indent_str_for(indent), elem.qname())?;
903
904        // Write attributes (sorted for deterministic output)
905        let mut attrs: Vec<(&String, &String)> = elem.attributes().iter().collect();
906        attrs.sort_by(|a, b| a.0.cmp(b.0));
907
908        for (name, value) in attrs {
909            write!(writer, " {}=\"{}\"", name, escape_xml_attr(value))?;
910        }
911
912        writeln!(writer, ">")?;
913        Ok(())
914    }
915
916    /// Returns the indentation string for a given level.
917    fn indent_str_for(level: usize) -> String {
918        "  ".repeat(level)
919    }
920}
921
922/// Partners for recursion.
923struct RecursionPartners {
924    first: Option<NodeRef>,
925    second: Option<NodeRef>,
926}
927
928/// Escapes special characters in XML text content.
929fn escape_xml_text(s: &str) -> String {
930    s.replace('&', "&amp;")
931        .replace('<', "&lt;")
932        .replace('>', "&gt;")
933}
934
935/// Escapes special characters in XML attribute values.
936fn escape_xml_attr(s: &str) -> String {
937    s.replace('&', "&amp;")
938        .replace('<', "&lt;")
939        .replace('>', "&gt;")
940        .replace('"', "&quot;")
941        .replace('\'', "&apos;")
942}
943
944impl crate::node::NodeInner {
945    /// Returns the first partner that matches the given type.
946    pub fn first_partner(&self, type_flags: MatchType) -> Option<NodeRef> {
947        if let crate::node::NodeKind::Branch {
948            match_type,
949            partners,
950            ..
951        } = self.kind()
952        {
953            if !match_type.intersects(type_flags) {
954                return None;
955            }
956
957            if let Some(partners) = partners {
958                let borrowed = partners.borrow();
959                for candidate in borrowed.matches() {
960                    if BranchNode::base_match_type(candidate).intersects(type_flags) {
961                        return Some(candidate.clone());
962                    }
963                }
964            }
965        }
966        None
967    }
968
969    /// Returns true if the match type includes the given flags.
970    pub fn is_match(&self, type_flags: MatchType) -> bool {
971        if let crate::node::NodeKind::Branch { match_type, .. } = self.kind() {
972            match_type.intersects(type_flags)
973        } else {
974            false
975        }
976    }
977}