UPDATE: Dump of initial files
This commit is contained in:
6
xlsx/src/export/_rels.rs
Normal file
6
xlsx/src/export/_rels.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use ironcalc_base::types::Workbook;
|
||||
|
||||
pub(crate) fn get_dot_rels(_: &Workbook) -> String {
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>"#.to_owned()
|
||||
}
|
||||
69
xlsx/src/export/doc_props.rs
Normal file
69
xlsx/src/export/doc_props.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use ironcalc_base::{
|
||||
new_empty::{APPLICATION, APP_VERSION, IRONCALC_USER},
|
||||
types::Workbook,
|
||||
};
|
||||
|
||||
use crate::error::XlsxError;
|
||||
|
||||
// Application-Defined File Properties part
|
||||
pub(crate) fn get_app_xml(_: &Workbook) -> String {
|
||||
// contains application name and version
|
||||
|
||||
// The next few are not needed:
|
||||
// security. It is password protected (not implemented)
|
||||
// Scale
|
||||
// Titles of parts
|
||||
|
||||
format!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
|
||||
<Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties\" \
|
||||
xmlns:vt=\"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes\">\
|
||||
<Application>{}</Application>\
|
||||
<AppVersion>{}</AppVersion>\
|
||||
</Properties>",
|
||||
APPLICATION, APP_VERSION
|
||||
)
|
||||
}
|
||||
|
||||
// Core File Properties part
|
||||
pub(crate) fn get_core_xml(workbook: &Workbook, milliseconds: i64) -> Result<String, XlsxError> {
|
||||
// contains the name of the creator, last modified and date
|
||||
let metadata = &workbook.metadata;
|
||||
let creator = metadata.creator.to_string();
|
||||
let last_modified_by = IRONCALC_USER.to_string();
|
||||
let created = metadata.created.to_string();
|
||||
// FIXME add now
|
||||
|
||||
let seconds = milliseconds / 1000;
|
||||
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
return Err(XlsxError::Xml(format!(
|
||||
"Invalid timestamp: {}",
|
||||
milliseconds
|
||||
)))
|
||||
}
|
||||
};
|
||||
let last_modified = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||
Ok(format!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
|
||||
<cp:coreProperties \
|
||||
xmlns:cp=\"http://schemas.openxmlformats.org/package/2006/metadata/core-properties\" \
|
||||
xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" \
|
||||
xmlns:dcmitype=\"http://purl.org/dc/dcmitype/\" \
|
||||
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"> \
|
||||
<dc:title></dc:title><dc:subject></dc:subject>\
|
||||
<dc:creator>{}</dc:creator>\
|
||||
<cp:keywords></cp:keywords>\
|
||||
<dc:description></dc:description>\
|
||||
<cp:lastModifiedBy>{}</cp:lastModifiedBy>\
|
||||
<cp:revision></cp:revision>\
|
||||
<dcterms:created xsi:type=\"dcterms:W3CDTF\">{}</dcterms:created>\
|
||||
<dcterms:modified xsi:type=\"dcterms:W3CDTF\">{}</dcterms:modified>\
|
||||
<cp:category></cp:category>\
|
||||
<cp:contentStatus></cp:contentStatus>\
|
||||
</cp:coreProperties>",
|
||||
creator, last_modified_by, created, last_modified
|
||||
))
|
||||
}
|
||||
99
xlsx/src/export/escape.rs
Normal file
99
xlsx/src/export/escape.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
// Taken from :
|
||||
|
||||
// https://docs.rs/xml-rs/latest/src/xml/escape.rs.html#1-125
|
||||
|
||||
//! Contains functions for performing XML special characters escaping.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
enum Value {
|
||||
Char(char),
|
||||
Str(&'static str),
|
||||
}
|
||||
|
||||
fn escape_char(c: char) -> Value {
|
||||
match c {
|
||||
'<' => Value::Str("<"),
|
||||
'>' => Value::Str(">"),
|
||||
'"' => Value::Str("""),
|
||||
'\'' => Value::Str("'"),
|
||||
'&' => Value::Str("&"),
|
||||
'\n' => Value::Str("
"),
|
||||
'\r' => Value::Str("
"),
|
||||
_ => Value::Char(c),
|
||||
}
|
||||
}
|
||||
|
||||
enum Process<'a> {
|
||||
Borrowed(&'a str),
|
||||
Owned(String),
|
||||
}
|
||||
|
||||
impl<'a> Process<'a> {
|
||||
fn process(&mut self, (i, next): (usize, Value)) {
|
||||
match next {
|
||||
Value::Str(s) => match *self {
|
||||
Process::Owned(ref mut o) => o.push_str(s),
|
||||
Process::Borrowed(b) => {
|
||||
let mut r = String::with_capacity(b.len() + s.len());
|
||||
r.push_str(&b[..i]);
|
||||
r.push_str(s);
|
||||
*self = Process::Owned(r);
|
||||
}
|
||||
},
|
||||
Value::Char(c) => match *self {
|
||||
Process::Borrowed(_) => {}
|
||||
Process::Owned(ref mut o) => o.push(c),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_result(self) -> Cow<'a, str> {
|
||||
match self {
|
||||
Process::Borrowed(b) => Cow::Borrowed(b),
|
||||
Process::Owned(o) => Cow::Owned(o),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Extend<(usize, Value)> for Process<'a> {
|
||||
fn extend<I: IntoIterator<Item = (usize, Value)>>(&mut self, it: I) {
|
||||
for v in it.into_iter() {
|
||||
self.process(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs escaping of common XML characters inside an attribute value.
|
||||
///
|
||||
/// This function replaces several important markup characters with their
|
||||
/// entity equivalents:
|
||||
///
|
||||
/// * `<` → `<`
|
||||
/// * `>` → `>`
|
||||
/// * `"` → `"`
|
||||
/// * `'` → `'`
|
||||
/// * `&` → `&`
|
||||
///
|
||||
/// The resulting string is safe to use inside XML attribute values.
|
||||
///
|
||||
/// Does not perform allocations if the given string does not contain escapable characters.
|
||||
pub fn escape_xml(s: &str) -> Cow<str> {
|
||||
let mut p = Process::Borrowed(s);
|
||||
p.extend(s.char_indices().map(|(ind, c)| (ind, escape_char(c))));
|
||||
p.into_result()
|
||||
}
|
||||
|
||||
// A simpler function that allocates memory for each replacement
|
||||
// fn escape_xml(value: &str) -> String {
|
||||
// value
|
||||
// .replace('&', "&")
|
||||
// .replace('<', "<")
|
||||
// .replace('>', ">")
|
||||
// .replace('"', """)
|
||||
// .replace('\'', "'")
|
||||
// }
|
||||
|
||||
// See also:
|
||||
// https://docs.rs/shell-escape/0.1.5/src/shell_escape/lib.rs.html#17-23
|
||||
// https://aaronerhardt.github.io/docs/relm4/src/quick_xml/escapei.rs.html#69-106
|
||||
138
xlsx/src/export/mod.rs
Normal file
138
xlsx/src/export/mod.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
mod _rels;
|
||||
mod doc_props;
|
||||
mod escape;
|
||||
mod shared_strings;
|
||||
mod styles;
|
||||
mod workbook;
|
||||
mod workbook_xml_rels;
|
||||
mod worksheets;
|
||||
mod xml_constants;
|
||||
|
||||
use std::io::BufWriter;
|
||||
use std::{
|
||||
fs,
|
||||
io::{Seek, Write},
|
||||
};
|
||||
|
||||
use ironcalc_base::expressions::utils::number_to_column;
|
||||
use ironcalc_base::model::{get_milliseconds_since_epoch, Model};
|
||||
use ironcalc_base::types::Workbook;
|
||||
|
||||
use self::xml_constants::XML_DECLARATION;
|
||||
|
||||
use crate::error::XlsxError;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
fn get_content_types_xml(workbook: &Workbook) -> String {
|
||||
// A list of all files in the zip
|
||||
let mut content = vec![
|
||||
r#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">"#.to_string(),
|
||||
r#"<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>"#.to_string(),
|
||||
r#"<Default Extension="xml" ContentType="application/xml"/>"#.to_string(),
|
||||
r#"<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>"#.to_string(),
|
||||
];
|
||||
for worksheet in 0..workbook.worksheets.len() {
|
||||
let sheet = format!(
|
||||
r#"<Override PartName="/xl/worksheets/sheet{}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>"#,
|
||||
worksheet + 1
|
||||
);
|
||||
content.push(sheet);
|
||||
}
|
||||
// we skip the theme and calcChain
|
||||
// r#"<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>"#,
|
||||
// r#"<Override PartName="/xl/calcChain.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml"/>"#,
|
||||
content.extend([
|
||||
r#"<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>"#.to_string(),
|
||||
r#"<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>"#.to_string(),
|
||||
r#"<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>"#.to_string(),
|
||||
r#"<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>"#.to_string(),
|
||||
r#"</Types>"#.to_string(),
|
||||
]);
|
||||
format!("{XML_DECLARATION}\n{}", content.join(""))
|
||||
}
|
||||
|
||||
/// Exports a model to an xlsx file
|
||||
pub fn save_to_xlsx(model: &Model, file_name: &str) -> Result<(), XlsxError> {
|
||||
let file_path = std::path::Path::new(&file_name);
|
||||
if file_path.exists() {
|
||||
return Err(XlsxError::IO(format!("file {} already exists", file_name)));
|
||||
}
|
||||
let file = fs::File::create(file_path).unwrap();
|
||||
let writer = BufWriter::new(file);
|
||||
save_xlsx_to_writer(model, writer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_xlsx_to_writer<W: Write + Seek>(model: &Model, writer: W) -> Result<W, XlsxError> {
|
||||
let workbook = &model.workbook;
|
||||
let mut zip = zip::ZipWriter::new(writer);
|
||||
|
||||
let options =
|
||||
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
|
||||
// root folder
|
||||
zip.start_file("[Content_Types].xml", options)?;
|
||||
zip.write_all(get_content_types_xml(workbook).as_bytes())?;
|
||||
|
||||
zip.add_directory("docProps", options)?;
|
||||
zip.start_file("docProps/app.xml", options)?;
|
||||
zip.write_all(doc_props::get_app_xml(workbook).as_bytes())?;
|
||||
zip.start_file("docProps/core.xml", options)?;
|
||||
let milliseconds = get_milliseconds_since_epoch();
|
||||
zip.write_all(doc_props::get_core_xml(workbook, milliseconds)?.as_bytes())?;
|
||||
|
||||
// Package-relationship item
|
||||
zip.add_directory("_rels", options)?;
|
||||
zip.start_file("_rels/.rels", options)?;
|
||||
zip.write_all(_rels::get_dot_rels(workbook).as_bytes())?;
|
||||
|
||||
zip.add_directory("xl", options)?;
|
||||
zip.start_file("xl/sharedStrings.xml", options)?;
|
||||
zip.write_all(shared_strings::get_shared_strings_xml(workbook).as_bytes())?;
|
||||
zip.start_file("xl/styles.xml", options)?;
|
||||
zip.write_all(styles::get_styles_xml(workbook).as_bytes())?;
|
||||
zip.start_file("xl/workbook.xml", options)?;
|
||||
zip.write_all(workbook::get_workbook_xml(workbook).as_bytes())?;
|
||||
|
||||
zip.add_directory("xl/_rels", options)?;
|
||||
zip.start_file("xl/_rels/workbook.xml.rels", options)?;
|
||||
zip.write_all(workbook_xml_rels::get_workbook_xml_rels(workbook).as_bytes())?;
|
||||
|
||||
zip.add_directory("xl/worksheets", options)?;
|
||||
for (sheet_index, worksheet) in workbook.worksheets.iter().enumerate() {
|
||||
let id = sheet_index + 1;
|
||||
zip.start_file(&format!("xl/worksheets/sheet{id}.xml"), options)?;
|
||||
let dimension = model
|
||||
.workbook
|
||||
.worksheet(sheet_index as u32)
|
||||
.unwrap()
|
||||
.dimension();
|
||||
let column_min_str = number_to_column(dimension.min_column).unwrap();
|
||||
let column_max_str = number_to_column(dimension.max_column).unwrap();
|
||||
let min_row = dimension.min_row;
|
||||
let max_row = dimension.max_row;
|
||||
let sheet_dimension_str = &format!("{column_min_str}{min_row}:{column_max_str}{max_row}");
|
||||
zip.write_all(
|
||||
worksheets::get_worksheet_xml(
|
||||
worksheet,
|
||||
&model.parsed_formulas[sheet_index],
|
||||
sheet_dimension_str,
|
||||
)
|
||||
.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
|
||||
let writer = zip.finish()?;
|
||||
Ok(writer)
|
||||
}
|
||||
|
||||
/// Exports an internal representation of a workbook into an equivalent IronCalc json format
|
||||
pub fn save_to_json(workbook: Workbook, output: &str) {
|
||||
let s = serde_json::to_string(&workbook).unwrap();
|
||||
let file_path = std::path::Path::new(output);
|
||||
let mut file = fs::File::create(file_path).unwrap();
|
||||
file.write_all(s.as_bytes()).unwrap();
|
||||
}
|
||||
16
xlsx/src/export/shared_strings.rs
Normal file
16
xlsx/src/export/shared_strings.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use ironcalc_base::types::Workbook;
|
||||
|
||||
use super::{escape::escape_xml, xml_constants::XML_DECLARATION};
|
||||
|
||||
pub(crate) fn get_shared_strings_xml(model: &Workbook) -> String {
|
||||
let mut shared_strings: Vec<String> = vec![];
|
||||
let count = &model.shared_strings.len();
|
||||
let unique_count = &model.shared_strings.len();
|
||||
for shared_string in &model.shared_strings {
|
||||
shared_strings.push(format!("<si><t>{}</t></si>", escape_xml(shared_string)));
|
||||
}
|
||||
format!("{}\n\
|
||||
<sst xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" count=\"{count}\" uniqueCount=\"{unique_count}\">\
|
||||
{}\
|
||||
</sst>", XML_DECLARATION, shared_strings.join(""))
|
||||
}
|
||||
282
xlsx/src/export/styles.rs
Normal file
282
xlsx/src/export/styles.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
use ironcalc_base::types::{
|
||||
Alignment, BorderItem, HorizontalAlignment, Styles, VerticalAlignment, Workbook,
|
||||
};
|
||||
|
||||
use super::{escape::escape_xml, xml_constants::XML_DECLARATION};
|
||||
|
||||
fn get_fonts_xml(styles: &Styles) -> String {
|
||||
let fonts = &styles.fonts;
|
||||
let mut fonts_str: Vec<String> = vec![];
|
||||
for font in fonts {
|
||||
let size = format!("<sz val=\"{}\"/>", font.sz);
|
||||
let color = if let Some(some_color) = &font.color {
|
||||
format!("<color rgb=\"FF{}\"/>", some_color.trim_start_matches('#'))
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let name = format!("<name val=\"{}\"/>", escape_xml(&font.name));
|
||||
let bold = if font.b { "<b/>" } else { "" };
|
||||
let italic = if font.i { "<i/>" } else { "" };
|
||||
let underline = if font.u { "<u/>" } else { "" };
|
||||
let strike = if font.strike { "<strike/>" } else { "" };
|
||||
let family = format!("<family val=\"{}\"/>", font.family);
|
||||
let scheme = format!("<scheme val=\"{}\"/>", font.scheme);
|
||||
fonts_str.push(format!(
|
||||
"<font>\
|
||||
{size}\
|
||||
{color}\
|
||||
{name}\
|
||||
{bold}\
|
||||
{italic}\
|
||||
{underline}\
|
||||
{strike}\
|
||||
{family}\
|
||||
{scheme}\
|
||||
</font>"
|
||||
));
|
||||
}
|
||||
let font_count = fonts.len();
|
||||
format!(
|
||||
"<fonts count=\"{font_count}\">{}</fonts>",
|
||||
fonts_str.join("")
|
||||
)
|
||||
}
|
||||
|
||||
fn get_color_xml(color: &Option<String>, name: &str) -> String {
|
||||
// We blindly append FF at the beginning of these RGB color to make it ARGB
|
||||
if let Some(some_color) = color {
|
||||
format!("<{name} rgb=\"FF{}\"/>", some_color.trim_start_matches('#'))
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_fills_xml(styles: &Styles) -> String {
|
||||
let fills = &styles.fills;
|
||||
let mut fills_str: Vec<String> = vec![];
|
||||
for fill in fills {
|
||||
let pattern_type = &fill.pattern_type;
|
||||
let fg_color = get_color_xml(&fill.fg_color, "fgColor");
|
||||
let bg_color = get_color_xml(&fill.bg_color, "bgColor");
|
||||
fills_str.push(format!(
|
||||
"<fill><patternFill patternType=\"{pattern_type}\">{fg_color}{bg_color}</patternFill></fill>"
|
||||
));
|
||||
}
|
||||
let fill_count = fills.len();
|
||||
format!(
|
||||
"<fills count=\"{fill_count}\">{}</fills>",
|
||||
fills_str.join("")
|
||||
)
|
||||
}
|
||||
|
||||
fn get_border_xml(border: &Option<BorderItem>, name: &str) -> String {
|
||||
if let Some(border_item) = border {
|
||||
let color = get_color_xml(&border_item.color, "color");
|
||||
return format!("<{name} style=\"{}\">{color}</{name}>", border_item.style);
|
||||
}
|
||||
format!("<{name}/>")
|
||||
}
|
||||
|
||||
fn get_borders_xml(styles: &Styles) -> String {
|
||||
let borders = &styles.borders;
|
||||
let mut borders_str: Vec<String> = vec![];
|
||||
let border_count = borders.len();
|
||||
for border in borders {
|
||||
// TODO: diagonal_up/diagonal_down?
|
||||
let border_left = get_border_xml(&border.left, "left");
|
||||
let border_right = get_border_xml(&border.right, "right");
|
||||
let border_top = get_border_xml(&border.top, "top");
|
||||
let border_bottom = get_border_xml(&border.bottom, "bottom");
|
||||
let border_diagonal = get_border_xml(&border.diagonal, "diagonal");
|
||||
borders_str.push(format!(
|
||||
"<border>{border_left}{border_right}{border_top}{border_bottom}{border_diagonal}</border>"
|
||||
));
|
||||
}
|
||||
format!(
|
||||
"<borders count=\"{border_count}\">{}</borders>",
|
||||
borders_str.join("")
|
||||
)
|
||||
}
|
||||
|
||||
// <numFmts count="1">
|
||||
// <numFmt numFmtId="164" formatCode="##,#00;[Blue]\-\-#,##0"/>
|
||||
// </numFmts>
|
||||
fn get_cell_number_formats_xml(styles: &Styles) -> String {
|
||||
let num_fmts = &styles.num_fmts;
|
||||
let mut num_fmts_str: Vec<String> = vec![];
|
||||
let num_fmt_count = num_fmts.len();
|
||||
for num_fmt in num_fmts {
|
||||
let num_fmt_id = num_fmt.num_fmt_id;
|
||||
let format_code = &num_fmt.format_code;
|
||||
let format_code = escape_xml(format_code);
|
||||
num_fmts_str.push(format!(
|
||||
"<numFmt numFmtId=\"{num_fmt_id}\" formatCode=\"{format_code}\"/>"
|
||||
));
|
||||
}
|
||||
if num_fmt_count == 0 {
|
||||
return "".to_string();
|
||||
}
|
||||
format!(
|
||||
"<numFmts count=\"{num_fmt_count}\">{}</numFmts>",
|
||||
num_fmts_str.join("")
|
||||
)
|
||||
}
|
||||
|
||||
fn get_alignment(alignment: &Alignment) -> String {
|
||||
let wrap_text = if alignment.wrap_text {
|
||||
" wrapText=\"1\""
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let horizontal = if alignment.horizontal != HorizontalAlignment::default() {
|
||||
format!(" horizontal=\"{}\"", alignment.horizontal)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let vertical = if alignment.vertical != VerticalAlignment::default() {
|
||||
format!(" vertical=\"{}\"", alignment.vertical)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
format!("<alignment{wrap_text}{horizontal}{vertical}/>")
|
||||
}
|
||||
|
||||
fn get_cell_style_xfs_xml(styles: &Styles) -> String {
|
||||
let cell_style_xfs = &styles.cell_style_xfs;
|
||||
let mut cell_style_str: Vec<String> = vec![];
|
||||
for cell_style_xf in cell_style_xfs {
|
||||
let border_id = cell_style_xf.border_id;
|
||||
let fill_id = cell_style_xf.fill_id;
|
||||
let font_id = cell_style_xf.font_id;
|
||||
let num_fmt_id = cell_style_xf.num_fmt_id;
|
||||
let apply_alignment_str = if cell_style_xf.apply_alignment {
|
||||
r#" applyAlignment="1""#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let apply_font_str = if cell_style_xf.apply_font {
|
||||
r#" applyFont="1""#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let apply_fill_str = if cell_style_xf.apply_fill {
|
||||
r#" applyFill="1""#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
cell_style_str.push(format!(
|
||||
"<xf \
|
||||
borderId=\"{border_id}\" \
|
||||
fillId=\"{fill_id}\" \
|
||||
fontId=\"{font_id}\" \
|
||||
numFmtId=\"{num_fmt_id}\"\
|
||||
{apply_alignment_str}\
|
||||
{apply_font_str}\
|
||||
{apply_fill_str}/>"
|
||||
));
|
||||
}
|
||||
let style_count = cell_style_xfs.len();
|
||||
format!(
|
||||
"<cellStyleXfs count=\"{style_count}\">{}</cellStyleXfs>",
|
||||
cell_style_str.join("")
|
||||
)
|
||||
}
|
||||
|
||||
fn get_cell_xfs_xml(styles: &Styles) -> String {
|
||||
let cell_xfs = &styles.cell_xfs;
|
||||
let mut cell_xfs_str: Vec<String> = vec![];
|
||||
for cell_xf in cell_xfs {
|
||||
let xf_id = cell_xf.xf_id;
|
||||
let border_id = cell_xf.border_id;
|
||||
let fill_id = cell_xf.fill_id;
|
||||
let font_id = cell_xf.font_id;
|
||||
let num_fmt_id = cell_xf.num_fmt_id;
|
||||
let quote_prefix_str = if cell_xf.quote_prefix {
|
||||
r#" quotePrefix="1""#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let apply_alignment_str = if cell_xf.apply_alignment {
|
||||
r#" applyAlignment="1""#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let apply_font_str = if cell_xf.apply_font {
|
||||
r#" applyFont="1""#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let apply_fill_str = if cell_xf.apply_fill {
|
||||
r#" applyFill="1""#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let properties = format!(
|
||||
"xfId=\"{xf_id}\" \
|
||||
borderId=\"{border_id}\" \
|
||||
fillId=\"{fill_id}\" \
|
||||
fontId=\"{font_id}\" \
|
||||
numFmtId=\"{num_fmt_id}\"\
|
||||
{quote_prefix_str}\
|
||||
{apply_alignment_str}\
|
||||
{apply_font_str}\
|
||||
{apply_fill_str}"
|
||||
);
|
||||
if let Some(alignment) = &cell_xf.alignment {
|
||||
let alignment = get_alignment(alignment);
|
||||
cell_xfs_str.push(format!("<xf {properties}>{alignment}</xf>"));
|
||||
} else {
|
||||
cell_xfs_str.push(format!("<xf {properties}/>"));
|
||||
}
|
||||
}
|
||||
let style_count = cell_xfs.len();
|
||||
format!(
|
||||
"<cellXfs count=\"{style_count}\">{}</cellXfs>",
|
||||
cell_xfs_str.join("")
|
||||
)
|
||||
}
|
||||
|
||||
// <cellStyle xfId="0" name="Normal" builtinId="0"/>
|
||||
fn get_cell_styles_xml(styles: &Styles) -> String {
|
||||
let cell_styles = &styles.cell_styles;
|
||||
let mut cell_styles_str: Vec<String> = vec![];
|
||||
for cell_style in cell_styles {
|
||||
let xf_id = cell_style.xf_id;
|
||||
let name = &cell_style.name;
|
||||
let name = escape_xml(name);
|
||||
let builtin_id = cell_style.builtin_id;
|
||||
cell_styles_str.push(format!(
|
||||
"<cellStyle xfId=\"{xf_id}\" name=\"{name}\" builtinId=\"{builtin_id}\"/>"
|
||||
));
|
||||
}
|
||||
let style_count = cell_styles.len();
|
||||
format!(
|
||||
"<cellStyles count=\"{style_count}\">{}</cellStyles>",
|
||||
cell_styles_str.join("")
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn get_styles_xml(model: &Workbook) -> String {
|
||||
let styles = &model.styles;
|
||||
let fonts = get_fonts_xml(styles);
|
||||
let fills = get_fills_xml(styles);
|
||||
let borders = get_borders_xml(styles);
|
||||
let number_formats = get_cell_number_formats_xml(styles);
|
||||
let cell_style_xfs = get_cell_style_xfs_xml(styles);
|
||||
let cell_xfs = get_cell_xfs_xml(styles);
|
||||
let cell_styles = get_cell_styles_xml(styles);
|
||||
|
||||
format!(
|
||||
"{XML_DECLARATION}
|
||||
<styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\
|
||||
{number_formats}\
|
||||
{fonts}\
|
||||
{fills}\
|
||||
{borders}\
|
||||
{cell_style_xfs}\
|
||||
{cell_xfs}\
|
||||
{cell_styles}\
|
||||
<dxfs count=\"0\"/>\
|
||||
</styleSheet>"
|
||||
)
|
||||
}
|
||||
2
xlsx/src/export/test/mod.rs
Normal file
2
xlsx/src/export/test/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod test_escape;
|
||||
mod test_export;
|
||||
25
xlsx/src/export/test/test_escape.rs
Normal file
25
xlsx/src/export/test/test_escape.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::export::escape::escape_xml;
|
||||
|
||||
#[test]
|
||||
fn test_escape_xml() {
|
||||
assert_eq!(escape_xml("all good"), "all good");
|
||||
assert_eq!(escape_xml("3 < 4"), "3 < 4");
|
||||
assert_eq!(escape_xml("3 > 4"), "3 > 4");
|
||||
assert_eq!(escape_xml("3 & 4"), "3 & 4");
|
||||
assert_eq!(escape_xml("3 && 4"), "3 && 4");
|
||||
assert_eq!(escape_xml("3 \"literal\" 4"), "3 "literal" 4");
|
||||
assert_eq!(
|
||||
escape_xml("I don't 'know'"),
|
||||
"I don't 'know'"
|
||||
);
|
||||
assert_eq!(
|
||||
escape_xml("This is <>&\"' say"),
|
||||
"This is <>&"' say"
|
||||
);
|
||||
}
|
||||
|
||||
// '&' => "&"
|
||||
// '<' "<")
|
||||
// '>' => ">"
|
||||
// '"' => """
|
||||
// '\'' => "'"
|
||||
134
xlsx/src/export/test/test_export.rs
Normal file
134
xlsx/src/export/test/test_export.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use std::fs;
|
||||
|
||||
use ironcalc_base::model::Model;
|
||||
|
||||
use crate::error::XlsxError;
|
||||
use crate::{export::save_to_xlsx, import::load_model_from_xlsx};
|
||||
|
||||
pub fn new_empty_model() -> Model {
|
||||
Model::new_empty("model", "en", "UTC").unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_values() {
|
||||
let mut model = new_empty_model();
|
||||
// numbers
|
||||
model.set_user_input(0, 1, 1, "123.456".to_string());
|
||||
// strings
|
||||
model.set_user_input(0, 2, 1, "Hello world!".to_string());
|
||||
model.set_user_input(0, 3, 1, "Hello world!".to_string());
|
||||
model.set_user_input(0, 4, 1, "你好世界!".to_string());
|
||||
// booleans
|
||||
model.set_user_input(0, 5, 1, "TRUE".to_string());
|
||||
model.set_user_input(0, 6, 1, "FALSE".to_string());
|
||||
// errors
|
||||
model.set_user_input(0, 7, 1, "#VALUE!".to_string());
|
||||
|
||||
// noop
|
||||
model.evaluate();
|
||||
|
||||
let temp_file_name = "temp_file_test_values.xlsx";
|
||||
save_to_xlsx(&model, temp_file_name).unwrap();
|
||||
|
||||
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap();
|
||||
assert_eq!(model.formatted_cell_value(0, 1, 1).unwrap(), "123.456");
|
||||
assert_eq!(model.formatted_cell_value(0, 2, 1).unwrap(), "Hello world!");
|
||||
assert_eq!(model.formatted_cell_value(0, 3, 1).unwrap(), "Hello world!");
|
||||
assert_eq!(model.formatted_cell_value(0, 4, 1).unwrap(), "你好世界!");
|
||||
assert_eq!(model.formatted_cell_value(0, 5, 1).unwrap(), "TRUE");
|
||||
assert_eq!(model.formatted_cell_value(0, 6, 1).unwrap(), "FALSE");
|
||||
assert_eq!(model.formatted_cell_value(0, 7, 1).unwrap(), "#VALUE!");
|
||||
|
||||
fs::remove_file(temp_file_name).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_formulas() {
|
||||
let mut model = new_empty_model();
|
||||
model.set_user_input(0, 1, 1, "5.5".to_string());
|
||||
model.set_user_input(0, 2, 1, "6.5".to_string());
|
||||
model.set_user_input(0, 3, 1, "7.5".to_string());
|
||||
|
||||
model.set_user_input(0, 1, 2, "=A1*2".to_string());
|
||||
model.set_user_input(0, 2, 2, "=A2*2".to_string());
|
||||
model.set_user_input(0, 3, 2, "=A3*2".to_string());
|
||||
model.set_user_input(0, 4, 2, "=SUM(A1:B3)".to_string());
|
||||
|
||||
model.evaluate();
|
||||
let temp_file_name = "temp_file_test_formulas.xlsx";
|
||||
save_to_xlsx(&model, temp_file_name).unwrap();
|
||||
|
||||
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap();
|
||||
assert_eq!(model.formatted_cell_value(0, 1, 2).unwrap(), "11");
|
||||
assert_eq!(model.formatted_cell_value(0, 2, 2).unwrap(), "13");
|
||||
assert_eq!(model.formatted_cell_value(0, 3, 2).unwrap(), "15");
|
||||
assert_eq!(model.formatted_cell_value(0, 4, 2).unwrap(), "58.5");
|
||||
fs::remove_file(temp_file_name).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sheets() {
|
||||
let mut model = new_empty_model();
|
||||
model.add_sheet("With space").unwrap();
|
||||
// xml escaped
|
||||
model.add_sheet("Tango & Cash").unwrap();
|
||||
model.add_sheet("你好世界").unwrap();
|
||||
|
||||
// noop
|
||||
model.evaluate();
|
||||
|
||||
let temp_file_name = "temp_file_test_sheets.xlsx";
|
||||
save_to_xlsx(&model, temp_file_name).unwrap();
|
||||
|
||||
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap();
|
||||
assert_eq!(
|
||||
model.workbook.get_worksheet_names(),
|
||||
vec!["Sheet1", "With space", "Tango & Cash", "你好世界"]
|
||||
);
|
||||
fs::remove_file(temp_file_name).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_named_styles() {
|
||||
let mut model = new_empty_model();
|
||||
model.set_user_input(0, 1, 1, "5.5".to_string());
|
||||
let mut style = model.get_style_for_cell(0, 1, 1);
|
||||
style.font.b = true;
|
||||
style.font.i = true;
|
||||
assert!(model.set_cell_style(0, 1, 1, &style).is_ok());
|
||||
let bold_style_index = model.get_cell_style_index(0, 1, 1);
|
||||
let e = model
|
||||
.workbook
|
||||
.styles
|
||||
.add_named_cell_style("bold & italics", bold_style_index);
|
||||
assert!(e.is_ok());
|
||||
|
||||
// noop
|
||||
model.evaluate();
|
||||
|
||||
let temp_file_name = "temp_file_test_named_styles.xlsx";
|
||||
save_to_xlsx(&model, temp_file_name).unwrap();
|
||||
|
||||
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap();
|
||||
assert!(model
|
||||
.workbook
|
||||
.styles
|
||||
.get_style_index_by_name("bold & italics")
|
||||
.is_ok());
|
||||
fs::remove_file(temp_file_name).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_existing_file() {
|
||||
let file_name = "existing_file.xlsx";
|
||||
fs::File::create(file_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
save_to_xlsx(&new_empty_model(), file_name),
|
||||
Err(XlsxError::IO(
|
||||
"file existing_file.xlsx already exists".to_string()
|
||||
)),
|
||||
);
|
||||
|
||||
fs::remove_file(file_name).unwrap();
|
||||
}
|
||||
91
xlsx/src/export/workbook.rs
Normal file
91
xlsx/src/export/workbook.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
|
||||
|
||||
//! A workbook is composed of workbook-level properties and a collection of 1 or more sheets.
|
||||
//! The workbook part and corresponding properties comprise data
|
||||
//! used to set application and workbook-level operational state. The workbook also serves to bind all the sheets
|
||||
//! and child elements into an organized single file. The workbook XML attributes and elements include information
|
||||
//! about what application last saved the file, where and how the windows of the workbook were positioned, and
|
||||
//! an enumeration of the worksheets in the workbook.
|
||||
//! This is the XML for the smallest possible (blank) workbook:
|
||||
//!
|
||||
//! <workbook>
|
||||
//! <sheets>
|
||||
//! <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
|
||||
//! </sheets>
|
||||
//! </workbook>
|
||||
//!
|
||||
//! Note that this workbook has a single sheet, named Sheet1. An Id for the sheet is required, and a relationship Id
|
||||
//! pointing to the location of the sheet definition is also required.
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! The most important objet of this part is a collection of all the sheets and all the defined names
|
||||
//! of the workbook.
|
||||
//!
|
||||
//! It also may hold state properties like the selected tab
|
||||
|
||||
//! # bookViews
|
||||
//!
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ironcalc_base::types::{SheetState, Workbook};
|
||||
|
||||
use super::escape::escape_xml;
|
||||
use super::xml_constants::XML_DECLARATION;
|
||||
|
||||
pub(crate) fn get_workbook_xml(workbook: &Workbook) -> String {
|
||||
// sheets
|
||||
// <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
|
||||
let mut sheets_str: Vec<String> = vec![];
|
||||
let mut sheet_id_to_sheet_index: HashMap<u32, u32> = HashMap::new();
|
||||
for (sheet_index, worksheet) in workbook.worksheets.iter().enumerate() {
|
||||
let name = &worksheet.name;
|
||||
let name = escape_xml(name);
|
||||
let sheet_id = worksheet.sheet_id;
|
||||
let state_str = match &worksheet.state {
|
||||
SheetState::Visible => "",
|
||||
SheetState::Hidden => " state=\"hidden\"",
|
||||
SheetState::VeryHidden => " state=\"veryHidden\"",
|
||||
};
|
||||
|
||||
sheets_str.push(format!(
|
||||
"<sheet name=\"{name}\" sheetId=\"{sheet_id}\" r:id=\"rId{}\"{state_str}/>",
|
||||
sheet_index + 1
|
||||
));
|
||||
sheet_id_to_sheet_index.insert(sheet_id, sheet_index as u32);
|
||||
}
|
||||
|
||||
// defined names
|
||||
// <definedName localSheetId="4" name="answer">shared!$G$5</definedName>
|
||||
// <definedName name="numbers">Sheet1!$A$16:$A$18</definedName>
|
||||
let mut defined_names_str: Vec<String> = vec![];
|
||||
for defined_name in &workbook.defined_names {
|
||||
let name = &defined_name.name;
|
||||
let name = escape_xml(name);
|
||||
let local_sheet_id = if let Some(sheet_id) = defined_name.sheet_id {
|
||||
// In Excel the localSheetId is actually the index of the sheet.
|
||||
let excel_local_sheet_id = sheet_id_to_sheet_index.get(&sheet_id).unwrap();
|
||||
format!(" localSheetId=\"{excel_local_sheet_id}\"")
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let formula = escape_xml(&defined_name.formula);
|
||||
defined_names_str.push(format!(
|
||||
"<definedName name=\"{name}\"{local_sheet_id}>{formula}</definedName>"
|
||||
))
|
||||
}
|
||||
|
||||
let sheets = sheets_str.join("");
|
||||
let defined_names = defined_names_str.join("");
|
||||
format!("{XML_DECLARATION}\n\
|
||||
<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\
|
||||
<sheets>\
|
||||
{sheets}\
|
||||
</sheets>\
|
||||
<definedNames>\
|
||||
{defined_names}\
|
||||
</definedNames>\
|
||||
<calcPr/>\
|
||||
</workbook>")
|
||||
}
|
||||
25
xlsx/src/export/workbook_xml_rels.rs
Normal file
25
xlsx/src/export/workbook_xml_rels.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use ironcalc_base::types::Workbook;
|
||||
|
||||
use super::xml_constants::{XML_DECLARATION, XML_WORKSHEET};
|
||||
|
||||
pub(crate) fn get_workbook_xml_rels(workbook: &Workbook) -> String {
|
||||
let mut relationships_str: Vec<String> = vec![];
|
||||
let worksheet_count = workbook.worksheets.len() + 1;
|
||||
for id in 1..worksheet_count {
|
||||
relationships_str.push(format!(
|
||||
"<Relationship Id=\"rId{id}\" Type=\"{XML_WORKSHEET}\" Target=\"worksheets/sheet{id}.xml\"/>"
|
||||
));
|
||||
}
|
||||
let mut id = worksheet_count;
|
||||
relationships_str.push(
|
||||
format!("<Relationship Id=\"rId{id}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>")
|
||||
);
|
||||
id += 1;
|
||||
relationships_str.push(
|
||||
format!("<Relationship Id=\"rId{id}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings\" Target=\"sharedStrings.xml\"/>")
|
||||
);
|
||||
format!(
|
||||
"{XML_DECLARATION}\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">{}</Relationships>",
|
||||
relationships_str.join("")
|
||||
)
|
||||
}
|
||||
267
xlsx/src/export/worksheets.rs
Normal file
267
xlsx/src/export/worksheets.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
//! # A note on shared formulas
|
||||
//! Although both Excel and IronCalc uses shared formulas they are used in a slightly different way that cannot be mapped 1-1
|
||||
//! In IronCalc _all_ formulas are shared and there is a list of shared formulas much like there is a list of shared strings.
|
||||
//! In Excel the situation in more nuanced. A shared formula is shared amongst a rage of cells.
|
||||
//! The top left cell would be the "mother" cell that would contain the shared formula:
|
||||
//! <c r="F4" t="str">
|
||||
//! <f t="shared" ref="F4:F8" si="42">A4+C4</f>
|
||||
//! <v>123</v>
|
||||
//! </c>
|
||||
//! Cells in the range F4:F8 will then link to that formula like so:
|
||||
//! <c r="F6">
|
||||
//! <f t="shared" si="42"/>
|
||||
//! <v>1</v>
|
||||
//! </c>
|
||||
//! Formula in F6 would then be 'A6+C6'
|
||||
use std::collections::HashMap;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use ironcalc_base::{
|
||||
expressions::{
|
||||
parser::{stringify::to_excel_string, Node},
|
||||
types::CellReferenceRC,
|
||||
utils::number_to_column,
|
||||
},
|
||||
types::{Cell, Worksheet},
|
||||
};
|
||||
|
||||
use super::{escape::escape_xml, xml_constants::XML_DECLARATION};
|
||||
|
||||
fn get_cell_style_attribute(s: i32) -> String {
|
||||
if s == 0 {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!(" s=\"{}\"", s)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_formula_attribute(
|
||||
sheet_name: String,
|
||||
row: i32,
|
||||
column: i32,
|
||||
parsed_formula: &Node,
|
||||
) -> String {
|
||||
let cell_ref = CellReferenceRC {
|
||||
sheet: sheet_name,
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let formula = &to_excel_string(parsed_formula, &cell_ref);
|
||||
escape_xml(formula).to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn get_worksheet_xml(
|
||||
worksheet: &Worksheet,
|
||||
parsed_formulas: &[Node],
|
||||
dimension: &str,
|
||||
) -> String {
|
||||
let mut sheet_data_str: Vec<String> = vec![];
|
||||
let mut cols_str: Vec<String> = vec![];
|
||||
|
||||
for col in &worksheet.cols {
|
||||
// <col min="4" max="4" width="12" customWidth="1"/>
|
||||
let min = col.min;
|
||||
let max = col.max;
|
||||
let width = col.width;
|
||||
let custom_width = i32::from(col.custom_width);
|
||||
let column_style = match col.style {
|
||||
Some(s) => format!(" style=\"{s}\""),
|
||||
None => "".to_string(),
|
||||
};
|
||||
cols_str.push(format!(
|
||||
"<col min=\"{min}\" max=\"{max}\" width=\"{width}\" customWidth=\"{custom_width}\"{column_style}/>"
|
||||
));
|
||||
}
|
||||
|
||||
// this is a bit of an overkill. A dictionary of the row styles by row_index
|
||||
let mut row_style_dict = HashMap::new();
|
||||
for row in &worksheet.rows {
|
||||
// {
|
||||
// "height": 13,
|
||||
// "r": 7,
|
||||
// "custom_format": false,
|
||||
// "custom_height": true,
|
||||
// "s": 0
|
||||
// "hidden": false,
|
||||
// },
|
||||
row_style_dict.insert(row.r, row.clone());
|
||||
}
|
||||
|
||||
for (row_index, row_data) in worksheet.sheet_data.iter().sorted_by_key(|x| x.0) {
|
||||
let mut row_data_str: Vec<String> = vec![];
|
||||
for (column_index, cell) in row_data.iter().sorted_by_key(|x| x.0) {
|
||||
let column_name = number_to_column(*column_index).unwrap();
|
||||
let cell_name = format!("{column_name}{row_index}");
|
||||
match cell {
|
||||
Cell::EmptyCell { s } => {
|
||||
// they only hold the style
|
||||
let style = get_cell_style_attribute(*s);
|
||||
row_data_str.push(format!("<c r=\"{cell_name}\"{style}/>"));
|
||||
}
|
||||
Cell::BooleanCell { v, s } => {
|
||||
// <c r="A8" t="b" s="1">
|
||||
// <v>1</v>
|
||||
// </c>
|
||||
let b = i32::from(*v);
|
||||
let style = get_cell_style_attribute(*s);
|
||||
row_data_str.push(format!(
|
||||
"<c r=\"{cell_name}\" t=\"b\"{style}><v>{b}</v></c>"
|
||||
));
|
||||
}
|
||||
Cell::NumberCell { v, s } => {
|
||||
// Normally the type number is left out. Example:
|
||||
// <c r="C6" s="1">
|
||||
// <v>3</v>
|
||||
// </c>
|
||||
let style = get_cell_style_attribute(*s);
|
||||
row_data_str.push(format!("<c r=\"{cell_name}\"{style}><v>{v}</v></c>"));
|
||||
}
|
||||
Cell::ErrorCell { ei, s } => {
|
||||
let style = get_cell_style_attribute(*s);
|
||||
row_data_str.push(format!(
|
||||
"<c r=\"{cell_name}\" t=\"e\"{style}><v>{ei}</v></c>"
|
||||
));
|
||||
}
|
||||
Cell::SharedString { si, s } => {
|
||||
// Example:
|
||||
// <c r="A1" s="1" t="s">
|
||||
// <v>5</v>
|
||||
// </c>
|
||||
// Cell on A1 contains a string (t="s") of style="1". The string is the 6th in the list of shared strings
|
||||
let style = get_cell_style_attribute(*s);
|
||||
row_data_str.push(format!(
|
||||
"<c r=\"{cell_name}\" t=\"s\"{style}><v>{si}</v></c>"
|
||||
));
|
||||
}
|
||||
Cell::CellFormula { f: _, s: _ } => {
|
||||
panic!("Model needs to be evaluated before saving!");
|
||||
}
|
||||
Cell::CellFormulaBoolean { f, v, s } => {
|
||||
// <c r="A4" t="b" s="3">
|
||||
// <f>ISTEXT(A5)</f>
|
||||
// <v>1</v>
|
||||
// </c>
|
||||
let style = get_cell_style_attribute(*s);
|
||||
|
||||
let formula = get_formula_attribute(
|
||||
worksheet.get_name(),
|
||||
*row_index,
|
||||
*column_index,
|
||||
&parsed_formulas[*f as usize],
|
||||
);
|
||||
|
||||
let b = i32::from(*v);
|
||||
row_data_str.push(format!(
|
||||
"<c r=\"{cell_name}\" t=\"b\"{style}><f>{formula}</f><v>{b}</v></c>"
|
||||
));
|
||||
}
|
||||
Cell::CellFormulaNumber { f, v, s } => {
|
||||
// Note again type is skipped
|
||||
// <c r="C4" s="3">
|
||||
// <f>A5+C3</f>
|
||||
// <v>123</v>
|
||||
// </c>
|
||||
|
||||
let formula = get_formula_attribute(
|
||||
worksheet.get_name(),
|
||||
*row_index,
|
||||
*column_index,
|
||||
&parsed_formulas[*f as usize],
|
||||
);
|
||||
let style = get_cell_style_attribute(*s);
|
||||
|
||||
row_data_str.push(format!(
|
||||
"<c r=\"{cell_name}\"{style}><f>{formula}</f><v>{v}</v></c>"
|
||||
));
|
||||
}
|
||||
Cell::CellFormulaString { f, v, s } => {
|
||||
// <c r="C6" t="str" s="5">
|
||||
// <f>CONCATENATE(A1, A2)</f>
|
||||
// <v>Hello world!</v>
|
||||
// </c>
|
||||
let formula = get_formula_attribute(
|
||||
worksheet.get_name(),
|
||||
*row_index,
|
||||
*column_index,
|
||||
&parsed_formulas[*f as usize],
|
||||
);
|
||||
let style = get_cell_style_attribute(*s);
|
||||
|
||||
row_data_str.push(format!(
|
||||
"<c r=\"{cell_name}\" t=\"str\"{style}><f>{formula}</f><v>{v}</v></c>"
|
||||
));
|
||||
}
|
||||
Cell::CellFormulaError {
|
||||
f,
|
||||
ei,
|
||||
s,
|
||||
o: _,
|
||||
m: _,
|
||||
} => {
|
||||
// <c r="C6" t="e" s="4">
|
||||
// <f>A1/A3<f/>
|
||||
// <v>#DIV/0!</v>
|
||||
// </c>
|
||||
let formula = get_formula_attribute(
|
||||
worksheet.get_name(),
|
||||
*row_index,
|
||||
*column_index,
|
||||
&parsed_formulas[*f as usize],
|
||||
);
|
||||
let style = get_cell_style_attribute(*s);
|
||||
row_data_str.push(format!(
|
||||
"<c r=\"{cell_name}\" t=\"e\"{style}><f>{formula}</f><v>{ei}</v></c>"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let row_style_str = match row_style_dict.get(row_index) {
|
||||
Some(row_style) => {
|
||||
let hidden_str = if row_style.hidden {
|
||||
r#" hidden="1""#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!(
|
||||
r#" s="{}" ht="{}" customHeight="{}" customFormat="{}"{}"#,
|
||||
row_style.s,
|
||||
row_style.height,
|
||||
i32::from(row_style.custom_height),
|
||||
i32::from(row_style.custom_format),
|
||||
hidden_str,
|
||||
)
|
||||
}
|
||||
None => "".to_string(),
|
||||
};
|
||||
sheet_data_str.push(format!(
|
||||
"<row r=\"{row_index}\"{row_style_str}>{}</row>",
|
||||
row_data_str.join("")
|
||||
))
|
||||
}
|
||||
let sheet_data = sheet_data_str.join("");
|
||||
let cols = cols_str.join("");
|
||||
let cols = if cols.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!("<cols>{cols}</cols>")
|
||||
};
|
||||
|
||||
format!(
|
||||
"{XML_DECLARATION}
|
||||
<worksheet \
|
||||
xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" \
|
||||
xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\
|
||||
<dimension ref=\"{dimension}\"/>\
|
||||
<sheetViews>\
|
||||
<sheetView workbookViewId=\"0\">\
|
||||
<selection activeCell=\"A1\" sqref=\"A1\"/>\
|
||||
</sheetView>\
|
||||
</sheetViews>\
|
||||
{cols}\
|
||||
<sheetData>\
|
||||
{sheet_data}\
|
||||
</sheetData>\
|
||||
</worksheet>"
|
||||
)
|
||||
}
|
||||
5
xlsx/src/export/xml_constants.rs
Normal file
5
xlsx/src/export/xml_constants.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub(crate) const XML_DECLARATION: &str =
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#;
|
||||
|
||||
pub(crate) const XML_WORKSHEET: &str =
|
||||
r#"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"#;
|
||||
Reference in New Issue
Block a user