1
//! Apply a [`DocumentOperation`] to a [`Dom`] tree — the helper for apps
2
//! WITHOUT their own document model (Path 2).
3
//!
4
//! The applier operates on azul's NATIVE node tree (`azul_core::dom::Dom` —
5
//! what a layout callback returns, what `reconstruct_dom_subtree` hands
6
//! back), not on markup. Operations are STRUCTURAL: subtrees move wholesale
7
//! (`<b>…</b>` inside a split paragraph survives intact, a `<ul>` splits
8
//! between `<li>`s, a table row inserts like any other subtree); the ONLY
9
//! thing ever cut is a text child, at a char boundary, when a
10
//! [`NodePosition`] points inside it.
11
//!
12
//! Azul records structural intent (`DocumentChangeset`); the app applies it
13
//! to ITS model and regenerates the DOM — the `StyledDom` is never mutated.
14
//! An app holding a `Dom` calls [`apply_document_operation`], then
15
//! `CallbackInfo::mark_document_edit_applied_with_inverse(changeset.id,
16
//! applied.inverse)` (the commit handshake), then returns
17
//! `Update::RefreshDom`.
18
//!
19
//! Every successful apply returns the INVERSE operation — tree-shaped undo
20
//! for free (undoing re-RECORDS the inverse through the same
21
//! record→apply→ack loop; it never mutates either).
22
//!
23
//! **Fragment semantics**: `content: Dom` payloads are DocumentFragment-like
24
//! — the fragment's ROOT is ignored, its CHILDREN are the inserted nodes.
25
//! This closes the inverse algebra for multi-child operations
26
//! (`RemoveChildren [s, e)` ⇄ `InsertChildren` of the removed fragment).
27

            
28
use azul_core::dom::{Dom, NodeType};
29

            
30
use crate::managers::changeset::{
31
    DocOpInsertChildren, DocOpMergeNodes, DocOpRemoveChildren, DocOpReplaceChildren,
32
    DocOpSplitNode, DocumentChangeset, DocumentOperation, NodePosition,
33
};
34

            
35
/// The outcome of a successful apply.
36
#[derive(Debug, Clone)]
37
pub struct AppliedEdit {
38
    /// Where the caret/anchor should land (passed through from the changeset
39
    /// — already expressed re-render-stably).
40
    pub resume: crate::managers::changeset::EditResumePoint,
41
    /// The operation that undoes this one. `DomNodeId` fields are advisory
42
    /// (they refer to the generation the ORIGINAL changeset was recorded
43
    /// against); the structural payload (positions, ranges, fragments) is
44
    /// what the undo path re-records.
45
    pub inverse: DocumentOperation,
46
    /// The resume point to re-record [`inverse`] WITH.
47
    ///
48
    /// Index resolution is asymmetric — a split targets
49
    /// `resume.node_path.last() - 1` while a merge keeps
50
    /// `resume.node_path.last()` — so replaying the inverse with the
51
    /// ORIGINAL resume point lands one node off and edits the wrong pair.
52
    /// An application undoing an edit must not have to know that: this is
53
    /// the resume point that makes `inverse` apply to exactly the nodes the
54
    /// forward operation touched.
55
    pub inverse_resume: crate::managers::changeset::EditResumePoint,
56
}
57

            
58
/// Why an apply failed. Failures leave the tree UNCHANGED.
59
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60
pub enum DocumentEditError {
61
    /// `host_path` did not resolve to a node in the tree.
62
    HostNotFound,
63
    /// An index/range in the operation does not exist under the host.
64
    TargetNotFound,
65
    /// The operation kind cannot be applied (reserved).
66
    Unsupported(&'static str),
67
}
68

            
69
/// Wrap subtrees in a fragment `Dom` (root ignored by the applier).
70
#[must_use]
71
8
pub fn fragment(children: Vec<Dom>) -> Dom {
72
8
    let mut f = Dom::create_div();
73
23
    for c in children {
74
15
        f.add_child(c);
75
15
    }
76
8
    f
77
8
}
78

            
79
/// Apply a structural changeset to the `Dom` the app holds.
80
///
81
/// * `root` — the app's document tree (e.g. from its own builder or
82
///   `reconstruct_dom_subtree`).
83
/// * `host_path` — child-index path from `root` to the node whose CHILD LIST
84
///   the operation edits (`[]` = `root` itself). For Split/Merge this is the
85
///   PARENT of the split/merged nodes.
86
/// * `changeset` — as delivered by `CallbackInfo::get_document_edit_clone`.
87
///
88
/// Index resolution for Split/Merge uses the changeset's OWN resume point
89
/// (recorded by the same engine that computes it, so the two cannot drift):
90
/// a split targets `resume.node_path.last() - 1` (the resume names the NEW
91
/// second node), a merge keeps `resume.node_path.last()`.
92
///
93
/// # Errors
94
///
95
/// Returns a [`DocumentEditError`] (tree unchanged) on unresolvable paths,
96
/// missing targets, or record-only operation kinds.
97
82
pub fn apply_document_operation(
98
82
    root: &mut Dom,
99
82
    host_path: &[u32],
100
82
    changeset: &DocumentChangeset,
101
82
) -> Result<AppliedEdit, DocumentEditError> {
102
82
    let host = resolve_path_mut(root, host_path).ok_or(DocumentEditError::HostNotFound)?;
103
81
    let resume_index = changeset
104
81
        .resume
105
81
        .node_path
106
81
        .as_ref()
107
81
        .last()
108
81
        .copied()
109
81
        .unwrap_or(0);
110

            
111
    // The index the inverse must address, in the units ITS OWN arm reads
112
    // (split reads `last - 1`, merge reads `last`), so the caller can replay
113
    // the inverse verbatim.
114
81
    let mut inverse_resume_last = resume_index;
115
81
    let inverse = match &changeset.operation {
116
43
        DocumentOperation::SplitNode(split) => {
117
43
            let node_index = resume_index.saturating_sub(1) as usize;
118
            // Inverse is a MERGE of (node_index, node_index + 1); merge reads
119
            // the index directly.
120
43
            inverse_resume_last = node_index as u32;
121
43
            apply_split(host, node_index, split)?
122
        }
123
31
        DocumentOperation::MergeNodes(merge) => {
124
31
            let first_index = resume_index as usize;
125
            // Inverse is a SPLIT of `first_index`; split reads `last - 1`.
126
31
            inverse_resume_last = first_index as u32 + 1;
127
31
            apply_merge(host, first_index, merge)?
128
        }
129
1
        DocumentOperation::InsertChildren(insert) => apply_insert(host, insert),
130
1
        DocumentOperation::RemoveChildren(remove) => apply_remove(host, remove)?,
131
2
        DocumentOperation::ReplaceChildren(replace) => apply_replace(host, replace)?,
132
2
        DocumentOperation::WrapRange(wrap) => apply_wrap(host, wrap)?,
133
1
        DocumentOperation::UnwrapRange(unwrap) => apply_unwrap(host, unwrap)?,
134
    };
135

            
136
    // Direct `children` mutation desyncs `estimated_total_children` (the
137
    // CompactDom conversion asserts on it); re-sync the WHOLE tree — counts
138
    // bubble up through every ancestor of the edited node.
139
80
    root.fixup_children_estimated();
140

            
141
80
    let mut inverse_resume = changeset.resume.clone();
142
    {
143
80
        let mut path = inverse_resume.node_path.as_ref().to_vec();
144
80
        match path.last_mut() {
145
80
            Some(last) => *last = inverse_resume_last,
146
            None => path.push(inverse_resume_last),
147
        }
148
80
        inverse_resume.node_path = path.into();
149
    }
150

            
151
80
    Ok(AppliedEdit {
152
80
        resume: changeset.resume.clone(),
153
80
        inverse,
154
80
        inverse_resume,
155
80
    })
156
82
}
157

            
158
/// Walk a child-index path down the tree.
159
82
fn resolve_path_mut<'a>(root: &'a mut Dom, path: &[u32]) -> Option<&'a mut Dom> {
160
82
    let mut node = root;
161
82
    for &idx in path {
162
1
        node = node.children.as_mut().get_mut(idx as usize)?;
163
    }
164
81
    Some(node)
165
82
}
166

            
167
/// Take a node's children out as a plain Vec (write back with `.into()`).
168
154
fn take_children(node: &mut Dom) -> Vec<Dom> {
169
154
    core::mem::take(&mut node.children).into_library_owned_vec()
170
154
}
171

            
172
/// Split the text of a text-node `Dom` at `byte` (char-boundary clamped),
173
/// truncating the node to the head and returning the tail as a new node.
174
34
fn split_text_dom(node: &mut Dom, byte: usize) -> Dom {
175
34
    let (head, tail) = match node.root.get_node_type() {
176
34
        NodeType::Text(t) => {
177
34
            let s = t.as_str();
178
34
            let cut = byte.min(s.len());
179
34
            let cut = (0..=cut)
180
34
                .rev()
181
35
                .find(|&c| s.is_char_boundary(c))
182
34
                .unwrap_or(0);
183
34
            (s[..cut].to_string(), s[cut..].to_string())
184
        }
185
        _ => return Dom::create_text_do_not_use_without_block_level_wrapper(""),
186
    };
187
34
    *node = Dom::create_text_do_not_use_without_block_level_wrapper(head);
188
34
    Dom::create_text_do_not_use_without_block_level_wrapper(tail)
189
34
}
190

            
191
/// Split `host.children[node_index]` at the structural position: children
192
/// BEFORE the position stay, children AFTER move to a new sibling of the
193
/// SAME node shape (the `NodeData` is cloned — a `<ul>` splits into two
194
/// `<ul>`s, an `<h1>` into two `<h1>`s; tag conversion is an editing policy
195
/// for the RECORDER, not the tree algebra). A text child AT the position is
196
/// cut at its byte. Inverse: the merge at the same seam.
197
43
fn apply_split(
198
43
    host: &mut Dom,
199
43
    node_index: usize,
200
43
    split: &DocOpSplitNode,
201
43
) -> Result<DocumentOperation, DocumentEditError> {
202
43
    let mut host_children = take_children(host);
203
43
    if node_index >= host_children.len() {
204
        host.children = host_children.into();
205
        return Err(DocumentEditError::TargetNotFound);
206
43
    }
207

            
208
43
    let node = &mut host_children[node_index];
209
43
    let mut node_children = take_children(node);
210
43
    let child_index = (split.at.child_index as usize).min(node_children.len());
211

            
212
    let mut second_children: Vec<Dom>;
213
43
    match split.at.text_byte.into_option() {
214
32
        Some(byte)
215
32
            if child_index < node_children.len()
216
                && matches!(
217
32
                    node_children[child_index].root.get_node_type(),
218
                    NodeType::Text(_)
219
32
                ) =>
220
32
        {
221
32
            // Cut the boundary TEXT child; everything after it moves.
222
32
            let tail_text = split_text_dom(&mut node_children[child_index], byte as usize);
223
32
            second_children = vec![tail_text];
224
32
            second_children.extend(node_children.drain(child_index + 1..));
225
32
        }
226
11
        _ => {
227
11
            // Pure structural boundary: children[child_index..] move wholesale.
228
11
            second_children = node_children.drain(child_index..).collect();
229
11
        }
230
    }
231
43
    node.children = node_children.into();
232

            
233
    // The second node clones the first's SHAPE (same NodeData: type, classes,
234
    // attributes) and takes the moved children.
235
43
    let mut second = Dom {
236
43
        root: node.root.clone(),
237
43
        children: Vec::<Dom>::new().into(),
238
43
        css: Vec::new().into(),
239
43
        estimated_total_children: 0,
240
43
    };
241
88
    for c in second_children {
242
45
        second.add_child(c);
243
45
    }
244
43
    host_children.insert(node_index + 1, second);
245
43
    host.children = host_children.into();
246

            
247
43
    Ok(DocumentOperation::MergeNodes(DocOpMergeNodes {
248
43
        first: split.node,
249
43
        second: split.node,
250
43
        join: split.at,
251
43
    }))
252
43
}
253

            
254
/// Merge `host.children[first_index + 1]` into `host.children[first_index]`:
255
/// the second node's children are appended WHOLESALE; two text nodes meeting
256
/// at the seam coalesce iff the join position carries a byte (the recorder
257
/// says the seam is text|text). Inverse: the split at the seam.
258
31
fn apply_merge(
259
31
    host: &mut Dom,
260
31
    first_index: usize,
261
31
    merge: &DocOpMergeNodes,
262
31
) -> Result<DocumentOperation, DocumentEditError> {
263
31
    let mut host_children = take_children(host);
264
31
    if first_index + 1 >= host_children.len() {
265
1
        host.children = host_children.into();
266
1
        return Err(DocumentEditError::TargetNotFound);
267
30
    }
268

            
269
30
    let second = host_children.remove(first_index + 1);
270
30
    let first = &mut host_children[first_index];
271
30
    let mut first_children = take_children(first);
272
30
    let second_children = second.children.into_library_owned_vec();
273

            
274
30
    let mut iter = second_children.into_iter();
275
30
    if merge.join.text_byte.into_option().is_some() {
276
        // The recorder marked the seam text|text: coalesce the two nodes so
277
        // a later split at the join byte round-trips.
278
29
        if let Some(second_first) = iter.next() {
279
29
            let coalesced = match (
280
29
                first_children.last().map(|n| n.root.get_node_type()),
281
29
                second_first.root.get_node_type(),
282
            ) {
283
29
                (Some(NodeType::Text(a)), NodeType::Text(b)) => {
284
29
                    Some(format!("{}{}", a.as_str(), b.as_str()))
285
                }
286
                _ => None,
287
            };
288
29
            match coalesced {
289
29
                Some(joined) => {
290
29
                    *first_children.last_mut().unwrap() = Dom::create_text_do_not_use_without_block_level_wrapper(joined);
291
29
                }
292
                None => first_children.push(second_first),
293
            }
294
        }
295
1
    }
296
30
    first_children.extend(iter);
297
30
    first.children = first_children.into();
298
30
    host.children = host_children.into();
299

            
300
30
    Ok(DocumentOperation::SplitNode(DocOpSplitNode {
301
30
        node: merge.first,
302
30
        at: merge.join,
303
30
    }))
304
31
}
305

            
306
/// Insert the fragment's children under `host` at `insert.index`.
307
/// Inverse: remove of exactly that range.
308
1
fn apply_insert(host: &mut Dom, insert: &DocOpInsertChildren) -> DocumentOperation {
309
1
    let mut host_children = take_children(host);
310
1
    let index = (insert.index as usize).min(host_children.len());
311
1
    let new_children = insert.content.children.as_ref().to_vec();
312
1
    let count = new_children.len();
313
2
    for (offset, child) in new_children.into_iter().enumerate() {
314
2
        host_children.insert(index + offset, child);
315
2
    }
316
1
    host.children = host_children.into();
317
1
    DocumentOperation::RemoveChildren(DocOpRemoveChildren {
318
1
        parent: insert.parent,
319
1
        start: u32::try_from(index).unwrap_or(u32::MAX),
320
1
        end: u32::try_from(index + count).unwrap_or(u32::MAX),
321
1
    })
322
1
}
323

            
324
/// Remove `host.children[start..end)`. Inverse: insert of the removed
325
/// fragment at `start`.
326
1
fn apply_remove(
327
1
    host: &mut Dom,
328
1
    remove: &DocOpRemoveChildren,
329
1
) -> Result<DocumentOperation, DocumentEditError> {
330
1
    let mut host_children = take_children(host);
331
1
    let start = remove.start as usize;
332
1
    let end = remove.end as usize;
333
1
    if start > end || end > host_children.len() {
334
        host.children = host_children.into();
335
        return Err(DocumentEditError::TargetNotFound);
336
1
    }
337
1
    let removed: Vec<Dom> = host_children.drain(start..end).collect();
338
1
    host.children = host_children.into();
339
1
    Ok(DocumentOperation::InsertChildren(DocOpInsertChildren {
340
1
        parent: remove.parent,
341
1
        index: remove.start,
342
1
        content: fragment(removed),
343
1
    }))
344
1
}
345

            
346
/// Replace `host.children[start..end)` with the fragment's children.
347
/// Inverse: the replace that puts the old range back.
348
2
fn apply_replace(
349
2
    host: &mut Dom,
350
2
    replace: &DocOpReplaceChildren,
351
2
) -> Result<DocumentOperation, DocumentEditError> {
352
2
    let mut host_children = take_children(host);
353
2
    let start = replace.start as usize;
354
2
    let end = replace.end as usize;
355
2
    if start > end || end > host_children.len() {
356
        host.children = host_children.into();
357
        return Err(DocumentEditError::TargetNotFound);
358
2
    }
359
2
    let new_children = replace.content.children.as_ref().to_vec();
360
2
    let count = new_children.len();
361
2
    let removed: Vec<Dom> = host_children.splice(start..end, new_children).collect();
362
2
    host.children = host_children.into();
363
2
    Ok(DocumentOperation::ReplaceChildren(DocOpReplaceChildren {
364
2
        parent: replace.parent,
365
2
        start: replace.start,
366
2
        end: u32::try_from(start + count).unwrap_or(u32::MAX),
367
2
        content: fragment(removed),
368
2
    }))
369
2
}
370

            
371
/// Wrap `host.children` between `start` and `end` in the wrapper element:
372
/// boundary TEXT children are cut at the range edges; everything covered
373
/// moves INTO a new wrapper node inserted at the range start. Inverse: the
374
/// unwrap at that position.
375
///
376
/// NOTE the host resolution difference: for wrap/unwrap `host_path` names
377
/// the node whose CONTENT the range covers (the edit happens inside it),
378
/// while split/merge name the PARENT (the edit adds/removes a sibling).
379
2
fn apply_wrap(
380
2
    host: &mut Dom,
381
2
    wrap: &crate::managers::changeset::DocOpWrapRange,
382
2
) -> Result<DocumentOperation, DocumentEditError> {
383
2
    let mut children = take_children(host);
384
2
    let len = children.len();
385

            
386
    // END boundary first (so start-side splits don't shift its index):
387
    // a byte inside a text child cuts it — the head stays IN the range,
388
    // the tail is re-inserted after it, outside.
389
2
    let mut end_exclusive = (wrap.end.child_index as usize).min(len);
390
2
    if let Some(byte) = wrap.end.text_byte.into_option() {
391
1
        if end_exclusive < children.len()
392
            && matches!(
393
1
                children[end_exclusive].root.get_node_type(),
394
                NodeType::Text(_)
395
            )
396
1
        {
397
1
            let tail = split_text_dom(&mut children[end_exclusive], byte as usize);
398
1
            children.insert(end_exclusive + 1, tail);
399
1
            end_exclusive += 1; // the head (now cut) is covered
400
1
        }
401
1
    }
402

            
403
    // START boundary: a byte inside a text child cuts it — the head stays
404
    // OUTSIDE, the tail begins the range (everything after shifts by one).
405
2
    let mut start_index = (wrap.start.child_index as usize).min(children.len());
406
2
    if let Some(byte) = wrap.start.text_byte.into_option() {
407
1
        if start_index < children.len()
408
            && matches!(
409
1
                children[start_index].root.get_node_type(),
410
                NodeType::Text(_)
411
            )
412
1
        {
413
1
            let tail = split_text_dom(&mut children[start_index], byte as usize);
414
1
            children.insert(start_index + 1, tail);
415
1
            start_index += 1;
416
1
            end_exclusive += 1;
417
1
        }
418
1
    }
419

            
420
2
    if start_index > end_exclusive || end_exclusive > children.len() {
421
        host.children = children.into();
422
        return Err(DocumentEditError::TargetNotFound);
423
2
    }
424

            
425
2
    let covered: Vec<Dom> = children.drain(start_index..end_exclusive).collect();
426
2
    let mut wrapper = Dom {
427
2
        root: wrap.wrapper.root.clone(),
428
2
        children: Vec::<Dom>::new().into(),
429
2
        css: Vec::new().into(),
430
2
        estimated_total_children: 0,
431
2
    };
432
5
    for c in covered {
433
3
        wrapper.add_child(c);
434
3
    }
435
2
    children.insert(start_index, wrapper);
436
2
    host.children = children.into();
437

            
438
2
    Ok(DocumentOperation::UnwrapRange(
439
2
        crate::managers::changeset::DocOpUnwrapRange {
440
2
            node: wrap.node,
441
2
            at: NodePosition::before_child(u32::try_from(start_index).unwrap_or(u32::MAX)),
442
2
        },
443
2
    ))
444
2
}
445

            
446
/// Remove the wrapper child at `at`, splicing its children into its place;
447
/// text meeting at either seam coalesces (so wrap → unwrap round-trips to
448
/// the original tree). Inverse: the wrap that re-covers the spliced range.
449
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose splice + both-seam coalesce + exact inverse
450
1
fn apply_unwrap(
451
1
    host: &mut Dom,
452
1
    unwrap: &crate::managers::changeset::DocOpUnwrapRange,
453
1
) -> Result<DocumentOperation, DocumentEditError> {
454
1
    let mut children = take_children(host);
455
1
    let index = unwrap.at.child_index as usize;
456
1
    if index >= children.len()
457
1
        || matches!(children[index].root.get_node_type(), NodeType::Text(_))
458
    {
459
        host.children = children.into();
460
        return Err(DocumentEditError::TargetNotFound);
461
1
    }
462

            
463
1
    let wrapper = children.remove(index);
464
1
    let wrapper_shape = Dom {
465
1
        root: wrapper.root.clone(),
466
1
        children: Vec::<Dom>::new().into(),
467
1
        css: Vec::new().into(),
468
1
        estimated_total_children: 0,
469
1
    };
470
1
    let mut spliced: Vec<Dom> = wrapper.children.into_library_owned_vec();
471

            
472
    // Inverse range start: coalescing with a preceding text node moves the
473
    // start INTO it (at its pre-join byte length).
474
1
    let mut start = NodePosition::before_child(u32::try_from(index).unwrap_or(u32::MAX));
475
1
    if index > 0 {
476
1
        let coalesce_left = matches!(
477
            (
478
1
                children.get(index - 1).map(|c| c.root.get_node_type()),
479
1
                spliced.first().map(|c| c.root.get_node_type()),
480
            ),
481
            (Some(NodeType::Text(_)), Some(NodeType::Text(_)))
482
        );
483
1
        if coalesce_left {
484
1
            let first_spliced = spliced.remove(0);
485
1
            let (prev_len, joined) = match (
486
1
                children[index - 1].root.get_node_type(),
487
1
                first_spliced.root.get_node_type(),
488
            ) {
489
1
                (NodeType::Text(a), NodeType::Text(b)) => (
490
1
                    u32::try_from(a.as_str().len()).unwrap_or(u32::MAX),
491
1
                    format!("{}{}", a.as_str(), b.as_str()),
492
1
                ),
493
                _ => unreachable!("checked above"),
494
            };
495
1
            children[index - 1] = Dom::create_text_do_not_use_without_block_level_wrapper(joined);
496
1
            start = NodePosition::in_text_child(
497
1
                u32::try_from(index - 1).unwrap_or(u32::MAX),
498
1
                prev_len,
499
1
            );
500
        }
501
    }
502

            
503
    // Splice the (remaining) children in.
504
1
    let spliced_count = spliced.len();
505
1
    let insert_at = index;
506
1
    for (offset, c) in spliced.into_iter().enumerate() {
507
        children.insert(insert_at + offset, c);
508
    }
509

            
510
    // The child that HOLDS the range end: the last spliced child, or — when
511
    // everything was absorbed into the left text node — that joined node.
512
1
    let (end_holder, mut end) = if spliced_count > 0 {
513
        let last = insert_at + spliced_count - 1;
514
        (
515
            Some(last),
516
            NodePosition::before_child(u32::try_from(last + 1).unwrap_or(u32::MAX)),
517
        )
518
1
    } else if start.text_byte.into_option().is_some() {
519
1
        let holder = index - 1;
520
1
        let byte = match children[holder].root.get_node_type() {
521
1
            NodeType::Text(t) => u32::try_from(t.as_str().len()).unwrap_or(u32::MAX),
522
            _ => 0,
523
        };
524
1
        (
525
1
            Some(holder),
526
1
            NodePosition::in_text_child(u32::try_from(holder).unwrap_or(u32::MAX), byte),
527
1
        )
528
    } else {
529
        // Empty wrapper removed: nothing to coalesce, range is empty.
530
        (
531
            None,
532
            NodePosition::before_child(u32::try_from(index).unwrap_or(u32::MAX)),
533
        )
534
    };
535

            
536
    // Right seam: the end-holder may coalesce with its follower (both text).
537
    // The inverse's end byte is the holder's length BEFORE the join — the
538
    // exact seam the original wrap cut.
539
1
    if let Some(holder) = end_holder {
540
1
        if holder + 1 < children.len() {
541
1
            let both_text = matches!(
542
                (
543
1
                    children[holder].root.get_node_type(),
544
1
                    children[holder + 1].root.get_node_type(),
545
                ),
546
                (NodeType::Text(_), NodeType::Text(_))
547
            );
548
1
            if both_text {
549
1
                let following = children.remove(holder + 1);
550
1
                let (seam_byte, joined) = match (
551
1
                    children[holder].root.get_node_type(),
552
1
                    following.root.get_node_type(),
553
                ) {
554
1
                    (NodeType::Text(a), NodeType::Text(b)) => (
555
1
                        u32::try_from(a.as_str().len()).unwrap_or(u32::MAX),
556
1
                        format!("{}{}", a.as_str(), b.as_str()),
557
1
                    ),
558
                    _ => unreachable!("checked above"),
559
                };
560
1
                children[holder] = Dom::create_text_do_not_use_without_block_level_wrapper(joined);
561
1
                end = NodePosition::in_text_child(
562
1
                    u32::try_from(holder).unwrap_or(u32::MAX),
563
1
                    seam_byte,
564
1
                );
565
            }
566
        }
567
    }
568

            
569
1
    host.children = children.into();
570
1
    Ok(DocumentOperation::WrapRange(
571
1
        crate::managers::changeset::DocOpWrapRange {
572
1
            node: unwrap.node,
573
1
            start,
574
1
            end,
575
1
            wrapper: wrapper_shape,
576
1
        },
577
1
    ))
578
1
}
579

            
580
/// Split a tree along a SPINE — the fragmentainer-flow cut.
581
///
582
/// `path` names, level by level, the child at which the document continues in
583
/// the NEXT fragmentainer (page/section). At every spine level the node's
584
/// shape (`NodeData`) is duplicated: children BEFORE the path index stay in
585
/// the head, the path child itself splits recursively, children AFTER move to
586
/// the tail. A `<section><ul>…` cut inside the `<ul>` yields two sections
587
/// each holding a `<ul>` — exactly how CSS fragmentation clones box chains
588
/// across fragmentainers (and how Word continues a list across a section
589
/// break).
590
///
591
/// An empty `path` puts EVERYTHING in the tail (cut before the root's
592
/// content); a path index past the child count puts the whole level in the
593
/// head. Counts are re-synced on both results.
594
#[must_use]
595
5
pub fn split_dom_at_path(dom: &Dom, path: &[u32]) -> (Dom, Dom) {
596
14
    fn shape_of(node: &Dom) -> Dom {
597
14
        Dom {
598
14
            root: node.root.clone(),
599
14
            children: Vec::new().into(),
600
14
            css: node.css.clone(),
601
14
            estimated_total_children: 0,
602
14
        }
603
14
    }
604
7
    fn rec(node: &Dom, path: &[u32]) -> (Dom, Dom) {
605
7
        let mut head = shape_of(node);
606
7
        let mut tail = shape_of(node);
607
7
        let Some((&idx, rest)) = path.split_first() else {
608
1
            tail.children = node.children.clone();
609
1
            return (head, tail);
610
        };
611
6
        let children = node.children.as_ref();
612
6
        let idx = idx as usize;
613
6
        let mut head_children: Vec<Dom> = children[..idx.min(children.len())].to_vec();
614
6
        let mut tail_children: Vec<Dom> = Vec::new();
615
6
        if idx < children.len() {
616
5
            if rest.is_empty() {
617
3
                // The cut lands BEFORE this child: it belongs to the tail.
618
3
                tail_children.push(children[idx].clone());
619
3
            } else {
620
2
                let (h, t) = rec(&children[idx], rest);
621
2
                head_children.push(h);
622
2
                tail_children.push(t);
623
2
            }
624
5
            tail_children.extend(children[idx + 1..].iter().cloned());
625
1
        }
626
6
        head.children = head_children.into();
627
6
        tail.children = tail_children.into();
628
6
        (head, tail)
629
7
    }
630
5
    let (mut head, mut tail) = rec(dom, path);
631
5
    head.fixup_children_estimated();
632
5
    tail.fixup_children_estimated();
633
5
    (head, tail)
634
5
}
635

            
636
#[cfg(test)]
637
mod tests {
638
    use super::*;
639
    use crate::managers::changeset::EditResumePoint;
640
    use azul_core::dom::{DomId, DomNodeId};
641
    use azul_core::styled_dom::NodeHierarchyItemId;
642
    use azul_core::task::{Instant, SystemTick};
643

            
644
37
    fn any_node() -> DomNodeId {
645
37
        DomNodeId {
646
37
            dom: DomId { inner: 0 },
647
37
            node: NodeHierarchyItemId::from_crate_internal(None),
648
37
        }
649
37
    }
650

            
651
19
    fn resume(node_index: u32, position: NodePosition) -> EditResumePoint {
652
19
        EditResumePoint {
653
19
            anchor_key: 1,
654
19
            node_path: vec![node_index].into(),
655
19
            position,
656
19
        }
657
19
    }
658

            
659
19
    fn changeset(op: DocumentOperation, r: EditResumePoint) -> DocumentChangeset {
660
19
        DocumentChangeset::new(any_node(), op, r, Instant::Tick(SystemTick::new(0)))
661
19
    }
662

            
663
25
    fn p(text: &str) -> Dom {
664
25
        let mut p = Dom::create_p();
665
25
        p.add_child(Dom::create_text_do_not_use_without_block_level_wrapper(text));
666
25
        p
667
25
    }
668

            
669
18
    fn el(tag: &str) -> Dom {
670
18
        Dom::create_node(azul_core::xml::tag_to_node_type(tag))
671
18
    }
672

            
673
6
    fn li(text: &str) -> Dom {
674
6
        let mut li = el("li");
675
6
        li.add_child(Dom::create_text_do_not_use_without_block_level_wrapper(text));
676
6
        li
677
6
    }
678

            
679
71
    fn collect_text(node: &Dom, out: &mut String) {
680
71
        if let NodeType::Text(t) = node.root.get_node_type() {
681
36
            out.push_str(t.as_str());
682
36
        }
683
71
        for c in node.children.as_ref() {
684
37
            collect_text(c, out);
685
37
        }
686
71
    }
687

            
688
    /// Flattened text of each direct child of the host (assertion helper —
689
    /// the OPERATIONS never flatten anything).
690
17
    fn texts(host: &Dom) -> Vec<String> {
691
17
        host.children
692
17
            .as_ref()
693
17
            .iter()
694
33
            .map(|c| {
695
33
                let mut t = String::new();
696
33
                collect_text(c, &mut t);
697
33
                t
698
33
            })
699
17
            .collect()
700
17
    }
701

            
702
    #[test]
703
1
    fn split_p_mid_text_at_start_and_at_end() {
704
3
        for (byte, first, second) in [
705
1
            (5, "hello", " world"),
706
1
            (0, "", "hello world"),
707
1
            (11, "hello world", ""),
708
        ] {
709
3
            let mut host = Dom::create_div();
710
3
            host.add_child(p("hello world"));
711
3
            let cs = changeset(
712
3
                DocumentOperation::SplitNode(DocOpSplitNode {
713
3
                    node: any_node(),
714
3
                    at: NodePosition::in_text_child(0, byte),
715
3
                }),
716
3
                resume(1, NodePosition::before_child(0)),
717
            );
718
3
            let applied = apply_document_operation(&mut host, &[], &cs).expect("split");
719
3
            assert_eq!(
720
3
                texts(&host),
721
3
                vec![first.to_string(), second.to_string()],
722
                "byte {byte}"
723
            );
724
3
            assert!(matches!(applied.inverse, DocumentOperation::MergeNodes(_)));
725
        }
726
1
    }
727

            
728
    #[test]
729
1
    fn split_preserves_nested_element_subtrees_wholesale() {
730
        // <p>["ab", <b>bold</b>, "cd"]</p> split at the BOUNDARY before <b>:
731
        // the <b> subtree must move to the second half INTACT — nothing is
732
        // flattened, re-parsed, or byte-walked.
733
1
        let mut host = Dom::create_div();
734
1
        let mut para = Dom::create_p();
735
1
        para.add_child(Dom::create_text_do_not_use_without_block_level_wrapper("ab"));
736
1
        let mut b = el("b");
737
1
        b.add_child(Dom::create_text_do_not_use_without_block_level_wrapper("bold"));
738
1
        para.add_child(b);
739
1
        para.add_child(Dom::create_text_do_not_use_without_block_level_wrapper("cd"));
740
1
        host.add_child(para);
741

            
742
1
        let cs = changeset(
743
1
            DocumentOperation::SplitNode(DocOpSplitNode {
744
1
                node: any_node(),
745
1
                at: NodePosition::before_child(1), // between "ab" and <b>
746
1
            }),
747
1
            resume(1, NodePosition::before_child(0)),
748
        );
749
1
        apply_document_operation(&mut host, &[], &cs).expect("split");
750

            
751
1
        assert_eq!(texts(&host), vec!["ab".to_string(), "boldcd".to_string()]);
752
        // The second half's first child is the <b> ELEMENT with its own text
753
        // child — subtree preserved.
754
1
        let second = &host.children.as_ref()[1];
755
1
        let b2 = &second.children.as_ref()[0];
756
1
        assert_eq!(b2.children.as_ref().len(), 1);
757
1
        let mut t = String::new();
758
1
        collect_text(b2, &mut t);
759
1
        assert_eq!(t, "bold");
760
1
    }
761

            
762
    #[test]
763
1
    fn split_ul_between_list_items_is_pure_structure() {
764
        // A <ul> with 3 <li> splits between items 1 and 2 — no text involved,
765
        // both halves keep the SAME node shape (ul → ul, never a tag swap).
766
1
        let mut host = Dom::create_div();
767
1
        let mut ul = el("ul");
768
1
        ul.add_child(li("one"));
769
1
        ul.add_child(li("two"));
770
1
        ul.add_child(li("three"));
771
1
        host.add_child(ul);
772

            
773
1
        let cs = changeset(
774
1
            DocumentOperation::SplitNode(DocOpSplitNode {
775
1
                node: any_node(),
776
1
                at: NodePosition::before_child(1),
777
1
            }),
778
1
            resume(1, NodePosition::before_child(0)),
779
        );
780
1
        apply_document_operation(&mut host, &[], &cs).expect("split ul");
781

            
782
1
        let kids = host.children.as_ref();
783
1
        assert_eq!(kids.len(), 2);
784
1
        assert_eq!(kids[0].children.as_ref().len(), 1, "first ul keeps [one]");
785
1
        assert_eq!(
786
1
            kids[1].children.as_ref().len(),
787
            2,
788
            "second ul takes [two, three]"
789
        );
790
1
        assert_eq!(
791
1
            core::mem::discriminant(kids[0].root.get_node_type()),
792
1
            core::mem::discriminant(kids[1].root.get_node_type()),
793
            "the second node clones the first's shape"
794
        );
795
1
    }
796

            
797
    #[test]
798
1
    fn split_never_cuts_inside_a_multibyte_char() {
799
1
        let mut host = Dom::create_div();
800
1
        host.add_child(p("aä!")); // ä = bytes 1..3
801
1
        let cs = changeset(
802
1
            DocumentOperation::SplitNode(DocOpSplitNode {
803
1
                node: any_node(),
804
1
                at: NodePosition::in_text_child(0, 2), // INSIDE ä
805
1
            }),
806
1
            resume(1, NodePosition::before_child(0)),
807
        );
808
1
        apply_document_operation(&mut host, &[], &cs).expect("split");
809
1
        assert_eq!(texts(&host), vec!["a".to_string(), "ä!".to_string()]);
810
1
    }
811

            
812
    #[test]
813
1
    fn merge_appends_wholesale_and_coalesces_text_only_at_a_text_seam() {
814
1
        let mut host = Dom::create_div();
815
1
        host.add_child(p("hello"));
816
1
        host.add_child(p(" world"));
817
1
        let cs = changeset(
818
1
            DocumentOperation::MergeNodes(DocOpMergeNodes {
819
1
                first: any_node(),
820
1
                second: any_node(),
821
1
                join: NodePosition::in_text_child(0, 5),
822
1
            }),
823
1
            resume(0, NodePosition::in_text_child(0, 5)),
824
        );
825
1
        let applied = apply_document_operation(&mut host, &[], &cs).expect("merge");
826
1
        assert_eq!(texts(&host), vec!["hello world".to_string()]);
827
1
        assert_eq!(
828
1
            host.children.as_ref()[0].children.as_ref().len(),
829
            1,
830
            "text seam coalesced into ONE text child"
831
        );
832
1
        match applied.inverse {
833
1
            DocumentOperation::SplitNode(s) => {
834
1
                assert_eq!(s.at, NodePosition::in_text_child(0, 5));
835
            }
836
            other => panic!("inverse must be the split at the seam, got {other:?}"),
837
        }
838

            
839
        // A pure structural merge (ul + ul) coalesces NOTHING.
840
1
        let mut host = Dom::create_div();
841
1
        let mut ul1 = el("ul");
842
1
        ul1.add_child(li("one"));
843
1
        let mut ul2 = el("ul");
844
1
        ul2.add_child(li("two"));
845
1
        host.add_child(ul1);
846
1
        host.add_child(ul2);
847
1
        let cs = changeset(
848
1
            DocumentOperation::MergeNodes(DocOpMergeNodes {
849
1
                first: any_node(),
850
1
                second: any_node(),
851
1
                join: NodePosition::before_child(1),
852
1
            }),
853
1
            resume(0, NodePosition::before_child(1)),
854
        );
855
1
        apply_document_operation(&mut host, &[], &cs).expect("merge uls");
856
1
        assert_eq!(host.children.as_ref().len(), 1);
857
1
        assert_eq!(host.children.as_ref()[0].children.as_ref().len(), 2);
858
1
    }
859

            
860
    #[test]
861
1
    fn split_then_inverse_merge_is_identity() {
862
1
        let mut original = Dom::create_div();
863
1
        original.add_child(p("hello world"));
864
1
        original.add_child(p("tail"));
865
1
        let mut host = original.clone();
866

            
867
1
        let split_cs = changeset(
868
1
            DocumentOperation::SplitNode(DocOpSplitNode {
869
1
                node: any_node(),
870
1
                at: NodePosition::in_text_child(0, 5),
871
1
            }),
872
1
            resume(1, NodePosition::before_child(0)),
873
        );
874
1
        let applied = apply_document_operation(&mut host, &[], &split_cs).expect("split");
875

            
876
1
        let merge_cs = changeset(
877
1
            applied.inverse,
878
1
            resume(0, NodePosition::in_text_child(0, 5)),
879
        );
880
1
        apply_document_operation(&mut host, &[], &merge_cs).expect("inverse merge");
881

            
882
1
        assert_eq!(
883
1
            texts(&host),
884
1
            texts(&original),
885
            "inverse-of-apply restores the tree"
886
        );
887
1
    }
888

            
889
    #[test]
890
1
    fn insert_remove_replace_close_their_inverse_algebra() {
891
        // insertChild: a fragment of TWO subtrees (a p and a whole ul) lands
892
        // at index 1; the inverse removes exactly that range.
893
1
        let mut host = Dom::create_div();
894
1
        host.add_child(p("one"));
895
1
        host.add_child(p("four"));
896

            
897
1
        let mut ul = el("ul");
898
1
        ul.add_child(li("x"));
899
1
        let cs = changeset(
900
1
            DocumentOperation::InsertChildren(DocOpInsertChildren {
901
1
                parent: any_node(),
902
1
                index: 1,
903
1
                content: fragment(vec![p("two"), ul]),
904
1
            }),
905
1
            resume(1, NodePosition::before_child(1)),
906
        );
907
1
        let inserted = apply_document_operation(&mut host, &[], &cs).expect("insert");
908
1
        assert_eq!(
909
1
            texts(&host),
910
1
            ["one", "two", "x", "four"].map(String::from).to_vec()
911
        );
912
1
        let DocumentOperation::RemoveChildren(ref rm) = inserted.inverse else {
913
            panic!("insert inverse must be a remove");
914
        };
915
1
        assert_eq!((rm.start, rm.end), (1, 3));
916

            
917
        // removeChild: applying the inverse removes both; ITS inverse
918
        // re-inserts the same fragment.
919
1
        let rm_cs = changeset(
920
1
            inserted.inverse.clone(),
921
1
            resume(1, NodePosition::before_child(1)),
922
        );
923
1
        let removed = apply_document_operation(&mut host, &[], &rm_cs).expect("remove");
924
1
        assert_eq!(texts(&host), vec!["one".to_string(), "four".to_string()]);
925
1
        let DocumentOperation::InsertChildren(ref ins) = removed.inverse else {
926
            panic!("remove inverse must be an insert");
927
        };
928
1
        assert_eq!(ins.index, 1);
929
1
        assert_eq!(
930
1
            ins.content.children.as_ref().len(),
931
            2,
932
            "removed fragment captured"
933
        );
934

            
935
        // replaceChild: swap [0..1) for two nodes; inverse restores.
936
1
        let mut host2 = Dom::create_div();
937
1
        host2.add_child(p("old"));
938
1
        let rep_cs = changeset(
939
1
            DocumentOperation::ReplaceChildren(DocOpReplaceChildren {
940
1
                parent: any_node(),
941
1
                start: 0,
942
1
                end: 1,
943
1
                content: fragment(vec![p("new1"), p("new2")]),
944
1
            }),
945
1
            resume(0, NodePosition::before_child(0)),
946
        );
947
1
        let replaced = apply_document_operation(&mut host2, &[], &rep_cs).expect("replace");
948
1
        assert_eq!(texts(&host2), vec!["new1".to_string(), "new2".to_string()]);
949
1
        let inv_cs = changeset(replaced.inverse, resume(0, NodePosition::before_child(0)));
950
1
        apply_document_operation(&mut host2, &[], &inv_cs).expect("inverse replace");
951
1
        assert_eq!(texts(&host2), vec!["old".to_string()]);
952
1
    }
953

            
954
    #[test]
955
1
    fn wrap_mid_text_range_cuts_boundaries_and_unwrap_round_trips() {
956
        // Bold "world" inside <p>"hello world extra"</p>: both boundaries cut
957
        // the SAME text child; the wrapper takes exactly the covered bytes.
958
1
        let mut host = p("hello world extra");
959
1
        let wrapper = el("b");
960
1
        let cs = changeset(
961
1
            DocumentOperation::WrapRange(crate::managers::changeset::DocOpWrapRange {
962
1
                node: any_node(),
963
1
                start: NodePosition::in_text_child(0, 6),
964
1
                end: NodePosition::in_text_child(0, 11),
965
1
                wrapper: wrapper.clone(),
966
1
            }),
967
1
            resume(0, NodePosition::before_child(0)),
968
        );
969
1
        let applied = apply_document_operation(&mut host, &[], &cs).expect("wrap");
970

            
971
        // Children now: ["hello ", <b>"world"</b>, " extra"].
972
1
        let kids = host.children.as_ref();
973
1
        assert_eq!(kids.len(), 3, "{:?}", texts(&host));
974
1
        assert_eq!(texts(&host), ["hello ", "world", " extra"].map(String::from).to_vec());
975
1
        assert!(matches!(
976
1
            kids[1].root.get_node_type(),
977
            NodeType::B | NodeType::Div | NodeType::Strong
978
        ) || !matches!(kids[1].root.get_node_type(), NodeType::Text(_)));
979

            
980
        // Unwrap (the inverse) restores ONE coalesced text child.
981
1
        let DocumentOperation::UnwrapRange(ref uw) = applied.inverse else {
982
            panic!("wrap inverse must be an unwrap");
983
        };
984
1
        assert_eq!(uw.at.child_index, 1);
985
1
        let un_cs = changeset(applied.inverse, resume(0, NodePosition::before_child(0)));
986
1
        let unwrapped = apply_document_operation(&mut host, &[], &un_cs).expect("unwrap");
987
1
        assert_eq!(host.children.as_ref().len(), 1, "seams coalesced back");
988
1
        assert_eq!(texts(&host), vec!["hello world extra".to_string()]);
989

            
990
        // And the unwrap's inverse is the wrap over the same byte range.
991
1
        match unwrapped.inverse {
992
1
            DocumentOperation::WrapRange(w) => {
993
1
                assert_eq!(w.start, NodePosition::in_text_child(0, 6));
994
1
                assert_eq!(w.end, NodePosition::in_text_child(0, 11));
995
            }
996
            other => panic!("unwrap inverse must be the wrap, got {other:?}"),
997
        }
998
1
    }
999

            
    #[test]
1
    fn wrap_whole_elements_is_the_same_op_as_bolding_a_word() {
        // Wrap paragraphs 2..4 of a div in a <blockquote> — pure structure,
        // no text involved, subtrees move wholesale.
1
        let mut host = Dom::create_div();
4
        for t in ["one", "two", "three", "four"] {
4
            host.add_child(p(t));
4
        }
1
        let cs = changeset(
1
            DocumentOperation::WrapRange(crate::managers::changeset::DocOpWrapRange {
1
                node: any_node(),
1
                start: NodePosition::before_child(1),
1
                end: NodePosition::before_child(3),
1
                wrapper: el("blockquote"),
1
            }),
1
            resume(0, NodePosition::before_child(1)),
        );
1
        apply_document_operation(&mut host, &[], &cs).expect("wrap blocks");
1
        let kids = host.children.as_ref();
1
        assert_eq!(kids.len(), 3, "{:?}", texts(&host));
1
        assert_eq!(kids[1].children.as_ref().len(), 2, "blockquote took [two, three]");
1
        assert_eq!(texts(&host), ["one", "twothree", "four"].map(String::from).to_vec());
1
    }
    #[test]
1
    fn failures_leave_the_tree_unchanged() {
1
        let mut original = Dom::create_div();
1
        original.add_child(p("only"));
1
        let mut host = original.clone();
1
        let cs = changeset(
1
            DocumentOperation::MergeNodes(DocOpMergeNodes {
1
                first: any_node(),
1
                second: any_node(),
1
                join: NodePosition::before_child(1),
1
            }),
1
            resume(0, NodePosition::before_child(1)),
        );
1
        assert_eq!(
1
            apply_document_operation(&mut host, &[], &cs).unwrap_err(),
            DocumentEditError::TargetNotFound
        );
1
        assert_eq!(texts(&host), texts(&original));
1
        let cs2 = changeset(
1
            DocumentOperation::RemoveChildren(DocOpRemoveChildren {
1
                parent: any_node(),
1
                start: 0,
1
                end: 1,
1
            }),
1
            resume(0, NodePosition::before_child(0)),
        );
1
        assert_eq!(
1
            apply_document_operation(&mut host, &[7, 7], &cs2).unwrap_err(),
            DocumentEditError::HostNotFound
        );
1
    }
    // ==================================================================
    // split_dom_at_path — the fragmentainer spine cut
    // ==================================================================
    #[test]
1
    fn spine_split_partitions_flat_children() {
1
        let doc = fragment(vec![p("a"), p("b"), p("c")]);
1
        let (head, tail) = split_dom_at_path(&doc, &[1]);
1
        assert_eq!(head.children.as_ref().len(), 1, "a stays");
1
        assert_eq!(tail.children.as_ref().len(), 2, "b + c flow on");
1
        assert_eq!(head.estimated_total_children, head.children.as_ref().len() + 1);
1
    }
    #[test]
1
    fn spine_split_clones_the_box_chain_at_depth() {
        // section > ul > li,li,li — cut before the third li: BOTH results
        // hold a section>ul chain (the CSS fragmentation box-chain clone).
1
        let mut ul = el("ul");
1
        ul.add_child(el("li"));
1
        ul.add_child(el("li"));
1
        ul.add_child(el("li"));
1
        let mut section = el("section");
1
        section.add_child(ul);
1
        let doc = fragment(vec![section]);
1
        let (head, tail) = split_dom_at_path(&doc, &[0, 0, 2]);
1
        let head_ul = &head.children.as_ref()[0].children.as_ref()[0];
1
        let tail_ul = &tail.children.as_ref()[0].children.as_ref()[0];
1
        assert_eq!(head_ul.children.as_ref().len(), 2);
1
        assert_eq!(tail_ul.children.as_ref().len(), 1);
1
        assert_eq!(
1
            head.children.as_ref()[0].root.get_node_type(),
1
            tail.children.as_ref()[0].root.get_node_type(),
            "the section shape duplicates across the cut"
        );
1
    }
    #[test]
1
    fn spine_split_edge_paths() {
1
        let doc = fragment(vec![p("a"), p("b")]);
        // Empty path: everything flows to the tail.
1
        let (h, t) = split_dom_at_path(&doc, &[]);
1
        assert_eq!(h.children.as_ref().len(), 0);
1
        assert_eq!(t.children.as_ref().len(), 2);
        // Past-the-end: everything stays in the head.
1
        let (h, t) = split_dom_at_path(&doc, &[9]);
1
        assert_eq!(h.children.as_ref().len(), 2);
1
        assert_eq!(t.children.as_ref().len(), 0);
1
    }
}