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