1mod 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#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum Refused {
39 Covered,
41 Formula,
43 Namespace,
45 NotFound,
47 Structure,
50 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71enum Kind {
72 Text(usize),
74 Spaces(usize),
76 Tab,
78 LineBreak,
80 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#[derive(Debug)]
96struct Segment {
97 path: Vec<usize>,
99 start: usize,
101 kind: Kind,
102}
103
104fn 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
117pub fn is_paragraph(element: &Element) -> bool {
119 element.is(&Ns::Text, "p") || element.is(&Ns::Text, "h")
120}
121
122pub fn holds_text(element: &Element) -> bool {
126 is_inline_container_name(element)
127}
128
129pub 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 None => String::new(),
139 _ => number.to_string(),
140 }
141}
142
143fn 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 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
188pub 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
208pub 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
259fn 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
306pub 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
332pub 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
351fn 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
370pub 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
381fn 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
397fn 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
425pub 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, ¤t, paragraph.clone())?;
468 }
469 let tail_at = slot_after(root, ¤t).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
478pub 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
515pub 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 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
549pub 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
571pub fn join(first: &mut Element, second: Element) {
574 first.children.extend(second.children);
575 first.self_closing = false;
576 normalize(first);
577}
578
579pub 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 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 replace(&mut second, 0..1, "");
619 after.push(second);
620 whole = first;
621 }
622 after.push(whole);
623 after.reverse();
624 after
625}
626
627pub 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
650pub 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
681pub 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 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
730pub(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
777pub 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 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 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
849fn 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
868fn 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
889fn 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
901pub 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 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
936fn 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
992fn 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
1015fn 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 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
1065fn 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
1075fn 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
1103fn 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 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
1155fn 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
1181fn 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 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 '\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}