Skip to main content

odox_core/
edit.rs

1//! Changing the text of a paragraph, and what an edit may refuse.
2//!
3//! A paragraph's text is not one string in the tree. It is text nodes, inside
4//! spans and links and marks, with `text:s`, `text:tab` and `text:line-break`
5//! standing for the whitespace ODF will not write literally, and with elements
6//! that contribute no characters at all — a bookmark, a frame, a note, a field
7//! — sitting between them. An editor works on the flat string and this module
8//! maps the edit back: characters are deleted from the nodes that hold them,
9//! new text goes into the node at the insertion point, and every zero-length
10//! element stays where it was. DESIGN.md §11.
11//!
12//! The string the editor sees, [`text`], is built from the tree and not from
13//! the renderer's layout, because the two differ: the renderer draws a tab as
14//! four spaces and a footnote as its citation. Here a tab is a tab, and a
15//! footnote contributes nothing and is kept.
16//
17// Author: David M. Anderson
18// Built with AI assistance (Claude, Anthropic)
19
20mod block;
21mod format;
22
23use std::fmt;
24use std::ops::Range;
25
26use crate::xml::{Attribute, Element, Name, Node, Ns};
27
28pub use block::{block_state, heading_level, set_heading, set_list};
29pub use format::{Mark, format, marked};
30
31/// Why an edit was not made. Each is a state of the document rather than a
32/// failure, and the window says which.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum Refused {
35    /// The cell is under a neighbour's span; the neighbour is the one to edit.
36    Covered,
37    /// The cell holds a formula, which this version does not edit.
38    Formula,
39    /// The document does not declare a namespace the edit would write in.
40    Namespace,
41    /// There is no such sheet, slide, shape or paragraph.
42    NotFound,
43    /// An end of the range is inside a table, a cell or a frame the range
44    /// does not wholly contain, which a join of text does not take apart.
45    Structure,
46}
47
48impl fmt::Display for Refused {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            Self::Covered => write!(f, "the cell is covered by a neighbour's span"),
52            Self::Formula => write!(f, "the cell holds a formula"),
53            Self::Namespace => write!(f, "the document does not declare the namespace needed"),
54            Self::NotFound => write!(f, "nothing is there to edit"),
55            Self::Structure => write!(f, "the range crosses a table or a frame"),
56        }
57    }
58}
59
60impl std::error::Error for Refused {}
61
62/// What one node contributes to a paragraph's text.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64enum Kind {
65    /// A text node, this many characters long.
66    Text(usize),
67    /// `text:s`, this many spaces.
68    Spaces(usize),
69    /// `text:tab`.
70    Tab,
71    /// `text:line-break`.
72    LineBreak,
73    /// An element that contributes nothing and is kept where it is.
74    Marker,
75}
76
77impl Kind {
78    fn len(self) -> usize {
79        match self {
80            Self::Text(n) | Self::Spaces(n) => n,
81            Self::Tab | Self::LineBreak => 1,
82            Self::Marker => 0,
83        }
84    }
85}
86
87/// One node's place in the paragraph's text.
88#[derive(Debug)]
89struct Segment {
90    /// The route from the paragraph to the node.
91    path: Vec<usize>,
92    /// Where its characters begin.
93    start: usize,
94    kind: Kind,
95}
96
97/// Whether an element holds text on the paragraph's behalf: a span, a link, a
98/// ruby or a field whose contents are the paragraph's own characters. Anything
99/// else inside a paragraph is a marker, and stays; so is one of these with
100/// nothing in it, which is what lets a split carry it to one side.
101fn is_inline_container(element: &Element) -> bool {
102    element.name.ns == Ns::Text
103        && !element.children.is_empty()
104        && matches!(
105            &*element.name.local,
106            "span" | "a" | "bibliography-mark" | "ruby" | "ruby-base" | "meta" | "meta-field"
107        )
108}
109
110/// Whether an element is a paragraph or a heading: what an editor edits.
111pub fn is_paragraph(element: &Element) -> bool {
112    element.is(&Ns::Text, "p") || element.is(&Ns::Text, "h")
113}
114
115/// Whether an element inside a paragraph holds characters of the paragraph's
116/// text, as a span or a link does, rather than being a mark or a field that
117/// contributes none of its own.
118pub fn holds_text(element: &Element) -> bool {
119    is_inline_container_name(element)
120}
121
122/// The containers above, whether or not they hold anything: what an edit may
123/// drop once it has emptied one.
124fn is_inline_container_name(element: &Element) -> bool {
125    element.name.ns == Ns::Text
126        && matches!(
127            &*element.name.local,
128            "span" | "a" | "bibliography-mark" | "ruby" | "ruby-base" | "meta" | "meta-field"
129        )
130}
131
132fn segments(paragraph: &Element) -> Vec<Segment> {
133    let mut out = Vec::new();
134    let mut path = Vec::new();
135    let mut at = 0;
136    collect(paragraph, &mut path, &mut at, &mut out);
137    out
138}
139
140fn collect(parent: &Element, path: &mut Vec<usize>, at: &mut usize, out: &mut Vec<Segment>) {
141    for (index, child) in parent.children.iter().enumerate() {
142        let kind = match child {
143            Node::Text(t) | Node::CData(t) => Kind::Text(t.chars().count()),
144            Node::Comment(_) | Node::ProcessingInstruction(_) => continue,
145            Node::Element(e) if e.is(&Ns::Text, "s") => {
146                Kind::Spaces(e.attr_usize(&Ns::Text, "c").unwrap_or(1))
147            }
148            Node::Element(e) if e.is(&Ns::Text, "tab") => Kind::Tab,
149            Node::Element(e) if e.is(&Ns::Text, "line-break") => Kind::LineBreak,
150            Node::Element(e) if is_inline_container(e) => {
151                path.push(index);
152                collect(e, path, at, out);
153                path.pop();
154                continue;
155            }
156            Node::Element(_) => Kind::Marker,
157        };
158        path.push(index);
159        out.push(Segment {
160            path: path.clone(),
161            start: *at,
162            kind,
163        });
164        path.pop();
165        *at += kind.len();
166    }
167}
168
169/// The paragraph's text as an editor sees it: every character the tree
170/// holds, in order, with ODF's whitespace elements as the characters they
171/// stand for.
172pub fn text(paragraph: &Element) -> String {
173    let mut out = String::new();
174    write_text(paragraph, &mut out);
175    out
176}
177
178fn write_text(parent: &Element, out: &mut String) {
179    for child in &parent.children {
180        match child {
181            Node::Text(t) | Node::CData(t) => out.push_str(t),
182            Node::Element(e) if e.is(&Ns::Text, "s") => {
183                for _ in 0..e.attr_usize(&Ns::Text, "c").unwrap_or(1) {
184                    out.push(' ');
185                }
186            }
187            Node::Element(e) if e.is(&Ns::Text, "tab") => out.push('\t'),
188            Node::Element(e) if e.is(&Ns::Text, "line-break") => out.push('\n'),
189            Node::Element(e) if is_inline_container(e) => write_text(e, out),
190            Node::Element(_) | Node::Comment(_) | Node::ProcessingInstruction(_) => {}
191        }
192    }
193}
194
195/// The hyperlinks in a paragraph: the characters each covers, in the same
196/// count as [`text`], and where it points. A link with no `xlink:href` is not
197/// one.
198pub fn links(paragraph: &Element) -> Vec<(Range<usize>, &str)> {
199    let mut found = Vec::new();
200    collect_links(paragraph, &mut 0, &mut found);
201    found
202}
203
204fn collect_links<'a>(
205    parent: &'a Element,
206    at: &mut usize,
207    found: &mut Vec<(Range<usize>, &'a str)>,
208) {
209    for child in &parent.children {
210        match child {
211            Node::Text(t) | Node::CData(t) => *at += t.chars().count(),
212            Node::Element(e) if e.is(&Ns::Text, "s") => {
213                *at += e.attr_usize(&Ns::Text, "c").unwrap_or(1);
214            }
215            Node::Element(e) if e.is(&Ns::Text, "tab") || e.is(&Ns::Text, "line-break") => {
216                *at += 1;
217            }
218            Node::Element(e) if is_inline_container(e) => {
219                let start = *at;
220                collect_links(e, at, found);
221                if e.is(&Ns::Text, "a")
222                    && let Some(href) = e.attr(&Ns::Xlink, "href")
223                {
224                    found.push((start..*at, href));
225                }
226            }
227            Node::Element(_) | Node::Comment(_) | Node::ProcessingInstruction(_) => {}
228        }
229    }
230}
231
232/// Replace a range of the paragraph's text, in characters, with new text.
233///
234/// The characters are removed from the nodes that hold them and the new text
235/// goes into the text node at the start of the range, or into a new one where
236/// none is there. Every element that contributes no characters stays where it
237/// was, so a bookmark inside the deleted range is a bookmark still. Whitespace
238/// is then written the way ODF requires, so the new text may hold tabs,
239/// newlines and runs of spaces.
240///
241/// A range past the end of the text is clamped to it.
242pub fn replace(paragraph: &mut Element, range: Range<usize>, with: &str) {
243    let len = text(paragraph).chars().count();
244    let start = range.start.min(len);
245    let end = range.end.clamp(start, len);
246    let added = with.chars().count();
247    // Where the replaced range begins inside a text node, the new text goes
248    // into that node before the old comes out, so that what replaces a bold
249    // word is bold and a span emptied by the edit is not emptied first.
250    let holder = segments(paragraph).into_iter().find(|s| {
251        matches!(s.kind, Kind::Text(_)) && s.start <= start && start < s.start + s.kind.len()
252    });
253    if start < end
254        && added > 0
255        && let Some(segment) = holder
256    {
257        insert_into(paragraph, &segment.path, start - segment.start, with);
258        cut(paragraph, start + added..end + added, |_| false);
259    } else {
260        cut(paragraph, start..end, |_| false);
261        insert(paragraph, start, with);
262    }
263    normalize(paragraph);
264}
265
266/// Split a paragraph at a character offset into the paragraph before it and
267/// the paragraph after, each with the original's style and attributes.
268///
269/// A zero-length element at the split point stays with the first half. The
270/// second half drops any `xml:id` or `text:id`, which name one element and
271/// cannot name two.
272pub fn split(paragraph: &Element, at: usize) -> (Element, Element) {
273    let len = text(paragraph).chars().count();
274    let at = at.min(len);
275    let mut first = paragraph.clone();
276    cut(&mut first, at..len, |start| start > at);
277    let mut second = paragraph.clone();
278    cut(&mut second, 0..at, |start| start <= at);
279    second.attrs.retain(|a| {
280        !(a.name.is(&Ns::Text, "id")
281            || a.name.local.as_ref() == "id" && a.name.prefix.as_deref() == Some("xml"))
282    });
283    normalize(&mut first);
284    normalize(&mut second);
285    (first, second)
286}
287
288/// Join a paragraph onto the end of another. The first keeps its style; the
289/// second's contents, markers included, follow the first's.
290pub fn join(first: &mut Element, second: Element) {
291    first.children.extend(second.children);
292    first.self_closing = false;
293    normalize(first);
294}
295
296/// What a paragraph becomes when its whole text is replaced by what an editor
297/// hands back: one paragraph, or several where the new text holds newlines
298/// the old one did not.
299///
300/// The old and the new text are compared from both ends, and only the middle
301/// they differ in is replaced, so spans and markers outside it are untouched
302/// and a marker inside it stays where it was. A newline inside that middle is
303/// a paragraph break: the paragraph is split there, each half with the
304/// original's style, and the newline itself is not kept. A newline outside
305/// it is a `text:line-break` the editor was shown and left alone.
306pub fn rewrite(paragraph: &Element, edited: &str) -> Vec<Element> {
307    let before = text(paragraph);
308    let old: Vec<char> = before.chars().collect();
309    let new: Vec<char> = edited.chars().collect();
310    let prefix = old.iter().zip(&new).take_while(|(a, b)| a == b).count();
311    let suffix = old[prefix..]
312        .iter()
313        .rev()
314        .zip(new[prefix..].iter().rev())
315        .take_while(|(a, b)| a == b)
316        .count();
317    let inserted: String = new[prefix..new.len() - suffix].iter().collect();
318
319    let mut whole = paragraph.clone();
320    replace(&mut whole, prefix..old.len() - suffix, &inserted);
321
322    // The breaks, as offsets into the rewritten text, last first so that each
323    // split leaves the earlier offsets where they were.
324    let mut breaks: Vec<usize> = inserted
325        .chars()
326        .enumerate()
327        .filter(|(_, c)| *c == '\n')
328        .map(|(i, _)| prefix + i)
329        .collect();
330    breaks.reverse();
331    let mut after = Vec::new();
332    for at in breaks {
333        let (first, mut second) = split(&whole, at);
334        // The newline itself, which the split left at the head of the second.
335        replace(&mut second, 0..1, "");
336        after.push(second);
337        whole = first;
338    }
339    after.push(whole);
340    after.reverse();
341    after
342}
343
344/// Replace the paragraph at a path under a root with what the editor hands
345/// back, which may be several paragraphs. Answers how many now stand there.
346///
347/// # Errors
348///
349/// The path leads to nothing, or to something that is not a paragraph.
350pub fn apply(root: &mut Element, path: &[usize], edited: &str) -> Result<usize, Refused> {
351    let (last, above) = path.split_last().ok_or(Refused::NotFound)?;
352    let parent = root.at_mut(above).ok_or(Refused::NotFound)?;
353    let Some(Node::Element(paragraph)) = parent.children.get(*last) else {
354        return Err(Refused::NotFound);
355    };
356    if !is_paragraph(paragraph) {
357        return Err(Refused::NotFound);
358    }
359    let paragraphs = rewrite(paragraph, edited);
360    let count = paragraphs.len();
361    parent
362        .children
363        .splice(*last..=*last, paragraphs.into_iter().map(Node::Element));
364    Ok(count)
365}
366
367/// Split the paragraph at a path under a root at a character offset, the
368/// second half becoming the paragraph after it, and answer where the second
369/// half is.
370///
371/// In a list item the second half begins a new item after it, as Enter does
372/// in a list, and whatever followed the paragraph in the item goes with it.
373///
374/// An item holding one empty paragraph is taken out of its list instead: the
375/// paragraph stands where the item was, between the two halves of the list.
376///
377/// # Errors
378///
379/// The path leads to nothing, or to something that is not a paragraph.
380pub fn split_at(root: &mut Element, path: &[usize], at: usize) -> Result<Vec<usize>, Refused> {
381    let (last, above) = path.split_last().ok_or(Refused::NotFound)?;
382    let parent = root.at_mut(above).ok_or(Refused::NotFound)?;
383    let Some(Node::Element(paragraph)) = parent.children.get(*last) else {
384        return Err(Refused::NotFound);
385    };
386    if !is_paragraph(paragraph) {
387        return Err(Refused::NotFound);
388    }
389    if parent.is(&Ns::Text, "list-item")
390        && parent.children.len() == 1
391        && paragraph.children.is_empty()
392    {
393        return take_out_of_list(root, above);
394    }
395    let (first, second) = split(paragraph, at);
396    let parent = root.at_mut(above).ok_or(Refused::NotFound)?;
397    if !parent.is(&Ns::Text, "list-item") {
398        parent
399            .children
400            .splice(*last..=*last, [Node::Element(first), Node::Element(second)]);
401        let mut second_at = above.to_vec();
402        second_at.push(last + 1);
403        return Ok(second_at);
404    }
405
406    // A new item, named the way the document names its items, holding the
407    // second half and what followed it. The item's attributes stay with the
408    // item: a start value or an id belongs to one item and not to two.
409    let mut item = Element {
410        name: parent.name.clone(),
411        attrs: Vec::new(),
412        children: vec![Node::Element(second)],
413        self_closing: false,
414    };
415    item.children.extend(parent.children.drain(last + 1..));
416    parent.children[*last] = Node::Element(first);
417    let (item_at, list_path) = above.split_last().ok_or(Refused::NotFound)?;
418    let enclosing = root.at_mut(list_path).ok_or(Refused::NotFound)?;
419    enclosing.children.insert(item_at + 1, Node::Element(item));
420    let mut second_at = list_path.to_vec();
421    second_at.extend([item_at + 1, 0]);
422    Ok(second_at)
423}
424
425/// Take an item out of its list: the list is split around it, and what the
426/// item held stands between the halves. The half after the item continues the
427/// numbering, and a half left with no items is dropped. Answers the path of
428/// what the item held first.
429///
430/// This is how Enter on an empty item leaves a list, and how a paragraph stops
431/// being one.
432pub(crate) fn take_out_of_list(
433    root: &mut Element,
434    item_path: &[usize],
435) -> Result<Vec<usize>, Refused> {
436    let (item_at, list_path) = item_path.split_last().ok_or(Refused::NotFound)?;
437    let (list_at, container_path) = list_path.split_last().ok_or(Refused::NotFound)?;
438    let continue_numbering = root.name_for(&Ns::Text, "continue-numbering");
439    let list = root.at(list_path).ok_or(Refused::NotFound)?;
440    let Some(Node::Element(item)) = list.children.get(*item_at) else {
441        return Err(Refused::NotFound);
442    };
443    let contents = item.children.clone();
444    let mut before = list.clone();
445    before.children.truncate(*item_at);
446    let mut after = list.clone();
447    after.children.drain(..=*item_at);
448    let before_holds_items = before.elements().next().is_some();
449    let after_holds_items = after.elements().next().is_some();
450    after.attrs.retain(|a| {
451        !(a.name.is(&Ns::Text, "id")
452            || a.name.local.as_ref() == "id" && a.name.prefix.as_deref() == Some("xml"))
453    });
454    after.set_attr(continue_numbering, "true");
455
456    let container = root.at_mut(container_path).ok_or(Refused::NotFound)?;
457    let mut replacement = Vec::new();
458    if before_holds_items {
459        replacement.push(Node::Element(before));
460    }
461    let first_at = *list_at + replacement.len();
462    replacement.extend(contents);
463    if after_holds_items {
464        replacement.push(Node::Element(after));
465    }
466    container.children.splice(*list_at..=*list_at, replacement);
467    let mut at = container_path.to_vec();
468    at.push(first_at);
469    Ok(at)
470}
471
472/// Replace the text from one position to another with new text, and join
473/// what is left of the last paragraph onto the first. A position is a
474/// paragraph's path under the root and a character offset into its text.
475///
476/// Everything between the two goes, as a selection over it takes it:
477/// paragraphs, tables and frames the range wholly contains, and list items
478/// and lists left with nothing in them. The first paragraph keeps its style
479/// and its place, and the last one's remaining text follows the new text in
480/// it.
481///
482/// # Errors
483///
484/// Either path does not lead to a paragraph, or the first is not before the
485/// last; or an end is inside a table, a cell or a frame that the range does
486/// not wholly contain ([`Refused::Structure`]), which no join takes apart.
487/// Each is refused before anything changes.
488pub fn replace_range(
489    root: &mut Element,
490    from: (&[usize], usize),
491    to: (&[usize], usize),
492    with: &str,
493) -> Result<(), Refused> {
494    let ((from, start), (to, end)) = (from, to);
495    if !root.at(from).is_some_and(is_paragraph) || !root.at(to).is_some_and(is_paragraph) {
496        return Err(Refused::NotFound);
497    }
498    if from == to {
499        let paragraph = root.at_mut(from).ok_or(Refused::NotFound)?;
500        replace(paragraph, start..end, with);
501        return Ok(());
502    }
503    // The deepest element holding both ends, and which of its children each
504    // end is in.
505    let common = from.iter().zip(to).take_while(|(a, b)| a == b).count();
506    if common >= from.len() || common >= to.len() || from[common] >= to[common] {
507        return Err(Refused::NotFound);
508    }
509    if end_inside_structure(root, common, from, to) {
510        return Err(Refused::Structure);
511    }
512    let (first_child, last_child) = (from[common], to[common]);
513
514    // The last paragraph first: it is after everything else touched, so
515    // taking it apart moves no path still to be used.
516    let last = root.at_mut(to).ok_or(Refused::NotFound)?;
517    replace(last, 0..end, "");
518    let tail = std::mem::take(&mut last.children);
519
520    let holder = root.at_mut(&from[..common]).ok_or(Refused::NotFound)?;
521    let emptied = match holder.children.get_mut(last_child) {
522        Some(Node::Element(child)) if common + 1 < to.len() => {
523            strip_before(child, &to[common + 1..])
524        }
525        _ => true,
526    };
527    if emptied {
528        holder.children.remove(last_child);
529    }
530    holder.children.drain(first_child + 1..last_child);
531    if let Some(Node::Element(child)) = holder.children.get_mut(first_child) {
532        strip_after(child, &from[common + 1..]);
533    }
534
535    let first = root.at_mut(from).ok_or(Refused::NotFound)?;
536    let len = text(first).chars().count();
537    replace(first, start..len, with);
538    let mut rest = Element::new("text", "p", Ns::Text);
539    rest.children = tail;
540    join(first, rest);
541    Ok(())
542}
543
544/// Whether either end of a range, whose two paths agree for their first
545/// `common` steps, sits inside a table, a cell or a frame that the range
546/// does not wholly contain. One wholly between the ends goes with the rest.
547fn end_inside_structure(root: &Element, common: usize, from: &[usize], to: &[usize]) -> bool {
548    let inside = |path: &[usize]| {
549        (common + 1..path.len()).any(|depth| root.at(&path[..depth]).is_some_and(is_structure))
550    };
551    inside(from) || inside(to)
552}
553
554fn is_structure(element: &Element) -> bool {
555    element.is(&Ns::Table, "table")
556        || element.is(&Ns::Table, "table-row")
557        || element.is(&Ns::Table, "table-cell")
558        || element.is(&Ns::Table, "covered-table-cell")
559        || element.is(&Ns::Draw, "frame")
560        || element.is(&Ns::Draw, "text-box")
561}
562
563/// Remove, under an element, everything before the end of a path, the end
564/// itself, and every element that leaves empty. Answers whether the element
565/// is left with no elements in it.
566fn strip_before(element: &mut Element, path: &[usize]) -> bool {
567    let Some((&at, rest)) = path.split_first() else {
568        return true;
569    };
570    let emptied = match element.children.get_mut(at) {
571        Some(Node::Element(child)) if !rest.is_empty() => strip_before(child, rest),
572        _ => true,
573    };
574    let through = if emptied { at + 1 } else { at };
575    element
576        .children
577        .drain(..through.min(element.children.len()));
578    !element
579        .children
580        .iter()
581        .any(|n| matches!(n, Node::Element(_)))
582}
583
584/// Remove, under an element, everything after the end of a path, at every
585/// level down to it.
586fn strip_after(element: &mut Element, path: &[usize]) {
587    let Some((&at, rest)) = path.split_first() else {
588        return;
589    };
590    if let Some(Node::Element(child)) = element.children.get_mut(at) {
591        strip_after(child, rest);
592    }
593    element.children.truncate(at + 1);
594}
595
596/// Join the paragraph at a path onto the paragraph before it among its
597/// parent's children, and answer where the joined paragraph is.
598///
599/// # Errors
600///
601/// The path leads to nothing, or the element before it is not a paragraph:
602/// there is none, or a table or a list stands between, which a join would
603/// otherwise delete.
604pub fn join_with_previous(root: &mut Element, path: &[usize]) -> Result<Vec<usize>, Refused> {
605    let (last, above) = path.split_last().ok_or(Refused::NotFound)?;
606    let parent = root.at_mut(above).ok_or(Refused::NotFound)?;
607    let paragraph_node = |node: &Node| matches!(node, Node::Element(e) if is_paragraph(e));
608    if !parent.children.get(*last).is_some_and(paragraph_node) {
609        return Err(Refused::NotFound);
610    }
611    let previous = parent.children[..*last]
612        .iter()
613        .rposition(|node| matches!(node, Node::Element(_)))
614        .filter(|&index| paragraph_node(&parent.children[index]))
615        .ok_or(Refused::NotFound)?;
616    // Whitespace between the two, which was between them and is now inside
617    // neither, goes too.
618    let Node::Element(second) = parent.children.remove(*last) else {
619        return Err(Refused::NotFound);
620    };
621    parent.children.drain(previous + 1..*last);
622    let Some(Node::Element(first)) = parent.children.get_mut(previous) else {
623        return Err(Refused::NotFound);
624    };
625    join(first, second);
626    let mut at = above.to_vec();
627    at.push(previous);
628    Ok(at)
629}
630
631/// Delete a range of characters, and any zero-length element whose position
632/// the predicate names.
633///
634/// Segments are visited last to first, so removing a node never moves one
635/// that is still to be visited.
636fn cut(paragraph: &mut Element, range: Range<usize>, drop_marker: impl Fn(usize) -> bool) {
637    for segment in segments(paragraph).into_iter().rev() {
638        let end = segment.start + segment.kind.len();
639        let overlap_start = range.start.max(segment.start);
640        let overlap_end = range.end.min(end);
641        let overlaps = overlap_start < overlap_end;
642        let remove = match segment.kind {
643            Kind::Marker => drop_marker(segment.start),
644            Kind::Tab | Kind::LineBreak => overlaps,
645            Kind::Spaces(n) => {
646                if !overlaps {
647                    continue;
648                }
649                let left = n - (overlap_end - overlap_start);
650                if left > 0
651                    && let Some(e) = paragraph.at_mut(&segment.path)
652                {
653                    set_count(e, left);
654                }
655                left == 0
656            }
657            Kind::Text(_) => {
658                if !overlaps {
659                    continue;
660                }
661                let Some((parent, index)) = parent_of(paragraph, &segment.path) else {
662                    continue;
663                };
664                let Some(Node::Text(t) | Node::CData(t)) = parent.children.get_mut(index) else {
665                    continue;
666                };
667                let kept: String = t
668                    .chars()
669                    .enumerate()
670                    .filter(|(i, _)| {
671                        let at = segment.start + i;
672                        !(overlap_start..overlap_end).contains(&at)
673                    })
674                    .map(|(_, c)| c)
675                    .collect();
676                *t = kept;
677                t.is_empty()
678            }
679        };
680        if remove && let Some((parent, index)) = parent_of(paragraph, &segment.path) {
681            parent.children.remove(index);
682            prune_emptied(paragraph, &segment.path[..segment.path.len() - 1]);
683        }
684    }
685}
686
687/// A span or a link left with nothing in it says nothing, and goes; and so
688/// does the one it was in, if that is now empty too. One that was empty
689/// before the edit is not touched, because it is not this edit's: it never
690/// had a child removed.
691///
692/// Done as soon as the container empties, while the segments still to be
693/// visited are all before it and its own index is still right.
694fn prune_emptied(paragraph: &mut Element, container: &[usize]) {
695    let mut path = container.to_vec();
696    while !path.is_empty() {
697        let Some((parent, index)) = parent_of(paragraph, &path) else {
698            return;
699        };
700        match parent.children.get(index) {
701            Some(Node::Element(e)) if is_inline_container_name(e) && e.children.is_empty() => {
702                parent.children.remove(index);
703                path.pop();
704            }
705            _ => return,
706        }
707    }
708}
709
710/// Put text at a character offset.
711///
712/// Into the text node whose characters surround the offset; failing that, the
713/// one that ends there, then the one that begins there; failing that, as a new
714/// text node after whatever ends there, which puts new text after a bookmark
715/// at the same offset rather than before it.
716fn insert(paragraph: &mut Element, at: usize, with: &str) {
717    if with.is_empty() {
718        return;
719    }
720    let segments = segments(paragraph);
721    let text_at = |predicate: &dyn Fn(&Segment, usize) -> bool| {
722        segments
723            .iter()
724            .find(|s| matches!(s.kind, Kind::Text(_)) && predicate(s, s.start + s.kind.len()))
725    };
726    let target = text_at(&|s, end| s.start < at && at < end)
727        .or_else(|| text_at(&|_, end| end == at))
728        .or_else(|| text_at(&|s, _| s.start == at));
729    if let Some(segment) = target {
730        insert_into(paragraph, &segment.path, at - segment.start, with);
731        return;
732    }
733    // No text node touches the offset: a new one goes after the last segment
734    // ending at it, before the first beginning at it, or at the paragraph's
735    // end.
736    let after = segments
737        .iter()
738        .rev()
739        .find(|s| s.start + s.kind.len() == at)
740        .map(|s| s.path.clone());
741    let before = segments
742        .iter()
743        .find(|s| s.start >= at)
744        .map(|s| s.path.clone());
745    let node = Node::Text(with.to_owned());
746    if let Some(path) = after
747        && let Some((parent, index)) = parent_of(paragraph, &path)
748    {
749        parent.children.insert(index + 1, node);
750    } else if let Some(path) = before
751        && let Some((parent, index)) = parent_of(paragraph, &path)
752    {
753        parent.children.insert(index, node);
754    } else {
755        paragraph.children.push(node);
756        paragraph.self_closing = false;
757    }
758}
759
760/// Put text into the text node at a path, at a character offset within it.
761fn insert_into(paragraph: &mut Element, path: &[usize], offset: usize, with: &str) {
762    if let Some((parent, index)) = parent_of(paragraph, path)
763        && let Some(Node::Text(t) | Node::CData(t)) = parent.children.get_mut(index)
764    {
765        let byte = t.char_indices().nth(offset).map_or(t.len(), |(b, _)| b);
766        t.insert_str(byte, with);
767    }
768}
769
770/// The parent of the node a path leads to, and the node's index in it.
771fn parent_of<'a>(paragraph: &'a mut Element, path: &[usize]) -> Option<(&'a mut Element, usize)> {
772    let (last, above) = path.split_last()?;
773    Some((paragraph.at_mut(above)?, *last))
774}
775
776fn set_count(space: &mut Element, count: usize) {
777    if count <= 1 {
778        space.remove_attr(&Ns::Text, "c");
779    } else {
780        let name = space
781            .attrs
782            .iter()
783            .find(|a| a.name.is(&Ns::Text, "c"))
784            .map_or_else(
785                || {
786                    Name::new(
787                        space.name.prefix.as_deref().unwrap_or("text"),
788                        "c",
789                        Ns::Text,
790                    )
791                },
792                |a| a.name.clone(),
793            );
794        space.set_attr(name, count.to_string());
795    }
796}
797
798/// Write whitespace the way ODF requires.
799///
800/// A conforming reader collapses a run of spaces to one and drops the spaces
801/// at the start of a paragraph, and reads a literal tab or newline as a
802/// space. So a tab becomes `text:tab`, a newline `text:line-break`, a run of
803/// spaces one literal space and a `text:s` for the rest, and a run at the very
804/// start a `text:s` for all of them. Text nodes are split where an element has
805/// to go; nothing else in the tree is touched.
806fn normalize(paragraph: &mut Element) {
807    let prefix = paragraph
808        .name
809        .prefix
810        .as_deref()
811        .unwrap_or("text")
812        .to_owned();
813    let mut previous_was_space = true;
814    normalize_in(paragraph, &prefix, &mut previous_was_space);
815}
816
817fn normalize_in(parent: &mut Element, prefix: &str, previous_was_space: &mut bool) {
818    merge_text(parent);
819    let mut index = 0;
820    while index < parent.children.len() {
821        match &mut parent.children[index] {
822            Node::Text(t) | Node::CData(t) => {
823                let replacement = encode(t, prefix, previous_was_space);
824                match replacement {
825                    None => index += 1,
826                    Some(nodes) => {
827                        let count = nodes.len();
828                        parent.children.splice(index..=index, nodes);
829                        index += count;
830                    }
831                }
832            }
833            Node::Element(e) if is_inline_container(e) => {
834                normalize_in(e, prefix, previous_was_space);
835                index += 1;
836            }
837            Node::Element(e) => {
838                // Whitespace elements are whitespace; a marker is nothing,
839                // and what came before it still stands.
840                if e.is(&Ns::Text, "s") || e.is(&Ns::Text, "tab") || e.is(&Ns::Text, "line-break") {
841                    *previous_was_space = true;
842                }
843                index += 1;
844            }
845            Node::Comment(_) | Node::ProcessingInstruction(_) => index += 1,
846        }
847    }
848}
849
850/// Join text nodes that sit side by side into one.
851///
852/// The parser keeps an entity reference as a text node of its own, so a
853/// paragraph reads `a`, `&lt;`, `b` as three nodes; delete the middle one and
854/// the two left would serialize as one and parse back as one, which is a tree
855/// that is not equal to itself across a round trip. Joined here, it is.
856fn merge_text(parent: &mut Element) {
857    let mut index = 1;
858    while index < parent.children.len() {
859        let joinable = matches!(
860            (&parent.children[index - 1], &parent.children[index]),
861            (Node::Text(_), Node::Text(_))
862        );
863        if joinable {
864            let Node::Text(tail) = parent.children.remove(index) else {
865                unreachable!("matched a text node");
866            };
867            if let Node::Text(head) = &mut parent.children[index - 1] {
868                head.push_str(&tail);
869            }
870        } else {
871            index += 1;
872        }
873    }
874}
875
876/// The nodes a text node becomes, or `None` where it is already as ODF
877/// writes it.
878fn encode(text: &str, prefix: &str, previous_was_space: &mut bool) -> Option<Vec<Node>> {
879    let needs_work = text.contains(['\t', '\n', '\r'])
880        || text.contains("  ")
881        || (*previous_was_space && text.starts_with(' '));
882    if !needs_work {
883        if let Some(last) = text.chars().last() {
884            *previous_was_space = last == ' ';
885        }
886        return None;
887    }
888    let mut nodes = Vec::new();
889    let mut run = String::new();
890    let mut spaces = 0usize;
891    let flush_spaces = |nodes: &mut Vec<Node>,
892                        run: &mut String,
893                        spaces: &mut usize,
894                        previous_was_space: &mut bool| {
895        if *spaces == 0 {
896            return;
897        }
898        // One literal space where a space may be literal, and `text:s` for
899        // whatever a reader would otherwise collapse.
900        let mut counted = *spaces;
901        if !*previous_was_space {
902            run.push(' ');
903            counted -= 1;
904        }
905        if counted > 0 {
906            if !run.is_empty() {
907                nodes.push(Node::Text(std::mem::take(run)));
908            }
909            let mut s = Element::new(prefix, "s", Ns::Text);
910            if counted > 1 {
911                s.attrs.push(Attribute {
912                    name: Name::new(prefix, "c", Ns::Text),
913                    value: counted.to_string(),
914                });
915            }
916            nodes.push(Node::Element(s));
917        }
918        *spaces = 0;
919        *previous_was_space = true;
920    };
921    for c in text.chars() {
922        match c {
923            ' ' => spaces += 1,
924            // A carriage return is not a character ODF has a spelling for; a
925            // newline follows it wherever it was typed.
926            '\r' => {}
927            '\t' | '\n' => {
928                flush_spaces(&mut nodes, &mut run, &mut spaces, previous_was_space);
929                if !run.is_empty() {
930                    nodes.push(Node::Text(std::mem::take(&mut run)));
931                }
932                let local = if c == '\t' { "tab" } else { "line-break" };
933                nodes.push(Node::Element(Element::new(prefix, local, Ns::Text)));
934                *previous_was_space = true;
935            }
936            other => {
937                flush_spaces(&mut nodes, &mut run, &mut spaces, previous_was_space);
938                run.push(other);
939                *previous_was_space = false;
940            }
941        }
942    }
943    flush_spaces(&mut nodes, &mut run, &mut spaces, previous_was_space);
944    if !run.is_empty() {
945        nodes.push(Node::Text(run));
946    }
947    Some(nodes)
948}