Skip to main content

odox_core/doc/sheet/
restructure.rs

1//! Inserting and deleting rows and columns of a sheet.
2//!
3//! A row or a column is not always one element: `table:number-rows-repeated`
4//! and `table:number-columns-repeated` stand for many, so an insert may split a
5//! run and a delete may shorten one. An inserted row or column is a copy of its
6//! neighbour with what it held cleared, as spreadsheets do, so it carries the
7//! neighbour's formats and nothing a formula could reach.
8//!
9//! What names a cell has to move with the cells. The references in formulas are
10//! shifted by [`super::super::refs`], and a change is refused, before anything
11//! is touched, when a formula holds a reference that cannot be read or points
12//! at a cell that is deleted, when the line cuts through a merged region, or
13//! when the document holds something else that names a range of cells (a
14//! conditional format, a validation, a chart, a print range), which this does
15//! not move.
16//
17// Author: David M. Anderson
18// Built with AI assistance (Claude, Anthropic)
19
20use super::{MAX_COLUMNS, SheetDocument, with_count};
21use crate::doc::refs::{self, Axis, Change, Kind, Refusal};
22use crate::xml::{Element, Name, Node, Ns};
23
24/// What is asked of a sheet.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Restructure {
27    /// Put `count` empty rows in before the row at `at`.
28    InsertRows {
29        /// The row the new ones go before, counting from zero.
30        at: usize,
31        /// How many.
32        count: usize,
33    },
34    /// Take `count` rows out, from the row at `at`.
35    DeleteRows {
36        /// The first row, counting from zero.
37        at: usize,
38        /// How many.
39        count: usize,
40    },
41    /// Put `count` empty columns in before the column at `at`.
42    InsertColumns {
43        /// The column the new ones go before, counting from zero.
44        at: usize,
45        /// How many.
46        count: usize,
47    },
48    /// Take `count` columns out, from the column at `at`.
49    DeleteColumns {
50        /// The first column, counting from zero.
51        at: usize,
52        /// How many.
53        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/// Why a sheet was not changed.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum Blocked {
71    /// There is no such sheet, or the count is nothing.
72    NotFound,
73    /// The line cuts through a merged region.
74    Merged,
75    /// A formula, or a named range, holds a reference that cannot be read.
76    Unreadable(String),
77    /// A formula, or a named range, points at what would be deleted.
78    Deleted(String),
79    /// The document holds something that names a range of cells and is not
80    /// moved: what, in the words a window can put in a sentence.
81    Names(&'static str),
82}
83
84impl SheetDocument {
85    /// Insert or delete rows or columns of a sheet, with the formulas that
86    /// refer to the cells moved to follow.
87    ///
88    /// Nothing is changed unless all of it can be: the change is made to a
89    /// copy of the content, which replaces the content only when it is done.
90    ///
91    /// # Errors
92    ///
93    /// [`Blocked`] says what is in the way.
94    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        // The rows or columns first, so that a formula in a cell that is
128        // deleted with them is not asked about what it pointed at.
129        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
154/// Something in the spreadsheet that names a range of cells and is not moved.
155fn 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
187/// The cells of a row, with the column each begins at and how many it stands
188/// for.
189fn 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
216/// Each row element of a table in order, with the row it begins at, how many
217/// it stands for, and the route to it.
218fn 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
245/// The same for the column definitions.
246fn 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
273/// Whether a line of an insert or a delete goes through a merged region.
274fn 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
312/// Move the references in everything under an element: the formulas in the
313/// cells, and the named ranges and expressions, each against the sheet it sits
314/// on or names.
315fn 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
368/// A cell with what it held taken out and its style kept: what an inserted
369/// cell is. A covered cell becomes an ordinary one, because the merged region
370/// that covered it does not grow.
371fn 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
392/// A row of the neighbour's shape with nothing in it: its own formats, its
393/// cells' formats and their repeats, and none of what they held.
394fn 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
422/// Put an element into a run of repeated ones before the position `offset`
423/// into the run at `index`, splitting the run where that is inside it.
424fn 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    // The column definitions first.
475    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    // Then a cell in every row.
496    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
530/// A row that has grown past the last column ODF allows loses the excess from
531/// the empty run that fills out its end.
532fn 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/// Which kind of line a delete goes along.
556#[derive(Clone, Copy)]
557enum Run {
558    Rows,
559    Columns,
560}
561
562/// Take `count` lines out, from `at`: whole elements are removed and the runs
563/// that only partly fall in the gap are shortened.
564fn 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
596/// Delete from a parent's children the elements named `local` that fall in
597/// `at..end` of the line they count along, descending into the containers that
598/// hold some.
599fn 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
644/// Delete the cells of a row that fall in `at..end`, covered cells with them.
645fn 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}