1use super::{MAX_COLUMNS, SheetDocument, with_count};
21use crate::doc::refs::{self, Axis, Change, Kind, Refusal};
22use crate::xml::{Element, Name, Node, Ns};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Restructure {
27 InsertRows {
29 at: usize,
31 count: usize,
33 },
34 DeleteRows {
36 at: usize,
38 count: usize,
40 },
41 InsertColumns {
43 at: usize,
45 count: usize,
47 },
48 DeleteColumns {
50 at: usize,
52 count: usize,
54 },
55}
56
57impl Restructure {
58 fn axis_at_kind(self) -> (Axis, usize, Kind) {
59 match self {
60 Self::InsertRows { at, count } => (Axis::Row, at, Kind::Insert(count)),
61 Self::DeleteRows { at, count } => (Axis::Row, at, Kind::Delete(count)),
62 Self::InsertColumns { at, count } => (Axis::Column, at, Kind::Insert(count)),
63 Self::DeleteColumns { at, count } => (Axis::Column, at, Kind::Delete(count)),
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum Blocked {
71 NotFound,
73 Merged,
75 Unreadable(String),
77 Deleted(String),
79 Names(&'static str),
82}
83
84impl SheetDocument {
85 pub fn restructure(&mut self, sheet: usize, change: Restructure) -> Result<(), Blocked> {
95 let (axis, at, kind) = change.axis_at_kind();
96 let (Kind::Insert(count) | Kind::Delete(count)) = kind;
97 let table_at = self.sheets.get(sheet).ok_or(Blocked::NotFound)?.table;
98 let name = self.sheets[sheet].name.clone();
99 if count == 0 {
100 return Err(Blocked::NotFound);
101 }
102 let names = RepeatNames {
103 rows: self.document.name(&Ns::Table, "number-rows-repeated"),
104 columns: self.document.name(&Ns::Table, "number-columns-repeated"),
105 };
106
107 let mut content = self.document.content.clone();
108 let spreadsheet = content
109 .child_mut(&Ns::Office, "body")
110 .and_then(|body| body.child_mut(&Ns::Office, "spreadsheet"))
111 .ok_or(Blocked::NotFound)?;
112 if let Some(what) = unmovable(spreadsheet) {
113 return Err(Blocked::Names(what));
114 }
115 let Some(Node::Element(table)) = spreadsheet.children.get(table_at) else {
116 return Err(Blocked::NotFound);
117 };
118 if cuts_a_merge(table, axis, at, kind) {
119 return Err(Blocked::Merged);
120 }
121 let moved = Change {
122 sheet: name,
123 axis,
124 at,
125 kind,
126 };
127 let Some(Node::Element(table)) = spreadsheet.children.get_mut(table_at) else {
130 return Err(Blocked::NotFound);
131 };
132 match change {
133 Restructure::InsertRows { at, count } => insert_rows(table, at, count, &names),
134 Restructure::DeleteRows { at, count } => {
135 delete_run(table, Run::Rows, at, count, &names);
136 }
137 Restructure::InsertColumns { at, count } => insert_columns(table, at, count, &names),
138 Restructure::DeleteColumns { at, count } => {
139 delete_run(table, Run::Columns, at, count, &names);
140 }
141 }
142 shift_references(spreadsheet, None, &moved)?;
143 self.document.content = content;
144 self.reindex();
145 Ok(())
146 }
147}
148
149struct RepeatNames {
150 rows: Name,
151 columns: Name,
152}
153
154fn unmovable(element: &Element) -> Option<&'static str> {
156 let has_content = element.elements().next().is_some();
157 let named = |local: &str| element.name.local.as_ref() == local;
158 if has_content {
159 if named("conditional-formats") {
160 return Some("conditional formats");
161 }
162 if named("content-validations") {
163 return Some("validations");
164 }
165 if named("database-ranges") {
166 return Some("database ranges");
167 }
168 if named("data-pilot-tables") {
169 return Some("pivot tables");
170 }
171 if named("consolidation") {
172 return Some("consolidations");
173 }
174 }
175 if element.is(&Ns::Draw, "object") || element.is(&Ns::Draw, "object-ole") {
176 return Some("charts and objects");
177 }
178 if element.attr(&Ns::Table, "print-ranges").is_some() {
179 return Some("print ranges");
180 }
181 if element.attr(&Ns::Table, "end-cell-address").is_some() {
182 return Some("shapes anchored to cells");
183 }
184 element.elements().find_map(unmovable)
185}
186
187fn cells_of(row: &Element) -> impl Iterator<Item = (usize, usize, &Element)> {
190 let mut at = 0;
191 row.elements()
192 .filter(|e| e.is(&Ns::Table, "table-cell") || e.is(&Ns::Table, "covered-table-cell"))
193 .map(move |cell| {
194 let repeat = cell
195 .attr_usize(&Ns::Table, "number-columns-repeated")
196 .unwrap_or(1)
197 .max(1);
198 let first = at;
199 at += repeat;
200 (first, repeat, cell)
201 })
202}
203
204fn is_row_container(element: &Element) -> bool {
205 element.is(&Ns::Table, "table-rows")
206 || element.is(&Ns::Table, "table-header-rows")
207 || element.is(&Ns::Table, "table-row-group")
208}
209
210fn is_column_container(element: &Element) -> bool {
211 element.is(&Ns::Table, "table-columns")
212 || element.is(&Ns::Table, "table-header-columns")
213 || element.is(&Ns::Table, "table-column-group")
214}
215
216fn rows_of(table: &Element) -> Vec<(Vec<usize>, usize, usize)> {
219 fn walk(
220 parent: &Element,
221 path: &mut Vec<usize>,
222 at: &mut usize,
223 out: &mut Vec<(Vec<usize>, usize, usize)>,
224 ) {
225 for (index, child) in parent.elements_indexed() {
226 path.push(index);
227 if child.is(&Ns::Table, "table-row") {
228 let repeat = child
229 .attr_usize(&Ns::Table, "number-rows-repeated")
230 .unwrap_or(1)
231 .max(1);
232 out.push((path.clone(), *at, repeat));
233 *at += repeat;
234 } else if is_row_container(child) {
235 walk(child, path, at, out);
236 }
237 path.pop();
238 }
239 }
240 let mut out = Vec::new();
241 walk(table, &mut Vec::new(), &mut 0, &mut out);
242 out
243}
244
245fn columns_of(table: &Element) -> Vec<(Vec<usize>, usize, usize)> {
247 fn walk(
248 parent: &Element,
249 path: &mut Vec<usize>,
250 at: &mut usize,
251 out: &mut Vec<(Vec<usize>, usize, usize)>,
252 ) {
253 for (index, child) in parent.elements_indexed() {
254 path.push(index);
255 if child.is(&Ns::Table, "table-column") {
256 let repeat = child
257 .attr_usize(&Ns::Table, "number-columns-repeated")
258 .unwrap_or(1)
259 .max(1);
260 out.push((path.clone(), *at, repeat));
261 *at += repeat;
262 } else if is_column_container(child) {
263 walk(child, path, at, out);
264 }
265 path.pop();
266 }
267 }
268 let mut out = Vec::new();
269 walk(table, &mut Vec::new(), &mut 0, &mut out);
270 out
271}
272
273fn cuts_a_merge(table: &Element, axis: Axis, at: usize, kind: Kind) -> bool {
275 let cuts = |low: usize, high: usize| match kind {
276 Kind::Insert(_) => low < at && at <= high,
277 Kind::Delete(count) => {
278 let last = at + count - 1;
279 let overlaps = low <= last && high >= at;
280 let inside = low >= at && high <= last;
281 overlaps && !inside
282 }
283 };
284 for (path, first_row, _) in rows_of(table) {
285 let Some(row) = table.at(&path) else {
286 continue;
287 };
288 for (first_column, _, cell) in cells_of(row) {
289 let across = cell
290 .attr_usize(&Ns::Table, "number-columns-spanned")
291 .unwrap_or(1)
292 .max(1);
293 let down = cell
294 .attr_usize(&Ns::Table, "number-rows-spanned")
295 .unwrap_or(1)
296 .max(1);
297 if across == 1 && down == 1 {
298 continue;
299 }
300 let (low, high) = match axis {
301 Axis::Row => (first_row, first_row + down - 1),
302 Axis::Column => (first_column, first_column + across - 1),
303 };
304 if cuts(low, high) {
305 return true;
306 }
307 }
308 }
309 false
310}
311
312fn shift_references(
316 element: &mut Element,
317 own_sheet: Option<&str>,
318 change: &Change,
319) -> Result<(), Blocked> {
320 let blocked = |refusal: Refusal| match refusal {
321 Refusal::Unreadable(text) => Blocked::Unreadable(text),
322 Refusal::Deleted(text) => Blocked::Deleted(text),
323 };
324 let own = if element.is(&Ns::Table, "table") {
325 element.attr(&Ns::Table, "name").map(ToOwned::to_owned)
326 } else {
327 own_sheet.map(ToOwned::to_owned)
328 };
329 for attribute in &mut element.attrs {
330 if attribute.name.is(&Ns::Table, "formula") {
331 attribute.value =
332 refs::shift_formula(&attribute.value, own.as_deref(), change).map_err(blocked)?;
333 }
334 }
335 if element.is(&Ns::Table, "named-range") {
336 for attribute in &mut element.attrs {
337 if attribute.name.is(&Ns::Table, "cell-range-address")
338 || attribute.name.is(&Ns::Table, "base-cell-address")
339 {
340 attribute.value =
341 refs::shift_address(&attribute.value, None, change).map_err(blocked)?;
342 }
343 }
344 }
345 if element.is(&Ns::Table, "named-expression") {
346 let context = element
347 .attr(&Ns::Table, "base-cell-address")
348 .and_then(refs::address_sheet);
349 for attribute in &mut element.attrs {
350 if attribute.name.is(&Ns::Table, "expression") {
351 attribute.value = refs::shift_formula(&attribute.value, context.as_deref(), change)
352 .map_err(blocked)?;
353 }
354 if attribute.name.is(&Ns::Table, "base-cell-address") {
355 attribute.value =
356 refs::shift_address(&attribute.value, None, change).map_err(blocked)?;
357 }
358 }
359 }
360 for child in &mut element.children {
361 if let Node::Element(child) = child {
362 shift_references(child, own.as_deref(), change)?;
363 }
364 }
365 Ok(())
366}
367
368fn cleared_cell(cell: &Element, repeat: usize, names: &RepeatNames) -> Element {
372 let mut fresh = Element {
373 name: cell.name.clone(),
374 attrs: cell
375 .attrs
376 .iter()
377 .filter(|a| a.name.is(&Ns::Table, "style-name"))
378 .cloned()
379 .collect(),
380 children: Vec::new(),
381 self_closing: true,
382 };
383 if cell.is(&Ns::Table, "covered-table-cell") {
384 fresh.name.local = "table-cell".into();
385 }
386 if repeat > 1 {
387 fresh.set_attr(names.columns.clone(), repeat.to_string());
388 }
389 fresh
390}
391
392fn cleared_row(row: &Element, count: usize, names: &RepeatNames) -> Element {
395 let mut fresh = row.clone();
396 fresh.remove_attr(&Ns::Table, "number-rows-repeated");
397 fresh.remove_attr(&Ns::Table, "visibility");
398 fresh.remove_attr(&Ns::Table, "filter");
399 fresh.children = row
400 .children
401 .iter()
402 .filter_map(|node| {
403 let Node::Element(cell) = node else {
404 return None;
405 };
406 if !(cell.is(&Ns::Table, "table-cell") || cell.is(&Ns::Table, "covered-table-cell")) {
407 return None;
408 }
409 let repeat = cell
410 .attr_usize(&Ns::Table, "number-columns-repeated")
411 .unwrap_or(1)
412 .max(1);
413 Some(Node::Element(cleared_cell(cell, repeat, names)))
414 })
415 .collect();
416 if count > 1 {
417 fresh.set_attr(names.rows.clone(), count.to_string());
418 }
419 fresh
420}
421
422fn put_in(
425 parent: &mut Element,
426 index: usize,
427 offset: usize,
428 repeat: usize,
429 new: Element,
430 repeated: &Name,
431) {
432 if offset == 0 {
433 parent.children.insert(index, Node::Element(new));
434 return;
435 }
436 let Some(Node::Element(original)) = parent.children.get(index) else {
437 return;
438 };
439 let before = with_count(original.clone(), offset, repeated);
440 let after = with_count(original.clone(), repeat - offset, repeated);
441 parent.children.splice(
442 index..=index,
443 [
444 Node::Element(before),
445 Node::Element(new),
446 Node::Element(after),
447 ],
448 );
449}
450
451fn insert_rows(table: &mut Element, at: usize, count: usize, names: &RepeatNames) {
452 let rows = rows_of(table);
453 let find = |row: usize| {
454 rows.iter()
455 .find(|(_, first, n)| (*first..first + n).contains(&row))
456 };
457 let Some((path, first, repeat)) = find(at) else {
458 return;
459 };
460 let reference = find(at.saturating_sub(1)).map_or(path, |(path, ..)| path);
461 let Some(neighbour) = table.at(reference) else {
462 return;
463 };
464 let new = cleared_row(neighbour, count, names);
465 let Some((index, above)) = path.split_last() else {
466 return;
467 };
468 if let Some(parent) = table.at_mut(above) {
469 put_in(parent, *index, at - first, *repeat, new, &names.rows);
470 }
471}
472
473fn insert_columns(table: &mut Element, at: usize, count: usize, names: &RepeatNames) {
474 let defs = columns_of(table);
476 let find = |column: usize| {
477 defs.iter()
478 .find(|(_, first, n)| (*first..first + n).contains(&column))
479 };
480 if let Some((path, first, repeat)) = find(at) {
481 let reference = find(at.saturating_sub(1)).map_or(path, |(path, ..)| path);
482 if let Some(neighbour) = table.at(reference) {
483 let mut new = neighbour.clone();
484 new.remove_attr(&Ns::Table, "number-columns-repeated");
485 if count > 1 {
486 new.set_attr(names.columns.clone(), count.to_string());
487 }
488 if let Some((index, above)) = path.split_last()
489 && let Some(parent) = table.at_mut(above)
490 {
491 put_in(parent, *index, at - first, *repeat, new, &names.columns);
492 }
493 }
494 }
495 for (path, ..) in rows_of(table).into_iter().rev() {
497 let Some(row) = table.at_mut(&path) else {
498 continue;
499 };
500 let cells: Vec<(usize, usize, usize)> = cells_of(row)
501 .enumerate()
502 .map(|(n, (first, repeat, _))| (n, first, repeat))
503 .collect();
504 let Some(&(which, first, repeat)) = cells
505 .iter()
506 .find(|(_, first, repeat)| (*first..first + repeat).contains(&at))
507 else {
508 continue;
509 };
510 let reference = cells
511 .iter()
512 .find(|(_, first, repeat)| (*first..first + repeat).contains(&at.saturating_sub(1)))
513 .map_or(which, |(n, ..)| *n);
514 let indices: Vec<usize> = row
515 .elements_indexed()
516 .filter(|(_, e)| {
517 e.is(&Ns::Table, "table-cell") || e.is(&Ns::Table, "covered-table-cell")
518 })
519 .map(|(i, _)| i)
520 .collect();
521 let Some(Node::Element(neighbour)) = row.children.get(indices[reference]) else {
522 continue;
523 };
524 let new = cleared_cell(neighbour, count, names);
525 put_in(row, indices[which], at - first, repeat, new, &names.columns);
526 trim_overflow(row, names);
527 }
528}
529
530fn trim_overflow(row: &mut Element, names: &RepeatNames) {
533 let total: usize = cells_of(row).map(|(_, repeat, _)| repeat).sum();
534 if total <= MAX_COLUMNS {
535 return;
536 }
537 let excess = total - MAX_COLUMNS;
538 let last = row
539 .children
540 .iter()
541 .rposition(|n| matches!(n, Node::Element(e) if e.is(&Ns::Table, "table-cell")));
542 if let Some(last) = last
543 && let Some(Node::Element(cell)) = row.children.get(last)
544 {
545 let repeat = cell
546 .attr_usize(&Ns::Table, "number-columns-repeated")
547 .unwrap_or(1);
548 if repeat > excess && cell.children.is_empty() {
549 let trimmed = with_count(cell.clone(), repeat - excess, &names.columns);
550 row.children[last] = Node::Element(trimmed);
551 }
552 }
553}
554
555#[derive(Clone, Copy)]
557enum Run {
558 Rows,
559 Columns,
560}
561
562fn delete_run(table: &mut Element, run: Run, at: usize, count: usize, names: &RepeatNames) {
565 let end = at + count;
566 match run {
567 Run::Rows => delete_in(
568 table,
569 &mut 0,
570 at,
571 end,
572 &names.rows,
573 is_row_container,
574 "table-row",
575 ),
576 Run::Columns => {
577 delete_in(
578 table,
579 &mut 0,
580 at,
581 end,
582 &names.columns,
583 is_column_container,
584 "table-column",
585 );
586 let rows: Vec<Vec<usize>> = rows_of(table).into_iter().map(|(path, ..)| path).collect();
587 for path in rows {
588 if let Some(row) = table.at_mut(&path) {
589 delete_cells(row, at, end, &names.columns);
590 }
591 }
592 }
593 }
594}
595
596fn delete_in(
600 parent: &mut Element,
601 cursor: &mut usize,
602 at: usize,
603 end: usize,
604 repeated: &Name,
605 is_container: fn(&Element) -> bool,
606 local: &str,
607) {
608 let mut index = 0;
609 while index < parent.children.len() {
610 let Node::Element(child) = &mut parent.children[index] else {
611 index += 1;
612 continue;
613 };
614 if child.is(&Ns::Table, local) {
615 let repeat = child
616 .attr_usize(&repeated.ns, &repeated.local)
617 .unwrap_or(1)
618 .max(1);
619 let (start, stop) = (*cursor, *cursor + repeat);
620 *cursor = stop;
621 let overlap = stop.min(end).saturating_sub(start.max(at));
622 if overlap == 0 {
623 index += 1;
624 } else if overlap == repeat {
625 parent.children.remove(index);
626 } else {
627 *child = with_count(child.clone(), repeat - overlap, repeated);
628 index += 1;
629 }
630 } else if is_container(child) {
631 delete_in(child, cursor, at, end, repeated, is_container, local);
632 let empty = child.elements().next().is_none();
633 if empty {
634 parent.children.remove(index);
635 } else {
636 index += 1;
637 }
638 } else {
639 index += 1;
640 }
641 }
642}
643
644fn delete_cells(row: &mut Element, at: usize, end: usize, repeated: &Name) {
646 let mut cursor = 0;
647 let mut index = 0;
648 while index < row.children.len() {
649 let Node::Element(cell) = &mut row.children[index] else {
650 index += 1;
651 continue;
652 };
653 if !(cell.is(&Ns::Table, "table-cell") || cell.is(&Ns::Table, "covered-table-cell")) {
654 index += 1;
655 continue;
656 }
657 let repeat = cell
658 .attr_usize(&Ns::Table, "number-columns-repeated")
659 .unwrap_or(1)
660 .max(1);
661 let (start, stop) = (cursor, cursor + repeat);
662 cursor = stop;
663 let overlap = stop.min(end).saturating_sub(start.max(at));
664 if overlap == 0 {
665 index += 1;
666 } else if overlap == repeat {
667 row.children.remove(index);
668 } else {
669 *cell = with_count(cell.clone(), repeat - overlap, repeated);
670 index += 1;
671 }
672 }
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678
679 fn spreadsheet(inner: &str) -> Element {
680 let source = format!(
681 r#"<office:spreadsheet xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0">{inner}</office:spreadsheet>"#
682 );
683 crate::xml::parse(source.as_bytes(), "test").expect("a spreadsheet")
684 }
685
686 #[test]
687 fn what_names_a_range_of_cells_is_found_and_empty_containers_are_not() {
688 let found = |inner: &str| unmovable(&spreadsheet(inner));
689 assert_eq!(found("<table:table table:name=\"a\"/>"), None);
690 assert_eq!(found("<table:named-expressions/>"), None);
691 assert_eq!(found("<table:database-ranges/>"), None);
692 assert_eq!(
693 found(
694 "<table:table table:name=\"a\"><calcext:conditional-formats><calcext:conditional-format/></calcext:conditional-formats></table:table>"
695 ),
696 Some("conditional formats")
697 );
698 assert_eq!(
699 found(
700 "<table:content-validations><table:content-validation/></table:content-validations>"
701 ),
702 Some("validations")
703 );
704 assert_eq!(
705 found("<table:table table:name=\"a\" table:print-ranges=\"a.A1:a.B2\"/>"),
706 Some("print ranges")
707 );
708 assert_eq!(
709 found(
710 "<table:table table:name=\"a\"><table:shapes><draw:frame table:end-cell-address=\"a.C3\"/></table:shapes></table:table>"
711 ),
712 Some("shapes anchored to cells")
713 );
714 assert_eq!(
715 found(
716 "<table:table table:name=\"a\"><draw:frame><draw:object/></draw:frame></table:table>"
717 ),
718 Some("charts and objects")
719 );
720 }
721
722 #[test]
723 fn a_repeated_run_is_split_for_an_insert_and_shortened_for_a_delete() {
724 let names = RepeatNames {
725 rows: Name::new("table", "number-rows-repeated", Ns::Table),
726 columns: Name::new("table", "number-columns-repeated", Ns::Table),
727 };
728 let mut table = spreadsheet(
729 "<table:table table:name=\"a\"><table:table-row table:number-rows-repeated=\"10\"><table:table-cell/></table:table-row></table:table>",
730 );
731 let table = table.child_mut(&Ns::Table, "table").expect("a table");
732 insert_rows(table, 4, 2, &names);
733 let counts: Vec<usize> = table
734 .elements()
735 .map(|row| {
736 row.attr_usize(&Ns::Table, "number-rows-repeated")
737 .unwrap_or(1)
738 })
739 .collect();
740 assert_eq!(counts, [4, 2, 6], "four before, two new, six after");
741 delete_run(table, Run::Rows, 3, 5, &names);
742 let counts: Vec<usize> = table
743 .elements()
744 .map(|row| {
745 row.attr_usize(&Ns::Table, "number-rows-repeated")
746 .unwrap_or(1)
747 })
748 .collect();
749 assert_eq!(counts.iter().sum::<usize>(), 12 - 5, "five rows fewer");
750 }
751}