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