1use super::Document;
14use crate::media_type;
15use crate::value::Length;
16use crate::xml::{Element, Ns};
17use crate::{Error, Family, Properties};
18
19pub struct SheetDocument {
21 pub document: Document,
23 sheets: Vec<Sheet>,
24}
25
26pub struct Sheet {
28 pub name: String,
30 pub columns: Vec<Column>,
34 pub used_rows: usize,
37 pub used_columns: usize,
39 pub visible: bool,
41 rows: Vec<RowRange>,
42 table: usize,
45}
46
47pub struct Column {
49 pub width: Option<Length>,
51 pub default_cell_style: Option<String>,
53 pub visible: bool,
55}
56
57struct RowRange {
59 first: usize,
61 count: usize,
63 path: RowPath,
65}
66
67enum RowPath {
75 Direct(usize),
77 Nested(Box<[usize]>),
79}
80
81#[derive(Debug, Clone, PartialEq)]
87pub enum Value {
88 Empty,
90 Number(f64),
92 Percentage(f64),
94 Currency(f64, Option<String>),
96 Date(String),
98 Time(String),
100 Boolean(bool),
102 Text(String),
104}
105
106impl Value {
107 pub fn is_numeric(&self) -> bool {
111 matches!(
112 self,
113 Self::Number(_)
114 | Self::Percentage(_)
115 | Self::Currency(..)
116 | Self::Date(_)
117 | Self::Time(_)
118 )
119 }
120}
121
122pub struct Cell<'a> {
124 pub element: &'a Element,
127 pub covered: bool,
131}
132
133impl Cell<'_> {
134 pub fn value(&self) -> Value {
136 let e = self.element;
137 match e.attr(&Ns::Office, "value-type") {
138 Some("float") => e
139 .attr(&Ns::Office, "value")
140 .and_then(|v| v.parse().ok())
141 .map_or(Value::Empty, Value::Number),
142 Some("percentage") => e
143 .attr(&Ns::Office, "value")
144 .and_then(|v| v.parse().ok())
145 .map_or(Value::Empty, Value::Percentage),
146 Some("currency") => e
147 .attr(&Ns::Office, "value")
148 .and_then(|v| v.parse().ok())
149 .map_or(Value::Empty, |amount| {
150 Value::Currency(
151 amount,
152 e.attr(&Ns::Office, "currency").map(ToOwned::to_owned),
153 )
154 }),
155 Some("date") => e
156 .attr(&Ns::Office, "date-value")
157 .map_or(Value::Empty, |v| Value::Date(v.to_owned())),
158 Some("time") => e
159 .attr(&Ns::Office, "time-value")
160 .map_or(Value::Empty, |v| Value::Time(v.to_owned())),
161 Some("boolean") => e
162 .attr(&Ns::Office, "boolean-value")
163 .and_then(crate::value::boolean)
164 .map_or(Value::Empty, Value::Boolean),
165 Some("string") => match e.attr(&Ns::Office, "string-value") {
169 Some(text) => Value::Text(text.to_owned()),
170 None => Value::Text(self.text()),
171 },
172 _ => {
173 let text = self.text();
174 if text.is_empty() {
175 Value::Empty
176 } else {
177 Value::Text(text)
178 }
179 }
180 }
181 }
182
183 pub fn text(&self) -> String {
196 let mut out = String::new();
197 for paragraph in self.element.elements() {
198 if paragraph.is(&Ns::Text, "p") {
199 if !out.is_empty() {
200 out.push('\n');
201 }
202 out.push_str(¶graph.plain_text());
203 }
204 }
205 out
206 }
207
208 pub fn formula(&self) -> Option<&str> {
215 let formula = self.element.attr(&Ns::Table, "formula")?;
216 Some(match formula.split_once(":=") {
217 Some((_, expression)) => expression,
218 None => formula,
219 })
220 }
221
222 pub fn style_name(&self) -> Option<&str> {
224 self.element.attr(&Ns::Table, "style-name")
225 }
226
227 pub fn columns_spanned(&self) -> usize {
229 self.element
230 .attr_usize(&Ns::Table, "number-columns-spanned")
231 .unwrap_or(1)
232 .max(1)
233 }
234
235 pub fn rows_spanned(&self) -> usize {
237 self.element
238 .attr_usize(&Ns::Table, "number-rows-spanned")
239 .unwrap_or(1)
240 .max(1)
241 }
242
243 fn occupied(&self) -> bool {
246 self.element.attr(&Ns::Office, "value-type").is_some()
247 || self.element.attr(&Ns::Table, "formula").is_some()
248 || self.element.elements().any(|e| e.is(&Ns::Text, "p"))
249 }
250}
251
252impl SheetDocument {
253 pub fn read(bytes: &[u8]) -> Result<Self, Error> {
259 let document = Document::read(bytes, media_type::SPREADSHEET_ANY)?;
260 let sheets = index_sheets(&document);
261 Ok(Self { document, sheets })
262 }
263
264 pub fn sheets(&self) -> &[Sheet] {
266 &self.sheets
267 }
268
269 fn table(&self, sheet: &Sheet) -> Option<&Element> {
271 let body = self.document.body_of("spreadsheet")?;
272 body.children.get(sheet.table).and_then(|node| match node {
273 crate::xml::Node::Element(e) => Some(e),
274 _ => None,
275 })
276 }
277
278 pub fn cell(&self, sheet: &Sheet, row: usize, column: usize) -> Option<Cell<'_>> {
284 cell_in_row(self.row_element(sheet, row)?, column)
285 }
286
287 pub fn row_element(&self, sheet: &Sheet, row: usize) -> Option<&Element> {
289 let table = self.table(sheet)?;
290 let range = sheet.row_range(row)?;
291 let mut element = table;
292 for step in range.path.steps() {
293 let crate::xml::Node::Element(child) = element.children.get(*step)? else {
294 return None;
295 };
296 element = child;
297 }
298 Some(element)
299 }
300
301 pub fn row_height(&self, sheet: &Sheet, row: usize) -> Option<Length> {
303 let name = self
304 .row_element(sheet, row)?
305 .attr(&Ns::Table, "style-name")?;
306 self.document
307 .styles
308 .resolve(&Family::TableRow, name)
309 .row_height
310 }
311
312 pub fn cell_style(
315 &self,
316 sheet: &Sheet,
317 cell: Option<&Cell<'_>>,
318 column: usize,
319 ) -> std::rc::Rc<Properties> {
320 let named = cell.and_then(Cell::style_name);
321 let from_column = sheet
322 .columns
323 .get(column)
324 .and_then(|c| c.default_cell_style.as_deref());
325 let name = named.or(from_column).unwrap_or("Default");
326 self.document.styles.resolve(&Family::TableCell, name)
327 }
328}
329
330impl Sheet {
331 fn row_range(&self, row: usize) -> Option<&RowRange> {
333 let found = self
334 .rows
335 .binary_search_by(|range| {
336 if row < range.first {
337 std::cmp::Ordering::Greater
338 } else if row >= range.first + range.count {
339 std::cmp::Ordering::Less
340 } else {
341 std::cmp::Ordering::Equal
342 }
343 })
344 .ok()?;
345 self.rows.get(found)
346 }
347
348 pub fn column_width(&self, column: usize) -> Option<Length> {
350 self.columns.get(column).and_then(|c| c.width)
351 }
352}
353
354fn cell_in_row(row: &Element, column: usize) -> Option<Cell<'_>> {
356 let mut at = 0usize;
357 for child in row.elements() {
358 let covered = child.is(&Ns::Table, "covered-table-cell");
359 if !covered && !child.is(&Ns::Table, "table-cell") {
360 continue;
361 }
362 let repeat = child
363 .attr_usize(&Ns::Table, "number-columns-repeated")
364 .unwrap_or(1)
365 .max(1);
366 if column < at + repeat {
367 return Some(Cell {
368 element: child,
369 covered,
370 });
371 }
372 at += repeat;
373 }
374 None
375}
376
377fn index_sheets(document: &Document) -> Vec<Sheet> {
379 let Some(body) = document.body_of("spreadsheet") else {
380 return Vec::new();
381 };
382 let mut sheets = Vec::new();
383 for (position, node) in body.children.iter().enumerate() {
384 let crate::xml::Node::Element(table) = node else {
385 continue;
386 };
387 if !table.is(&Ns::Table, "table") {
388 continue;
389 }
390 sheets.push(index_sheet(document, table, position));
391 }
392 sheets
393}
394
395fn index_sheet(document: &Document, table: &Element, position: usize) -> Sheet {
396 let mut index = Index {
397 document,
398 rows: Vec::new(),
399 columns: Vec::new(),
400 at_row: 0,
401 used_rows: 0,
402 used_columns: 0,
403 };
404 index.walk(table, &mut Vec::new());
405
406 Sheet {
407 name: table
408 .attr(&Ns::Table, "name")
409 .unwrap_or_default()
410 .to_owned(),
411 columns: index.columns,
412 used_rows: index.used_rows,
413 used_columns: index.used_columns,
414 visible: table.attr(&Ns::Table, "display").unwrap_or("true") != "false",
415 rows: index.rows,
416 table: position,
417 }
418}
419
420struct Index<'a> {
422 document: &'a Document,
423 rows: Vec<RowRange>,
424 columns: Vec<Column>,
425 at_row: usize,
426 used_rows: usize,
427 used_columns: usize,
428}
429
430impl Index<'_> {
431 fn walk(&mut self, parent: &Element, path: &mut Vec<usize>) {
437 for (child_index, child) in parent.children.iter().enumerate() {
438 let crate::xml::Node::Element(element) = child else {
439 continue;
440 };
441
442 if element.is(&Ns::Table, "table-column") {
443 self.column(element);
444 } else if element.is(&Ns::Table, "table-row") {
445 path.push(child_index);
446 self.row(element, path);
447 path.pop();
448 } else if is_row_container(element) || is_column_container(element) {
449 path.push(child_index);
454 self.walk(element, path);
455 path.pop();
456 }
457 }
458 }
459
460 fn column(&mut self, element: &Element) {
461 let repeat = element
462 .attr_usize(&Ns::Table, "number-columns-repeated")
463 .unwrap_or(1)
464 .max(1);
465 let width = element
466 .attr(&Ns::Table, "style-name")
467 .map(|name| self.document.styles.resolve(&Family::TableColumn, name))
468 .and_then(|p| p.column_width);
469 let default_cell_style = element
470 .attr(&Ns::Table, "default-cell-style-name")
471 .map(ToOwned::to_owned);
472 let visible = element.attr(&Ns::Table, "visibility").unwrap_or("visible") == "visible";
473 let keep = repeat.min(MAX_COLUMNS.saturating_sub(self.columns.len()));
478 for _ in 0..keep {
479 self.columns.push(Column {
480 width,
481 default_cell_style: default_cell_style.clone(),
482 visible,
483 });
484 }
485 }
486
487 fn row(&mut self, element: &Element, path: &[usize]) {
488 let repeat = element
489 .attr_usize(&Ns::Table, "number-rows-repeated")
490 .unwrap_or(1)
491 .max(1);
492 if let Some(last) = last_occupied_column(element) {
493 self.used_rows = self.at_row + repeat;
494 self.used_columns = self.used_columns.max(last + 1);
495 }
496 self.rows.push(RowRange {
497 first: self.at_row,
498 count: repeat,
499 path: RowPath::of(path),
500 });
501 self.at_row += repeat;
502 }
503}
504
505impl RowPath {
506 fn of(path: &[usize]) -> Self {
509 match path {
510 [only] => Self::Direct(*only),
511 nested => Self::Nested(nested.into()),
512 }
513 }
514
515 fn steps(&self) -> &[usize] {
517 match self {
518 Self::Direct(only) => std::slice::from_ref(only),
519 Self::Nested(path) => path,
520 }
521 }
522}
523
524fn is_row_container(element: &Element) -> bool {
526 element.is(&Ns::Table, "table-rows")
527 || element.is(&Ns::Table, "table-header-rows")
528 || element.is(&Ns::Table, "table-row-group")
529}
530
531fn is_column_container(element: &Element) -> bool {
533 element.is(&Ns::Table, "table-columns")
534 || element.is(&Ns::Table, "table-header-columns")
535 || element.is(&Ns::Table, "table-column-group")
536}
537
538const MAX_COLUMNS: usize = 16_384;
541
542fn last_occupied_column(row: &Element) -> Option<usize> {
544 let mut at = 0usize;
545 let mut last = None;
546 for child in row.elements() {
547 let covered = child.is(&Ns::Table, "covered-table-cell");
548 if !covered && !child.is(&Ns::Table, "table-cell") {
549 continue;
550 }
551 let repeat = child
552 .attr_usize(&Ns::Table, "number-columns-repeated")
553 .unwrap_or(1)
554 .max(1);
555 let cell = Cell {
556 element: child,
557 covered,
558 };
559 if cell.occupied() {
560 last = Some(at + repeat - 1);
561 }
562 at += repeat;
563 }
564 last
565}