///|
struct Sheet {
name : String
cells : Map[(Int, Int), Cell]
mut rows : Int
mut cols : Int
mut text_units : Int
widths : Map[Int, Int]
spans : Map[(Int, Int), (Int, Int)]
covered : Map[(Int, Int), Bool]
}
///|
struct Workbook {
sheets : Array[Sheet]
}
///|
pub fn Workbook::new() -> Workbook {
{ sheets: [], }
}
///|
pub fn Workbook::add_sheet(
self : Workbook,
name : String,
) -> Sheet raise OdsError {
validate_name(name)
if self.sheets.length() >= 32 {
raise Invalid("workbook exceeds 32 sheets")
}
for s in self.sheets {
if s.name.to_lower() == name.to_lower() {
raise Invalid("duplicate sheet name")
}
}
let s = {
name,
cells: {},
rows: 1,
cols: 1,
text_units: 0,
widths: {},
spans: {},
covered: {},
}
self.sheets.push(s)
s
}
///|
pub fn Sheet::name(self : Sheet) -> String {
self.name
}
///|
pub fn Workbook::sheet_count(self : Workbook) -> Int {
self.sheets.length()
}
///|
pub fn Sheet::get(self : Sheet, row : Int, col : Int) -> Cell? {
self.cells.get((row, col))
}
///|
fn validate_name(s : String) -> Unit raise OdsError {
validate_text(s)
if s.length() == 0 ||
s.length() > 31 ||
s.trim() == "" ||
s.has_prefix("'") ||
s.has_suffix("'") {
raise Invalid(
"sheet name must have 1..31 units, nonblank, without boundary apostrophes",
)
}
for c in s {
if "[]:*?/\\".contains(c.to_string()) || c.to_int() < 32 {
raise Invalid("invalid interoperable sheet name")
}
}
}
///|
fn position(row : Int, col : Int) -> Unit raise OdsError {
if row < 0 || col < 0 || row >= 1024 || col >= 128 {
raise Invalid("cell index exceeds 1024 rows or 128 columns")
}
}
///|
fn Cell::units(self : Cell) -> Int {
let n = match self.value {
Text(s) => s.length()
_ => 0
}
n +
(match self.formula {
Some(f) => f.length()
None => 0
})
}
///|
/// Zero-based coordinate. Validation precedes mutation.
pub fn Sheet::set(
self : Sheet,
row : Int,
col : Int,
cell : Cell,
) -> Unit raise OdsError {
position(row, col)
if self.covered.contains((row, col)) {
raise Invalid("cannot write covered merge cell")
}
let rows = if row + 1 > self.rows { row + 1 } else { self.rows }
let cols = if col + 1 > self.cols { col + 1 } else { self.cols }
if rows * cols > 65536 {
raise Invalid("sheet rendered grid exceeds 65536 cells")
}
let old = match self.cells.get((row, col)) {
Some(c) => c.units()
None => 0
}
let units = self.text_units - old + cell.units()
if units > 2097152 {
raise Invalid("sheet text exceeds 2097152 UTF-16 units")
}
self.cells.set((row, col), cell)
self.rows = rows
self.cols = cols
self.text_units = units
}
///|
let namespaces : String = " xmlns:office=\"urn:oasis:names:tc:opendocument:xmlns:office:1.0\" xmlns:table=\"urn:oasis:names:tc:opendocument:xmlns:table:1.0\" xmlns:text=\"urn:oasis:names:tc:opendocument:xmlns:text:1.0\" xmlns:style=\"urn:oasis:names:tc:opendocument:xmlns:style:1.0\" xmlns:fo=\"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0\" xmlns:number=\"urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0\" xmlns:of=\"urn:oasis:names:tc:opendocument:xmlns:of:1.2\""
///|
/// Inspectable XML for diagnostics. ODS output is provided by to_ods().
pub fn Workbook::content_xml(self : Workbook) -> String raise OdsError {
if self.sheets.is_empty() {
raise Invalid("workbook needs at least one sheet")
}
let mut grid = 0
let mut units = 0
for s in self.sheets {
grid += s.rows * s.cols
units += s.text_units
if grid > 262144 || units > 4194304 {
raise Invalid("workbook resource budget exceeded")
}
}
// Preflight workbook totals before allocating serialized XML. Escaping can
// expand text, so a second byte budget is enforced at every append.
let b = XmlOutput::new()
b.write_string(
"\{self.column_styles()}",
)
for s in self.sheets {
b.write_string("")
for col in 0.. " table:style-name=\"W\{mm}\""
None => ""
}
b.write_string("")
}
for row in 0..")
for col in 0..")
} else {
let c = match s.cells.get((row, col)) {
Some(c) => c
None => { value: Empty, formula: None, style: Plain, }
}
let extra = match s.spans.get((row, col)) {
Some((nr, nc)) =>
" table:number-rows-spanned=\"\{nr}\" table:number-columns-spanned=\"\{nc}\""
None => ""
}
b.write_string(c.xml(extra))
}
}
b.write_string("")
}
b.write_string("")
}
b.write_string(
"",
)
b.to_string()
}