pub mod audit;
pub mod cone;
pub mod cursor;
pub mod diff;
pub mod odf;
pub mod ooxml;
pub mod print;
use std::collections::HashSet;
use crate::intrinsics::fold;
use crate::messages::Refusal;
use crate::refers::{self, FormulaRef, Kind};
pub const DISTINCT_CAP: usize = 1 << 20;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Value {
Number(String),
Text(String),
Bool(bool),
Error(String),
Date(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Visibility {
Visible,
Hidden,
VeryHidden,
}
impl Visibility {
pub fn word(self) -> &'static str {
match self {
Visibility::Visible => "visible",
Visibility::Hidden => "hidden",
Visibility::VeryHidden => "very-hidden",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RefersTo {
Reference(String),
Unreadable(&'static str),
}
impl RefersTo {
pub fn of(r: &FormulaRef, home_sheet: &str) -> RefersTo {
match r.kind {
Kind::Unreadable => RefersTo::Unreadable(r.unreadable_name()),
_ => RefersTo::Reference(refers::spell(r, home_sheet)),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Row<'r> {
Sheet {
name: &'r str,
visibility: Visibility,
},
Name {
name: &'r str,
refers_to: &'r str,
},
Table {
name: &'r str,
sheet: &'r str,
range: &'r str,
},
Cell {
sheet: &'r str,
addr: &'r str,
value: &'r Value,
},
Formula {
sheet: &'r str,
addr: &'r str,
text: &'r str,
},
Refers {
sheet: &'r str,
addr: &'r str,
to: &'r RefersTo,
},
}
pub trait Sink {
fn row(&mut self, row: &Row<'_>);
}
pub struct Discard;
impl Sink for Discard {
fn row(&mut self, _row: &Row<'_>) {}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SheetStats {
pub cells: u64,
pub formulas: u64,
pub array_anchors: u64,
pub rows: u32,
pub columns: u32,
pub part_bytes: usize,
pub distinct_r1c1: u64,
pub unreadable: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SheetInfo {
pub name: String,
pub visibility: Visibility,
pub part: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NameInfo {
pub name: String,
pub refers_to: String,
pub sheet: Option<usize>,
pub hidden: bool,
pub placeholder: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TableInfo {
pub name: String,
pub sheet: usize,
pub range: String,
pub columns: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Summary {
pub names: usize,
pub placeholders: usize,
pub tables: usize,
pub external_books: usize,
pub strings: usize,
pub string_bytes: usize,
}
pub trait Source: std::fmt::Debug {
fn label(&self) -> &str;
fn sheets(&self) -> &[SheetInfo];
fn names(&self) -> &[NameInfo];
fn tables(&self) -> &[TableInfo];
fn external_books(&self) -> &[String];
fn summary(&self) -> Summary;
fn walk_sheet_with(
&self,
index: usize,
sink: &mut dyn Sink,
book_distinct: &mut HashSet<String>,
) -> Result<SheetStats, Refusal>;
fn walk_sheet(&self, index: usize, sink: &mut dyn Sink) -> Result<SheetStats, Refusal> {
self.walk_sheet_with(index, sink, &mut HashSet::new())
}
fn printed_name(&self, name: &NameInfo) -> String {
match name.sheet.and_then(|i| self.sheets().get(i)) {
Some(sheet) => format!("{}!{}", sheet_prefix(&sheet.name), name.name),
None => name.name.clone(),
}
}
fn header_rows(&self, sink: &mut dyn Sink) {
for sheet in self.sheets() {
sink.row(&Row::Sheet {
name: &sheet.name,
visibility: sheet.visibility,
});
}
let mut names: Vec<(String, &str)> = self
.names()
.iter()
.filter(|n| !n.placeholder)
.map(|n| (self.printed_name(n), n.refers_to.as_str()))
.collect();
names.sort_by(|a, b| fold(&a.0).cmp(&fold(&b.0)).then_with(|| a.0.cmp(&b.0)));
for (name, refers_to) in &names {
sink.row(&Row::Name { name, refers_to });
}
let mut tables: Vec<&TableInfo> = self.tables().iter().collect();
tables.sort_by(|a, b| {
a.sheet
.cmp(&b.sheet)
.then_with(|| fold(&a.name).cmp(&fold(&b.name)))
});
let sheets = self.sheets();
for table in tables {
let Some(sheet) = sheets.get(table.sheet) else {
continue;
};
sink.row(&Row::Table {
name: &table.name,
sheet: &sheet.name,
range: &table.range,
});
}
}
}
pub fn sheet_prefix(name: &str) -> String {
refers::quote_sheet(name)
}
pub fn open<'a>(bytes: &'a [u8], label: &str) -> Result<Box<dyn Source + 'a>, Refusal> {
if odf::is_opendocument(bytes) {
Ok(Box::new(odf::Document::open(bytes, label)?))
} else {
Ok(Box::new(ooxml::Package::open(bytes, label)?))
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn emit_formula(
sink: &mut dyn Sink,
sheet: &str,
addr: &str,
text: &str,
row: u32,
col: u32,
stats: &mut SheetStats,
sheet_distinct: &mut HashSet<String>,
book_distinct: &mut HashSet<String>,
) {
stats.formulas += 1;
sink.row(&Row::Formula { sheet, addr, text });
let found = refers::scan(text);
stats.unreadable += found.iter().filter(|r| r.kind == Kind::Unreadable).count() as u64;
let mut targets: Vec<RefersTo> = found.iter().map(|r| RefersTo::of(r, sheet)).collect();
targets.sort_by_key(print::target);
targets.dedup();
for to in &targets {
sink.row(&Row::Refers { sheet, addr, to });
}
let rendered = refers::r1c1(text, row, col);
if sheet_distinct.len() < DISTINCT_CAP {
sheet_distinct.insert(rendered.clone());
}
if book_distinct.len() < DISTINCT_CAP {
book_distinct.insert(rendered);
}
}
pub fn reflect_text(bytes: &[u8], label: &str) -> Result<String, Refusal> {
let source = open(bytes, label)?;
let mut printer = print::Printer::default();
source.header_rows(&mut printer);
for i in 0..source.sheets().len() {
source.walk_sheet(i, &mut printer)?;
}
Ok(printer.out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_sheet_name_is_quoted_as_a_reference_quotes_it() {
assert_eq!(sheet_prefix("Model"), "Model");
assert_eq!(sheet_prefix("Sheet.1"), "Sheet.1");
assert_eq!(sheet_prefix("Q1 Data"), "'Q1 Data'");
assert_eq!(sheet_prefix("It's"), "'It''s'");
assert_eq!(sheet_prefix("2026"), "'2026'");
assert_eq!(sheet_prefix("Donn\u{e9}es"), "Donn\u{e9}es");
assert_eq!(sheet_prefix("a-b"), "'a-b'");
assert_eq!(sheet_prefix("A1"), "'A1'");
assert_eq!(sheet_prefix("R1C1"), "'R1C1'");
}
#[test]
fn a_target_is_a_reference_or_an_unreadable_call() {
let refs = refers::scan("=OFFSET(B1,1,0)+'Q1 Data'!A1");
assert_eq!(
RefersTo::of(&refs[0], "Model"),
RefersTo::Unreadable("OFFSET")
);
assert_eq!(
RefersTo::of(&refs[1], "Model"),
RefersTo::Reference("Model!B1".to_string())
);
assert_eq!(
RefersTo::of(&refs[2], "Model"),
RefersTo::Reference("'Q1 Data'!A1".to_string())
);
}
}