use super::Document;
use crate::edit::Refused;
use crate::media_type;
use crate::value::Length;
use crate::xml::{Element, Name, Node, Ns};
use crate::{Error, Family, Properties};
pub struct SheetDocument {
pub document: Document,
sheets: Vec<Sheet>,
}
pub struct Sheet {
pub name: String,
pub columns: Vec<Column>,
pub used_rows: usize,
pub used_columns: usize,
pub visible: bool,
rows: Vec<RowRange>,
table: usize,
}
pub struct Column {
pub width: Option<Length>,
pub default_cell_style: Option<String>,
pub visible: bool,
}
struct RowRange {
first: usize,
count: usize,
path: RowPath,
}
enum RowPath {
Direct(usize),
Nested(Box<[usize]>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Empty,
Number(f64),
Percentage(f64),
Currency(f64, Option<String>),
Date(String),
Time(String),
Boolean(bool),
Text(String),
}
impl Value {
pub fn from_input(input: &str) -> Self {
let trimmed = input.trim();
if trimmed.is_empty() {
return Self::Empty;
}
if trimmed.eq_ignore_ascii_case("true") {
return Self::Boolean(true);
}
if trimmed.eq_ignore_ascii_case("false") {
return Self::Boolean(false);
}
if let Ok(number) = trimmed.parse::<f64>()
&& number.is_finite()
&& !trimmed.contains(['i', 'n', 'I', 'N'])
{
return Self::Number(number);
}
Self::Text(input.to_owned())
}
pub fn input_text(&self) -> String {
match self {
Self::Percentage(n) => n.to_string(),
_ => self.cached_text(),
}
}
pub fn cached_text(&self) -> String {
match self {
Self::Empty => String::new(),
Self::Number(n) | Self::Currency(n, _) => n.to_string(),
Self::Percentage(n) => format!("{}%", n * 100.0),
Self::Date(s) | Self::Time(s) | Self::Text(s) => s.clone(),
Self::Boolean(true) => "TRUE".to_owned(),
Self::Boolean(false) => "FALSE".to_owned(),
}
}
pub fn is_numeric(&self) -> bool {
matches!(
self,
Self::Number(_)
| Self::Percentage(_)
| Self::Currency(..)
| Self::Date(_)
| Self::Time(_)
)
}
}
pub struct Cell<'a> {
pub element: &'a Element,
pub covered: bool,
}
impl Cell<'_> {
pub fn value(&self) -> Value {
let e = self.element;
match e.attr(&Ns::Office, "value-type") {
Some("float") => e
.attr(&Ns::Office, "value")
.and_then(|v| v.parse().ok())
.map_or(Value::Empty, Value::Number),
Some("percentage") => e
.attr(&Ns::Office, "value")
.and_then(|v| v.parse().ok())
.map_or(Value::Empty, Value::Percentage),
Some("currency") => e
.attr(&Ns::Office, "value")
.and_then(|v| v.parse().ok())
.map_or(Value::Empty, |amount| {
Value::Currency(
amount,
e.attr(&Ns::Office, "currency").map(ToOwned::to_owned),
)
}),
Some("date") => e
.attr(&Ns::Office, "date-value")
.map_or(Value::Empty, |v| Value::Date(v.to_owned())),
Some("time") => e
.attr(&Ns::Office, "time-value")
.map_or(Value::Empty, |v| Value::Time(v.to_owned())),
Some("boolean") => e
.attr(&Ns::Office, "boolean-value")
.and_then(crate::value::boolean)
.map_or(Value::Empty, Value::Boolean),
Some("string") => match e.attr(&Ns::Office, "string-value") {
Some(text) => Value::Text(text.to_owned()),
None => Value::Text(self.text()),
},
_ => {
let text = self.text();
if text.is_empty() {
Value::Empty
} else {
Value::Text(text)
}
}
}
}
pub fn text(&self) -> String {
let mut out = String::new();
for paragraph in self.element.elements() {
if paragraph.is(&Ns::Text, "p") {
if !out.is_empty() {
out.push('\n');
}
out.push_str(¶graph.plain_text());
}
}
out
}
pub fn formula(&self) -> Option<&str> {
let formula = self.element.attr(&Ns::Table, "formula")?;
Some(match formula.split_once(":=") {
Some((_, expression)) => expression,
None => formula,
})
}
pub fn style_name(&self) -> Option<&str> {
self.element.attr(&Ns::Table, "style-name")
}
pub fn columns_spanned(&self) -> usize {
self.element
.attr_usize(&Ns::Table, "number-columns-spanned")
.unwrap_or(1)
.max(1)
}
pub fn rows_spanned(&self) -> usize {
self.element
.attr_usize(&Ns::Table, "number-rows-spanned")
.unwrap_or(1)
.max(1)
}
fn occupied(&self) -> bool {
self.element.attr(&Ns::Office, "value-type").is_some()
|| self.element.attr(&Ns::Table, "formula").is_some()
|| self.element.elements().any(|e| e.is(&Ns::Text, "p"))
}
}
impl SheetDocument {
pub fn read(bytes: &[u8]) -> Result<Self, Error> {
let document = Document::read(bytes, media_type::SPREADSHEET_ANY)?;
let sheets = index_sheets(&document);
Ok(Self { document, sheets })
}
pub fn sheets(&self) -> &[Sheet] {
&self.sheets
}
pub fn reindex(&mut self) {
self.sheets = index_sheets(&self.document);
}
pub fn can_edit(&self, sheet: usize, row: usize, column: usize) -> Result<(), Refused> {
let index = self.sheets.get(sheet).ok_or(Refused::NotFound)?;
if let Some(cell) = self.cell(index, row, column) {
if cell.covered {
return Err(Refused::Covered);
}
if cell.formula().is_some() {
return Err(Refused::Formula);
}
}
Ok(())
}
pub fn set_cell(
&mut self,
sheet: usize,
row: usize,
column: usize,
value: &Value,
) -> Result<(), Refused> {
self.can_edit(sheet, row, column)?;
let index = self.sheets.get(sheet).ok_or(Refused::NotFound)?;
let table_position = index.table;
let range = index
.row_range(row)
.map(|r| (r.path.steps().to_vec(), r.first, r.count));
let rows_written = index.rows.last().map_or(0, |r| r.first + r.count);
let names = CellNames::of(&self.document);
let calcext = self.document.declares(&Ns::Calcext);
let table = self
.document
.content
.child_mut(&Ns::Office, "body")
.and_then(|body| body.child_mut(&Ns::Office, "spreadsheet"))
.and_then(|sheet| sheet.at_mut(&[table_position]))
.ok_or(Refused::NotFound)?;
let row_element = reach_row(table, range.as_ref(), row, rows_written, &names)?;
let cell_index = reach_cell(row_element, column, &names);
let cell = row_element.at_mut(&[cell_index]).ok_or(Refused::NotFound)?;
write_value(cell, value, &names, calcext);
self.reindex();
Ok(())
}
fn table(&self, sheet: &Sheet) -> Option<&Element> {
let body = self.document.body_of("spreadsheet")?;
body.children.get(sheet.table).and_then(|node| match node {
crate::xml::Node::Element(e) => Some(e),
_ => None,
})
}
pub fn cell(&self, sheet: &Sheet, row: usize, column: usize) -> Option<Cell<'_>> {
cell_in_row(self.row_element(sheet, row)?, column)
}
pub fn row_element(&self, sheet: &Sheet, row: usize) -> Option<&Element> {
let table = self.table(sheet)?;
let range = sheet.row_range(row)?;
let mut element = table;
for step in range.path.steps() {
let crate::xml::Node::Element(child) = element.children.get(*step)? else {
return None;
};
element = child;
}
Some(element)
}
pub fn row_height(&self, sheet: &Sheet, row: usize) -> Option<Length> {
let name = self
.row_element(sheet, row)?
.attr(&Ns::Table, "style-name")?;
self.document
.styles
.resolve(&Family::TableRow, name)
.row_height
}
pub fn cell_style(
&self,
sheet: &Sheet,
cell: Option<&Cell<'_>>,
column: usize,
) -> std::rc::Rc<Properties> {
let named = cell.and_then(Cell::style_name);
let from_column = sheet
.columns
.get(column)
.and_then(|c| c.default_cell_style.as_deref());
let name = named.or(from_column).unwrap_or("Default");
self.document.styles.resolve(&Family::TableCell, name)
}
}
impl Sheet {
fn row_range(&self, row: usize) -> Option<&RowRange> {
let found = self
.rows
.binary_search_by(|range| {
if row < range.first {
std::cmp::Ordering::Greater
} else if row >= range.first + range.count {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Equal
}
})
.ok()?;
self.rows.get(found)
}
pub fn column_width(&self, column: usize) -> Option<Length> {
self.columns.get(column).and_then(|c| c.width)
}
}
struct CellNames {
row: Name,
cell: Name,
rows_repeated: Name,
columns_repeated: Name,
value_type: Name,
value: Name,
boolean_value: Name,
date_value: Name,
time_value: Name,
currency: Name,
calcext_value_type: Name,
paragraph: Name,
}
impl CellNames {
fn of(document: &Document) -> Self {
Self {
row: document.name(&Ns::Table, "table-row"),
cell: document.name(&Ns::Table, "table-cell"),
rows_repeated: document.name(&Ns::Table, "number-rows-repeated"),
columns_repeated: document.name(&Ns::Table, "number-columns-repeated"),
value_type: document.name(&Ns::Office, "value-type"),
value: document.name(&Ns::Office, "value"),
boolean_value: document.name(&Ns::Office, "boolean-value"),
date_value: document.name(&Ns::Office, "date-value"),
time_value: document.name(&Ns::Office, "time-value"),
currency: document.name(&Ns::Office, "currency"),
calcext_value_type: document.name(&Ns::Calcext, "value-type"),
paragraph: document.name(&Ns::Text, "p"),
}
}
}
fn empty_cell(names: &CellNames) -> Element {
let mut cell = Element::new("", "table-cell", Ns::Table);
cell.name = names.cell.clone();
cell
}
fn reach_row<'a>(
table: &'a mut Element,
range: Option<&(Vec<usize>, usize, usize)>,
row: usize,
rows_written: usize,
names: &CellNames,
) -> Result<&'a mut Element, Refused> {
let Some((steps, first, count)) = range else {
let gap = row - rows_written;
if gap > 0 {
let mut filler = empty_row(names);
if gap > 1 {
filler.set_attr(names.rows_repeated.clone(), gap.to_string());
}
table.children.push(Node::Element(filler));
}
table.children.push(Node::Element(empty_row(names)));
let last = table.children.len() - 1;
return table.at_mut(&[last]).ok_or(Refused::NotFound);
};
let (last, above) = steps.split_last().ok_or(Refused::NotFound)?;
let parent = table.at_mut(above).ok_or(Refused::NotFound)?;
let at = split_run(parent, *last, row - first, *count, &names.rows_repeated);
parent.at_mut(&[at]).ok_or(Refused::NotFound)
}
fn reach_cell(row: &mut Element, column: usize, names: &CellNames) -> usize {
let mut at = 0usize;
let mut found = None;
for (index, child) in row.elements_indexed() {
if !child.is(&Ns::Table, "table-cell") && !child.is(&Ns::Table, "covered-table-cell") {
continue;
}
let repeat = child
.attr_usize(&Ns::Table, "number-columns-repeated")
.unwrap_or(1)
.max(1);
if column < at + repeat {
found = Some((index, column - at, repeat));
break;
}
at += repeat;
}
if let Some((index, offset, repeat)) = found {
return split_run(row, index, offset, repeat, &names.columns_repeated);
}
let gap = column - at;
if gap > 0 {
let mut filler = empty_cell(names);
if gap > 1 {
filler.set_attr(names.columns_repeated.clone(), gap.to_string());
}
row.children.push(Node::Element(filler));
}
row.children.push(Node::Element(empty_cell(names)));
row.self_closing = false;
row.children.len() - 1
}
fn empty_row(names: &CellNames) -> Element {
let mut row = Element::new("", "table-row", Ns::Table);
row.name = names.row.clone();
row.children.push(Node::Element(empty_cell(names)));
row.self_closing = false;
row
}
fn split_run(
parent: &mut Element,
index: usize,
offset: usize,
repeat: usize,
repeated: &Name,
) -> usize {
if repeat <= 1 {
return index;
}
let Some(Node::Element(original)) = parent.children.get(index) else {
return index;
};
let one = {
let mut one = original.clone();
one.remove_attr(&repeated.ns, &repeated.local);
one
};
let mut replacement = Vec::with_capacity(3);
let before = offset;
let after = repeat - offset - 1;
if before > 0 {
replacement.push(Node::Element(with_count(
original.clone(),
before,
repeated,
)));
}
replacement.push(Node::Element(one));
if after > 0 {
replacement.push(Node::Element(with_count(original.clone(), after, repeated)));
}
parent.children.splice(index..=index, replacement);
index + usize::from(before > 0)
}
fn with_count(mut element: Element, count: usize, repeated: &Name) -> Element {
if count > 1 {
element.set_attr(repeated.clone(), count.to_string());
} else {
element.remove_attr(&repeated.ns, &repeated.local);
}
element
}
fn write_value(cell: &mut Element, value: &Value, names: &CellNames, calcext: bool) {
for local in [
"value-type",
"value",
"boolean-value",
"date-value",
"time-value",
"string-value",
"currency",
] {
cell.remove_attr(&Ns::Office, local);
}
cell.remove_attr(&Ns::Calcext, "value-type");
cell.remove_attr(&Ns::Table, "formula");
cell.children
.retain(|node| !matches!(node, Node::Element(e) if e.is(&Ns::Text, "p")));
let value_type = match value {
Value::Empty => None,
Value::Number(_) => Some("float"),
Value::Percentage(_) => Some("percentage"),
Value::Currency(..) => Some("currency"),
Value::Date(_) => Some("date"),
Value::Time(_) => Some("time"),
Value::Boolean(_) => Some("boolean"),
Value::Text(_) => Some("string"),
};
if let Some(value_type) = value_type {
cell.set_attr(names.value_type.clone(), value_type);
if calcext {
cell.set_attr(names.calcext_value_type.clone(), value_type);
}
}
match value {
Value::Empty | Value::Text(_) => {}
Value::Number(n) | Value::Percentage(n) => {
cell.set_attr(names.value.clone(), n.to_string());
}
Value::Currency(n, code) => {
cell.set_attr(names.value.clone(), n.to_string());
if let Some(code) = code {
cell.set_attr(names.currency.clone(), code.clone());
}
}
Value::Date(s) => cell.set_attr(names.date_value.clone(), s.clone()),
Value::Time(s) => cell.set_attr(names.time_value.clone(), s.clone()),
Value::Boolean(b) => cell.set_attr(names.boolean_value.clone(), b.to_string()),
}
let text = value.cached_text();
if !text.is_empty() {
for line in text.split('\n') {
let mut paragraph = Element::new("", "p", Ns::Text);
paragraph.name = names.paragraph.clone();
if !line.is_empty() {
paragraph.children.push(Node::Text(line.to_owned()));
paragraph.self_closing = false;
}
cell.children.push(Node::Element(paragraph));
}
}
cell.self_closing = cell.children.is_empty();
for node in &mut cell.children {
if let Node::Element(e) = node
&& e.is(&Ns::Text, "p")
{
crate::edit::replace(e, 0..0, "");
}
}
}
fn cell_in_row(row: &Element, column: usize) -> Option<Cell<'_>> {
let mut at = 0usize;
for child in row.elements() {
let covered = child.is(&Ns::Table, "covered-table-cell");
if !covered && !child.is(&Ns::Table, "table-cell") {
continue;
}
let repeat = child
.attr_usize(&Ns::Table, "number-columns-repeated")
.unwrap_or(1)
.max(1);
if column < at + repeat {
return Some(Cell {
element: child,
covered,
});
}
at += repeat;
}
None
}
fn index_sheets(document: &Document) -> Vec<Sheet> {
let Some(body) = document.body_of("spreadsheet") else {
return Vec::new();
};
let mut sheets = Vec::new();
for (position, node) in body.children.iter().enumerate() {
let crate::xml::Node::Element(table) = node else {
continue;
};
if !table.is(&Ns::Table, "table") {
continue;
}
sheets.push(index_sheet(document, table, position));
}
sheets
}
fn index_sheet(document: &Document, table: &Element, position: usize) -> Sheet {
let mut index = Index {
document,
rows: Vec::new(),
columns: Vec::new(),
at_row: 0,
used_rows: 0,
used_columns: 0,
};
index.walk(table, &mut Vec::new());
Sheet {
name: table
.attr(&Ns::Table, "name")
.unwrap_or_default()
.to_owned(),
columns: index.columns,
used_rows: index.used_rows,
used_columns: index.used_columns,
visible: table.attr(&Ns::Table, "display").unwrap_or("true") != "false",
rows: index.rows,
table: position,
}
}
struct Index<'a> {
document: &'a Document,
rows: Vec<RowRange>,
columns: Vec<Column>,
at_row: usize,
used_rows: usize,
used_columns: usize,
}
impl Index<'_> {
fn walk(&mut self, parent: &Element, path: &mut Vec<usize>) {
for (child_index, child) in parent.children.iter().enumerate() {
let crate::xml::Node::Element(element) = child else {
continue;
};
if element.is(&Ns::Table, "table-column") {
self.column(element);
} else if element.is(&Ns::Table, "table-row") {
path.push(child_index);
self.row(element, path);
path.pop();
} else if is_row_container(element) || is_column_container(element) {
path.push(child_index);
self.walk(element, path);
path.pop();
}
}
}
fn column(&mut self, element: &Element) {
let repeat = element
.attr_usize(&Ns::Table, "number-columns-repeated")
.unwrap_or(1)
.max(1);
let width = element
.attr(&Ns::Table, "style-name")
.map(|name| self.document.styles.resolve(&Family::TableColumn, name))
.and_then(|p| p.column_width);
let default_cell_style = element
.attr(&Ns::Table, "default-cell-style-name")
.map(ToOwned::to_owned);
let visible = element.attr(&Ns::Table, "visibility").unwrap_or("visible") == "visible";
let keep = repeat.min(MAX_COLUMNS.saturating_sub(self.columns.len()));
for _ in 0..keep {
self.columns.push(Column {
width,
default_cell_style: default_cell_style.clone(),
visible,
});
}
}
fn row(&mut self, element: &Element, path: &[usize]) {
let repeat = element
.attr_usize(&Ns::Table, "number-rows-repeated")
.unwrap_or(1)
.max(1);
if let Some(last) = last_occupied_column(element) {
self.used_rows = self.at_row + repeat;
self.used_columns = self.used_columns.max(last + 1);
}
self.rows.push(RowRange {
first: self.at_row,
count: repeat,
path: RowPath::of(path),
});
self.at_row += repeat;
}
}
impl RowPath {
fn of(path: &[usize]) -> Self {
match path {
[only] => Self::Direct(*only),
nested => Self::Nested(nested.into()),
}
}
fn steps(&self) -> &[usize] {
match self {
Self::Direct(only) => std::slice::from_ref(only),
Self::Nested(path) => path,
}
}
}
fn is_row_container(element: &Element) -> bool {
element.is(&Ns::Table, "table-rows")
|| element.is(&Ns::Table, "table-header-rows")
|| element.is(&Ns::Table, "table-row-group")
}
fn is_column_container(element: &Element) -> bool {
element.is(&Ns::Table, "table-columns")
|| element.is(&Ns::Table, "table-header-columns")
|| element.is(&Ns::Table, "table-column-group")
}
const MAX_COLUMNS: usize = 16_384;
fn last_occupied_column(row: &Element) -> Option<usize> {
let mut at = 0usize;
let mut last = None;
for child in row.elements() {
let covered = child.is(&Ns::Table, "covered-table-cell");
if !covered && !child.is(&Ns::Table, "table-cell") {
continue;
}
let repeat = child
.attr_usize(&Ns::Table, "number-columns-repeated")
.unwrap_or(1)
.max(1);
let cell = Cell {
element: child,
covered,
};
if cell.occupied() {
last = Some(at + repeat - 1);
}
at += repeat;
}
last
}