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