mod format;
use std::fmt;
use std::ops::Range;
use crate::xml::{Attribute, Element, Name, Node, Ns};
pub use format::{Mark, format, marked};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Refused {
Covered,
Formula,
Namespace,
NotFound,
Structure,
}
impl fmt::Display for Refused {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Covered => write!(f, "the cell is covered by a neighbour's span"),
Self::Formula => write!(f, "the cell holds a formula"),
Self::Namespace => write!(f, "the document does not declare the namespace needed"),
Self::NotFound => write!(f, "nothing is there to edit"),
Self::Structure => write!(f, "the range crosses a table or a frame"),
}
}
}
impl std::error::Error for Refused {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Kind {
Text(usize),
Spaces(usize),
Tab,
LineBreak,
Marker,
}
impl Kind {
fn len(self) -> usize {
match self {
Self::Text(n) | Self::Spaces(n) => n,
Self::Tab | Self::LineBreak => 1,
Self::Marker => 0,
}
}
}
#[derive(Debug)]
struct Segment {
path: Vec<usize>,
start: usize,
kind: Kind,
}
fn is_inline_container(element: &Element) -> bool {
element.name.ns == Ns::Text
&& !element.children.is_empty()
&& matches!(
&*element.name.local,
"span" | "a" | "bibliography-mark" | "ruby" | "ruby-base" | "meta" | "meta-field"
)
}
pub fn is_paragraph(element: &Element) -> bool {
element.is(&Ns::Text, "p") || element.is(&Ns::Text, "h")
}
pub fn holds_text(element: &Element) -> bool {
is_inline_container_name(element)
}
fn is_inline_container_name(element: &Element) -> bool {
element.name.ns == Ns::Text
&& matches!(
&*element.name.local,
"span" | "a" | "bibliography-mark" | "ruby" | "ruby-base" | "meta" | "meta-field"
)
}
fn segments(paragraph: &Element) -> Vec<Segment> {
let mut out = Vec::new();
let mut path = Vec::new();
let mut at = 0;
collect(paragraph, &mut path, &mut at, &mut out);
out
}
fn collect(parent: &Element, path: &mut Vec<usize>, at: &mut usize, out: &mut Vec<Segment>) {
for (index, child) in parent.children.iter().enumerate() {
let kind = match child {
Node::Text(t) | Node::CData(t) => Kind::Text(t.chars().count()),
Node::Comment(_) | Node::ProcessingInstruction(_) => continue,
Node::Element(e) if e.is(&Ns::Text, "s") => {
Kind::Spaces(e.attr_usize(&Ns::Text, "c").unwrap_or(1))
}
Node::Element(e) if e.is(&Ns::Text, "tab") => Kind::Tab,
Node::Element(e) if e.is(&Ns::Text, "line-break") => Kind::LineBreak,
Node::Element(e) if is_inline_container(e) => {
path.push(index);
collect(e, path, at, out);
path.pop();
continue;
}
Node::Element(_) => Kind::Marker,
};
path.push(index);
out.push(Segment {
path: path.clone(),
start: *at,
kind,
});
path.pop();
*at += kind.len();
}
}
pub fn text(paragraph: &Element) -> String {
let mut out = String::new();
write_text(paragraph, &mut out);
out
}
fn write_text(parent: &Element, out: &mut String) {
for child in &parent.children {
match child {
Node::Text(t) | Node::CData(t) => out.push_str(t),
Node::Element(e) if e.is(&Ns::Text, "s") => {
for _ in 0..e.attr_usize(&Ns::Text, "c").unwrap_or(1) {
out.push(' ');
}
}
Node::Element(e) if e.is(&Ns::Text, "tab") => out.push('\t'),
Node::Element(e) if e.is(&Ns::Text, "line-break") => out.push('\n'),
Node::Element(e) if is_inline_container(e) => write_text(e, out),
Node::Element(_) | Node::Comment(_) | Node::ProcessingInstruction(_) => {}
}
}
}
pub fn replace(paragraph: &mut Element, range: Range<usize>, with: &str) {
let len = text(paragraph).chars().count();
let start = range.start.min(len);
let end = range.end.clamp(start, len);
let added = with.chars().count();
let holder = segments(paragraph).into_iter().find(|s| {
matches!(s.kind, Kind::Text(_)) && s.start <= start && start < s.start + s.kind.len()
});
if start < end
&& added > 0
&& let Some(segment) = holder
{
insert_into(paragraph, &segment.path, start - segment.start, with);
cut(paragraph, start + added..end + added, |_| false);
} else {
cut(paragraph, start..end, |_| false);
insert(paragraph, start, with);
}
normalize(paragraph);
}
pub fn split(paragraph: &Element, at: usize) -> (Element, Element) {
let len = text(paragraph).chars().count();
let at = at.min(len);
let mut first = paragraph.clone();
cut(&mut first, at..len, |start| start > at);
let mut second = paragraph.clone();
cut(&mut second, 0..at, |start| start <= at);
second.attrs.retain(|a| {
!(a.name.is(&Ns::Text, "id")
|| a.name.local.as_ref() == "id" && a.name.prefix.as_deref() == Some("xml"))
});
normalize(&mut first);
normalize(&mut second);
(first, second)
}
pub fn join(first: &mut Element, second: Element) {
first.children.extend(second.children);
first.self_closing = false;
normalize(first);
}
pub fn rewrite(paragraph: &Element, edited: &str) -> Vec<Element> {
let before = text(paragraph);
let old: Vec<char> = before.chars().collect();
let new: Vec<char> = edited.chars().collect();
let prefix = old.iter().zip(&new).take_while(|(a, b)| a == b).count();
let suffix = old[prefix..]
.iter()
.rev()
.zip(new[prefix..].iter().rev())
.take_while(|(a, b)| a == b)
.count();
let inserted: String = new[prefix..new.len() - suffix].iter().collect();
let mut whole = paragraph.clone();
replace(&mut whole, prefix..old.len() - suffix, &inserted);
let mut breaks: Vec<usize> = inserted
.chars()
.enumerate()
.filter(|(_, c)| *c == '\n')
.map(|(i, _)| prefix + i)
.collect();
breaks.reverse();
let mut after = Vec::new();
for at in breaks {
let (first, mut second) = split(&whole, at);
replace(&mut second, 0..1, "");
after.push(second);
whole = first;
}
after.push(whole);
after.reverse();
after
}
pub fn apply(root: &mut Element, path: &[usize], edited: &str) -> Result<usize, Refused> {
let (last, above) = path.split_last().ok_or(Refused::NotFound)?;
let parent = root.at_mut(above).ok_or(Refused::NotFound)?;
let Some(Node::Element(paragraph)) = parent.children.get(*last) else {
return Err(Refused::NotFound);
};
if !is_paragraph(paragraph) {
return Err(Refused::NotFound);
}
let paragraphs = rewrite(paragraph, edited);
let count = paragraphs.len();
parent
.children
.splice(*last..=*last, paragraphs.into_iter().map(Node::Element));
Ok(count)
}
pub fn split_at(root: &mut Element, path: &[usize], at: usize) -> Result<Vec<usize>, Refused> {
let (last, above) = path.split_last().ok_or(Refused::NotFound)?;
let parent = root.at_mut(above).ok_or(Refused::NotFound)?;
let Some(Node::Element(paragraph)) = parent.children.get(*last) else {
return Err(Refused::NotFound);
};
if !is_paragraph(paragraph) {
return Err(Refused::NotFound);
}
let (first, second) = split(paragraph, at);
if !parent.is(&Ns::Text, "list-item") {
parent
.children
.splice(*last..=*last, [Node::Element(first), Node::Element(second)]);
let mut second_at = above.to_vec();
second_at.push(last + 1);
return Ok(second_at);
}
let mut item = Element {
name: parent.name.clone(),
attrs: Vec::new(),
children: vec![Node::Element(second)],
self_closing: false,
};
item.children.extend(parent.children.drain(last + 1..));
parent.children[*last] = Node::Element(first);
let (item_at, list_path) = above.split_last().ok_or(Refused::NotFound)?;
let enclosing = root.at_mut(list_path).ok_or(Refused::NotFound)?;
enclosing.children.insert(item_at + 1, Node::Element(item));
let mut second_at = list_path.to_vec();
second_at.extend([item_at + 1, 0]);
Ok(second_at)
}
pub fn replace_range(
root: &mut Element,
from: (&[usize], usize),
to: (&[usize], usize),
with: &str,
) -> Result<(), Refused> {
let ((from, start), (to, end)) = (from, to);
if !root.at(from).is_some_and(is_paragraph) || !root.at(to).is_some_and(is_paragraph) {
return Err(Refused::NotFound);
}
if from == to {
let paragraph = root.at_mut(from).ok_or(Refused::NotFound)?;
replace(paragraph, start..end, with);
return Ok(());
}
let common = from.iter().zip(to).take_while(|(a, b)| a == b).count();
if common >= from.len() || common >= to.len() || from[common] >= to[common] {
return Err(Refused::NotFound);
}
if end_inside_structure(root, common, from, to) {
return Err(Refused::Structure);
}
let (first_child, last_child) = (from[common], to[common]);
let last = root.at_mut(to).ok_or(Refused::NotFound)?;
replace(last, 0..end, "");
let tail = std::mem::take(&mut last.children);
let holder = root.at_mut(&from[..common]).ok_or(Refused::NotFound)?;
let emptied = match holder.children.get_mut(last_child) {
Some(Node::Element(child)) if common + 1 < to.len() => {
strip_before(child, &to[common + 1..])
}
_ => true,
};
if emptied {
holder.children.remove(last_child);
}
holder.children.drain(first_child + 1..last_child);
if let Some(Node::Element(child)) = holder.children.get_mut(first_child) {
strip_after(child, &from[common + 1..]);
}
let first = root.at_mut(from).ok_or(Refused::NotFound)?;
let len = text(first).chars().count();
replace(first, start..len, with);
let mut rest = Element::new("text", "p", Ns::Text);
rest.children = tail;
join(first, rest);
Ok(())
}
fn end_inside_structure(root: &Element, common: usize, from: &[usize], to: &[usize]) -> bool {
let inside = |path: &[usize]| {
(common + 1..path.len()).any(|depth| root.at(&path[..depth]).is_some_and(is_structure))
};
inside(from) || inside(to)
}
fn is_structure(element: &Element) -> bool {
element.is(&Ns::Table, "table")
|| element.is(&Ns::Table, "table-row")
|| element.is(&Ns::Table, "table-cell")
|| element.is(&Ns::Table, "covered-table-cell")
|| element.is(&Ns::Draw, "frame")
|| element.is(&Ns::Draw, "text-box")
}
fn strip_before(element: &mut Element, path: &[usize]) -> bool {
let Some((&at, rest)) = path.split_first() else {
return true;
};
let emptied = match element.children.get_mut(at) {
Some(Node::Element(child)) if !rest.is_empty() => strip_before(child, rest),
_ => true,
};
let through = if emptied { at + 1 } else { at };
element
.children
.drain(..through.min(element.children.len()));
!element
.children
.iter()
.any(|n| matches!(n, Node::Element(_)))
}
fn strip_after(element: &mut Element, path: &[usize]) {
let Some((&at, rest)) = path.split_first() else {
return;
};
if let Some(Node::Element(child)) = element.children.get_mut(at) {
strip_after(child, rest);
}
element.children.truncate(at + 1);
}
pub fn join_with_previous(root: &mut Element, path: &[usize]) -> Result<Vec<usize>, Refused> {
let (last, above) = path.split_last().ok_or(Refused::NotFound)?;
let parent = root.at_mut(above).ok_or(Refused::NotFound)?;
let paragraph_node = |node: &Node| matches!(node, Node::Element(e) if is_paragraph(e));
if !parent.children.get(*last).is_some_and(paragraph_node) {
return Err(Refused::NotFound);
}
let previous = parent.children[..*last]
.iter()
.rposition(|node| matches!(node, Node::Element(_)))
.filter(|&index| paragraph_node(&parent.children[index]))
.ok_or(Refused::NotFound)?;
let Node::Element(second) = parent.children.remove(*last) else {
return Err(Refused::NotFound);
};
parent.children.drain(previous + 1..*last);
let Some(Node::Element(first)) = parent.children.get_mut(previous) else {
return Err(Refused::NotFound);
};
join(first, second);
let mut at = above.to_vec();
at.push(previous);
Ok(at)
}
fn cut(paragraph: &mut Element, range: Range<usize>, drop_marker: impl Fn(usize) -> bool) {
for segment in segments(paragraph).into_iter().rev() {
let end = segment.start + segment.kind.len();
let overlap_start = range.start.max(segment.start);
let overlap_end = range.end.min(end);
let overlaps = overlap_start < overlap_end;
let remove = match segment.kind {
Kind::Marker => drop_marker(segment.start),
Kind::Tab | Kind::LineBreak => overlaps,
Kind::Spaces(n) => {
if !overlaps {
continue;
}
let left = n - (overlap_end - overlap_start);
if left > 0
&& let Some(e) = paragraph.at_mut(&segment.path)
{
set_count(e, left);
}
left == 0
}
Kind::Text(_) => {
if !overlaps {
continue;
}
let Some((parent, index)) = parent_of(paragraph, &segment.path) else {
continue;
};
let Some(Node::Text(t) | Node::CData(t)) = parent.children.get_mut(index) else {
continue;
};
let kept: String = t
.chars()
.enumerate()
.filter(|(i, _)| {
let at = segment.start + i;
!(overlap_start..overlap_end).contains(&at)
})
.map(|(_, c)| c)
.collect();
*t = kept;
t.is_empty()
}
};
if remove && let Some((parent, index)) = parent_of(paragraph, &segment.path) {
parent.children.remove(index);
prune_emptied(paragraph, &segment.path[..segment.path.len() - 1]);
}
}
}
fn prune_emptied(paragraph: &mut Element, container: &[usize]) {
let mut path = container.to_vec();
while !path.is_empty() {
let Some((parent, index)) = parent_of(paragraph, &path) else {
return;
};
match parent.children.get(index) {
Some(Node::Element(e)) if is_inline_container_name(e) && e.children.is_empty() => {
parent.children.remove(index);
path.pop();
}
_ => return,
}
}
}
fn insert(paragraph: &mut Element, at: usize, with: &str) {
if with.is_empty() {
return;
}
let segments = segments(paragraph);
let text_at = |predicate: &dyn Fn(&Segment, usize) -> bool| {
segments
.iter()
.find(|s| matches!(s.kind, Kind::Text(_)) && predicate(s, s.start + s.kind.len()))
};
let target = text_at(&|s, end| s.start < at && at < end)
.or_else(|| text_at(&|_, end| end == at))
.or_else(|| text_at(&|s, _| s.start == at));
if let Some(segment) = target {
insert_into(paragraph, &segment.path, at - segment.start, with);
return;
}
let after = segments
.iter()
.rev()
.find(|s| s.start + s.kind.len() == at)
.map(|s| s.path.clone());
let before = segments
.iter()
.find(|s| s.start >= at)
.map(|s| s.path.clone());
let node = Node::Text(with.to_owned());
if let Some(path) = after
&& let Some((parent, index)) = parent_of(paragraph, &path)
{
parent.children.insert(index + 1, node);
} else if let Some(path) = before
&& let Some((parent, index)) = parent_of(paragraph, &path)
{
parent.children.insert(index, node);
} else {
paragraph.children.push(node);
paragraph.self_closing = false;
}
}
fn insert_into(paragraph: &mut Element, path: &[usize], offset: usize, with: &str) {
if let Some((parent, index)) = parent_of(paragraph, path)
&& let Some(Node::Text(t) | Node::CData(t)) = parent.children.get_mut(index)
{
let byte = t.char_indices().nth(offset).map_or(t.len(), |(b, _)| b);
t.insert_str(byte, with);
}
}
fn parent_of<'a>(paragraph: &'a mut Element, path: &[usize]) -> Option<(&'a mut Element, usize)> {
let (last, above) = path.split_last()?;
Some((paragraph.at_mut(above)?, *last))
}
fn set_count(space: &mut Element, count: usize) {
if count <= 1 {
space.remove_attr(&Ns::Text, "c");
} else {
let name = space
.attrs
.iter()
.find(|a| a.name.is(&Ns::Text, "c"))
.map_or_else(
|| {
Name::new(
space.name.prefix.as_deref().unwrap_or("text"),
"c",
Ns::Text,
)
},
|a| a.name.clone(),
);
space.set_attr(name, count.to_string());
}
}
fn normalize(paragraph: &mut Element) {
let prefix = paragraph
.name
.prefix
.as_deref()
.unwrap_or("text")
.to_owned();
let mut previous_was_space = true;
normalize_in(paragraph, &prefix, &mut previous_was_space);
}
fn normalize_in(parent: &mut Element, prefix: &str, previous_was_space: &mut bool) {
merge_text(parent);
let mut index = 0;
while index < parent.children.len() {
match &mut parent.children[index] {
Node::Text(t) | Node::CData(t) => {
let replacement = encode(t, prefix, previous_was_space);
match replacement {
None => index += 1,
Some(nodes) => {
let count = nodes.len();
parent.children.splice(index..=index, nodes);
index += count;
}
}
}
Node::Element(e) if is_inline_container(e) => {
normalize_in(e, prefix, previous_was_space);
index += 1;
}
Node::Element(e) => {
if e.is(&Ns::Text, "s") || e.is(&Ns::Text, "tab") || e.is(&Ns::Text, "line-break") {
*previous_was_space = true;
}
index += 1;
}
Node::Comment(_) | Node::ProcessingInstruction(_) => index += 1,
}
}
}
fn merge_text(parent: &mut Element) {
let mut index = 1;
while index < parent.children.len() {
let joinable = matches!(
(&parent.children[index - 1], &parent.children[index]),
(Node::Text(_), Node::Text(_))
);
if joinable {
let Node::Text(tail) = parent.children.remove(index) else {
unreachable!("matched a text node");
};
if let Node::Text(head) = &mut parent.children[index - 1] {
head.push_str(&tail);
}
} else {
index += 1;
}
}
}
fn encode(text: &str, prefix: &str, previous_was_space: &mut bool) -> Option<Vec<Node>> {
let needs_work = text.contains(['\t', '\n', '\r'])
|| text.contains(" ")
|| (*previous_was_space && text.starts_with(' '));
if !needs_work {
if let Some(last) = text.chars().last() {
*previous_was_space = last == ' ';
}
return None;
}
let mut nodes = Vec::new();
let mut run = String::new();
let mut spaces = 0usize;
let flush_spaces = |nodes: &mut Vec<Node>,
run: &mut String,
spaces: &mut usize,
previous_was_space: &mut bool| {
if *spaces == 0 {
return;
}
let mut counted = *spaces;
if !*previous_was_space {
run.push(' ');
counted -= 1;
}
if counted > 0 {
if !run.is_empty() {
nodes.push(Node::Text(std::mem::take(run)));
}
let mut s = Element::new(prefix, "s", Ns::Text);
if counted > 1 {
s.attrs.push(Attribute {
name: Name::new(prefix, "c", Ns::Text),
value: counted.to_string(),
});
}
nodes.push(Node::Element(s));
}
*spaces = 0;
*previous_was_space = true;
};
for c in text.chars() {
match c {
' ' => spaces += 1,
'\r' => {}
'\t' | '\n' => {
flush_spaces(&mut nodes, &mut run, &mut spaces, previous_was_space);
if !run.is_empty() {
nodes.push(Node::Text(std::mem::take(&mut run)));
}
let local = if c == '\t' { "tab" } else { "line-break" };
nodes.push(Node::Element(Element::new(prefix, local, Ns::Text)));
*previous_was_space = true;
}
other => {
flush_spaces(&mut nodes, &mut run, &mut spaces, previous_was_space);
run.push(other);
*previous_was_space = false;
}
}
}
flush_spaces(&mut nodes, &mut run, &mut spaces, previous_was_space);
if !run.is_empty() {
nodes.push(Node::Text(run));
}
Some(nodes)
}