use std::collections::HashMap;
use std::io::{Read, Seek};
use opc::Package;
use xml_core::{BytesStart, Event, Reader};
use crate::error::{Error, Result};
use crate::model::{
AutoFilterColumn, Border, BorderEdge, BorderStyle, CalculationMode, CalculationProperties,
Cell, CellAnchorPoint, CellComment, CellFormat, CellHyperlink, CellValue, ColumnSetting,
ComparisonOperator, ConditionalFormattingRule, CustomPropertyValue, DataValidationKind,
DataValidationRule, DefinedName, DocumentProperties, DrawingAnchor, DrawingObject, ExcelTable,
ExternalWorkbookLink, FreezePane, GradientFill, GradientStop, HorizontalAlignment,
HyperlinkTarget, IgnoredError, Message, NamedCellStyle, PageOrientation, PatternFill,
PatternType, PictureFormat, PrintSettings, ProtectedRange, RichTextRun, Row, Scenario, Sheet,
SheetChart, SheetDrawing, SheetPicture, SheetProtection, SheetVisibility, SortCondition,
SortState, TableColumn, TableStyle, TableStyleElement, TableStyleElementType,
TextComparisonOperator, TotalsRowFunction, VerticalAlignment, Workbook, WorkbookProtection,
WriteProtection,
};
const EXCEL_MAX_ROWS: usize = 1_048_576;
const EXCEL_MAX_COLUMNS: usize = 16_384;
const MAIN_WORKBOOK_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
const SHARED_STRINGS_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings";
const STYLES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
const TABLE_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/table";
const COMMENTS_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
const DRAWING_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing";
const IMAGE_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
const CHART_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
const CORE_PROPERTIES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
const EXTENDED_PROPERTIES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties";
const VBA_PROJECT_RELATIONSHIP_TYPE: &str =
"http://schemas.microsoft.com/office/2006/relationships/vbaProject";
const CUSTOM_PROPERTIES_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
const EXTERNAL_LINK_TARGET_RELATIONSHIP_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLinkPath";
impl Workbook {
pub fn read_from<R: Read + Seek>(reader: R) -> Result<Self> {
let package = Package::read_from(reader)?;
let main_workbook_relationship = package
.relationships()
.iter()
.find(|relationship| relationship.rel_type == MAIN_WORKBOOK_RELATIONSHIP_TYPE)
.ok_or(Error::MissingWorkbook)?;
let workbook_part_name = format!("/{}", main_workbook_relationship.target);
let workbook_part = package
.part(&workbook_part_name)
.ok_or(Error::MissingWorkbook)?;
let workbook_xml = std::str::from_utf8(&workbook_part.data)
.map_err(|error| Error::InvalidUtf8(workbook_part_name.clone(), error))?;
let parsed_workbook = parse_workbook_xml(workbook_xml)?;
let sheet_entries = parsed_workbook.sheets;
let defined_names = parsed_workbook.defined_names;
let core_properties_relationship = package
.relationships()
.iter()
.find(|relationship| relationship.rel_type == CORE_PROPERTIES_RELATIONSHIP_TYPE);
let ParsedCoreProperties {
title,
author,
subject,
keywords,
} = match core_properties_relationship {
Some(relationship) => {
let part_name = format!("/{}", relationship.target);
match package.part(&part_name) {
Some(part) => {
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
parse_core_properties_xml(xml)?
}
None => ParsedCoreProperties::default(),
}
}
None => ParsedCoreProperties::default(),
};
let extended_properties_relationship = package
.relationships()
.iter()
.find(|relationship| relationship.rel_type == EXTENDED_PROPERTIES_RELATIONSHIP_TYPE);
let (company, manager, hyperlink_base) = match extended_properties_relationship {
Some(relationship) => {
let part_name = format!("/{}", relationship.target);
match package.part(&part_name) {
Some(part) => {
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
parse_extended_properties_xml(xml)?
}
None => (None, None, None),
}
}
None => (None, None, None),
};
let custom_properties_relationship = package
.relationships()
.iter()
.find(|relationship| relationship.rel_type == CUSTOM_PROPERTIES_RELATIONSHIP_TYPE);
let custom_properties = match custom_properties_relationship {
Some(relationship) => {
let part_name = format!("/{}", relationship.target);
match package.part(&part_name) {
Some(part) => {
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
parse_custom_properties_xml(xml)?
}
None => Vec::new(),
}
}
None => Vec::new(),
};
let properties = DocumentProperties {
title,
author,
subject,
keywords,
company,
manager,
hyperlink_base,
custom_properties,
};
let vba_project = workbook_part
.relationships
.iter()
.find(|relationship| relationship.rel_type == VBA_PROJECT_RELATIONSHIP_TYPE)
.and_then(|relationship| package.part(&format!("/xl/{}", relationship.target)))
.map(|part| part.data.clone());
let mut external_links = Vec::new();
for relationship_id in &parsed_workbook.external_reference_relationship_ids {
let Some(relationship) = workbook_part.relationships.by_id(relationship_id) else {
continue;
};
let part_name = format!("/xl/{}", relationship.target);
let Some(part) = package.part(&part_name) else {
continue;
};
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
let ParsedExternalLink {
sheet_names,
defined_names,
} = parse_external_link_xml(xml)?;
let target = part
.relationships
.iter()
.find(|relationship| {
relationship.rel_type == EXTERNAL_LINK_TARGET_RELATIONSHIP_TYPE
})
.map(|relationship| relationship.target.clone())
.unwrap_or_default();
external_links.push(ExternalWorkbookLink {
target,
sheet_names,
defined_names,
});
}
let shared_strings_relationship = workbook_part
.relationships
.iter()
.find(|relationship| relationship.rel_type == SHARED_STRINGS_RELATIONSHIP_TYPE);
let shared_strings = match shared_strings_relationship {
Some(relationship) => {
let part_name = format!("/xl/{}", relationship.target);
match package.part(&part_name) {
Some(part) => {
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
parse_shared_strings_xml(xml)?
}
None => Vec::new(),
}
}
None => Vec::new(),
};
let styles_relationship = workbook_part
.relationships
.iter()
.find(|relationship| relationship.rel_type == STYLES_RELATIONSHIP_TYPE);
let parsed_styles: ParsedStyles = match styles_relationship {
Some(relationship) => {
let part_name = format!("/xl/{}", relationship.target);
match package.part(&part_name) {
Some(part) => {
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
parse_styles_xml(xml)?
}
None => ParsedStyles::default(),
}
}
None => ParsedStyles::default(),
};
let ParsedStyles {
cell_formats,
dxfs,
table_styles,
named_cell_styles,
} = parsed_styles;
let mut sheets = Vec::with_capacity(sheet_entries.len());
for (name, relationship_id, visibility) in sheet_entries {
let relationship = workbook_part
.relationships
.by_id(&relationship_id)
.ok_or_else(|| Error::MissingSheetRelationship(relationship_id.clone()))?;
let worksheet_part_name = format!("/xl/{}", relationship.target);
let worksheet_part = package
.part(&worksheet_part_name)
.ok_or_else(|| Error::MissingWorksheetPart(worksheet_part_name.clone()))?;
let worksheet_xml = std::str::from_utf8(&worksheet_part.data)
.map_err(|error| Error::InvalidUtf8(worksheet_part_name.clone(), error))?;
let sheet_relationships = &worksheet_part.relationships;
let parsed = parse_worksheet_xml(
worksheet_xml,
&shared_strings,
&cell_formats,
&dxfs,
sheet_relationships,
)?;
let comments = match sheet_relationships
.iter()
.find(|relationship| relationship.rel_type == COMMENTS_RELATIONSHIP_TYPE)
{
Some(relationship) => {
let part_name =
format!("/xl/{}", relationship.target.trim_start_matches("../"));
match package.part(&part_name) {
Some(part) => {
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
parse_comments_xml(xml)?
}
None => Vec::new(),
}
}
None => Vec::new(),
};
let table = match sheet_relationships
.iter()
.find(|relationship| relationship.rel_type == TABLE_RELATIONSHIP_TYPE)
{
Some(relationship) => {
let part_name =
format!("/xl/{}", relationship.target.trim_start_matches("../"));
match package.part(&part_name) {
Some(part) => {
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
Some(parse_table_xml(xml)?)
}
None => None,
}
}
None => None,
};
let drawing = match sheet_relationships
.iter()
.find(|relationship| relationship.rel_type == DRAWING_RELATIONSHIP_TYPE)
{
Some(relationship) => {
let part_name =
format!("/xl/{}", relationship.target.trim_start_matches("../"));
match package.part(&part_name) {
Some(part) => {
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
Some(parse_drawing_xml(xml, &part.relationships, &package)?)
}
None => None,
}
}
None => None,
};
sheets.push(Sheet {
name,
rows: parsed.rows,
conditional_formatting_rules: parsed.conditional_formatting_rules,
data_validation_rules: parsed.data_validation_rules,
merged_ranges: parsed.merged_ranges,
column_settings: parsed.column_settings,
auto_filter_range: parsed.auto_filter_range,
freeze_panes: parsed.freeze_panes,
print_settings: parsed.print_settings,
protection: parsed.protection,
hyperlinks: parsed.hyperlinks,
comments,
table,
drawing,
visibility,
zoom_scale: parsed.zoom_scale,
row_breaks: parsed.row_breaks,
column_breaks: parsed.column_breaks,
scenarios: parsed.scenarios,
auto_filter_columns: parsed.auto_filter_columns,
tab_color: parsed.tab_color,
show_grid_lines: parsed.show_grid_lines,
show_row_col_headers: parsed.show_row_col_headers,
show_zeros: parsed.show_zeros,
right_to_left: parsed.right_to_left,
active_cell: parsed.active_cell,
default_column_width: parsed.default_column_width,
default_row_height: parsed.default_row_height,
ignored_errors: parsed.ignored_errors,
protected_ranges: parsed.protected_ranges,
sort_state: parsed.sort_state,
outline_summary_below: parsed.outline_summary_below,
outline_summary_right: parsed.outline_summary_right,
});
}
Ok(Workbook {
sheets,
defined_names,
properties,
protection: parsed_workbook.protection,
write_protection: parsed_workbook.write_protection,
active_tab: parsed_workbook.active_tab,
external_links,
vba_project,
calculation: parsed_workbook.calculation,
table_styles,
named_cell_styles,
})
}
}
struct ParsedWorkbook {
sheets: Vec<(String, String, SheetVisibility)>,
defined_names: Vec<DefinedName>,
write_protection: Option<WriteProtection>,
protection: Option<WorkbookProtection>,
active_tab: Option<usize>,
external_reference_relationship_ids: Vec<String>,
calculation: Option<CalculationProperties>,
}
fn parse_workbook_xml(xml: &str) -> Result<ParsedWorkbook> {
let mut reader = Reader::from_xml_str(xml);
let mut sheets = Vec::new();
let mut defined_names = Vec::new();
let mut write_protection: Option<WriteProtection> = None;
let mut protection: Option<WorkbookProtection> = None;
let mut active_tab: Option<usize> = None;
let mut external_reference_relationship_ids = Vec::new();
let mut calculation: Option<CalculationProperties> = None;
let mut in_defined_name = false;
let mut current_name: Option<String> = None;
let mut current_local_sheet_id: Option<usize> = None;
let mut current_hidden = false;
let mut current_comment: Option<String> = None;
let mut refers_to_buffer = String::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) | Event::Empty(start)
if start.local_name().as_ref() == b"sheet" =>
{
let name = raw_attribute(&start, b"name").unwrap_or_default();
let relationship_id = raw_attribute(&start, b"id").unwrap_or_default();
let visibility = match raw_attribute(&start, b"state").as_deref() {
Some("hidden") => SheetVisibility::Hidden,
Some("veryHidden") => SheetVisibility::VeryHidden,
_ => SheetVisibility::Visible,
};
sheets.push((name, relationship_id, visibility));
}
Event::Empty(start) if start.local_name().as_ref() == b"calcPr" => {
let mode = match raw_attribute(&start, b"calcMode").as_deref() {
Some("manual") => Some(CalculationMode::Manual),
Some("autoNoTable") => Some(CalculationMode::AutomaticExceptTables),
Some("auto") => Some(CalculationMode::Automatic),
_ => None,
};
let iterate = raw_attribute(&start, b"iterate").as_deref() == Some("1");
calculation = Some(CalculationProperties { mode, iterate });
}
Event::Empty(start) if start.local_name().as_ref() == b"workbookProtection" => {
protection = Some(WorkbookProtection {
password: None,
lock_structure: raw_attribute(&start, b"lockStructure").as_deref() == Some("1"),
lock_windows: raw_attribute(&start, b"lockWindows").as_deref() == Some("1"),
});
}
Event::Empty(start) if start.local_name().as_ref() == b"fileSharing" => {
write_protection = Some(WriteProtection {
read_only_recommended: raw_attribute(&start, b"readOnlyRecommended").as_deref()
== Some("1"),
user_name: raw_attribute(&start, b"userName"),
password: None,
});
}
Event::Empty(start) if start.local_name().as_ref() == b"workbookView" => {
active_tab = raw_attribute(&start, b"activeTab")
.and_then(|value| value.parse::<usize>().ok());
}
Event::Empty(start) if start.local_name().as_ref() == b"externalReference" => {
if let Some(id) = raw_attribute(&start, b"id") {
external_reference_relationship_ids.push(id);
}
}
Event::Start(start) if start.local_name().as_ref() == b"definedName" => {
in_defined_name = true;
current_name = raw_attribute(&start, b"name");
current_local_sheet_id = raw_attribute(&start, b"localSheetId")
.and_then(|value| value.parse::<usize>().ok());
current_hidden = raw_attribute(&start, b"hidden").as_deref() == Some("1");
current_comment = raw_attribute(&start, b"comment");
refers_to_buffer.clear();
}
Event::Empty(start) if start.local_name().as_ref() == b"definedName" => {
if let Some(name) = raw_attribute(&start, b"name") {
defined_names.push(DefinedName {
name,
refers_to: String::new(),
local_sheet_id: raw_attribute(&start, b"localSheetId")
.and_then(|value| value.parse::<usize>().ok()),
hidden: raw_attribute(&start, b"hidden").as_deref() == Some("1"),
comment: raw_attribute(&start, b"comment"),
});
}
}
Event::End(end) if in_defined_name && end.local_name().as_ref() == b"definedName" => {
in_defined_name = false;
if let Some(name) = current_name.take() {
defined_names.push(DefinedName {
name,
refers_to: std::mem::take(&mut refers_to_buffer),
local_sheet_id: current_local_sheet_id.take(),
hidden: std::mem::take(&mut current_hidden),
comment: current_comment.take(),
});
}
}
Event::Text(text) if in_defined_name => {
refers_to_buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if in_defined_name => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
refers_to_buffer.push_str(&resolved);
}
}
_ => {}
}
}
Ok(ParsedWorkbook {
sheets,
defined_names,
write_protection,
protection,
active_tab,
external_reference_relationship_ids,
calculation,
})
}
#[derive(Debug, Clone, PartialEq)]
enum SharedStringEntry {
Plain(String),
Rich(Vec<RichTextRun>),
}
fn parse_shared_strings_xml(xml: &str) -> Result<Vec<SharedStringEntry>> {
let mut reader = Reader::from_xml_str(xml);
let mut entries = Vec::new();
let mut in_si = false;
let mut si_is_rich = false;
let mut si_plain_buffer = String::new();
let mut si_runs: Vec<RichTextRun> = Vec::new();
let mut in_run = false;
let mut run_text_buffer = String::new();
let mut run_bold = false;
let mut run_italic = false;
let mut run_size: Option<f64> = None;
let mut run_color: Option<String> = None;
let mut run_name: Option<String> = None;
let mut run_underline = false;
let mut run_strike = false;
let mut in_rpr = false;
let mut in_text = false;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"si" => {
in_si = true;
si_is_rich = false;
si_plain_buffer.clear();
si_runs.clear();
}
Event::End(end) if end.local_name().as_ref() == b"si" => {
in_si = false;
if si_is_rich {
entries.push(SharedStringEntry::Rich(std::mem::take(&mut si_runs)));
} else {
entries.push(SharedStringEntry::Plain(std::mem::take(
&mut si_plain_buffer,
)));
}
}
Event::Start(start) if in_si && start.local_name().as_ref() == b"r" => {
in_run = true;
si_is_rich = true;
run_text_buffer.clear();
run_bold = false;
run_italic = false;
run_size = None;
run_color = None;
run_name = None;
run_underline = false;
run_strike = false;
}
Event::End(end) if in_si && end.local_name().as_ref() == b"r" => {
in_run = false;
si_runs.push(RichTextRun {
text: std::mem::take(&mut run_text_buffer),
bold: std::mem::take(&mut run_bold),
italic: std::mem::take(&mut run_italic),
font_color: run_color.take(),
font_size: run_size.take(),
font_name: run_name.take(),
underline: std::mem::take(&mut run_underline),
strike: std::mem::take(&mut run_strike),
});
}
Event::Start(start) if in_run && start.local_name().as_ref() == b"rPr" => {
in_rpr = true;
}
Event::End(end) if in_run && end.local_name().as_ref() == b"rPr" => {
in_rpr = false;
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"b" => {
run_bold = true;
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"i" => {
run_italic = true;
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"strike" => {
run_strike = true;
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"u" => {
run_underline = true;
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"rFont" => {
run_name = raw_attribute(&start, b"val");
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"sz" => {
run_size =
raw_attribute(&start, b"val").and_then(|value| value.parse::<f64>().ok());
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"color" => {
run_color = resolve_color(&start);
}
Event::Start(start) if start.local_name().as_ref() == b"t" => {
in_text = true;
}
Event::End(end) if end.local_name().as_ref() == b"t" => {
in_text = false;
}
Event::Text(text) if in_text && in_run => {
run_text_buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if in_text && in_run => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
run_text_buffer.push_str(&resolved);
}
}
Event::Text(text) if in_text && !in_run => {
si_plain_buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if in_text && !in_run => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
si_plain_buffer.push_str(&resolved);
}
}
_ => {}
}
}
Ok(entries)
}
struct RawFont {
bold: bool,
italic: bool,
size: f64,
color: Option<String>,
name: Option<String>,
underline: bool,
strike: bool,
}
enum RawFill {
None,
Gray125,
Solid(String),
Pattern(PatternFill),
Gradient(GradientFill),
Other,
}
#[derive(Default)]
struct RawBorder {
top: Option<(String, Option<String>)>,
bottom: Option<(String, Option<String>)>,
left: Option<(String, Option<String>)>,
right: Option<(String, Option<String>)>,
diagonal: Option<(String, Option<String>)>,
diagonal_up: bool,
diagonal_down: bool,
}
struct RawXf {
num_fmt_id: u32,
font_id: usize,
fill_id: usize,
border_id: usize,
xf_id: usize,
horizontal: Option<String>,
vertical: Option<String>,
wrap_text: bool,
indent: Option<u32>,
text_rotation: Option<u16>,
shrink_to_fit: bool,
locked: Option<bool>,
formula_hidden: Option<bool>,
}
#[derive(Default)]
struct ParsedStyles {
cell_formats: Vec<Option<CellFormat>>,
dxfs: Vec<CellFormat>,
table_styles: Vec<TableStyle>,
named_cell_styles: Vec<NamedCellStyle>,
}
fn parse_styles_xml(xml: &str) -> Result<ParsedStyles> {
let mut reader = Reader::from_xml_str(xml);
let mut num_fmts: HashMap<u32, String> = HashMap::new();
let mut fonts: Vec<RawFont> = Vec::new();
let mut fills: Vec<RawFill> = Vec::new();
let mut borders: Vec<RawBorder> = Vec::new();
let mut xfs: Vec<RawXf> = Vec::new();
let mut current_font: Option<RawFont> = None;
let mut in_fill = false;
let mut current_fill_pattern_type: Option<String> = None;
let mut current_fill_fg_color: Option<String> = None;
let mut current_fill_bg_color: Option<String> = None;
let mut current_fill_is_gradient = false;
let mut current_gradient_angle: Option<f64> = None;
let mut current_gradient_stops: Vec<GradientStop> = Vec::new();
let mut current_gradient_stop_position: Option<f64> = None;
let mut in_borders = false;
let mut current_border: Option<RawBorder> = None;
let mut current_border_edge_name: Option<&'static str> = None;
let mut current_border_edge_style: Option<String> = None;
let mut current_border_edge_color: Option<String> = None;
let mut in_cell_xfs = false;
let mut current_xf: Option<RawXf> = None;
let mut in_cell_style_xfs = false;
let mut current_style_xf: Option<RawXf> = None;
let mut style_xfs: Vec<RawXf> = Vec::new();
let mut cell_styles: Vec<(String, usize, Option<u32>)> = Vec::new();
let mut in_dxfs = false;
let mut current_dxf: Option<CellFormat> = None;
let mut in_dxf_font = false;
let mut dxf_font_bold = false;
let mut dxf_font_italic = false;
let mut dxf_font_color: Option<String> = None;
let mut in_dxf_fill = false;
let mut dxf_fill_pattern_type: Option<String> = None;
let mut dxf_fill_bg_color: Option<String> = None;
let mut dxfs: Vec<CellFormat> = Vec::new();
let mut table_styles: Vec<TableStyle> = Vec::new();
let mut current_table_style: Option<TableStyle> = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"tableStyle" => {
current_table_style = Some(TableStyle::new(
raw_attribute(&start, b"name").unwrap_or_default(),
));
}
Event::End(end) if end.local_name().as_ref() == b"tableStyle" => {
if let Some(style) = current_table_style.take() {
table_styles.push(style);
}
}
Event::Empty(start)
if current_table_style.is_some()
&& start.local_name().as_ref() == b"tableStyleElement" =>
{
if let Some(element_type) = raw_attribute(&start, b"type")
.as_deref()
.and_then(parse_table_style_element_type)
{
let dxf_id = raw_attribute(&start, b"dxfId")
.and_then(|value| value.parse::<usize>().ok());
let format = dxf_id
.and_then(|index| dxfs.get(index).cloned())
.unwrap_or_default();
if let Some(style) = current_table_style.as_mut() {
style
.elements
.push(TableStyleElement::new(element_type, format));
}
}
}
Event::Empty(start) if !in_dxfs && start.local_name().as_ref() == b"numFmt" => {
let id =
raw_attribute(&start, b"numFmtId").and_then(|value| value.parse::<u32>().ok());
let code = raw_attribute(&start, b"formatCode");
if let (Some(id), Some(code)) = (id, code) {
num_fmts.insert(id, code);
}
}
Event::Start(start) if !in_dxfs && start.local_name().as_ref() == b"font" => {
current_font = Some(RawFont {
bold: false,
italic: false,
size: 11.0,
color: None,
name: None,
underline: false,
strike: false,
});
}
Event::End(end) if !in_dxfs && end.local_name().as_ref() == b"font" => {
if let Some(font) = current_font.take() {
fonts.push(font);
}
}
Event::Empty(start)
if current_font.is_some() && start.local_name().as_ref() == b"b" =>
{
if let Some(font) = current_font.as_mut() {
font.bold = true;
}
}
Event::Empty(start)
if current_font.is_some() && start.local_name().as_ref() == b"i" =>
{
if let Some(font) = current_font.as_mut() {
font.italic = true;
}
}
Event::Empty(start)
if current_font.is_some() && start.local_name().as_ref() == b"strike" =>
{
if let Some(font) = current_font.as_mut() {
font.strike = true;
}
}
Event::Empty(start)
if current_font.is_some() && start.local_name().as_ref() == b"u" =>
{
if let Some(font) = current_font.as_mut() {
font.underline = true;
}
}
Event::Empty(start)
if current_font.is_some() && start.local_name().as_ref() == b"name" =>
{
if let Some(font) = current_font.as_mut() {
font.name = raw_attribute(&start, b"val");
}
}
Event::Empty(start)
if current_font.is_some() && start.local_name().as_ref() == b"sz" =>
{
if let Some(size) =
raw_attribute(&start, b"val").and_then(|value| value.parse::<f64>().ok())
{
if let Some(font) = current_font.as_mut() {
font.size = size;
}
}
}
Event::Empty(start)
if current_font.is_some() && start.local_name().as_ref() == b"color" =>
{
if let Some(font) = current_font.as_mut() {
font.color = resolve_color(&start);
}
}
Event::Start(start) if !in_dxfs && start.local_name().as_ref() == b"fill" => {
in_fill = true;
current_fill_pattern_type = None;
current_fill_fg_color = None;
current_fill_bg_color = None;
current_fill_is_gradient = false;
current_gradient_angle = None;
current_gradient_stops.clear();
}
Event::End(end) if !in_dxfs && end.local_name().as_ref() == b"fill" => {
in_fill = false;
let fill = if current_fill_is_gradient {
RawFill::Gradient(GradientFill {
angle: current_gradient_angle.unwrap_or(0.0),
stops: std::mem::take(&mut current_gradient_stops),
})
} else {
match current_fill_pattern_type.take().as_deref() {
Some("none") => RawFill::None,
Some("gray125") => RawFill::Gray125,
Some("solid") => match current_fill_fg_color.take() {
Some(color) => RawFill::Solid(color),
None => RawFill::Other,
},
Some(other) => match parse_pattern_type(other) {
Some(pattern_type) => RawFill::Pattern(PatternFill {
pattern_type,
fg_color: current_fill_fg_color.take(),
bg_color: current_fill_bg_color.take(),
}),
None => RawFill::Other,
},
None => RawFill::Other,
}
};
fills.push(fill);
}
Event::Start(start) if in_fill && start.local_name().as_ref() == b"patternFill" => {
current_fill_pattern_type = raw_attribute(&start, b"patternType");
}
Event::Empty(start) if in_fill && start.local_name().as_ref() == b"patternFill" => {
current_fill_pattern_type = raw_attribute(&start, b"patternType");
}
Event::Empty(start) if in_fill && start.local_name().as_ref() == b"fgColor" => {
current_fill_fg_color = resolve_color(&start);
}
Event::Empty(start) if in_fill && start.local_name().as_ref() == b"bgColor" => {
current_fill_bg_color = resolve_color(&start);
}
Event::Start(start) if in_fill && start.local_name().as_ref() == b"gradientFill" => {
current_fill_is_gradient = true;
current_gradient_angle =
raw_attribute(&start, b"degree").and_then(|value| value.parse::<f64>().ok());
}
Event::Start(start)
if in_fill
&& current_fill_is_gradient
&& start.local_name().as_ref() == b"stop" =>
{
current_gradient_stop_position =
raw_attribute(&start, b"position").and_then(|value| value.parse::<f64>().ok());
}
Event::Empty(start)
if in_fill
&& current_fill_is_gradient
&& start.local_name().as_ref() == b"color" =>
{
if let (Some(position), Some(color)) =
(current_gradient_stop_position.take(), resolve_color(&start))
{
current_gradient_stops.push(GradientStop::new(position, color));
}
}
Event::Start(start) if !in_dxfs && start.local_name().as_ref() == b"borders" => {
in_borders = true;
}
Event::End(end) if end.local_name().as_ref() == b"borders" => {
in_borders = false;
}
Event::Start(start) if in_borders && start.local_name().as_ref() == b"border" => {
current_border = Some(RawBorder {
diagonal_up: raw_attribute(&start, b"diagonalUp").as_deref() == Some("1"),
diagonal_down: raw_attribute(&start, b"diagonalDown").as_deref() == Some("1"),
..RawBorder::default()
});
}
Event::End(end) if in_borders && end.local_name().as_ref() == b"border" => {
if let Some(border) = current_border.take() {
borders.push(border);
}
}
Event::Start(start)
if current_border.is_some()
&& matches!(
start.local_name().as_ref(),
b"left" | b"right" | b"top" | b"bottom" | b"diagonal"
) =>
{
current_border_edge_name = Some(match start.local_name().as_ref() {
b"left" => "left",
b"right" => "right",
b"top" => "top",
b"bottom" => "bottom",
_ => "diagonal",
});
current_border_edge_style = raw_attribute(&start, b"style");
current_border_edge_color = None;
}
Event::Empty(start)
if current_border.is_some()
&& matches!(
start.local_name().as_ref(),
b"left" | b"right" | b"top" | b"bottom" | b"diagonal"
) =>
{
}
Event::Empty(start)
if current_border_edge_name.is_some()
&& start.local_name().as_ref() == b"color" =>
{
current_border_edge_color = resolve_color(&start);
}
Event::End(end)
if current_border_edge_name.is_some()
&& matches!(
end.local_name().as_ref(),
b"left" | b"right" | b"top" | b"bottom" | b"diagonal"
) =>
{
if let (Some(name), Some(style), Some(border)) = (
current_border_edge_name.take(),
current_border_edge_style.take(),
current_border.as_mut(),
) {
let edge = Some((style, current_border_edge_color.take()));
match name {
"left" => border.left = edge,
"right" => border.right = edge,
"top" => border.top = edge,
"bottom" => border.bottom = edge,
_ => border.diagonal = edge,
}
}
current_border_edge_color = None;
}
Event::Start(start) if start.local_name().as_ref() == b"cellXfs" => {
in_cell_xfs = true;
}
Event::End(end) if end.local_name().as_ref() == b"cellXfs" => {
in_cell_xfs = false;
}
Event::Empty(start) if in_cell_xfs && start.local_name().as_ref() == b"xf" => {
xfs.push(raw_xf_from_attributes(&start));
}
Event::Start(start) if in_cell_xfs && start.local_name().as_ref() == b"xf" => {
current_xf = Some(raw_xf_from_attributes(&start));
}
Event::End(end) if in_cell_xfs && end.local_name().as_ref() == b"xf" => {
if let Some(xf) = current_xf.take() {
xfs.push(xf);
}
}
Event::Empty(start)
if in_cell_xfs
&& current_xf.is_some()
&& start.local_name().as_ref() == b"alignment" =>
{
if let Some(xf) = current_xf.as_mut() {
xf.horizontal = raw_attribute(&start, b"horizontal");
xf.vertical = raw_attribute(&start, b"vertical");
xf.wrap_text = raw_attribute(&start, b"wrapText").as_deref() == Some("1");
xf.indent = raw_attribute(&start, b"indent")
.and_then(|value| value.parse::<u32>().ok());
xf.text_rotation = raw_attribute(&start, b"textRotation")
.and_then(|value| value.parse::<u16>().ok());
xf.shrink_to_fit =
raw_attribute(&start, b"shrinkToFit").as_deref() == Some("1");
}
}
Event::Empty(start)
if in_cell_xfs
&& current_xf.is_some()
&& start.local_name().as_ref() == b"protection" =>
{
if let Some(xf) = current_xf.as_mut() {
xf.locked = raw_attribute(&start, b"locked").map(|value| value == "1");
xf.formula_hidden = raw_attribute(&start, b"hidden").map(|value| value == "1");
}
}
Event::Start(start) if start.local_name().as_ref() == b"cellStyleXfs" => {
in_cell_style_xfs = true;
}
Event::End(end) if end.local_name().as_ref() == b"cellStyleXfs" => {
in_cell_style_xfs = false;
}
Event::Empty(start) if in_cell_style_xfs && start.local_name().as_ref() == b"xf" => {
style_xfs.push(raw_xf_from_attributes(&start));
}
Event::Start(start) if in_cell_style_xfs && start.local_name().as_ref() == b"xf" => {
current_style_xf = Some(raw_xf_from_attributes(&start));
}
Event::End(end) if in_cell_style_xfs && end.local_name().as_ref() == b"xf" => {
if let Some(xf) = current_style_xf.take() {
style_xfs.push(xf);
}
}
Event::Empty(start)
if in_cell_style_xfs
&& current_style_xf.is_some()
&& start.local_name().as_ref() == b"alignment" =>
{
if let Some(xf) = current_style_xf.as_mut() {
xf.horizontal = raw_attribute(&start, b"horizontal");
xf.vertical = raw_attribute(&start, b"vertical");
xf.wrap_text = raw_attribute(&start, b"wrapText").as_deref() == Some("1");
xf.indent = raw_attribute(&start, b"indent")
.and_then(|value| value.parse::<u32>().ok());
xf.text_rotation = raw_attribute(&start, b"textRotation")
.and_then(|value| value.parse::<u16>().ok());
xf.shrink_to_fit =
raw_attribute(&start, b"shrinkToFit").as_deref() == Some("1");
}
}
Event::Empty(start)
if in_cell_style_xfs
&& current_style_xf.is_some()
&& start.local_name().as_ref() == b"protection" =>
{
if let Some(xf) = current_style_xf.as_mut() {
xf.locked = raw_attribute(&start, b"locked").map(|value| value == "1");
xf.formula_hidden = raw_attribute(&start, b"hidden").map(|value| value == "1");
}
}
Event::Empty(start) | Event::Start(start)
if start.local_name().as_ref() == b"cellStyle" =>
{
if let Some(name) = raw_attribute(&start, b"name") {
let xf_id = raw_attribute(&start, b"xfId")
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(0);
let builtin_id = raw_attribute(&start, b"builtinId")
.and_then(|value| value.parse::<u32>().ok());
cell_styles.push((name, xf_id, builtin_id));
}
}
Event::Start(start) if start.local_name().as_ref() == b"dxfs" => {
in_dxfs = true;
}
Event::End(end) if end.local_name().as_ref() == b"dxfs" => {
in_dxfs = false;
}
Event::Start(start) if in_dxfs && start.local_name().as_ref() == b"dxf" => {
current_dxf = Some(CellFormat::default());
}
Event::End(end) if in_dxfs && end.local_name().as_ref() == b"dxf" => {
if let Some(dxf) = current_dxf.take() {
dxfs.push(dxf);
}
}
Event::Start(start)
if in_dxfs && current_dxf.is_some() && start.local_name().as_ref() == b"font" =>
{
in_dxf_font = true;
dxf_font_bold = false;
dxf_font_italic = false;
dxf_font_color = None;
}
Event::End(end) if in_dxfs && end.local_name().as_ref() == b"font" => {
in_dxf_font = false;
if let Some(dxf) = current_dxf.as_mut() {
dxf.bold = dxf_font_bold;
dxf.italic = dxf_font_italic;
dxf.font_color = dxf_font_color.take();
}
}
Event::Empty(start) if in_dxf_font && start.local_name().as_ref() == b"b" => {
dxf_font_bold = true;
}
Event::Empty(start) if in_dxf_font && start.local_name().as_ref() == b"i" => {
dxf_font_italic = true;
}
Event::Empty(start) if in_dxf_font && start.local_name().as_ref() == b"color" => {
dxf_font_color = raw_attribute(&start, b"rgb");
}
Event::Empty(start)
if in_dxfs && current_dxf.is_some() && start.local_name().as_ref() == b"numFmt" =>
{
let code = raw_attribute(&start, b"formatCode");
if let Some(dxf) = current_dxf.as_mut() {
dxf.number_format = code;
}
}
Event::Start(start)
if in_dxfs && current_dxf.is_some() && start.local_name().as_ref() == b"fill" =>
{
in_dxf_fill = true;
dxf_fill_pattern_type = None;
dxf_fill_bg_color = None;
}
Event::End(end) if in_dxfs && end.local_name().as_ref() == b"fill" => {
in_dxf_fill = false;
if dxf_fill_pattern_type.as_deref() == Some("solid") {
if let Some(dxf) = current_dxf.as_mut() {
dxf.fill_color = dxf_fill_bg_color.take();
}
}
}
Event::Start(start) if in_dxf_fill && start.local_name().as_ref() == b"patternFill" => {
dxf_fill_pattern_type = raw_attribute(&start, b"patternType");
}
Event::Empty(start) if in_dxf_fill && start.local_name().as_ref() == b"patternFill" => {
dxf_fill_pattern_type = raw_attribute(&start, b"patternType");
}
Event::Empty(start) if in_dxf_fill && start.local_name().as_ref() == b"bgColor" => {
dxf_fill_bg_color = raw_attribute(&start, b"rgb");
}
Event::Empty(start)
if in_dxfs
&& current_dxf.is_some()
&& start.local_name().as_ref() == b"alignment" =>
{
if let Some(dxf) = current_dxf.as_mut() {
dxf.horizontal_alignment = raw_attribute(&start, b"horizontal")
.as_deref()
.and_then(parse_horizontal_alignment);
dxf.vertical_alignment = raw_attribute(&start, b"vertical")
.as_deref()
.and_then(parse_vertical_alignment);
}
}
_ => {}
}
}
let style_name_by_xf_id: HashMap<usize, String> = cell_styles
.iter()
.filter(|(_, xf_id, _)| *xf_id != 0)
.map(|(name, xf_id, _)| (*xf_id, name.clone()))
.collect();
let cell_formats = (0..xfs.len())
.map(|index| {
resolve_cell_format(
index,
&xfs,
&fonts,
&fills,
&borders,
&num_fmts,
&style_name_by_xf_id,
)
})
.collect();
let named_cell_styles: Vec<NamedCellStyle> = cell_styles
.iter()
.filter(|(_, xf_id, _)| *xf_id != 0)
.filter_map(|(name, xf_id, builtin_id)| {
let xf = style_xfs.get(*xf_id)?;
let format = resolve_cell_format_raw(xf, &fonts, &fills, &borders, &num_fmts, None);
let mut style = NamedCellStyle::new(name.clone(), format);
if let Some(builtin_id) = builtin_id {
style = style.with_builtin_id(*builtin_id);
}
Some(style)
})
.collect();
Ok(ParsedStyles {
cell_formats,
dxfs,
table_styles,
named_cell_styles,
})
}
fn parse_table_style_element_type(value: &str) -> Option<TableStyleElementType> {
match value {
"wholeTable" => Some(TableStyleElementType::WholeTable),
"headerRow" => Some(TableStyleElementType::HeaderRow),
"totalRow" => Some(TableStyleElementType::TotalRow),
"firstRowStripe" => Some(TableStyleElementType::FirstRowStripe),
"secondRowStripe" => Some(TableStyleElementType::SecondRowStripe),
"firstColumnStripe" => Some(TableStyleElementType::FirstColumnStripe),
"secondColumnStripe" => Some(TableStyleElementType::SecondColumnStripe),
"firstColumn" => Some(TableStyleElementType::FirstColumn),
"lastColumn" => Some(TableStyleElementType::LastColumn),
_ => None,
}
}
fn raw_xf_from_attributes(start: &BytesStart) -> RawXf {
RawXf {
num_fmt_id: raw_attribute(start, b"numFmtId")
.and_then(|value| value.parse::<u32>().ok())
.unwrap_or(0),
font_id: raw_attribute(start, b"fontId")
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(0),
fill_id: raw_attribute(start, b"fillId")
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(0),
border_id: raw_attribute(start, b"borderId")
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(0),
xf_id: raw_attribute(start, b"xfId")
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(0),
horizontal: None,
vertical: None,
wrap_text: false,
indent: None,
text_rotation: None,
shrink_to_fit: false,
locked: None,
formula_hidden: None,
}
}
fn indexed_color(index: u32) -> Option<&'static str> {
const PALETTE: [&str; 64] = [
"FF000000", "FFFFFFFF", "FFFF0000", "FF00FF00", "FF0000FF", "FFFFFF00", "FFFF00FF",
"FF00FFFF", "FF000000", "FFFFFFFF", "FFFF0000", "FF00FF00", "FF0000FF", "FFFFFF00",
"FFFF00FF", "FF00FFFF", "FF800000", "FF008000", "FF000080", "FF808000", "FF800080",
"FF008080", "FFC0C0C0", "FF808080", "FF9999FF", "FF993366", "FFFFFFCC", "FFCCFFFF",
"FF660066", "FFFF8080", "FF0066CC", "FFCCCCFF", "FF000080", "FFFF00FF", "FFFFFF00",
"FF00FFFF", "FF800080", "FF800000", "FF008080", "FF0000FF", "FF00CCFF", "FFCCFFFF",
"FFCCFFCC", "FFFFFF99", "FF99CCFF", "FFFF99CC", "FFCC99FF", "FFFFCC99", "FF3366FF",
"FF33CCCC", "FF99CC00", "FFFFCC00", "FFFF9900", "FFFF6600", "FF666699", "FF969696",
"FF003366", "FF339966", "FF003300", "FF333300", "FF993300", "FF993366", "FF333399",
"FF333333",
];
match index {
0..=63 => Some(PALETTE[index as usize]),
64 => Some("FF000000"),
65 => Some("FFFFFFFF"),
_ => None,
}
}
fn resolve_color(start: &BytesStart) -> Option<String> {
if let Some(rgb) = raw_attribute(start, b"rgb") {
return Some(rgb);
}
raw_attribute(start, b"indexed")
.and_then(|value| value.parse::<u32>().ok())
.and_then(indexed_color)
.map(|value| value.to_string())
}
fn parse_pattern_type(value: &str) -> Option<PatternType> {
match value {
"gray0625" => Some(PatternType::Gray0625),
"darkGray" => Some(PatternType::DarkGray),
"mediumGray" => Some(PatternType::MediumGray),
"lightGray" => Some(PatternType::LightGray),
"darkHorizontal" => Some(PatternType::DarkHorizontal),
"darkVertical" => Some(PatternType::DarkVertical),
"darkDown" => Some(PatternType::DarkDown),
"darkUp" => Some(PatternType::DarkUp),
"darkGrid" => Some(PatternType::DarkGrid),
"darkTrellis" => Some(PatternType::DarkTrellis),
"lightHorizontal" => Some(PatternType::LightHorizontal),
"lightVertical" => Some(PatternType::LightVertical),
"lightDown" => Some(PatternType::LightDown),
"lightUp" => Some(PatternType::LightUp),
"lightGrid" => Some(PatternType::LightGrid),
"lightTrellis" => Some(PatternType::LightTrellis),
_ => None,
}
}
fn parse_border_style(value: &str) -> Option<BorderStyle> {
match value {
"thin" => Some(BorderStyle::Thin),
"medium" => Some(BorderStyle::Medium),
"thick" => Some(BorderStyle::Thick),
"dashed" => Some(BorderStyle::Dashed),
"dotted" => Some(BorderStyle::Dotted),
"double" => Some(BorderStyle::Double),
"hair" => Some(BorderStyle::Hair),
_ => None,
}
}
fn resolve_border(border_id: usize, borders: &[RawBorder]) -> Border {
let Some(raw) = borders.get(border_id) else {
return Border::default();
};
let resolve_edge = |edge: &Option<(String, Option<String>)>| -> Option<BorderEdge> {
edge.as_ref().and_then(|(style, color)| {
parse_border_style(style).map(|style| {
let mut edge = BorderEdge::new(style);
if let Some(color) = color {
edge = edge.with_color(color.clone());
}
edge
})
})
};
Border {
top: resolve_edge(&raw.top),
bottom: resolve_edge(&raw.bottom),
left: resolve_edge(&raw.left),
right: resolve_edge(&raw.right),
diagonal: resolve_edge(&raw.diagonal),
diagonal_up: raw.diagonal_up,
diagonal_down: raw.diagonal_down,
}
}
fn parse_horizontal_alignment(value: &str) -> Option<HorizontalAlignment> {
match value {
"left" => Some(HorizontalAlignment::Left),
"center" => Some(HorizontalAlignment::Center),
"right" => Some(HorizontalAlignment::Right),
_ => None,
}
}
fn parse_page_orientation(value: &str) -> Option<PageOrientation> {
match value {
"portrait" => Some(PageOrientation::Portrait),
"landscape" => Some(PageOrientation::Landscape),
_ => None,
}
}
fn parse_vertical_alignment(value: &str) -> Option<VerticalAlignment> {
match value {
"top" => Some(VerticalAlignment::Top),
"center" => Some(VerticalAlignment::Center),
"bottom" => Some(VerticalAlignment::Bottom),
_ => None,
}
}
fn parse_comparison_operator(value: &str) -> Option<ComparisonOperator> {
match value {
"lessThan" => Some(ComparisonOperator::LessThan),
"lessThanOrEqual" => Some(ComparisonOperator::LessThanOrEqual),
"equal" => Some(ComparisonOperator::Equal),
"notEqual" => Some(ComparisonOperator::NotEqual),
"greaterThanOrEqual" => Some(ComparisonOperator::GreaterThanOrEqual),
"greaterThan" => Some(ComparisonOperator::GreaterThan),
"between" => Some(ComparisonOperator::Between),
"notBetween" => Some(ComparisonOperator::NotBetween),
_ => None,
}
}
fn text_comparison_operator_from_type(value: &str) -> Option<TextComparisonOperator> {
match value {
"containsText" => Some(TextComparisonOperator::Contains),
"notContainsText" => Some(TextComparisonOperator::NotContains),
"beginsWith" => Some(TextComparisonOperator::BeginsWith),
"endsWith" => Some(TextComparisonOperator::EndsWith),
_ => None,
}
}
fn resolve_cell_format(
xf_index: usize,
xfs: &[RawXf],
fonts: &[RawFont],
fills: &[RawFill],
borders: &[RawBorder],
num_fmts: &HashMap<u32, String>,
style_name_by_xf_id: &HashMap<usize, String>,
) -> Option<CellFormat> {
if xf_index == 0 {
return None;
}
let xf = xfs.get(xf_index)?;
let named_style = style_name_by_xf_id.get(&xf.xf_id).cloned();
Some(resolve_cell_format_raw(
xf,
fonts,
fills,
borders,
num_fmts,
named_style,
))
}
fn resolve_cell_format_raw(
xf: &RawXf,
fonts: &[RawFont],
fills: &[RawFill],
borders: &[RawBorder],
num_fmts: &HashMap<u32, String>,
named_style: Option<String>,
) -> CellFormat {
let number_format = num_fmts.get(&xf.num_fmt_id).cloned();
let font = fonts.get(xf.font_id);
let bold = font.map(|font| font.bold).unwrap_or(false);
let italic = font.map(|font| font.italic).unwrap_or(false);
let underline = font.map(|font| font.underline).unwrap_or(false);
let strike = font.map(|font| font.strike).unwrap_or(false);
let (font_size, font_color, font_name) = if xf.font_id == 0 {
(None, None, None)
} else {
(
font.map(|font| font.size),
font.and_then(|font| font.color.clone()),
font.and_then(|font| font.name.clone()),
)
};
let (fill_color, pattern_fill, gradient_fill) = match fills.get(xf.fill_id) {
Some(RawFill::Solid(color)) => (Some(color.clone()), None, None),
Some(RawFill::Pattern(pattern)) => (None, Some(pattern.clone()), None),
Some(RawFill::Gradient(gradient)) => (None, None, Some(gradient.clone())),
_ => (None, None, None),
};
let horizontal_alignment = xf
.horizontal
.as_deref()
.and_then(parse_horizontal_alignment);
let vertical_alignment = xf.vertical.as_deref().and_then(parse_vertical_alignment);
CellFormat {
number_format,
bold,
italic,
font_size,
font_color,
fill_color,
horizontal_alignment,
vertical_alignment,
border: resolve_border(xf.border_id, borders),
font_name,
underline,
strike,
wrap_text: xf.wrap_text,
indent: xf.indent,
text_rotation: xf.text_rotation,
shrink_to_fit: xf.shrink_to_fit,
pattern_fill,
gradient_fill,
locked: xf.locked,
formula_hidden: xf.formula_hidden,
named_style,
}
}
#[derive(Default)]
struct ParsedCoreProperties {
title: Option<String>,
author: Option<String>,
subject: Option<String>,
keywords: Option<String>,
}
fn parse_core_properties_xml(xml: &str) -> Result<ParsedCoreProperties> {
let mut reader = Reader::from_xml_str(xml);
let mut title = None;
let mut author = None;
let mut subject = None;
let mut keywords = None;
let mut in_element: Option<&'static str> = None;
let mut buffer = String::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"title" => {
in_element = Some("title");
buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"creator" => {
in_element = Some("creator");
buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"subject" => {
in_element = Some("subject");
buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"keywords" => {
in_element = Some("keywords");
buffer.clear();
}
Event::Text(text) if in_element.is_some() => {
buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if in_element.is_some() => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
buffer.push_str(&resolved);
}
}
Event::End(end)
if in_element.is_some()
&& matches!(
end.local_name().as_ref(),
b"title" | b"creator" | b"subject" | b"keywords"
) =>
{
let text = std::mem::take(&mut buffer);
match in_element.take() {
Some("title") => title = Some(text),
Some("creator") => author = Some(text),
Some("subject") => subject = Some(text),
Some("keywords") => keywords = Some(text),
_ => {}
}
}
_ => {}
}
}
Ok(ParsedCoreProperties {
title,
author,
subject,
keywords,
})
}
fn parse_extended_properties_xml(
xml: &str,
) -> Result<(Option<String>, Option<String>, Option<String>)> {
let mut reader = Reader::from_xml_str(xml);
let mut company = None;
let mut manager = None;
let mut hyperlink_base = None;
let mut in_element: Option<&'static str> = None;
let mut buffer = String::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"Company" => {
in_element = Some("Company");
buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"Manager" => {
in_element = Some("Manager");
buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"HyperlinkBase" => {
in_element = Some("HyperlinkBase");
buffer.clear();
}
Event::Text(text) if in_element.is_some() => {
buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if in_element.is_some() => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
buffer.push_str(&resolved);
}
}
Event::End(end)
if in_element.is_some()
&& matches!(
end.local_name().as_ref(),
b"Company" | b"Manager" | b"HyperlinkBase"
) =>
{
let text = std::mem::take(&mut buffer);
match in_element.take() {
Some("Company") => company = Some(text),
Some("Manager") => manager = Some(text),
Some("HyperlinkBase") => hyperlink_base = Some(text),
_ => {}
}
}
_ => {}
}
}
Ok((company, manager, hyperlink_base))
}
fn parse_custom_properties_xml(xml: &str) -> Result<Vec<(String, CustomPropertyValue)>> {
let mut reader = Reader::from_xml_str(xml);
let mut custom_properties = Vec::new();
let mut current_name: Option<String> = None;
let mut current_variant: Option<Vec<u8>> = None;
let mut buffer = String::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"property" => {
current_name = raw_attribute(&start, b"name");
}
Event::Start(start) | Event::Empty(start)
if current_name.is_some()
&& matches!(
start.local_name().as_ref(),
b"lpwstr" | b"bool" | b"i4" | b"r8"
) =>
{
current_variant = Some(start.local_name().as_ref().to_vec());
buffer.clear();
}
Event::Text(text) if current_variant.is_some() => {
buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if current_variant.is_some() => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
buffer.push_str(&resolved);
}
}
Event::End(end) => {
if let Some(variant) = current_variant.take() {
if end.local_name().as_ref() == variant.as_slice() {
if let Some(name) = current_name.clone() {
let value = std::mem::take(&mut buffer);
let parsed = match variant.as_slice() {
b"lpwstr" => Some(CustomPropertyValue::Text(value)),
b"bool" => {
Some(CustomPropertyValue::Bool(value == "true" || value == "1"))
}
b"i4" => value.parse::<i32>().ok().map(CustomPropertyValue::Int),
b"r8" => value.parse::<f64>().ok().map(CustomPropertyValue::Number),
_ => None,
};
if let Some(value) = parsed {
custom_properties.push((name, value));
}
}
} else {
current_variant = Some(variant);
}
} else if end.local_name().as_ref() == b"property" {
current_name = None;
}
}
_ => {}
}
}
Ok(custom_properties)
}
struct ParsedExternalLink {
sheet_names: Vec<String>,
defined_names: Vec<(String, String)>,
}
fn parse_external_link_xml(xml: &str) -> Result<ParsedExternalLink> {
let mut reader = Reader::from_xml_str(xml);
let mut sheet_names = Vec::new();
let mut defined_names = Vec::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Empty(start) if start.local_name().as_ref() == b"sheetName" => {
if let Some(name) = raw_attribute(&start, b"val") {
sheet_names.push(name);
}
}
Event::Empty(start) if start.local_name().as_ref() == b"definedName" => {
if let (Some(name), Some(refers_to)) = (
raw_attribute(&start, b"name"),
raw_attribute(&start, b"refersTo"),
) {
defined_names.push((name, refers_to));
}
}
_ => {}
}
}
Ok(ParsedExternalLink {
sheet_names,
defined_names,
})
}
fn parse_comments_xml(xml: &str) -> Result<Vec<CellComment>> {
let mut reader = Reader::from_xml_str(xml);
let mut authors: Vec<String> = Vec::new();
let mut in_author = false;
let mut author_buffer = String::new();
let mut comments = Vec::new();
let mut in_comment = false;
let mut current_ref: Option<String> = None;
let mut current_author_id: Option<usize> = None;
let mut in_comment_text = false;
let mut text_is_rich = false;
let mut plain_buffer = String::new();
let mut runs: Vec<RichTextRun> = Vec::new();
let mut in_run = false;
let mut in_rpr = false;
let mut in_text_element = false;
let mut run_text_buffer = String::new();
let mut run_bold = false;
let mut run_italic = false;
let mut run_size: Option<f64> = None;
let mut run_color: Option<String> = None;
let mut run_name: Option<String> = None;
let mut run_underline = false;
let mut run_strike = false;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"author" => {
in_author = true;
author_buffer.clear();
}
Event::End(end) if end.local_name().as_ref() == b"author" => {
in_author = false;
authors.push(std::mem::take(&mut author_buffer));
}
Event::Text(text) if in_author => {
author_buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::Start(start) if start.local_name().as_ref() == b"comment" => {
in_comment = true;
current_ref = raw_attribute(&start, b"ref");
current_author_id = raw_attribute(&start, b"authorId")
.and_then(|value| value.parse::<usize>().ok());
}
Event::End(end) if end.local_name().as_ref() == b"comment" => {
in_comment = false;
if let Some(cell) = current_ref.take() {
let author = current_author_id
.take()
.and_then(|id| authors.get(id).cloned())
.unwrap_or_default();
let comment = if text_is_rich {
CellComment::new(cell, author, String::new())
.with_rich_text(std::mem::take(&mut runs))
} else {
CellComment::new(cell, author, std::mem::take(&mut plain_buffer))
};
comments.push(comment);
}
}
Event::Start(start) if in_comment && start.local_name().as_ref() == b"text" => {
in_comment_text = true;
text_is_rich = false;
plain_buffer.clear();
runs.clear();
}
Event::End(end) if in_comment_text && end.local_name().as_ref() == b"text" => {
in_comment_text = false;
}
Event::Start(start) if in_comment_text && start.local_name().as_ref() == b"r" => {
in_run = true;
text_is_rich = true;
run_text_buffer.clear();
run_bold = false;
run_italic = false;
run_size = None;
run_color = None;
run_name = None;
run_underline = false;
run_strike = false;
}
Event::End(end) if in_comment_text && end.local_name().as_ref() == b"r" => {
in_run = false;
runs.push(RichTextRun {
text: std::mem::take(&mut run_text_buffer),
bold: std::mem::take(&mut run_bold),
italic: std::mem::take(&mut run_italic),
font_color: run_color.take(),
font_size: run_size.take(),
font_name: run_name.take(),
underline: std::mem::take(&mut run_underline),
strike: std::mem::take(&mut run_strike),
});
}
Event::Start(start) if in_run && start.local_name().as_ref() == b"rPr" => {
in_rpr = true;
}
Event::End(end) if in_run && end.local_name().as_ref() == b"rPr" => {
in_rpr = false;
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"b" => {
run_bold = true;
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"i" => {
run_italic = true;
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"strike" => {
run_strike = true;
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"u" => {
run_underline = true;
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"rFont" => {
run_name = raw_attribute(&start, b"val");
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"sz" => {
run_size =
raw_attribute(&start, b"val").and_then(|value| value.parse::<f64>().ok());
}
Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"color" => {
run_color = resolve_color(&start);
}
Event::Start(start) if in_comment_text && start.local_name().as_ref() == b"t" => {
in_text_element = true;
}
Event::End(end) if in_comment_text && end.local_name().as_ref() == b"t" => {
in_text_element = false;
}
Event::Text(text) if in_text_element && in_run => {
run_text_buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if in_text_element && in_run => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
run_text_buffer.push_str(&resolved);
}
}
Event::Text(text) if in_text_element && !in_run => {
plain_buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if in_text_element && !in_run => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
plain_buffer.push_str(&resolved);
}
}
_ => {}
}
}
Ok(comments)
}
fn parse_drawing_xml(
xml: &str,
drawing_relationships: &opc::Relationships,
package: &Package,
) -> Result<SheetDrawing> {
let mut reader = Reader::from_xml_str(xml);
let mut drawing = SheetDrawing::new();
loop {
match reader.read_event()? {
Event::Start(start) if start.local_name().as_ref() == b"twoCellAnchor" => {
if let Some(anchor) =
parse_drawing_anchor(&mut reader, drawing_relationships, package)?
{
drawing.anchors.push(anchor);
}
}
Event::Eof => break,
_ => {}
}
}
Ok(drawing)
}
fn parse_drawing_anchor(
reader: &mut Reader,
drawing_relationships: &opc::Relationships,
package: &Package,
) -> Result<Option<DrawingAnchor>> {
let mut from: Option<CellAnchorPoint> = None;
let mut to: Option<CellAnchorPoint> = None;
let mut object: Option<DrawingObject> = None;
loop {
match reader.read_event()? {
Event::Start(start) if start.local_name().as_ref() == b"from" => {
from = Some(parse_cell_anchor_point(reader)?);
}
Event::Start(start) if start.local_name().as_ref() == b"to" => {
to = Some(parse_cell_anchor_point(reader)?);
}
Event::Start(start) if start.local_name().as_ref() == b"pic" => {
object = parse_sheet_picture(reader, drawing_relationships, package)?
.map(|picture| DrawingObject::Picture(Box::new(picture)));
}
Event::Start(start) if start.local_name().as_ref() == b"graphicFrame" => {
object = parse_sheet_chart(reader, drawing_relationships, package)?
.map(|chart| DrawingObject::Chart(Box::new(chart)));
}
Event::Start(_) => skip_subtree(reader)?,
Event::Empty(start) if start.local_name().as_ref() == b"clientData" => {}
Event::End(_) => break,
Event::Eof => break,
_ => {}
}
}
match (from, to, object) {
(Some(from), Some(to), Some(object)) => Ok(Some(DrawingAnchor::new(from, to, object))),
_ => Ok(None),
}
}
fn parse_cell_anchor_point(reader: &mut Reader) -> Result<CellAnchorPoint> {
let mut point = CellAnchorPoint::default();
let mut text_buffer = String::new();
let mut current_field: Option<&str> = None;
loop {
match reader.read_event()? {
Event::Start(start) => {
current_field = match start.local_name().as_ref() {
b"col" => Some("col"),
b"colOff" => Some("colOff"),
b"row" => Some("row"),
b"rowOff" => Some("rowOff"),
_ => None,
};
text_buffer.clear();
}
Event::Text(text) => {
text_buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::End(_) => {
match current_field.take() {
Some("col") => point.column = text_buffer.trim().parse().unwrap_or(0),
Some("colOff") => {
point.column_offset_emu = text_buffer.trim().parse().unwrap_or(0)
}
Some("row") => point.row = text_buffer.trim().parse().unwrap_or(0),
Some("rowOff") => {
point.row_offset_emu = text_buffer.trim().parse().unwrap_or(0)
}
Some(_) | None => {
if current_field.is_none() && text_buffer.is_empty() {
return Ok(point);
}
}
}
text_buffer.clear();
}
Event::Eof => return Ok(point),
_ => {}
}
}
}
fn parse_sheet_picture(
reader: &mut Reader,
drawing_relationships: &opc::Relationships,
package: &Package,
) -> Result<Option<SheetPicture>> {
let mut name = String::new();
let mut relationship_id: Option<String> = None;
let mut shape_properties = None;
loop {
match reader.read_event()? {
Event::Start(start) if start.local_name().as_ref() == b"nvPicPr" => {
name = parse_non_visual_name(reader, b"nvPicPr")?;
}
Event::Start(start) if start.local_name().as_ref() == b"blipFill" => {
relationship_id = parse_picture_blip_fill_relationship_id(reader)?;
}
Event::Empty(start) if start.local_name().as_ref() == b"blip" => {
relationship_id = raw_attribute(&start, b"embed");
}
Event::Start(start) if start.local_name().as_ref() == b"spPr" => {
shape_properties = Some(drawing::read_shape_properties(reader)?);
}
Event::Start(_) => skip_subtree(reader)?,
Event::End(end) if end.local_name().as_ref() == b"pic" => break,
Event::Eof => break,
_ => {}
}
}
let Some(relationship_id) = relationship_id else {
return Ok(None);
};
let Some(relationship) = drawing_relationships.by_id(&relationship_id) else {
return Ok(None);
};
if relationship.rel_type != IMAGE_RELATIONSHIP_TYPE {
return Ok(None);
}
let part_name = format!("/xl/{}", relationship.target.trim_start_matches("../"));
let Some(part) = package.part(&part_name) else {
return Ok(None);
};
let extension = part_name.rsplit('.').next().unwrap_or("");
let Some(format) = PictureFormat::from_extension(extension) else {
return Ok(None);
};
let mut picture = SheetPicture::new(part.data.clone(), format).with_name(name);
if let Some(mut shape_properties) = shape_properties {
shape_properties.transform = None;
if matches!(
shape_properties.geometry,
Some(drawing::Geometry::Preset(drawing::PresetShape::Rectangle))
) {
shape_properties.geometry = None;
}
let is_fixed_default = shape_properties.geometry.is_none()
&& shape_properties.fill.is_none()
&& shape_properties.line.is_none()
&& shape_properties.effects.is_none();
if !is_fixed_default {
picture = picture.with_shape_properties(shape_properties);
}
}
Ok(Some(picture))
}
fn parse_picture_blip_fill_relationship_id(reader: &mut Reader) -> Result<Option<String>> {
let mut relationship_id = None;
loop {
match reader.read_event()? {
Event::Empty(start) if start.local_name().as_ref() == b"blip" => {
relationship_id = raw_attribute(&start, b"embed");
}
Event::Start(start) if start.local_name().as_ref() == b"blip" => {
relationship_id = raw_attribute(&start, b"embed");
skip_subtree(reader)?;
}
Event::Start(_) => skip_subtree(reader)?,
Event::End(end) if end.local_name().as_ref() == b"blipFill" => break,
Event::Eof => break,
_ => {}
}
}
Ok(relationship_id)
}
fn parse_sheet_chart(
reader: &mut Reader,
drawing_relationships: &opc::Relationships,
package: &Package,
) -> Result<Option<SheetChart>> {
let mut name = String::new();
let mut relationship_id: Option<String> = None;
loop {
match reader.read_event()? {
Event::Start(start) if start.local_name().as_ref() == b"nvGraphicFramePr" => {
name = parse_non_visual_name(reader, b"nvGraphicFramePr")?;
}
Event::Empty(start) if start.local_name().as_ref() == b"chart" => {
relationship_id = raw_attribute(&start, b"id");
}
Event::Start(start)
if matches!(start.local_name().as_ref(), b"graphic" | b"graphicData") => {}
Event::Start(_) => skip_subtree(reader)?,
Event::End(end) if end.local_name().as_ref() == b"graphicFrame" => break,
Event::Eof => break,
_ => {}
}
}
let Some(relationship_id) = relationship_id else {
return Ok(None);
};
let Some(relationship) = drawing_relationships.by_id(&relationship_id) else {
return Ok(None);
};
if relationship.rel_type != CHART_RELATIONSHIP_TYPE {
return Ok(None);
}
let part_name = format!("/xl/{}", relationship.target.trim_start_matches("../"));
let Some(part) = package.part(&part_name) else {
return Ok(None);
};
let xml = std::str::from_utf8(&part.data)
.map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
let chart_space = chart::read_chart_space(xml)?;
Ok(Some(SheetChart::new(chart_space).with_name(name)))
}
fn parse_non_visual_name(reader: &mut Reader, wrapper_local_name: &[u8]) -> Result<String> {
let mut name = String::new();
loop {
match reader.read_event()? {
Event::Empty(start) if start.local_name().as_ref() == b"cNvPr" => {
name = raw_attribute(&start, b"name").unwrap_or_default();
}
Event::Start(start) if start.local_name().as_ref() == b"cNvPr" => {
name = raw_attribute(&start, b"name").unwrap_or_default();
skip_subtree(reader)?;
}
Event::Start(_) => skip_subtree(reader)?,
Event::End(end) if end.local_name().as_ref() == wrapper_local_name => break,
Event::Eof => break,
_ => {}
}
}
Ok(name)
}
fn skip_subtree(reader: &mut Reader) -> Result<()> {
let mut depth: u32 = 0;
loop {
match reader.read_event()? {
Event::Start(_) => depth += 1,
Event::End(_) => {
if depth == 0 {
return Ok(());
}
depth -= 1;
}
Event::Eof => return Ok(()),
_ => {}
}
}
}
fn parse_table_xml(xml: &str) -> Result<ExcelTable> {
let mut reader = Reader::from_xml_str(xml);
let mut name = String::new();
let mut range = String::new();
let mut show_totals_row = false;
let mut columns: Vec<TableColumn> = Vec::new();
let mut current_column: Option<TableColumn> = None;
let mut in_calculated_column_formula = false;
let mut in_totals_row_formula = false;
let mut formula_buffer = String::new();
let mut sort_state: Option<SortState> = None;
let mut current_sort_state: Option<SortState> = None;
let mut table_style_name: Option<String> = None;
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) | Event::Empty(start)
if start.local_name().as_ref() == b"table" =>
{
name = raw_attribute(&start, b"name").unwrap_or_default();
range = raw_attribute(&start, b"ref").unwrap_or_default();
let totals_row_count = raw_attribute(&start, b"totalsRowCount")
.and_then(|value| value.parse::<u32>().ok())
.unwrap_or(0);
let totals_row_shown = raw_attribute(&start, b"totalsRowShown")
.as_deref()
.map(|value| value != "0")
.unwrap_or(true);
show_totals_row = totals_row_count > 0 && totals_row_shown;
}
Event::Empty(start) if start.local_name().as_ref() == b"tableColumn" => {
let mut column =
TableColumn::new(raw_attribute(&start, b"name").unwrap_or_default());
column.totals_row_function =
parse_totals_row_function(raw_attribute(&start, b"totalsRowFunction"));
column.totals_row_label = raw_attribute(&start, b"totalsRowLabel");
columns.push(column);
}
Event::Start(start) if start.local_name().as_ref() == b"tableColumn" => {
let mut column =
TableColumn::new(raw_attribute(&start, b"name").unwrap_or_default());
column.totals_row_function =
parse_totals_row_function(raw_attribute(&start, b"totalsRowFunction"));
column.totals_row_label = raw_attribute(&start, b"totalsRowLabel");
current_column = Some(column);
}
Event::End(end) if end.local_name().as_ref() == b"tableColumn" => {
if let Some(column) = current_column.take() {
columns.push(column);
}
}
Event::Start(start)
if current_column.is_some()
&& start.local_name().as_ref() == b"calculatedColumnFormula" =>
{
in_calculated_column_formula = true;
formula_buffer.clear();
}
Event::End(end)
if in_calculated_column_formula
&& end.local_name().as_ref() == b"calculatedColumnFormula" =>
{
in_calculated_column_formula = false;
if let Some(column) = current_column.as_mut() {
column.calculated_formula = Some(std::mem::take(&mut formula_buffer));
}
}
Event::Start(start)
if current_column.is_some()
&& start.local_name().as_ref() == b"totalsRowFormula" =>
{
in_totals_row_formula = true;
formula_buffer.clear();
}
Event::End(end)
if in_totals_row_formula && end.local_name().as_ref() == b"totalsRowFormula" =>
{
in_totals_row_formula = false;
if let Some(column) = current_column.as_mut() {
if column.totals_row_function == Some(TotalsRowFunction::Custom) {
column.totals_row_formula = Some(std::mem::take(&mut formula_buffer));
} else {
formula_buffer.clear();
}
}
}
Event::Text(text) if in_calculated_column_formula || in_totals_row_formula => {
formula_buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference)
if in_calculated_column_formula || in_totals_row_formula =>
{
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
formula_buffer.push_str(&resolved);
}
}
Event::Start(start) if start.local_name().as_ref() == b"sortState" => {
current_sort_state = Some(SortState::new(
raw_attribute(&start, b"ref").unwrap_or_default(),
));
}
Event::Empty(start) if start.local_name().as_ref() == b"sortState" => {
sort_state = Some(SortState::new(
raw_attribute(&start, b"ref").unwrap_or_default(),
));
}
Event::End(end) if end.local_name().as_ref() == b"sortState" => {
sort_state = current_sort_state.take();
}
Event::Empty(start)
if current_sort_state.is_some()
&& start.local_name().as_ref() == b"sortCondition" =>
{
if let Some(range) = raw_attribute(&start, b"ref") {
let descending = raw_attribute(&start, b"descending").as_deref() == Some("1");
if let Some(state) = current_sort_state.as_mut() {
let mut condition = SortCondition::new(range);
condition.descending = descending;
state.conditions.push(condition);
}
}
}
Event::Start(start) | Event::Empty(start)
if start.local_name().as_ref() == b"tableStyleInfo" =>
{
table_style_name = raw_attribute(&start, b"name");
}
_ => {}
}
}
Ok(ExcelTable {
name,
range,
columns,
show_totals_row,
sort_state,
table_style_name,
})
}
fn parse_totals_row_function(value: Option<String>) -> Option<TotalsRowFunction> {
match value.as_deref() {
Some("sum") => Some(TotalsRowFunction::Sum),
Some("average") => Some(TotalsRowFunction::Average),
Some("count") => Some(TotalsRowFunction::Count),
Some("countNums") => Some(TotalsRowFunction::CountNums),
Some("max") => Some(TotalsRowFunction::Max),
Some("min") => Some(TotalsRowFunction::Min),
Some("stdDev") => Some(TotalsRowFunction::StdDev),
Some("var") => Some(TotalsRowFunction::Var),
Some("custom") => Some(TotalsRowFunction::Custom),
_ => None,
}
}
struct ParsedWorksheet {
rows: Vec<Row>,
conditional_formatting_rules: Vec<ConditionalFormattingRule>,
data_validation_rules: Vec<DataValidationRule>,
merged_ranges: Vec<String>,
column_settings: Vec<ColumnSetting>,
auto_filter_range: Option<String>,
freeze_panes: Option<FreezePane>,
protection: Option<SheetProtection>,
hyperlinks: Vec<CellHyperlink>,
zoom_scale: Option<u32>,
row_breaks: Vec<u32>,
column_breaks: Vec<u32>,
scenarios: Vec<Scenario>,
auto_filter_columns: Vec<AutoFilterColumn>,
print_settings: PrintSettings,
tab_color: Option<String>,
show_grid_lines: Option<bool>,
show_row_col_headers: Option<bool>,
show_zeros: Option<bool>,
right_to_left: Option<bool>,
active_cell: Option<String>,
default_column_width: Option<f64>,
default_row_height: Option<f64>,
ignored_errors: Vec<IgnoredError>,
protected_ranges: Vec<ProtectedRange>,
sort_state: Option<SortState>,
outline_summary_below: Option<bool>,
outline_summary_right: Option<bool>,
}
fn parse_worksheet_xml(
xml: &str,
shared_strings: &[SharedStringEntry],
cell_formats: &[Option<CellFormat>],
dxfs: &[CellFormat],
sheet_relationships: &opc::Relationships,
) -> Result<ParsedWorksheet> {
let mut reader = Reader::from_xml_str(xml);
let mut rows: Vec<Row> = Vec::new();
let mut in_sheet_data = false;
let mut current_row_cells: Vec<Cell> = Vec::new();
let mut current_row_number: Option<usize> = None;
let mut current_row_height: Option<f64> = None;
let mut current_row_hidden = false;
let mut current_row_outline_level: u8 = 0;
let mut current_row_collapsed = false;
let mut current_row_style: Option<usize> = None;
let mut current_cell_column: Option<usize> = None;
let mut current_cell_type: Option<String> = None;
let mut current_cell_style: Option<usize> = None;
let mut in_value = false;
let mut value_buffer = String::new();
let mut in_formula = false;
let mut has_formula = false;
let mut formula_buffer = String::new();
let mut current_formula_type: Option<String> = None;
let mut current_formula_ref: Option<String> = None;
let mut current_formula_si: Option<u32> = None;
let mut column_settings: Vec<ColumnSetting> = Vec::new();
let mut merged_ranges: Vec<String> = Vec::new();
let mut auto_filter_range: Option<String> = None;
let mut protection: Option<SheetProtection> = None;
let mut freeze_panes: Option<FreezePane> = None;
let mut hyperlinks: Vec<CellHyperlink> = Vec::new();
let mut zoom_scale: Option<u32> = None;
let mut show_grid_lines: Option<bool> = None;
let mut show_row_col_headers: Option<bool> = None;
let mut show_zeros: Option<bool> = None;
let mut right_to_left: Option<bool> = None;
let mut active_cell: Option<String> = None;
let mut tab_color: Option<String> = None;
let mut outline_summary_below: Option<bool> = None;
let mut outline_summary_right: Option<bool> = None;
let mut default_column_width: Option<f64> = None;
let mut default_row_height: Option<f64> = None;
let mut ignored_errors: Vec<IgnoredError> = Vec::new();
let mut protected_ranges: Vec<ProtectedRange> = Vec::new();
let mut row_breaks: Vec<u32> = Vec::new();
let mut column_breaks: Vec<u32> = Vec::new();
let mut in_row_breaks = false;
let mut in_col_breaks = false;
let mut scenarios: Vec<Scenario> = Vec::new();
let mut current_scenario: Option<Scenario> = None;
let mut auto_filter_columns: Vec<AutoFilterColumn> = Vec::new();
let mut current_filter_column: Option<AutoFilterColumn> = None;
let mut sort_state: Option<SortState> = None;
let mut current_sort_state: Option<SortState> = None;
let mut orientation: Option<PageOrientation> = None;
let mut fit_to_width: Option<u32> = None;
let mut fit_to_height: Option<u32> = None;
let mut scale: Option<u32> = None;
let mut header: Option<String> = None;
let mut footer: Option<String> = None;
let mut header_first: Option<String> = None;
let mut footer_first: Option<String> = None;
let mut header_even: Option<String> = None;
let mut footer_even: Option<String> = None;
let mut in_header_footer_element: Option<&'static str> = None;
let mut header_footer_buffer = String::new();
let mut conditional_formatting_rules: Vec<ConditionalFormattingRule> = Vec::new();
let mut current_cf_sqref: Option<String> = None;
let mut in_cf_rule = false;
let mut cf_rule_type: Option<String> = None;
let mut cf_rule_dxf_id: Option<usize> = None;
let mut cf_rule_operator: Option<String> = None;
let mut cf_rule_text: Option<String> = None;
let mut cf_formulas: Vec<String> = Vec::new();
let mut in_cf_formula = false;
let mut cf_formula_buffer = String::new();
let mut data_validation_rules: Vec<DataValidationRule> = Vec::new();
let mut in_data_validation = false;
let mut dv_type: Option<String> = None;
let mut dv_operator: Option<String> = None;
let mut dv_allow_blank = false;
let mut dv_show_input_message = false;
let mut dv_show_error_message = false;
let mut dv_prompt_title: Option<String> = None;
let mut dv_prompt: Option<String> = None;
let mut dv_error_title: Option<String> = None;
let mut dv_error: Option<String> = None;
let mut dv_sqref: Option<String> = None;
let mut dv_formula1: Option<String> = None;
let mut dv_formula2: Option<String> = None;
let mut in_dv_formula1 = false;
let mut in_dv_formula2 = false;
let mut dv_formula_buffer = String::new();
loop {
match reader.read_event()? {
Event::Eof => break,
Event::Start(start) if start.local_name().as_ref() == b"sheetData" => {
in_sheet_data = true;
}
Event::End(end) if end.local_name().as_ref() == b"sheetData" => {
in_sheet_data = false;
}
Event::Start(start) if in_sheet_data && start.local_name().as_ref() == b"row" => {
current_row_number =
raw_attribute(&start, b"r").and_then(|value| value.parse::<usize>().ok());
current_row_cells = Vec::new();
current_row_height =
raw_attribute(&start, b"ht").and_then(|value| value.parse::<f64>().ok());
current_row_hidden = raw_attribute(&start, b"hidden").as_deref() == Some("1");
current_row_outline_level = raw_attribute(&start, b"outlineLevel")
.and_then(|value| value.parse::<u8>().ok())
.unwrap_or(0);
current_row_collapsed = raw_attribute(&start, b"collapsed").as_deref() == Some("1");
current_row_style =
if raw_attribute(&start, b"customFormat").as_deref() == Some("1") {
raw_attribute(&start, b"s").and_then(|value| value.parse::<usize>().ok())
} else {
None
};
}
Event::End(end) if in_sheet_data && end.local_name().as_ref() == b"row" => {
let row_number = current_row_number.take().unwrap_or(rows.len() + 1).max(1);
let default_format = current_row_style
.take()
.and_then(|index| cell_formats.get(index).cloned().flatten());
let cells = std::mem::take(&mut current_row_cells);
let height = current_row_height.take();
let hidden = std::mem::take(&mut current_row_hidden);
let outline_level = std::mem::take(&mut current_row_outline_level);
let collapsed = std::mem::take(&mut current_row_collapsed);
if row_number <= EXCEL_MAX_ROWS {
ensure_len(&mut rows, row_number, Row::default);
rows[row_number - 1] = Row {
cells,
height,
hidden,
outline_level,
collapsed,
default_format,
};
}
}
Event::Start(start) if in_sheet_data && start.local_name().as_ref() == b"c" => {
current_cell_type = raw_attribute(&start, b"t");
current_cell_column = raw_attribute(&start, b"r")
.as_deref()
.and_then(column_index_from_reference);
current_cell_style =
raw_attribute(&start, b"s").and_then(|value| value.parse::<usize>().ok());
has_formula = false;
formula_buffer.clear();
}
Event::Empty(start) if in_sheet_data && start.local_name().as_ref() == b"c" => {
if let Some(column_index) = raw_attribute(&start, b"r")
.as_deref()
.and_then(column_index_from_reference)
{
ensure_len(&mut current_row_cells, column_index + 1, Cell::default);
let format = raw_attribute(&start, b"s")
.and_then(|value| value.parse::<usize>().ok())
.and_then(|index| cell_formats.get(index).cloned().flatten());
current_row_cells[column_index] = Cell {
value: CellValue::Empty,
format,
};
}
}
Event::End(end) if in_sheet_data && end.local_name().as_ref() == b"c" => {
if let Some(column_index) = current_cell_column.take() {
ensure_len(&mut current_row_cells, column_index + 1, Cell::default);
let raw_value = std::mem::take(&mut value_buffer);
let cell_type = current_cell_type.take();
let format = current_cell_style
.take()
.and_then(|index| cell_formats.get(index).cloned().flatten());
let formula_text = std::mem::take(&mut formula_buffer);
let had_formula = std::mem::take(&mut has_formula);
let formula_type = current_formula_type.take();
let formula_ref = current_formula_ref.take();
let formula_si = current_formula_si.take();
let value = if had_formula && formula_type.as_deref() == Some("array") {
CellValue::ArrayFormula {
formula: formula_text,
range: formula_ref.unwrap_or_default(),
cached_value: raw_value.parse::<f64>().ok(),
}
} else if had_formula && formula_type.as_deref() == Some("shared") {
CellValue::SharedFormula {
formula: if formula_text.is_empty() {
None
} else {
Some(formula_text)
},
group_index: formula_si.unwrap_or(0),
range: formula_ref,
cached_value: raw_value.parse::<f64>().ok(),
}
} else if had_formula {
CellValue::Formula {
formula: if formula_text.is_empty() {
None
} else {
Some(formula_text)
},
cached_value: raw_value.parse::<f64>().ok(),
}
} else {
match cell_type.as_deref() {
Some("s") => raw_value
.parse::<usize>()
.ok()
.and_then(|index| shared_strings.get(index))
.map(|entry| match entry {
SharedStringEntry::Plain(text) => CellValue::Text(text.clone()),
SharedStringEntry::Rich(runs) => {
CellValue::RichText(runs.clone())
}
})
.unwrap_or(CellValue::Empty),
Some("b") => CellValue::Boolean(raw_value == "1"),
None | Some("n") => raw_value
.parse::<f64>()
.ok()
.map(CellValue::Number)
.unwrap_or(CellValue::Empty),
_ => CellValue::Empty,
}
};
current_row_cells[column_index] = Cell { value, format };
}
}
Event::Start(start) if in_sheet_data && start.local_name().as_ref() == b"v" => {
in_value = true;
value_buffer.clear();
}
Event::End(end) if in_sheet_data && end.local_name().as_ref() == b"v" => {
in_value = false;
}
Event::Text(text) if in_value => {
value_buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::Start(start) if in_sheet_data && start.local_name().as_ref() == b"f" => {
in_formula = true;
has_formula = true;
current_formula_type = raw_attribute(&start, b"t");
current_formula_ref = raw_attribute(&start, b"ref");
current_formula_si =
raw_attribute(&start, b"si").and_then(|value| value.parse::<u32>().ok());
}
Event::End(end) if in_sheet_data && end.local_name().as_ref() == b"f" => {
in_formula = false;
}
Event::Empty(start) if in_sheet_data && start.local_name().as_ref() == b"f" => {
has_formula = true;
current_formula_type = raw_attribute(&start, b"t");
current_formula_ref = raw_attribute(&start, b"ref");
current_formula_si =
raw_attribute(&start, b"si").and_then(|value| value.parse::<u32>().ok());
}
Event::Text(text) if in_formula => {
formula_buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::Empty(start) if start.local_name().as_ref() == b"col" => {
let min =
raw_attribute(&start, b"min").and_then(|value| value.parse::<usize>().ok());
let max =
raw_attribute(&start, b"max").and_then(|value| value.parse::<usize>().ok());
let width =
raw_attribute(&start, b"width").and_then(|value| value.parse::<f64>().ok());
let hidden = raw_attribute(&start, b"hidden").as_deref() == Some("1");
let outline_level = raw_attribute(&start, b"outlineLevel")
.and_then(|value| value.parse::<u8>().ok())
.unwrap_or(0);
let collapsed = raw_attribute(&start, b"collapsed").as_deref() == Some("1");
let style = raw_attribute(&start, b"style")
.and_then(|value| value.parse::<usize>().ok())
.and_then(|index| cell_formats.get(index).cloned().flatten());
if let (Some(min), Some(max)) = (min, max) {
let min = min.max(1);
let max = max.min(EXCEL_MAX_COLUMNS);
if min <= max {
for column_number in min..=max {
let mut setting = ColumnSetting::new(column_number - 1)
.with_hidden(hidden)
.with_outline_level(outline_level)
.with_collapsed(collapsed);
if let Some(width) = width {
setting = setting.with_width(width);
}
if let Some(style) = style.clone() {
setting = setting.with_style(style);
}
column_settings.push(setting);
}
}
}
}
Event::Empty(start) if start.local_name().as_ref() == b"mergeCell" => {
if let Some(range) = raw_attribute(&start, b"ref") {
merged_ranges.push(range);
}
}
Event::Start(start) | Event::Empty(start)
if start.local_name().as_ref() == b"autoFilter" =>
{
auto_filter_range = raw_attribute(&start, b"ref");
}
Event::Start(start) | Event::Empty(start)
if start.local_name().as_ref() == b"sheetProtection" =>
{
if raw_attribute(&start, b"sheet").as_deref() == Some("1") {
protection = Some(SheetProtection::locked());
}
}
Event::Empty(start) if start.local_name().as_ref() == b"pane" => {
if raw_attribute(&start, b"state").as_deref() == Some("frozen") {
let frozen_columns = raw_attribute(&start, b"xSplit")
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(0);
let frozen_rows = raw_attribute(&start, b"ySplit")
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(0);
freeze_panes = Some(FreezePane::new(frozen_columns, frozen_rows));
}
}
Event::Empty(start)
if active_cell.is_none() && start.local_name().as_ref() == b"selection" =>
{
active_cell = raw_attribute(&start, b"activeCell");
}
Event::Empty(start) if start.local_name().as_ref() == b"tabColor" => {
tab_color = raw_attribute(&start, b"rgb");
}
Event::Empty(start) if start.local_name().as_ref() == b"outlinePr" => {
outline_summary_below =
raw_attribute(&start, b"summaryBelow").map(|value| value == "1");
outline_summary_right =
raw_attribute(&start, b"summaryRight").map(|value| value == "1");
}
Event::Start(start) | Event::Empty(start)
if start.local_name().as_ref() == b"sheetFormatPr" =>
{
default_column_width = raw_attribute(&start, b"defaultColWidth")
.and_then(|value| value.parse::<f64>().ok());
default_row_height = raw_attribute(&start, b"defaultRowHeight")
.and_then(|value| value.parse::<f64>().ok());
}
Event::Start(start) | Event::Empty(start)
if start.local_name().as_ref() == b"sheetView" =>
{
zoom_scale =
raw_attribute(&start, b"zoomScale").and_then(|value| value.parse::<u32>().ok());
show_grid_lines = raw_attribute(&start, b"showGridLines")
.as_deref()
.map(|value| value == "1");
show_row_col_headers = raw_attribute(&start, b"showRowColHeaders")
.as_deref()
.map(|value| value == "1");
show_zeros = raw_attribute(&start, b"showZeros")
.as_deref()
.map(|value| value == "1");
right_to_left = raw_attribute(&start, b"rightToLeft")
.as_deref()
.map(|value| value == "1");
}
Event::Start(start) if start.local_name().as_ref() == b"rowBreaks" => {
in_row_breaks = true;
}
Event::End(end) if end.local_name().as_ref() == b"rowBreaks" => {
in_row_breaks = false;
}
Event::Start(start) if start.local_name().as_ref() == b"colBreaks" => {
in_col_breaks = true;
}
Event::End(end) if end.local_name().as_ref() == b"colBreaks" => {
in_col_breaks = false;
}
Event::Empty(start)
if (in_row_breaks || in_col_breaks) && start.local_name().as_ref() == b"brk" =>
{
if let Some(id) =
raw_attribute(&start, b"id").and_then(|value| value.parse::<u32>().ok())
{
if in_row_breaks {
row_breaks.push(id);
} else {
column_breaks.push(id);
}
}
}
Event::Empty(start) if start.local_name().as_ref() == b"protectedRange" => {
if let (Some(name), Some(range)) = (
raw_attribute(&start, b"name"),
raw_attribute(&start, b"sqref"),
) {
protected_ranges.push(ProtectedRange::new(name, range));
}
}
Event::Empty(start) if start.local_name().as_ref() == b"ignoredError" => {
if let Some(range) = raw_attribute(&start, b"sqref") {
let mut entry = IgnoredError::new(range);
entry.eval_error = raw_attribute(&start, b"evalError").as_deref() == Some("1");
entry.two_digit_text_year =
raw_attribute(&start, b"twoDigitTextYear").as_deref() == Some("1");
entry.number_stored_as_text =
raw_attribute(&start, b"numberStoredAsText").as_deref() == Some("1");
entry.formula = raw_attribute(&start, b"formula").as_deref() == Some("1");
entry.formula_range =
raw_attribute(&start, b"formulaRange").as_deref() == Some("1");
entry.unlocked_formula =
raw_attribute(&start, b"unlockedFormula").as_deref() == Some("1");
entry.empty_cell_reference =
raw_attribute(&start, b"emptyCellReference").as_deref() == Some("1");
entry.list_data_validation =
raw_attribute(&start, b"listDataValidation").as_deref() == Some("1");
entry.calculated_column =
raw_attribute(&start, b"calculatedColumn").as_deref() == Some("1");
ignored_errors.push(entry);
}
}
Event::Start(start) if start.local_name().as_ref() == b"scenario" => {
let mut scenario =
Scenario::new(raw_attribute(&start, b"name").unwrap_or_default());
scenario.comment = raw_attribute(&start, b"comment");
current_scenario = Some(scenario);
}
Event::End(end) if end.local_name().as_ref() == b"scenario" => {
if let Some(scenario) = current_scenario.take() {
scenarios.push(scenario);
}
}
Event::Empty(start)
if current_scenario.is_some() && start.local_name().as_ref() == b"inputCells" =>
{
if let (Some(cell), Some(value)) =
(raw_attribute(&start, b"r"), raw_attribute(&start, b"val"))
{
if let Some(scenario) = current_scenario.as_mut() {
scenario.inputs.push((cell, value));
}
}
}
Event::Start(start) if start.local_name().as_ref() == b"filterColumn" => {
let column_offset = raw_attribute(&start, b"colId")
.and_then(|value| value.parse::<u32>().ok())
.unwrap_or(0);
current_filter_column = Some(AutoFilterColumn::new(column_offset, Vec::new()));
}
Event::End(end) if end.local_name().as_ref() == b"filterColumn" => {
if let Some(column) = current_filter_column.take() {
auto_filter_columns.push(column);
}
}
Event::Empty(start)
if current_filter_column.is_some() && start.local_name().as_ref() == b"filter" =>
{
if let Some(value) = raw_attribute(&start, b"val") {
if let Some(column) = current_filter_column.as_mut() {
column.visible_values.push(value);
}
}
}
Event::Start(start) if start.local_name().as_ref() == b"sortState" => {
current_sort_state = Some(SortState::new(
raw_attribute(&start, b"ref").unwrap_or_default(),
));
}
Event::Empty(start) if start.local_name().as_ref() == b"sortState" => {
sort_state = Some(SortState::new(
raw_attribute(&start, b"ref").unwrap_or_default(),
));
}
Event::End(end) if end.local_name().as_ref() == b"sortState" => {
sort_state = current_sort_state.take();
}
Event::Empty(start)
if current_sort_state.is_some()
&& start.local_name().as_ref() == b"sortCondition" =>
{
if let Some(range) = raw_attribute(&start, b"ref") {
let descending = raw_attribute(&start, b"descending").as_deref() == Some("1");
if let Some(state) = current_sort_state.as_mut() {
let mut condition = SortCondition::new(range);
condition.descending = descending;
state.conditions.push(condition);
}
}
}
Event::Start(start) | Event::Empty(start)
if start.local_name().as_ref() == b"pageSetup" =>
{
orientation = raw_attribute(&start, b"orientation")
.as_deref()
.and_then(parse_page_orientation);
fit_to_width = raw_attribute(&start, b"fitToWidth")
.and_then(|value| value.parse::<u32>().ok());
fit_to_height = raw_attribute(&start, b"fitToHeight")
.and_then(|value| value.parse::<u32>().ok());
scale = raw_attribute(&start, b"scale").and_then(|value| value.parse::<u32>().ok());
if fit_to_width == Some(0) {
fit_to_width = None;
}
if fit_to_height == Some(0) {
fit_to_height = None;
}
}
Event::Start(start) if start.local_name().as_ref() == b"oddHeader" => {
in_header_footer_element = Some("oddHeader");
header_footer_buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"oddFooter" => {
in_header_footer_element = Some("oddFooter");
header_footer_buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"evenHeader" => {
in_header_footer_element = Some("evenHeader");
header_footer_buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"evenFooter" => {
in_header_footer_element = Some("evenFooter");
header_footer_buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"firstHeader" => {
in_header_footer_element = Some("firstHeader");
header_footer_buffer.clear();
}
Event::Start(start) if start.local_name().as_ref() == b"firstFooter" => {
in_header_footer_element = Some("firstFooter");
header_footer_buffer.clear();
}
Event::Text(text) if in_header_footer_element.is_some() => {
header_footer_buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if in_header_footer_element.is_some() => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
header_footer_buffer.push_str(&resolved);
}
}
Event::End(end)
if in_header_footer_element.is_some()
&& matches!(
end.local_name().as_ref(),
b"oddHeader"
| b"oddFooter"
| b"evenHeader"
| b"evenFooter"
| b"firstHeader"
| b"firstFooter"
) =>
{
let text = std::mem::take(&mut header_footer_buffer);
match in_header_footer_element.take() {
Some("oddHeader") => header = Some(text),
Some("oddFooter") => footer = Some(text),
Some("evenHeader") => header_even = Some(text),
Some("evenFooter") => footer_even = Some(text),
Some("firstHeader") => header_first = Some(text),
Some("firstFooter") => footer_first = Some(text),
_ => {}
}
}
Event::Start(start) | Event::Empty(start)
if start.local_name().as_ref() == b"hyperlink" =>
{
if let Some(cell) = raw_attribute(&start, b"ref") {
let tooltip = raw_attribute(&start, b"tooltip");
let target = if let Some(location) = raw_attribute(&start, b"location") {
Some(HyperlinkTarget::Internal(location))
} else {
raw_attribute(&start, b"id")
.and_then(|relationship_id| sheet_relationships.by_id(&relationship_id))
.map(|relationship| {
HyperlinkTarget::External(relationship.target.clone())
})
};
if let Some(target) = target {
hyperlinks.push(CellHyperlink {
cell,
target,
tooltip,
});
}
}
}
Event::Start(start) if start.local_name().as_ref() == b"conditionalFormatting" => {
current_cf_sqref = raw_attribute(&start, b"sqref");
}
Event::End(end) if end.local_name().as_ref() == b"conditionalFormatting" => {
current_cf_sqref = None;
}
Event::Start(start) if start.local_name().as_ref() == b"cfRule" => {
in_cf_rule = true;
cf_rule_type = raw_attribute(&start, b"type");
cf_rule_dxf_id =
raw_attribute(&start, b"dxfId").and_then(|value| value.parse::<usize>().ok());
cf_rule_operator = raw_attribute(&start, b"operator");
cf_rule_text = raw_attribute(&start, b"text");
cf_formulas.clear();
}
Event::Empty(start) if start.local_name().as_ref() == b"cfRule" => {
let rule_type = raw_attribute(&start, b"type");
let dxf_id =
raw_attribute(&start, b"dxfId").and_then(|value| value.parse::<usize>().ok());
if let Some(sqref) = current_cf_sqref.clone() {
let format = dxf_id
.and_then(|id| dxfs.get(id).cloned())
.unwrap_or_default();
match rule_type.as_deref() {
Some("top10") => {
let rank = raw_attribute(&start, b"rank")
.and_then(|value| value.parse::<u32>().ok())
.unwrap_or(10);
let percent = raw_attribute(&start, b"percent").as_deref() == Some("1");
let bottom = raw_attribute(&start, b"bottom").as_deref() == Some("1");
let rule =
ConditionalFormattingRule::top10(sqref, rank, percent, format)
.with_bottom(bottom);
conditional_formatting_rules.push(rule);
}
Some("aboveAverage") => {
let above =
raw_attribute(&start, b"aboveAverage").as_deref() != Some("0");
conditional_formatting_rules.push(
ConditionalFormattingRule::above_average(sqref, above, format),
);
}
Some("duplicateValues") => {
conditional_formatting_rules
.push(ConditionalFormattingRule::duplicate_values(sqref, format));
}
Some("uniqueValues") => {
conditional_formatting_rules
.push(ConditionalFormattingRule::unique_values(sqref, format));
}
_ => {}
}
}
}
Event::End(end) if in_cf_rule && end.local_name().as_ref() == b"cfRule" => {
in_cf_rule = false;
if let Some(sqref) = current_cf_sqref.clone() {
let format = cf_rule_dxf_id
.take()
.and_then(|id| dxfs.get(id).cloned())
.unwrap_or_default();
let mut formulas = std::mem::take(&mut cf_formulas).into_iter();
match cf_rule_type.take().as_deref() {
Some("cellIs") => {
if let Some(operator) = cf_rule_operator
.take()
.as_deref()
.and_then(parse_comparison_operator)
{
if let Some(formula1) = formulas.next() {
let mut rule = ConditionalFormattingRule::cell_is(
sqref, operator, formula1, format,
);
if let Some(formula2) = formulas.next() {
rule = rule.with_second_formula(formula2);
}
conditional_formatting_rules.push(rule);
}
}
}
Some("expression") => {
if let Some(formula) = formulas.next() {
conditional_formatting_rules.push(
ConditionalFormattingRule::expression(sqref, formula, format),
);
}
}
Some(
text_type @ ("containsText" | "notContainsText" | "beginsWith"
| "endsWith"),
) => {
if let Some(operator) = text_comparison_operator_from_type(text_type) {
if let Some(text) = cf_rule_text.take() {
conditional_formatting_rules.push(
ConditionalFormattingRule::contains_text(
sqref, operator, text, format,
),
);
}
}
}
_ => {}
}
}
cf_rule_operator = None;
cf_rule_text = None;
}
Event::Start(start) if in_cf_rule && start.local_name().as_ref() == b"formula" => {
in_cf_formula = true;
cf_formula_buffer.clear();
}
Event::End(end) if in_cf_rule && end.local_name().as_ref() == b"formula" => {
in_cf_formula = false;
cf_formulas.push(std::mem::take(&mut cf_formula_buffer));
}
Event::Text(text) if in_cf_formula => {
cf_formula_buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if in_cf_formula => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
cf_formula_buffer.push_str(&resolved);
}
}
Event::Start(start) if start.local_name().as_ref() == b"dataValidation" => {
in_data_validation = true;
dv_type = raw_attribute(&start, b"type");
dv_operator = raw_attribute(&start, b"operator");
dv_allow_blank = raw_attribute(&start, b"allowBlank").as_deref() == Some("1");
dv_show_input_message =
raw_attribute(&start, b"showInputMessage").as_deref() == Some("1");
dv_show_error_message =
raw_attribute(&start, b"showErrorMessage").as_deref() == Some("1");
dv_prompt_title = raw_attribute(&start, b"promptTitle");
dv_prompt = raw_attribute(&start, b"prompt");
dv_error_title = raw_attribute(&start, b"errorTitle");
dv_error = raw_attribute(&start, b"error");
dv_sqref = raw_attribute(&start, b"sqref");
dv_formula1 = None;
dv_formula2 = None;
}
Event::End(end) if end.local_name().as_ref() == b"dataValidation" => {
in_data_validation = false;
if let (Some(sqref), Some(formula1)) = (dv_sqref.take(), dv_formula1.take()) {
let kind = match dv_type.take().as_deref() {
Some("list") => Some(DataValidationKind::List { source: formula1 }),
Some("whole") => dv_operator
.as_deref()
.and_then(parse_comparison_operator)
.map(|operator| DataValidationKind::WholeNumber {
operator,
formula1,
formula2: dv_formula2.clone(),
}),
Some("decimal") => dv_operator
.as_deref()
.and_then(parse_comparison_operator)
.map(|operator| DataValidationKind::Decimal {
operator,
formula1,
formula2: dv_formula2.clone(),
}),
Some("date") => dv_operator
.as_deref()
.and_then(parse_comparison_operator)
.map(|operator| DataValidationKind::Date {
operator,
formula1,
formula2: dv_formula2.clone(),
}),
Some("time") => dv_operator
.as_deref()
.and_then(parse_comparison_operator)
.map(|operator| DataValidationKind::Time {
operator,
formula1,
formula2: dv_formula2.clone(),
}),
Some("textLength") => dv_operator
.as_deref()
.and_then(parse_comparison_operator)
.map(|operator| DataValidationKind::TextLength {
operator,
formula1,
formula2: dv_formula2.clone(),
}),
Some("custom") => Some(DataValidationKind::Custom { formula: formula1 }),
_ => None,
};
if let Some(kind) = kind {
data_validation_rules.push(DataValidationRule {
range: sqref,
kind,
allow_blank: dv_allow_blank,
input_message: if dv_show_input_message {
Some(Message {
title: dv_prompt_title.take(),
text: dv_prompt.take().unwrap_or_default(),
})
} else {
None
},
error_message: if dv_show_error_message {
Some(Message {
title: dv_error_title.take(),
text: dv_error.take().unwrap_or_default(),
})
} else {
None
},
});
}
}
dv_operator = None;
dv_formula2 = None;
}
Event::Start(start)
if in_data_validation && start.local_name().as_ref() == b"formula1" =>
{
in_dv_formula1 = true;
dv_formula_buffer.clear();
}
Event::End(end) if in_data_validation && end.local_name().as_ref() == b"formula1" => {
in_dv_formula1 = false;
dv_formula1 = Some(std::mem::take(&mut dv_formula_buffer));
}
Event::Start(start)
if in_data_validation && start.local_name().as_ref() == b"formula2" =>
{
in_dv_formula2 = true;
dv_formula_buffer.clear();
}
Event::End(end) if in_data_validation && end.local_name().as_ref() == b"formula2" => {
in_dv_formula2 = false;
dv_formula2 = Some(std::mem::take(&mut dv_formula_buffer));
}
Event::Text(text) if in_dv_formula1 || in_dv_formula2 => {
dv_formula_buffer.push_str(&xml_core::decode_text(&text)?);
}
Event::GeneralRef(reference) if in_dv_formula1 || in_dv_formula2 => {
if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
dv_formula_buffer.push_str(&resolved);
}
}
_ => {}
}
}
let print_settings = PrintSettings {
print_area: None,
repeat_rows: None,
repeat_columns: None,
fit_to_width,
fit_to_height,
orientation,
header,
footer,
header_first,
footer_first,
header_even,
footer_even,
scale,
};
Ok(ParsedWorksheet {
rows,
conditional_formatting_rules,
data_validation_rules,
merged_ranges,
column_settings,
auto_filter_range,
freeze_panes,
protection,
hyperlinks,
zoom_scale,
row_breaks,
column_breaks,
scenarios,
auto_filter_columns,
print_settings,
tab_color,
show_grid_lines,
show_row_col_headers,
show_zeros,
right_to_left,
active_cell,
default_column_width,
default_row_height,
ignored_errors,
protected_ranges,
sort_state,
outline_summary_below,
outline_summary_right,
})
}
fn ensure_len<T>(items: &mut Vec<T>, len: usize, default: fn() -> T) {
while items.len() < len {
items.push(default());
}
}
fn column_index_from_reference(reference: &str) -> Option<usize> {
let letters: String = reference
.chars()
.take_while(|character| character.is_ascii_alphabetic())
.collect();
if letters.is_empty() || letters.len() > 3 {
return None;
}
let mut index: usize = 0;
for character in letters.chars() {
let digit = (character.to_ascii_uppercase() as u8 - b'A' + 1) as usize;
index = index.checked_mul(26)?.checked_add(digit)?;
}
let column_index = index.checked_sub(1)?;
(column_index < EXCEL_MAX_COLUMNS).then_some(column_index)
}
fn raw_attribute(start: &BytesStart, name: &[u8]) -> Option<String> {
start
.attributes()
.flatten()
.find(|attribute| attribute.key.local_name().as_ref() == name)
.and_then(|attribute| {
xml_core::decode_attribute_value(attribute.value.as_ref())
.ok()
.flatten()
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_sheet_names_and_relationship_ids_from_workbook_xml() {
let xml = r#"<?xml version="1.0"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets>
<sheet name="Feuil1" sheetId="1" r:id="rId1"/>
<sheet name="Feuil2" sheetId="2" r:id="rId2"/>
</sheets>
</workbook>"#;
let parsed = parse_workbook_xml(xml).unwrap();
assert_eq!(
parsed.sheets,
vec![
(
"Feuil1".to_string(),
"rId1".to_string(),
SheetVisibility::Visible
),
(
"Feuil2".to_string(),
"rId2".to_string(),
SheetVisibility::Visible
),
]
);
assert!(parsed.defined_names.is_empty());
}
#[test]
fn parses_defined_names_from_workbook_xml() {
let xml = r#"<?xml version="1.0"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets>
<sheet name="Feuil1" sheetId="1" r:id="rId1"/>
<sheet name="Feuil2" sheetId="2" r:id="rId2"/>
</sheets>
<definedNames>
<definedName name="Plage_Ventes">Feuil1!$A$1:$C$12</definedName>
<definedName name="Northwind_Database" localSheetId="1" hidden="1" comment="Source externe">Feuil2!$A$1:$T$47</definedName>
</definedNames>
</workbook>"#;
let parsed = parse_workbook_xml(xml).unwrap();
let defined_names = parsed.defined_names;
assert_eq!(defined_names.len(), 2);
assert_eq!(defined_names[0].name, "Plage_Ventes");
assert_eq!(defined_names[0].refers_to, "Feuil1!$A$1:$C$12");
assert_eq!(defined_names[0].local_sheet_id, None);
assert!(!defined_names[0].hidden);
assert_eq!(defined_names[0].comment, None);
assert_eq!(defined_names[1].name, "Northwind_Database");
assert_eq!(defined_names[1].refers_to, "Feuil2!$A$1:$T$47");
assert_eq!(defined_names[1].local_sheet_id, Some(1));
assert!(defined_names[1].hidden);
assert_eq!(defined_names[1].comment.as_deref(), Some("Source externe"));
}
#[test]
fn parses_simple_shared_strings() {
let xml = r#"<?xml version="1.0"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="2" uniqueCount="2">
<si><t>Nombre_1</t></si>
<si><t>Nombre_2</t></si>
</sst>"#;
let strings = parse_shared_strings_xml(xml).unwrap();
assert_eq!(
strings,
vec![
SharedStringEntry::Plain("Nombre_1".to_string()),
SharedStringEntry::Plain("Nombre_2".to_string())
]
);
}
#[test]
fn parses_rich_text_run_shared_strings_into_distinct_runs() {
let xml = r#"<?xml version="1.0"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="1" uniqueCount="1">
<si><r><rPr><b/></rPr><t>Hello, </t></r><r><t>world!</t></r></si>
</sst>"#;
let strings = parse_shared_strings_xml(xml).unwrap();
assert_eq!(strings.len(), 1);
assert_eq!(
strings[0],
SharedStringEntry::Rich(vec![
RichTextRun::new("Hello, ").with_bold(true),
RichTextRun::new("world!"),
])
);
}
fn no_relationships() -> opc::Relationships {
opc::Relationships::new()
}
#[test]
fn parses_a_simple_worksheet_with_text_and_numeric_cells() {
let shared_strings = vec![
SharedStringEntry::Plain("Nombre_1".to_string()),
SharedStringEntry::Plain("Nombre_2".to_string()),
];
let xml = r#"<?xml version="1.0"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1"><c r="A1" t="s"><v>0</v></c><c r="B1" t="s"><v>1</v></c></row>
<row r="2"><c r="A2"><v>10</v></c><c r="B2"><v>1</v></c></row>
</sheetData>
</worksheet>"#;
let parsed =
parse_worksheet_xml(xml, &shared_strings, &[], &[], &no_relationships()).unwrap();
assert_eq!(parsed.rows.len(), 2);
assert_eq!(
parsed.rows[0].cells[0].value,
CellValue::Text("Nombre_1".to_string())
);
assert_eq!(
parsed.rows[0].cells[1].value,
CellValue::Text("Nombre_2".to_string())
);
assert_eq!(parsed.rows[1].cells[0].value, CellValue::Number(10.0));
assert_eq!(parsed.rows[1].cells[1].value, CellValue::Number(1.0));
}
#[test]
fn fills_in_a_gap_left_by_a_skipped_column_as_an_empty_cell() {
let xml = r#"<?xml version="1.0"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1"><c r="A1"><v>1</v></c><c r="C1"><v>3</v></c></row>
</sheetData>
</worksheet>"#;
let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
assert_eq!(parsed.rows[0].cells.len(), 3);
assert_eq!(parsed.rows[0].cells[0].value, CellValue::Number(1.0));
assert_eq!(parsed.rows[0].cells[1].value, CellValue::Empty);
assert_eq!(parsed.rows[0].cells[2].value, CellValue::Number(3.0));
}
#[test]
fn fills_in_a_gap_left_by_a_skipped_row_as_an_empty_row() {
let xml = r#"<?xml version="1.0"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1"><c r="A1"><v>1</v></c></row>
<row r="3"><c r="A3"><v>3</v></c></row>
</sheetData>
</worksheet>"#;
let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
assert_eq!(parsed.rows.len(), 3);
assert!(parsed.rows[1].cells.is_empty());
assert_eq!(parsed.rows[2].cells[0].value, CellValue::Number(3.0));
}
#[test]
fn converts_a_cell_reference_to_a_0_based_column_index() {
assert_eq!(column_index_from_reference("A1"), Some(0));
assert_eq!(column_index_from_reference("C5"), Some(2));
assert_eq!(column_index_from_reference("AA11"), Some(26));
assert_eq!(column_index_from_reference(""), None);
}
#[test]
fn column_index_from_reference_rejects_columns_beyond_excel_s_own_limit() {
assert_eq!(column_index_from_reference("XFD1"), Some(16_383));
assert_eq!(column_index_from_reference("XFE1"), None);
assert_eq!(column_index_from_reference("ZZZZZZZZZZZZZZ1"), None);
}
#[test]
fn drops_a_row_whose_number_is_beyond_excel_s_own_limit_instead_of_allocating() {
let xml = r#"<?xml version="1.0"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1"><c r="A1"><v>1</v></c></row>
<row r="9999999999"><c r="A1"><v>2</v></c></row>
<row r="2"><c r="A2"><v>3</v></c></row>
</sheetData>
</worksheet>"#;
let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
assert_eq!(parsed.rows.len(), 2);
assert_eq!(parsed.rows[0].cells[0].value, CellValue::Number(1.0));
assert_eq!(parsed.rows[1].cells[0].value, CellValue::Number(3.0));
}
#[test]
fn clamps_a_malformed_col_range_to_excel_s_own_column_bounds() {
let xml = r#"<?xml version="1.0"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<cols>
<col min="0" max="2" width="10"/>
</cols>
<sheetData/>
</worksheet>"#;
let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
assert_eq!(parsed.column_settings.len(), 2);
let xml_unbounded = r#"<?xml version="1.0"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<cols>
<col min="1" max="4000000000" width="10"/>
</cols>
<sheetData/>
</worksheet>"#;
let parsed_unbounded =
parse_worksheet_xml(xml_unbounded, &[], &[], &[], &no_relationships()).unwrap();
assert_eq!(parsed_unbounded.column_settings.len(), 16_384);
}
#[test]
fn parses_a_normal_formula_cell_with_a_cached_value() {
let xml = r#"<?xml version="1.0"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1"><c r="A1"><f>A2*B2</f><v>10</v></c></row>
</sheetData>
</worksheet>"#;
let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
assert_eq!(
parsed.rows[0].cells[0].value,
CellValue::Formula {
formula: Some("A2*B2".to_string()),
cached_value: Some(10.0)
}
);
}
#[test]
fn parses_a_shared_formula_follower_cell_with_no_literal_text_into_shared_formula() {
let xml = r#"<?xml version="1.0"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1"><c r="C4"><f t="shared" si="0"/><v>30</v></c></row>
</sheetData>
</worksheet>"#;
let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
assert_eq!(
parsed.rows[0].cells[2].value,
CellValue::SharedFormula {
formula: None,
group_index: 0,
range: None,
cached_value: Some(30.0)
}
);
}
#[test]
fn parses_styles_xml_into_cell_formats_indexed_by_cellxfs_position() {
let xml = r#"<?xml version="1.0"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<numFmts count="1"><numFmt numFmtId="164" formatCode="0.00"/></numFmts>
<fonts count="2">
<font><sz val="11"/><color rgb="FF000000"/><name val="Calibri"/><family val="2"/></font>
<font><b/><sz val="11"/><color rgb="FF000000"/><name val="Calibri"/><family val="2"/></font>
</fonts>
<fills count="3">
<fill><patternFill patternType="none"/></fill>
<fill><patternFill patternType="gray125"/></fill>
<fill><patternFill patternType="solid"><fgColor rgb="FFFFFF00"/></patternFill></fill>
</fills>
<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
<cellXfs count="2">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
<xf numFmtId="164" fontId="1" fillId="2" borderId="0" xfId="0" applyNumberFormat="1" applyFont="1" applyFill="1" applyAlignment="1"><alignment horizontal="center" vertical="top"/></xf>
</cellXfs>
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
</styleSheet>"#;
let ParsedStyles {
cell_formats: formats,
dxfs,
..
} = parse_styles_xml(xml).unwrap();
assert_eq!(formats.len(), 2);
assert_eq!(formats[0], None);
assert_eq!(
formats[1],
Some(CellFormat {
number_format: Some("0.00".to_string()),
bold: true,
italic: false,
font_size: Some(11.0),
font_color: Some("FF000000".to_string()),
fill_color: Some("FFFFFF00".to_string()),
horizontal_alignment: Some(HorizontalAlignment::Center),
vertical_alignment: Some(VerticalAlignment::Top),
border: crate::model::Border::default(),
font_name: Some("Calibri".to_string()),
underline: false,
strike: false,
wrap_text: false,
indent: None,
text_rotation: None,
shrink_to_fit: false,
pattern_fill: None,
gradient_fill: None,
locked: None,
formula_hidden: None,
named_style: None,
})
);
assert!(dxfs.is_empty());
}
#[test]
fn parses_dxfs_as_inline_self_contained_cell_formats() {
let xml = r#"<?xml version="1.0"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<fonts count="1"><font><sz val="11"/><color rgb="FF000000"/><name val="Calibri"/><family val="2"/></font></fonts>
<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>
<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
<dxfs count="1">
<dxf>
<font><b/><color rgb="FF9C0006"/></font>
<fill><patternFill patternType="solid"><bgColor rgb="FFFFC7CE"/></patternFill></fill>
</dxf>
</dxfs>
</styleSheet>"#;
let ParsedStyles { dxfs, .. } = parse_styles_xml(xml).unwrap();
assert_eq!(dxfs.len(), 1);
assert_eq!(
dxfs[0],
CellFormat {
number_format: None,
bold: true,
italic: false,
font_size: None,
font_color: Some("FF9C0006".to_string()),
fill_color: Some("FFFFC7CE".to_string()),
horizontal_alignment: None,
vertical_alignment: None,
border: crate::model::Border::default(),
font_name: None,
underline: false,
strike: false,
wrap_text: false,
indent: None,
text_rotation: None,
shrink_to_fit: false,
pattern_fill: None,
gradient_fill: None,
locked: None,
formula_hidden: None,
named_style: None,
}
);
}
#[test]
fn parses_a_cell_is_conditional_formatting_rule_resolving_its_dxf() {
let dxfs = vec![
CellFormat::new()
.with_bold(true)
.with_fill_color("FFFFFF00"),
];
let xml = r#"<?xml version="1.0"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData/>
<conditionalFormatting sqref="E3:E9">
<cfRule type="cellIs" dxfId="0" priority="1" operator="greaterThan"><formula>0.5</formula></cfRule>
</conditionalFormatting>
</worksheet>"#;
let parsed = parse_worksheet_xml(xml, &[], &[], &dxfs, &no_relationships()).unwrap();
let rules = parsed.conditional_formatting_rules;
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].range, "E3:E9");
assert_eq!(rules[0].format, dxfs[0]);
assert_eq!(
rules[0].condition,
crate::model::ConditionalFormattingCondition::CellIs {
operator: ComparisonOperator::GreaterThan,
formula1: "0.5".to_string(),
formula2: None,
}
);
}
#[test]
fn parses_a_data_validation_list_rule() {
let xml = r#"<?xml version="1.0"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData/>
<dataValidations count="1">
<dataValidation type="list" allowBlank="1" sqref="B2:B20"><formula1>"Oui,Non"</formula1></dataValidation>
</dataValidations>
</worksheet>"#;
let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
let rules = parsed.data_validation_rules;
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].range, "B2:B20");
assert!(rules[0].allow_blank);
assert_eq!(
rules[0].kind,
DataValidationKind::List {
source: "\"Oui,Non\"".to_string()
}
);
}
#[test]
fn parses_a_whole_number_data_validation_rule_with_messages() {
let xml = r#"<?xml version="1.0"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData/>
<dataValidations count="1">
<dataValidation type="whole" operator="greaterThanOrEqual" allowBlank="1" showInputMessage="1" showErrorMessage="1" promptTitle="Saisie" prompt="Entrez un nombre positif" errorTitle="Erreur" error="Valeur invalide" sqref="C2:C20"><formula1>0</formula1></dataValidation>
</dataValidations>
</worksheet>"#;
let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
let rules = parsed.data_validation_rules;
assert_eq!(rules.len(), 1);
assert_eq!(
rules[0].kind,
DataValidationKind::WholeNumber {
operator: ComparisonOperator::GreaterThanOrEqual,
formula1: "0".to_string(),
formula2: None,
}
);
let input_message = rules[0].input_message.as_ref().unwrap();
assert_eq!(input_message.title.as_deref(), Some("Saisie"));
assert_eq!(input_message.text, "Entrez un nombre positif");
let error_message = rules[0].error_message.as_ref().unwrap();
assert_eq!(error_message.title.as_deref(), Some("Erreur"));
assert_eq!(error_message.text, "Valeur invalide");
}
}