Skip to main content

odox_core/doc/
sheet.rs

1//! A spreadsheet: `.ods`.
2//!
3//! The body is indexed rather than flattened. ODF writes a run of identical rows
4//! or cells once with a repeat count, and a sheet whose last column says
5//! `table:number-columns-repeated="16384"` is ordinary: expanding that into cells
6//! would turn a small file into a large allocation, and every office application
7//! writes one. So a sheet keeps the rows it was given, each with the range of row
8//! numbers it stands for, and a lookup is a search through those ranges.
9//
10// Author: David M. Anderson
11// Built with AI assistance (Claude, Anthropic)
12
13mod restructure;
14pub use restructure::{Blocked, Restructure};
15
16use super::Document;
17use crate::edit::Refused;
18use crate::media_type;
19use crate::value::Length;
20use crate::xml::{Element, Name, Node, Ns};
21use crate::{Error, Family, Properties};
22
23/// An `OpenDocument` spreadsheet.
24pub struct SheetDocument {
25    /// The package and everything shared with the other two formats.
26    pub document: Document,
27    sheets: Vec<Sheet>,
28}
29
30/// One sheet, indexed for lookup by row and column.
31pub struct Sheet {
32    /// The sheet's name, as its tab shows it.
33    pub name: String,
34    /// The columns that carry a width or a default cell style, expanded from the
35    /// column elements' repeat counts and truncated at the last column any cell
36    /// reaches. A column past the end of this list is the default width.
37    pub columns: Vec<Column>,
38    /// The number of rows that carry anything. The grid may show more than this
39    /// and a document ends here.
40    pub used_rows: usize,
41    /// The number of columns that carry anything.
42    pub used_columns: usize,
43    /// Whether the sheet is hidden, from `table:display`.
44    pub visible: bool,
45    rows: Vec<RowRange>,
46    /// Where the `table:table` element sits among the body's children, so that
47    /// the element can be reached again without holding a reference to it.
48    table: usize,
49}
50
51/// A column's own properties.
52pub struct Column {
53    /// Its width, where the column style gives one.
54    pub width: Option<Length>,
55    /// The cell style every cell in the column takes unless it names its own.
56    pub default_cell_style: Option<String>,
57    /// Whether the column is shown.
58    pub visible: bool,
59}
60
61/// A run of rows the document wrote once.
62struct RowRange {
63    /// The first row number this run covers, counting from zero.
64    first: usize,
65    /// How many rows it covers.
66    count: usize,
67    /// Where the `table:table-row` element is, relative to the `table:table`.
68    path: RowPath,
69}
70
71/// Where a row element sits under its table.
72///
73/// Rows are usually the table's own children, and a sheet with a frozen header
74/// or a collapsible outline nests them one or more levels deeper inside
75/// `table:table-header-rows` and `table:table-row-group`. The common case costs
76/// no allocation and the nested case is a path of child indices, which is what
77/// lets a row be reached again without holding a reference into the tree.
78enum RowPath {
79    /// A child of the table.
80    Direct(usize),
81    /// A child of a child of the table, to any depth.
82    Nested(Box<[usize]>),
83}
84
85/// What a cell holds, as ODF types it.
86///
87/// The type is the cell's own declaration and is independent of how it is shown:
88/// a date is a date whatever format it is displayed in, which is what makes a
89/// spreadsheet sortable.
90#[derive(Debug, Clone, PartialEq)]
91pub enum Value {
92    /// Nothing. A cell that exists to carry a style, or to be spanned over.
93    Empty,
94    /// A number.
95    Number(f64),
96    /// A number that means a proportion: `0.15` shown as `15%`.
97    Percentage(f64),
98    /// An amount of money, with the currency code where the cell names one.
99    Currency(f64, Option<String>),
100    /// A date, as written: an ISO 8601 date or date and time.
101    Date(String),
102    /// A duration, as written: an ISO 8601 duration.
103    Time(String),
104    /// True or false.
105    Boolean(bool),
106    /// Text.
107    Text(String),
108}
109
110impl Value {
111    /// What a person typed into a cell, read the way a spreadsheet reads it: a
112    /// number is a number, `true` and `false` are booleans, nothing is empty,
113    /// and anything else is text.
114    ///
115    /// A formula is not recognised here, because nothing in this version
116    /// evaluates one; text beginning with `=` is text. Percentages, currencies
117    /// and dates need the document's number formats to read and are text too.
118    pub fn from_input(input: &str) -> Self {
119        let trimmed = input.trim();
120        if trimmed.is_empty() {
121            return Self::Empty;
122        }
123        if trimmed.eq_ignore_ascii_case("true") {
124            return Self::Boolean(true);
125        }
126        if trimmed.eq_ignore_ascii_case("false") {
127            return Self::Boolean(false);
128        }
129        if let Ok(number) = trimmed.parse::<f64>()
130            && number.is_finite()
131            && !trimmed.contains(['i', 'n', 'I', 'N'])
132        {
133            return Self::Number(number);
134        }
135        Self::Text(input.to_owned())
136    }
137
138    /// The text a person edits for the value: a number's plain spelling, a
139    /// date's or a duration's ISO form, `TRUE` and `FALSE`, the text itself.
140    /// What [`Self::from_input`] reads back into the same value where it can,
141    /// which is not everywhere: a percentage, a currency or a date typed again
142    /// comes back as the number or the text it reads as, because reading it as
143    /// what it was needs the number format the cell carries.
144    pub fn input_text(&self) -> String {
145        match self {
146            Self::Percentage(n) => n.to_string(),
147            _ => self.cached_text(),
148        }
149    }
150
151    /// The text a cell shows for the value, as this crate formats it: the
152    /// shortest spelling of a number, `TRUE` and `FALSE`, the text itself. A
153    /// document's own number format is not applied; a spreadsheet application
154    /// reformats the cell from the value when it next opens the file.
155    pub fn cached_text(&self) -> String {
156        match self {
157            Self::Empty => String::new(),
158            Self::Number(n) | Self::Currency(n, _) => n.to_string(),
159            Self::Percentage(n) => format!("{}%", n * 100.0),
160            Self::Date(s) | Self::Time(s) | Self::Text(s) => s.clone(),
161            Self::Boolean(true) => "TRUE".to_owned(),
162            Self::Boolean(false) => "FALSE".to_owned(),
163        }
164    }
165
166    /// Whether the value is one a spreadsheet puts against the right edge of its
167    /// cell: every type but text, which is ODF's own rule and every
168    /// application's default.
169    pub fn is_numeric(&self) -> bool {
170        matches!(
171            self,
172            Self::Number(_)
173                | Self::Percentage(_)
174                | Self::Currency(..)
175                | Self::Date(_)
176                | Self::Time(_)
177        )
178    }
179}
180
181/// One cell, as found in the tree.
182pub struct Cell<'a> {
183    /// The `table:table-cell` element, so that anything not modelled here is
184    /// still reachable.
185    pub element: &'a Element,
186    /// True when this is a `table:covered-table-cell`: a cell hidden underneath
187    /// a neighbour's span. It is kept rather than skipped because the grid has to
188    /// know not to draw a border there.
189    pub covered: bool,
190}
191
192impl Cell<'_> {
193    /// The cell's typed value.
194    pub fn value(&self) -> Value {
195        let e = self.element;
196        match e.attr(&Ns::Office, "value-type") {
197            Some("float") => e
198                .attr(&Ns::Office, "value")
199                .and_then(|v| v.parse().ok())
200                .map_or(Value::Empty, Value::Number),
201            Some("percentage") => e
202                .attr(&Ns::Office, "value")
203                .and_then(|v| v.parse().ok())
204                .map_or(Value::Empty, Value::Percentage),
205            Some("currency") => e
206                .attr(&Ns::Office, "value")
207                .and_then(|v| v.parse().ok())
208                .map_or(Value::Empty, |amount| {
209                    Value::Currency(
210                        amount,
211                        e.attr(&Ns::Office, "currency").map(ToOwned::to_owned),
212                    )
213                }),
214            Some("date") => e
215                .attr(&Ns::Office, "date-value")
216                .map_or(Value::Empty, |v| Value::Date(v.to_owned())),
217            Some("time") => e
218                .attr(&Ns::Office, "time-value")
219                .map_or(Value::Empty, |v| Value::Time(v.to_owned())),
220            Some("boolean") => e
221                .attr(&Ns::Office, "boolean-value")
222                .and_then(crate::value::boolean)
223                .map_or(Value::Empty, Value::Boolean),
224            // A string cell carries its text in its paragraphs, and
225            // `office:string-value` only where the producer chose to write it
226            // there as well.
227            Some("string") => match e.attr(&Ns::Office, "string-value") {
228                Some(text) => Value::Text(text.to_owned()),
229                None => Value::Text(self.text()),
230            },
231            _ => {
232                let text = self.text();
233                if text.is_empty() {
234                    Value::Empty
235                } else {
236                    Value::Text(text)
237                }
238            }
239        }
240    }
241
242    /// What the cell shows: the text the producing application formatted and
243    /// stored in the cell's paragraphs.
244    ///
245    /// This is why a viewer needs neither a number-format engine nor a formula
246    /// evaluator. ODF requires a cell to carry both its typed value and the text
247    /// of that value as the document was last displayed, so the formatted string
248    /// — thousands separators, currency symbol, date order, decimal places — is
249    /// already in the file. A cell edited here would have to be re-formatted
250    /// from its data style, which is the work an editor adds and a viewer does
251    /// not.
252    ///
253    /// A cell with more than one paragraph gives them separated by newlines.
254    pub fn text(&self) -> String {
255        let mut out = String::new();
256        for paragraph in self.element.elements() {
257            if paragraph.is(&Ns::Text, "p") {
258                if !out.is_empty() {
259                    out.push('\n');
260                }
261                out.push_str(&paragraph.plain_text());
262            }
263        }
264        out
265    }
266
267    /// The formula, without the namespace prefix ODF writes in front of it.
268    ///
269    /// A formula is written `of:=SUM([.A1:.A9])`, where the part before the
270    /// colon says which formula language it is in. The prefix is dropped here
271    /// and the expression given as it stands: nothing in this release evaluates
272    /// one, and a formula bar shows what the document says.
273    pub fn formula(&self) -> Option<&str> {
274        let formula = self.element.attr(&Ns::Table, "formula")?;
275        Some(match formula.split_once(":=") {
276            Some((_, expression)) => expression,
277            None => formula,
278        })
279    }
280
281    /// The cell style the cell names, if it names one.
282    pub fn style_name(&self) -> Option<&str> {
283        self.element.attr(&Ns::Table, "style-name")
284    }
285
286    /// How many columns the cell spans, which is one unless it says otherwise.
287    pub fn columns_spanned(&self) -> usize {
288        self.element
289            .attr_usize(&Ns::Table, "number-columns-spanned")
290            .unwrap_or(1)
291            .max(1)
292    }
293
294    /// How many rows the cell spans.
295    pub fn rows_spanned(&self) -> usize {
296        self.element
297            .attr_usize(&Ns::Table, "number-rows-spanned")
298            .unwrap_or(1)
299            .max(1)
300    }
301
302    /// Whether the cell has anything in it: a value, text, or a formula. A cell
303    /// that carries only a style is empty.
304    fn occupied(&self) -> bool {
305        self.element.attr(&Ns::Office, "value-type").is_some()
306            || self.element.attr(&Ns::Table, "formula").is_some()
307            || self.element.elements().any(|e| e.is(&Ns::Text, "p"))
308    }
309}
310
311impl SheetDocument {
312    /// Read a `.ods` package.
313    ///
314    /// # Errors
315    ///
316    /// The bytes are not a spreadsheet, or its `content.xml` cannot be read.
317    pub fn read(bytes: &[u8]) -> Result<Self, Error> {
318        let document = Document::read(bytes, media_type::SPREADSHEET_ANY)?;
319        let sheets = index_sheets(&document);
320        Ok(Self { document, sheets })
321    }
322
323    /// The sheets, in the order the document holds them.
324    pub fn sheets(&self) -> &[Sheet] {
325        &self.sheets
326    }
327
328    /// Rebuild the index after the content tree has been replaced under it,
329    /// which is what undo does.
330    pub fn reindex(&mut self) {
331        self.sheets = index_sheets(&self.document);
332    }
333
334    /// Whether a cell may be written, which is what [`Self::set_cell`] asks
335    /// first and what a window asks before it opens an editor.
336    ///
337    /// # Errors
338    ///
339    /// The cell is under a neighbour's span, holds a formula, or the sheet does
340    /// not exist.
341    pub fn can_edit(&self, sheet: usize, row: usize, column: usize) -> Result<(), Refused> {
342        let index = self.sheets.get(sheet).ok_or(Refused::NotFound)?;
343        if let Some(cell) = self.cell(index, row, column) {
344            if cell.covered {
345                return Err(Refused::Covered);
346            }
347            if cell.formula().is_some() {
348                return Err(Refused::Formula);
349            }
350        }
351        Ok(())
352    }
353
354    /// Put a value in a cell, by sheet, row and column, all counting from zero.
355    ///
356    /// A row or a cell the document wrote once with a repeat count is split
357    /// into the run before, the one, and the run after, with the counts fixed,
358    /// so that the one cell changes and its neighbours in the run keep what
359    /// they had. A row or cell past what the document wrote is created, with a
360    /// repeated empty run filling the gap. The cell's own attributes and
361    /// paragraphs are replaced; its style, and anything else in it, stay.
362    ///
363    /// # Errors
364    ///
365    /// The cell is under a neighbour's span, holds a formula, or the sheet does
366    /// not exist. Nothing is changed in any of those cases.
367    pub fn set_cell(
368        &mut self,
369        sheet: usize,
370        row: usize,
371        column: usize,
372        value: &Value,
373    ) -> Result<(), Refused> {
374        self.can_edit(sheet, row, column)?;
375        let index = self.sheets.get(sheet).ok_or(Refused::NotFound)?;
376        let table_position = index.table;
377        let range = index
378            .row_range(row)
379            .map(|r| (r.path.steps().to_vec(), r.first, r.count));
380        let rows_written = index.rows.last().map_or(0, |r| r.first + r.count);
381
382        let names = CellNames::of(&self.document);
383        let calcext = self.document.declares(&Ns::Calcext);
384
385        let table = self
386            .document
387            .content
388            .child_mut(&Ns::Office, "body")
389            .and_then(|body| body.child_mut(&Ns::Office, "spreadsheet"))
390            .and_then(|sheet| sheet.at_mut(&[table_position]))
391            .ok_or(Refused::NotFound)?;
392
393        let row_element = reach_row(table, range.as_ref(), row, rows_written, &names)?;
394        let cell_index = reach_cell(row_element, column, &names);
395        let cell = row_element.at_mut(&[cell_index]).ok_or(Refused::NotFound)?;
396        write_value(cell, value, &names, calcext);
397        self.reindex();
398        Ok(())
399    }
400
401    /// The `table:table` element of a sheet.
402    fn table(&self, sheet: &Sheet) -> Option<&Element> {
403        let body = self.document.body_of("spreadsheet")?;
404        body.children.get(sheet.table).and_then(|node| match node {
405            crate::xml::Node::Element(e) => Some(e),
406            _ => None,
407        })
408    }
409
410    /// One cell, by sheet, row and column, all counting from zero.
411    ///
412    /// `None` for a cell the document never wrote, which is the usual answer
413    /// past the edge of the used range and means an empty cell rather than an
414    /// error.
415    pub fn cell(&self, sheet: &Sheet, row: usize, column: usize) -> Option<Cell<'_>> {
416        cell_in_row(self.row_element(sheet, row)?, column)
417    }
418
419    /// The `table:table-row` element a row number falls in.
420    pub fn row_element(&self, sheet: &Sheet, row: usize) -> Option<&Element> {
421        let table = self.table(sheet)?;
422        let range = sheet.row_range(row)?;
423        let mut element = table;
424        for step in range.path.steps() {
425            let crate::xml::Node::Element(child) = element.children.get(*step)? else {
426                return None;
427            };
428            element = child;
429        }
430        Some(element)
431    }
432
433    /// A row's height, where its style gives one.
434    pub fn row_height(&self, sheet: &Sheet, row: usize) -> Option<Length> {
435        let name = self
436            .row_element(sheet, row)?
437            .attr(&Ns::Table, "style-name")?;
438        self.document
439            .styles
440            .resolve(&Family::TableRow, name)
441            .row_height
442    }
443
444    /// The resolved style of a cell: the style it names, or the one its column
445    /// gives every cell that names none.
446    pub fn cell_style(
447        &self,
448        sheet: &Sheet,
449        cell: Option<&Cell<'_>>,
450        column: usize,
451    ) -> std::rc::Rc<Properties> {
452        let named = cell.and_then(Cell::style_name);
453        let from_column = sheet
454            .columns
455            .get(column)
456            .and_then(|c| c.default_cell_style.as_deref());
457        let name = named.or(from_column).unwrap_or("Default");
458        self.document.styles.resolve(&Family::TableCell, name)
459    }
460}
461
462impl Sheet {
463    /// The run of rows a row number falls in.
464    fn row_range(&self, row: usize) -> Option<&RowRange> {
465        let found = self
466            .rows
467            .binary_search_by(|range| {
468                if row < range.first {
469                    std::cmp::Ordering::Greater
470                } else if row >= range.first + range.count {
471                    std::cmp::Ordering::Less
472                } else {
473                    std::cmp::Ordering::Equal
474                }
475            })
476            .ok()?;
477        self.rows.get(found)
478    }
479
480    /// The width of a column, where its column style gives one.
481    pub fn column_width(&self, column: usize) -> Option<Length> {
482        self.columns.get(column).and_then(|c| c.width)
483    }
484}
485
486/// The names a cell write spells, in the document's own prefixes.
487struct CellNames {
488    row: Name,
489    cell: Name,
490    rows_repeated: Name,
491    columns_repeated: Name,
492    value_type: Name,
493    value: Name,
494    boolean_value: Name,
495    date_value: Name,
496    time_value: Name,
497    currency: Name,
498    calcext_value_type: Name,
499    paragraph: Name,
500}
501
502impl CellNames {
503    fn of(document: &Document) -> Self {
504        Self {
505            row: document.name(&Ns::Table, "table-row"),
506            cell: document.name(&Ns::Table, "table-cell"),
507            rows_repeated: document.name(&Ns::Table, "number-rows-repeated"),
508            columns_repeated: document.name(&Ns::Table, "number-columns-repeated"),
509            value_type: document.name(&Ns::Office, "value-type"),
510            value: document.name(&Ns::Office, "value"),
511            boolean_value: document.name(&Ns::Office, "boolean-value"),
512            date_value: document.name(&Ns::Office, "date-value"),
513            time_value: document.name(&Ns::Office, "time-value"),
514            currency: document.name(&Ns::Office, "currency"),
515            calcext_value_type: document.name(&Ns::Calcext, "value-type"),
516            paragraph: document.name(&Ns::Text, "p"),
517        }
518    }
519}
520
521fn empty_cell(names: &CellNames) -> Element {
522    let mut cell = Element::new("", "table-cell", Ns::Table);
523    cell.name = names.cell.clone();
524    cell
525}
526
527/// The row element for a row number, standing alone: split out of the run it
528/// was written in, or created past the end of the table with a repeated empty
529/// row filling the gap.
530fn reach_row<'a>(
531    table: &'a mut Element,
532    range: Option<&(Vec<usize>, usize, usize)>,
533    row: usize,
534    rows_written: usize,
535    names: &CellNames,
536) -> Result<&'a mut Element, Refused> {
537    let Some((steps, first, count)) = range else {
538        let gap = row - rows_written;
539        if gap > 0 {
540            let mut filler = empty_row(names);
541            if gap > 1 {
542                filler.set_attr(names.rows_repeated.clone(), gap.to_string());
543            }
544            table.children.push(Node::Element(filler));
545        }
546        table.children.push(Node::Element(empty_row(names)));
547        let last = table.children.len() - 1;
548        return table.at_mut(&[last]).ok_or(Refused::NotFound);
549    };
550    let (last, above) = steps.split_last().ok_or(Refused::NotFound)?;
551    let parent = table.at_mut(above).ok_or(Refused::NotFound)?;
552    let at = split_run(parent, *last, row - first, *count, &names.rows_repeated);
553    parent.at_mut(&[at]).ok_or(Refused::NotFound)
554}
555
556/// The index in a row of the cell element for a column, standing alone: split
557/// out of its run, or appended with a repeated empty cell filling the gap.
558fn reach_cell(row: &mut Element, column: usize, names: &CellNames) -> usize {
559    // Found first and split after, because the split borrows the row.
560    let mut at = 0usize;
561    let mut found = None;
562    for (index, child) in row.elements_indexed() {
563        if !child.is(&Ns::Table, "table-cell") && !child.is(&Ns::Table, "covered-table-cell") {
564            continue;
565        }
566        let repeat = child
567            .attr_usize(&Ns::Table, "number-columns-repeated")
568            .unwrap_or(1)
569            .max(1);
570        if column < at + repeat {
571            found = Some((index, column - at, repeat));
572            break;
573        }
574        at += repeat;
575    }
576    if let Some((index, offset, repeat)) = found {
577        return split_run(row, index, offset, repeat, &names.columns_repeated);
578    }
579    let gap = column - at;
580    if gap > 0 {
581        let mut filler = empty_cell(names);
582        if gap > 1 {
583            filler.set_attr(names.columns_repeated.clone(), gap.to_string());
584        }
585        row.children.push(Node::Element(filler));
586    }
587    row.children.push(Node::Element(empty_cell(names)));
588    row.self_closing = false;
589    row.children.len() - 1
590}
591
592/// A row holding one empty cell, which is the least a row may hold.
593fn empty_row(names: &CellNames) -> Element {
594    let mut row = Element::new("", "table-row", Ns::Table);
595    row.name = names.row.clone();
596    row.children.push(Node::Element(empty_cell(names)));
597    row.self_closing = false;
598    row
599}
600
601/// Split the repeated element at `index` in `parent` so that the `offset`th of
602/// its `repeat` copies stands alone, and answer where it now is.
603///
604/// The copies before and after keep the element's attributes and children
605/// with their counts fixed, so the run reads the same as before at every
606/// position but the one.
607fn split_run(
608    parent: &mut Element,
609    index: usize,
610    offset: usize,
611    repeat: usize,
612    repeated: &Name,
613) -> usize {
614    if repeat <= 1 {
615        return index;
616    }
617    let Some(Node::Element(original)) = parent.children.get(index) else {
618        return index;
619    };
620    let one = {
621        let mut one = original.clone();
622        one.remove_attr(&repeated.ns, &repeated.local);
623        one
624    };
625    let mut replacement = Vec::with_capacity(3);
626    let before = offset;
627    let after = repeat - offset - 1;
628    if before > 0 {
629        replacement.push(Node::Element(with_count(
630            original.clone(),
631            before,
632            repeated,
633        )));
634    }
635    replacement.push(Node::Element(one));
636    if after > 0 {
637        replacement.push(Node::Element(with_count(original.clone(), after, repeated)));
638    }
639    parent.children.splice(index..=index, replacement);
640    index + usize::from(before > 0)
641}
642
643fn with_count(mut element: Element, count: usize, repeated: &Name) -> Element {
644    if count > 1 {
645        element.set_attr(repeated.clone(), count.to_string());
646    } else {
647        element.remove_attr(&repeated.ns, &repeated.local);
648    }
649    element
650}
651
652/// Put a value into a cell element: the typed attributes and the displayed
653/// paragraphs replaced, the style and everything else kept.
654fn write_value(cell: &mut Element, value: &Value, names: &CellNames, calcext: bool) {
655    for local in [
656        "value-type",
657        "value",
658        "boolean-value",
659        "date-value",
660        "time-value",
661        "string-value",
662        "currency",
663    ] {
664        cell.remove_attr(&Ns::Office, local);
665    }
666    cell.remove_attr(&Ns::Calcext, "value-type");
667    cell.remove_attr(&Ns::Table, "formula");
668    cell.children
669        .retain(|node| !matches!(node, Node::Element(e) if e.is(&Ns::Text, "p")));
670
671    let value_type = match value {
672        Value::Empty => None,
673        Value::Number(_) => Some("float"),
674        Value::Percentage(_) => Some("percentage"),
675        Value::Currency(..) => Some("currency"),
676        Value::Date(_) => Some("date"),
677        Value::Time(_) => Some("time"),
678        Value::Boolean(_) => Some("boolean"),
679        Value::Text(_) => Some("string"),
680    };
681    if let Some(value_type) = value_type {
682        cell.set_attr(names.value_type.clone(), value_type);
683        // LibreOffice writes its own copy of the type in every cell and reads
684        // a cell without one fine; it is written where the document declares
685        // the namespace, and a document that never heard of it is left alone.
686        if calcext {
687            cell.set_attr(names.calcext_value_type.clone(), value_type);
688        }
689    }
690    match value {
691        Value::Empty | Value::Text(_) => {}
692        Value::Number(n) | Value::Percentage(n) => {
693            cell.set_attr(names.value.clone(), n.to_string());
694        }
695        Value::Currency(n, code) => {
696            cell.set_attr(names.value.clone(), n.to_string());
697            if let Some(code) = code {
698                cell.set_attr(names.currency.clone(), code.clone());
699            }
700        }
701        Value::Date(s) => cell.set_attr(names.date_value.clone(), s.clone()),
702        Value::Time(s) => cell.set_attr(names.time_value.clone(), s.clone()),
703        Value::Boolean(b) => cell.set_attr(names.boolean_value.clone(), b.to_string()),
704    }
705    let text = value.cached_text();
706    if !text.is_empty() {
707        for line in text.split('\n') {
708            let mut paragraph = Element::new("", "p", Ns::Text);
709            paragraph.name = names.paragraph.clone();
710            if !line.is_empty() {
711                paragraph.children.push(Node::Text(line.to_owned()));
712                paragraph.self_closing = false;
713            }
714            cell.children.push(Node::Element(paragraph));
715        }
716    }
717    cell.self_closing = cell.children.is_empty();
718    // A paragraph that is inserted plain may hold runs of spaces or a tab; it
719    // is written the way ODF requires like any other.
720    for node in &mut cell.children {
721        if let Node::Element(e) = node
722            && e.is(&Ns::Text, "p")
723        {
724            crate::edit::replace(e, 0..0, "");
725        }
726    }
727}
728
729/// Find a cell by column number inside a row, stepping over repeat counts.
730fn cell_in_row(row: &Element, column: usize) -> Option<Cell<'_>> {
731    let mut at = 0usize;
732    for child in row.elements() {
733        let covered = child.is(&Ns::Table, "covered-table-cell");
734        if !covered && !child.is(&Ns::Table, "table-cell") {
735            continue;
736        }
737        let repeat = child
738            .attr_usize(&Ns::Table, "number-columns-repeated")
739            .unwrap_or(1)
740            .max(1);
741        if column < at + repeat {
742            return Some(Cell {
743                element: child,
744                covered,
745            });
746        }
747        at += repeat;
748    }
749    None
750}
751
752/// Build the row and column index of every sheet in the document.
753fn index_sheets(document: &Document) -> Vec<Sheet> {
754    let Some(body) = document.body_of("spreadsheet") else {
755        return Vec::new();
756    };
757    let mut sheets = Vec::new();
758    for (position, node) in body.children.iter().enumerate() {
759        let crate::xml::Node::Element(table) = node else {
760            continue;
761        };
762        if !table.is(&Ns::Table, "table") {
763            continue;
764        }
765        sheets.push(index_sheet(document, table, position));
766    }
767    sheets
768}
769
770fn index_sheet(document: &Document, table: &Element, position: usize) -> Sheet {
771    let mut index = Index {
772        document,
773        rows: Vec::new(),
774        columns: Vec::new(),
775        at_row: 0,
776        used_rows: 0,
777        used_columns: 0,
778    };
779    index.walk(table, &mut Vec::new());
780
781    Sheet {
782        name: table
783            .attr(&Ns::Table, "name")
784            .unwrap_or_default()
785            .to_owned(),
786        columns: index.columns,
787        used_rows: index.used_rows,
788        used_columns: index.used_columns,
789        visible: table.attr(&Ns::Table, "display").unwrap_or("true") != "false",
790        rows: index.rows,
791        table: position,
792    }
793}
794
795/// The state of one sheet's indexing pass.
796struct Index<'a> {
797    document: &'a Document,
798    rows: Vec<RowRange>,
799    columns: Vec<Column>,
800    at_row: usize,
801    used_rows: usize,
802    used_columns: usize,
803}
804
805impl Index<'_> {
806    /// Collect the rows and columns under an element, descending through the
807    /// containers that hold them.
808    ///
809    /// `path` is the route from the table to whatever is being walked, and is the
810    /// route a lookup will take back.
811    fn walk(&mut self, parent: &Element, path: &mut Vec<usize>) {
812        for (child_index, child) in parent.children.iter().enumerate() {
813            let crate::xml::Node::Element(element) = child else {
814                continue;
815            };
816
817            if element.is(&Ns::Table, "table-column") {
818                self.column(element);
819            } else if element.is(&Ns::Table, "table-row") {
820                path.push(child_index);
821                self.row(element, path);
822                path.pop();
823            } else if is_row_container(element) || is_column_container(element) {
824                // A header band or an outline group holds rows and columns that
825                // belong to the sheet as if they were the table's own. The
826                // grouping itself is what a view would draw a collapse handle
827                // for, and the tree still carries it.
828                path.push(child_index);
829                self.walk(element, path);
830                path.pop();
831            }
832        }
833    }
834
835    fn column(&mut self, element: &Element) {
836        let repeat = element
837            .attr_usize(&Ns::Table, "number-columns-repeated")
838            .unwrap_or(1)
839            .max(1);
840        let width = element
841            .attr(&Ns::Table, "style-name")
842            .map(|name| self.document.styles.resolve(&Family::TableColumn, name))
843            .and_then(|p| p.column_width);
844        let default_cell_style = element
845            .attr(&Ns::Table, "default-cell-style-name")
846            .map(ToOwned::to_owned);
847        let visible = element.attr(&Ns::Table, "visibility").unwrap_or("visible") == "visible";
848        // A trailing column run covering the whole sheet is ordinary, and
849        // expanding it is what this index exists to avoid. The run is kept only
850        // as far as ODF permits a column to exist; past that the document is
851        // saying *the rest of the sheet* and the last entry answers for all of it.
852        let keep = repeat.min(MAX_COLUMNS.saturating_sub(self.columns.len()));
853        for _ in 0..keep {
854            self.columns.push(Column {
855                width,
856                default_cell_style: default_cell_style.clone(),
857                visible,
858            });
859        }
860    }
861
862    fn row(&mut self, element: &Element, path: &[usize]) {
863        let repeat = element
864            .attr_usize(&Ns::Table, "number-rows-repeated")
865            .unwrap_or(1)
866            .max(1);
867        if let Some(last) = last_occupied_column(element) {
868            self.used_rows = self.at_row + repeat;
869            self.used_columns = self.used_columns.max(last + 1);
870        }
871        self.rows.push(RowRange {
872            first: self.at_row,
873            count: repeat,
874            path: RowPath::of(path),
875        });
876        self.at_row += repeat;
877    }
878}
879
880impl RowPath {
881    /// The route to a row, taking the cheap form where it is a child of the
882    /// table, which is what a sheet with no grouping gives for every row.
883    fn of(path: &[usize]) -> Self {
884        match path {
885            [only] => Self::Direct(*only),
886            nested => Self::Nested(nested.into()),
887        }
888    }
889
890    /// The child indices to follow, from the table down to the row.
891    fn steps(&self) -> &[usize] {
892        match self {
893            Self::Direct(only) => std::slice::from_ref(only),
894            Self::Nested(path) => path,
895        }
896    }
897}
898
899/// Whether an element holds rows on the sheet's behalf.
900fn is_row_container(element: &Element) -> bool {
901    element.is(&Ns::Table, "table-rows")
902        || element.is(&Ns::Table, "table-header-rows")
903        || element.is(&Ns::Table, "table-row-group")
904}
905
906/// Whether an element holds columns on the sheet's behalf.
907fn is_column_container(element: &Element) -> bool {
908    element.is(&Ns::Table, "table-columns")
909        || element.is(&Ns::Table, "table-header-columns")
910        || element.is(&Ns::Table, "table-column-group")
911}
912
913/// ODF's own column limit, and the point past which a repeat count is a way of
914/// saying *the rest of the sheet*.
915const MAX_COLUMNS: usize = 16_384;
916
917/// The last column in a row that carries anything, or `None` for an empty row.
918fn last_occupied_column(row: &Element) -> Option<usize> {
919    let mut at = 0usize;
920    let mut last = None;
921    for child in row.elements() {
922        let covered = child.is(&Ns::Table, "covered-table-cell");
923        if !covered && !child.is(&Ns::Table, "table-cell") {
924            continue;
925        }
926        let repeat = child
927            .attr_usize(&Ns::Table, "number-columns-repeated")
928            .unwrap_or(1)
929            .max(1);
930        let cell = Cell {
931            element: child,
932            covered,
933        };
934        if cell.occupied() {
935            last = Some(at + repeat - 1);
936        }
937        at += repeat;
938    }
939    last
940}