1mod 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#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum Refused {
35 Covered,
37 Formula,
39 Namespace,
41 NotFound,
43 Structure,
46 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67enum Kind {
68 Text(usize),
70 Spaces(usize),
72 Tab,
74 LineBreak,
76 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#[derive(Debug)]
92struct Segment {
93 path: Vec<usize>,
95 start: usize,
97 kind: Kind,
98}
99
100fn 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
113pub fn is_paragraph(element: &Element) -> bool {
115 element.is(&Ns::Text, "p") || element.is(&Ns::Text, "h")
116}
117
118pub fn holds_text(element: &Element) -> bool {
122 is_inline_container_name(element)
123}
124
125fn 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
172pub 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
198pub 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
217fn 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
236pub 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
247fn 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
263fn 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
291pub 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, ¤t, paragraph.clone())?;
334 }
335 let tail_at = slot_after(root, ¤t).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
344pub 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
381pub 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 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
415pub 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
437pub fn join(first: &mut Element, second: Element) {
440 first.children.extend(second.children);
441 first.self_closing = false;
442 normalize(first);
443}
444
445pub 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 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 replace(&mut second, 0..1, "");
485 after.push(second);
486 whole = first;
487 }
488 after.push(whole);
489 after.reverse();
490 after
491}
492
493pub 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
516pub 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
547pub 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 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
596pub(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
643pub 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 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 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
715fn 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
734fn 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
755fn 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
767pub 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 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
802fn 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
858fn 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
881fn 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 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
931fn 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
941fn 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
969fn 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 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
1021fn 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
1047fn 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 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 '\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}