mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
craft-xlsx: tables (ListObjects) read and written back (#1062)
* craft-xlsx: read parts with canonical prefixes so kept trees can be written back The part layer read every part by local name, so extLst children (x14:, xr:, xm:), mc:AlternateContent and r:id lost their namespaces and could not be written back. Parts are now read with one fixed prefix per namespace (the main namespace unprefixed), mc:Ignorable and mc:Choice/@Requires are rewritten to match, and ns::declare prepares a kept tree for writing. craft-xml: a namespace listed with the empty prefix reads unprefixed. * craft-xlsx: write SpreadsheetML workbooks (Document read and write) Document holds a whole workbook: the workbook part, shared strings, styles, each worksheet's rows and the rest of its part, and every other part kept as bytes. write regenerates the workbook part, worksheets, shared strings and styles from the model, keeps unknown children in place (schema order for regenerated ones), keeps part names and relationship ids, and writes kept parts unchanged. Writers: Styles::to_xml and to_el for each table entry, StringItem::to_el (phonetic runs checked as on reading), SheetPart::to_xml with rows streamed, Workbook::to_el, XlsxKind. Row and DefinedName keep their other attributes; CellStyle reads iLevel. craft-opc: LazyPackage::canonical gives a part's stored name. * craft-xlsx: typed worksheet features and comments, written back SheetPart::features lifts sheet views, print setup, data validation, conditional formats, hyperlinks and the legacy drawing into types that mirror their elements (optional attributes, unnamed ones kept) and writes them in schema order. Comments come from the comments part, note shapes from the VML drawing; a sheet that gains comments without a VML drawing gets one. A hyperlink to a new URL gets its own relationship. craft-sheet: Range derives Default. * craft-xlsx: drawings, charts and the theme on craft-drawingml Drawing reads a sheet's xdr:wsDr into anchors with typed placements and objects kept as written; Anchor gives the object's kind, properties, chart or image relationship and xfrm. Package::drawing, Package::chart (c: and cx: parts through craft-drawingml) and Package::theme. Document worksheets hold their drawing part, regenerated with its relationships kept by id; add_chart and add_picture add new chart parts (chartEx behind mc:AlternateContent) and images with their anchors. More drawing namespaces get prefixes (a16, asvg, aink, am3d, cx1-cx4, dgm). * craft-xlsx: tables (ListObjects) read and written back Table mirrors CT_Table: every attribute, columns with calculated and totals formulas, the style info, autoFilter/sortState/extensions kept as written. Package::tables reads a sheet's table parts; Document worksheets hold them as TableParts, regenerated with their relationships and tableParts entries. Part names a document keeps are reserved before new parts are named, so a new part never takes a name a later sheet keeps.
This commit is contained in:
1 parent
b15cd61ab6
commit
a19cf6753b
6 files changed
+301
-17
No files matched your search
@@ -56,6 +56,14 @@ or image relationship and its `a:xfrm`, and `craft_drawingml::read` reads its fi
|
||||
(a `chartEx` part behind `mc:AlternateContent` for the Office 2016 types) or an image, with its
|
||||
anchor. Chart parts and media are kept parts.
|
||||
|
||||
Tables: `Package::tables` reads a sheet's table parts (in `tableParts` order) into `Table`
|
||||
(every `CT_Table` attribute, `TableColumn`s with calculated and totals formulas,
|
||||
`TableStyleInfo`; `autoFilter`, `sortState` and extensions kept as written). A `Document`
|
||||
worksheet holds them as `TablePart`s, regenerated on writing with their relationships and
|
||||
`tableParts` entries; a removed table takes both with it. Every part name a document keeps is
|
||||
reserved before new parts are named, so a new part never takes a name another sheet's part
|
||||
keeps.
|
||||
|
||||
The parts this crate writes use the transitional namespaces; a strict file's kept parts stay as
|
||||
they were. A binary workbook is not written.
|
||||
|
||||
|
||||
+50
-11
@@ -6,7 +6,7 @@ use craft_opc::{ContentTypes, PackageOut, RT_BASE, Rel, RelOut, Rels, RelsOut, r
|
||||
use craft_drawingml::chart::Chart;
|
||||
|
||||
use crate::drawing::{Anchor, Drawing, Item, Placement};
|
||||
use crate::features::{Comments, NoteShape, notes_vml};
|
||||
use crate::features::{Comments, NoteShape, Table, notes_vml};
|
||||
use crate::sheet::{CellValue, Row, SheetPart};
|
||||
use crate::strings::StringItem;
|
||||
use crate::styles::Styles;
|
||||
@@ -18,6 +18,7 @@ const CT_STRINGS: &str = "application/vnd.openxmlformats-officedocument.spreadsh
|
||||
const CT_STYLES: &str = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml";
|
||||
const CT_COMMENTS: &str = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml";
|
||||
const CT_VML: &str = "application/vnd.openxmlformats-officedocument.vmlDrawing";
|
||||
const CT_TABLE: &str = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml";
|
||||
const CT_DRAWING: &str = "application/vnd.openxmlformats-officedocument.drawing+xml";
|
||||
const CT_CHART: &str = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml";
|
||||
const CT_CHARTEX: &str = "application/vnd.ms-office.chartex+xml";
|
||||
@@ -86,6 +87,16 @@ pub struct Worksheet {
|
||||
pub notes: Vec<NoteShape>,
|
||||
/// The drawing part, written from here. `None` removes it.
|
||||
pub drawing: Option<DrawingPart>,
|
||||
/// The table parts, written from here, in the order of the sheet's `tableParts`.
|
||||
pub tables: Vec<TablePart>,
|
||||
}
|
||||
|
||||
/// A table part. Its relationships (a query table) are kept parts.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct TablePart {
|
||||
/// The part's name; a new name is chosen on writing when empty.
|
||||
pub part: String,
|
||||
pub table: Table,
|
||||
}
|
||||
|
||||
/// A worksheet's drawing part and its relationships (to charts and pictures, kept with their
|
||||
@@ -185,7 +196,8 @@ impl Document {
|
||||
let comments = p.comments(&part)?;
|
||||
let notes = if comments.is_some() { p.note_shapes(&part)? } else { Vec::new() };
|
||||
let drawing = p.drawing(&part)?.map(|(name, drawing, rels)| DrawingPart { part: name, drawing, rels: rels.list });
|
||||
worksheets.push(Some(Worksheet { rows, part, comments, notes, drawing }));
|
||||
let tables = p.tables(&part)?.into_iter().map(|(part, table)| TablePart { part, table }).collect();
|
||||
worksheets.push(Some(Worksheet { rows, part, comments, notes, drawing, tables }));
|
||||
}
|
||||
Err(Error::TooLarge(m)) => return Err(Error::TooLarge(m)),
|
||||
Err(e) => {
|
||||
@@ -206,6 +218,7 @@ impl Document {
|
||||
made.push(d.part.clone());
|
||||
made.push(rels_path(&d.part));
|
||||
}
|
||||
made.extend(ws.tables.iter().map(|t| t.part.clone()));
|
||||
}
|
||||
made.extend(workbook_rels.list.iter().filter(|r| !r.external && regenerated(r)).map(|r| r.target.clone()));
|
||||
let opc = p.opc();
|
||||
@@ -310,7 +323,14 @@ impl Document {
|
||||
}
|
||||
let mut workbook = self.workbook.clone();
|
||||
let mut sheet_parts = Vec::new();
|
||||
// Every name a part keeps is taken before any new part is named, so a new one never
|
||||
// takes the name of a part written later.
|
||||
let mut taken: Vec<String> = self.parts.iter().map(|p| p.name.to_ascii_lowercase()).collect();
|
||||
for ws in self.worksheets.iter().flatten() {
|
||||
let comments = ws.comments.as_ref().and(ws.part.rels.first("comments")).map(|r| r.target.clone());
|
||||
let own = [ws.part.part.clone()].into_iter().chain(comments).chain(ws.drawing.iter().map(|d| d.part.clone()));
|
||||
taken.extend(own.chain(ws.tables.iter().map(|t| t.part.clone())).filter(|n| !n.is_empty()).map(|n| n.to_ascii_lowercase()));
|
||||
}
|
||||
for (i, (entry, ws)) in workbook.sheets.iter_mut().zip(&self.worksheets).enumerate() {
|
||||
let Some(ws) = ws else {
|
||||
if rels.items.iter().any(|r| r.id == entry.rel_id) {
|
||||
@@ -319,7 +339,7 @@ impl Document {
|
||||
return Err(Error::Invalid(format!("sheet \"{}\" has neither a worksheet nor a kept part", entry.name)));
|
||||
};
|
||||
let mut name = ws.part.part.clone();
|
||||
if name.is_empty() || taken.contains(&name.to_ascii_lowercase()) {
|
||||
if name.is_empty() {
|
||||
name = fresh(&taken, |n| format!("xl/worksheets/sheet{}.xml", i + n));
|
||||
}
|
||||
taken.push(name.to_ascii_lowercase());
|
||||
@@ -357,8 +377,9 @@ impl Document {
|
||||
out.rels("", &package);
|
||||
}
|
||||
let mut blocks = vml_blocks(&self.parts);
|
||||
let kept_names: Vec<String> = self.parts.iter().map(|p| p.name.to_ascii_lowercase()).collect();
|
||||
for (name, ws) in &sheet_parts {
|
||||
write_sheet(&mut out, name, ws, &mut taken, &mut blocks);
|
||||
write_sheet(&mut out, name, ws, &kept_names, &mut taken, &mut blocks);
|
||||
}
|
||||
if let Some(name) = &strings {
|
||||
let count: usize =
|
||||
@@ -385,13 +406,15 @@ impl Document {
|
||||
|
||||
/// A worksheet, its relationships, and its comments with a VML drawing to show them when it has
|
||||
/// none. A hyperlink to a URL its relationship does not lead to gets a relationship of its own.
|
||||
fn write_sheet(out: &mut PackageOut, name: &str, ws: &Worksheet, taken: &mut Vec<String>, blocks: &mut Vec<u32>) {
|
||||
fn write_sheet(out: &mut PackageOut, name: &str, ws: &Worksheet, kept_names: &[String], taken: &mut Vec<String>, blocks: &mut Vec<u32>) {
|
||||
let mut part = ws.part.clone();
|
||||
let mut rels = RelsOut::default();
|
||||
let kept_part = |r: &Rel| kept_names.contains(&r.target.to_ascii_lowercase());
|
||||
// A comments or drawing relationship stays when this writes that part, or the part is kept.
|
||||
let stays = |r: &&Rel| match r.kind() {
|
||||
"comments" => ws.comments.is_some(),
|
||||
"drawing" => ws.drawing.as_ref().map_or(taken.contains(&r.target.to_ascii_lowercase()), |d| d.part.eq_ignore_ascii_case(&r.target)),
|
||||
"drawing" => ws.drawing.as_ref().map_or(kept_part(r), |d| d.part.eq_ignore_ascii_case(&r.target)),
|
||||
"table" => ws.tables.iter().any(|t| t.part.eq_ignore_ascii_case(&r.target)) || kept_part(r),
|
||||
_ => true,
|
||||
};
|
||||
rels.items.extend(ws.part.rels.list.iter().filter(stays).map(kept));
|
||||
@@ -399,11 +422,7 @@ fn write_sheet(out: &mut PackageOut, name: &str, ws: &Worksheet, taken: &mut Vec
|
||||
part.features.drawing = None;
|
||||
}
|
||||
if let Some(d) = &ws.drawing {
|
||||
let target = if d.part.is_empty() || taken.contains(&d.part.to_ascii_lowercase()) {
|
||||
fresh(taken, |n| format!("xl/drawings/drawing{n}.xml"))
|
||||
} else {
|
||||
d.part.clone()
|
||||
};
|
||||
let target = if d.part.is_empty() { fresh(taken, |n| format!("xl/drawings/drawing{n}.xml")) } else { d.part.clone() };
|
||||
taken.push(target.to_ascii_lowercase());
|
||||
let id = match ws.part.rels.list.iter().find(|r| r.kind() == "drawing" && r.target.eq_ignore_ascii_case(&target)) {
|
||||
Some(r) => r.id.clone(),
|
||||
@@ -457,6 +476,26 @@ fn write_sheet(out: &mut PackageOut, name: &str, ws: &Worksheet, taken: &mut Vec
|
||||
out.binary(&vml, CT_VML, notes_vml(&c.comments, &ws.notes, block));
|
||||
}
|
||||
}
|
||||
let mut table_ids = Vec::new();
|
||||
for t in &ws.tables {
|
||||
let target = if t.part.is_empty() { fresh(taken, |n| format!("xl/tables/table{n}.xml")) } else { t.part.clone() };
|
||||
taken.push(target.to_ascii_lowercase());
|
||||
let id = match ws.part.rels.list.iter().find(|r| r.kind() == "table" && r.target.eq_ignore_ascii_case(&target)) {
|
||||
Some(r) => r.id.clone(),
|
||||
None => {
|
||||
let id = fresh_id(&rels);
|
||||
rels.items.push(RelOut { id: id.clone(), ty: format!("{RT_BASE}table"), target: relative(name, &target), external: false });
|
||||
id
|
||||
}
|
||||
};
|
||||
table_ids.push(id);
|
||||
out.xml_part(&target, CT_TABLE, t.table.to_xml());
|
||||
}
|
||||
// Tables kept as parts (unreadable ones) keep their place after those written.
|
||||
let kept_tables =
|
||||
part.features.table_parts.iter().filter(|id| !table_ids.contains(id) && rels.items.iter().any(|r| r.id == **id && r.ty.ends_with("/table")));
|
||||
table_ids.extend(kept_tables.cloned().collect::<Vec<_>>());
|
||||
part.features.table_parts = table_ids;
|
||||
out.xml_part(name, CT_WORKSHEET, part.to_xml(&ws.rows));
|
||||
out.rels(name, &rels);
|
||||
}
|
||||
|
||||
@@ -651,6 +651,8 @@ pub struct Features {
|
||||
pub legacy_drawing: Option<String>,
|
||||
/// `drawing/@r:id`: the drawing part (shapes, pictures, charts).
|
||||
pub drawing: Option<String>,
|
||||
/// `tableParts/tablePart/@r:id`: the sheet's table parts, in order.
|
||||
pub table_parts: Vec<String>,
|
||||
}
|
||||
|
||||
impl Features {
|
||||
@@ -677,6 +679,10 @@ impl Features {
|
||||
.unwrap_or_default(),
|
||||
legacy_drawing: root.child("legacyDrawing").and_then(|l| l.attr_or_plain("r:id")).map(str::to_string),
|
||||
drawing: root.child("drawing").and_then(|l| l.attr_or_plain("r:id")).map(str::to_string),
|
||||
table_parts: root
|
||||
.child("tableParts")
|
||||
.map(|t| t.children("tablePart").filter_map(|p| p.attr_or_plain("r:id")).map(str::to_string).collect())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,6 +719,14 @@ impl Features {
|
||||
("colBreaks", Vec::from_iter(breaks_el("colBreaks", &p.col_breaks))),
|
||||
("legacyDrawing", Vec::from_iter(self.legacy_drawing.as_ref().map(|id| El::new("legacyDrawing").with_attr("r:id", id)))),
|
||||
("drawing", Vec::from_iter(self.drawing.as_ref().map(|id| El::new("drawing").with_attr("r:id", id)))),
|
||||
(
|
||||
"tableParts",
|
||||
Vec::from_iter((!self.table_parts.is_empty()).then(|| {
|
||||
let mut e = El::new("tableParts").with_attr("count", self.table_parts.len());
|
||||
self.table_parts.iter().for_each(|id| e.push(El::new("tablePart").with_attr("r:id", id)));
|
||||
e
|
||||
})),
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -921,3 +935,153 @@ mod tests {
|
||||
assert_eq!(parse_sqref(" A1 B2:C3 bad 4:4").len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------- tables
|
||||
|
||||
element! {
|
||||
/// `<tableColumn>`.
|
||||
pub struct TableColumn {
|
||||
id: u32 = "id",
|
||||
unique_name: String = "uniqueName",
|
||||
name: String = "name",
|
||||
/// `none`, `sum`, `min`, `max`, `average`, `count`, `countNums`, `stdDev`, `var`, `custom`.
|
||||
totals_row_function: String = "totalsRowFunction",
|
||||
totals_row_label: String = "totalsRowLabel",
|
||||
query_table_field_id: u32 = "queryTableFieldId",
|
||||
header_row_dxf_id: u32 = "headerRowDxfId",
|
||||
data_dxf_id: u32 = "dataDxfId",
|
||||
totals_row_dxf_id: u32 = "totalsRowDxfId",
|
||||
header_row_cell_style: String = "headerRowCellStyle",
|
||||
data_cell_style: String = "dataCellStyle",
|
||||
totals_row_cell_style: String = "totalsRowCellStyle",
|
||||
}
|
||||
+ {
|
||||
/// `calculatedColumnFormula`, as written, and whether it is an array formula.
|
||||
calculated_formula: Option<(String, bool)>,
|
||||
/// `totalsRowFormula` (for `custom`), and whether it is an array formula.
|
||||
totals_row_formula: Option<(String, bool)>,
|
||||
/// `xmlColumnPr` and `extLst`, as written.
|
||||
other: Vec<El>,
|
||||
}
|
||||
}
|
||||
|
||||
element! {
|
||||
/// `<tableStyleInfo>`.
|
||||
pub struct TableStyleInfo {
|
||||
name: String = "name",
|
||||
show_first_column: bool = "showFirstColumn",
|
||||
show_last_column: bool = "showLastColumn",
|
||||
show_row_stripes: bool = "showRowStripes",
|
||||
show_column_stripes: bool = "showColumnStripes",
|
||||
}
|
||||
}
|
||||
|
||||
element! {
|
||||
/// A table (`<table>`, 18.5): a named range with a header row, columns and a style.
|
||||
pub struct Table {
|
||||
id: u32 = "id",
|
||||
name: String = "name",
|
||||
display_name: String = "displayName",
|
||||
comment: String = "comment",
|
||||
/// `worksheet`, `xml`, `queryTable`.
|
||||
table_type: String = "tableType",
|
||||
header_row_count: u32 = "headerRowCount",
|
||||
insert_row: bool = "insertRow",
|
||||
insert_row_shift: bool = "insertRowShift",
|
||||
totals_row_count: u32 = "totalsRowCount",
|
||||
totals_row_shown: bool = "totalsRowShown",
|
||||
published: bool = "published",
|
||||
header_row_dxf_id: u32 = "headerRowDxfId",
|
||||
data_dxf_id: u32 = "dataDxfId",
|
||||
totals_row_dxf_id: u32 = "totalsRowDxfId",
|
||||
header_row_border_dxf_id: u32 = "headerRowBorderDxfId",
|
||||
table_border_dxf_id: u32 = "tableBorderDxfId",
|
||||
totals_row_border_dxf_id: u32 = "totalsRowBorderDxfId",
|
||||
header_row_cell_style: String = "headerRowCellStyle",
|
||||
data_cell_style: String = "dataCellStyle",
|
||||
totals_row_cell_style: String = "totalsRowCellStyle",
|
||||
connection_id: u32 = "connectionId",
|
||||
}
|
||||
+ {
|
||||
range: Range,
|
||||
/// `autoFilter` and `sortState`, as written (they may hold their own extensions).
|
||||
auto_filter: Option<El>,
|
||||
sort_state: Option<El>,
|
||||
columns: Vec<TableColumn>,
|
||||
style: Option<TableStyleInfo>,
|
||||
ext: Option<El>,
|
||||
}
|
||||
}
|
||||
|
||||
impl Table {
|
||||
/// A table part read with [`crate::ns::CANONICAL`]. `None` without a readable `ref`.
|
||||
pub fn read(t: &El) -> Option<Table> {
|
||||
let formula = |c: &El, n: &str| c.child(n).map(|f| (f.text().into_owned(), f.flag("array", false)));
|
||||
let columns = t
|
||||
.child("tableColumns")
|
||||
.map(|cs| {
|
||||
cs.children("tableColumn")
|
||||
.map(|c| TableColumn {
|
||||
calculated_formula: formula(c, "calculatedColumnFormula"),
|
||||
totals_row_formula: formula(c, "totalsRowFormula"),
|
||||
other: kids(c, &["calculatedColumnFormula", "totalsRowFormula"]),
|
||||
..TableColumn::read_attrs(c)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Some(Table {
|
||||
range: Range::parse(t.attr("ref")?)?,
|
||||
auto_filter: t.child("autoFilter").cloned(),
|
||||
sort_state: t.child("sortState").cloned(),
|
||||
columns,
|
||||
style: t.child("tableStyleInfo").map(TableStyleInfo::read_attrs),
|
||||
ext: t.child("extLst").cloned(),
|
||||
..Table::read_attrs(t)
|
||||
})
|
||||
}
|
||||
|
||||
/// The table part.
|
||||
pub fn to_xml(&self) -> Vec<u8> {
|
||||
let mut e = El::new("table");
|
||||
self.write_attrs(&mut e);
|
||||
e.attrs.insert(0, ("ref".into(), self.range.a1()));
|
||||
// `id`, `name` and `displayName` lead, as Excel writes them.
|
||||
e.attrs.sort_by_key(|(k, _)| match k.as_str() {
|
||||
"id" => 0,
|
||||
"name" => 1,
|
||||
"displayName" => 2,
|
||||
"ref" => 3,
|
||||
_ => 4,
|
||||
});
|
||||
for x in [&self.auto_filter, &self.sort_state].into_iter().flatten() {
|
||||
e.push(x.clone());
|
||||
}
|
||||
let mut cols = El::new("tableColumns").with_attr("count", self.columns.len());
|
||||
for c in &self.columns {
|
||||
let mut ce = El::new("tableColumn");
|
||||
c.write_attrs(&mut ce);
|
||||
for (n, f) in [("calculatedColumnFormula", &c.calculated_formula), ("totalsRowFormula", &c.totals_row_formula)] {
|
||||
if let Some((text, array)) = f {
|
||||
let fe = El::new(n).with_text(text.as_str());
|
||||
ce.push(if *array { fe.with_attr("array", 1) } else { fe });
|
||||
}
|
||||
}
|
||||
ce.children.extend(c.other.iter().cloned().map(Node::El));
|
||||
cols.push(ce);
|
||||
}
|
||||
e.push(cols);
|
||||
if let Some(s) = &self.style {
|
||||
let mut se = El::new("tableStyleInfo");
|
||||
s.write_attrs(&mut se);
|
||||
e.push(se);
|
||||
}
|
||||
if let Some(x) = &self.ext {
|
||||
e.push(x.clone());
|
||||
}
|
||||
crate::ns::declare(&mut e);
|
||||
let mut w = craft_xml::Writer::document();
|
||||
w.tree(&e);
|
||||
w.into_bytes()
|
||||
}
|
||||
}
|
||||
@@ -68,11 +68,12 @@ pub use craft_sheet::{
|
||||
BorderStyle, CellError, CellRef, DEFAULT_PALETTE, HAlign, MAX_COLS, MAX_ROWS, Pattern, Range, Script, Underline, VAlign, Value, Visibility,
|
||||
builtin_format, builtin_format_id, quote_sheet, sheet_needs_quotes,
|
||||
};
|
||||
pub use document::{Document, DrawingPart, KeptPart, Worksheet, XlsxKind};
|
||||
pub use document::{Document, DrawingPart, KeptPart, TablePart, Worksheet, XlsxKind};
|
||||
pub use drawing::{Anchor, Drawing, Item, Marker, ObjectKind, Placement};
|
||||
pub use features::{
|
||||
Break, CfRule, Cfvo, ColorScale, Comment, Comments, ConditionalFormatting, DataBar, DataValidation, DataValidations, Features, HeaderFooter,
|
||||
Hyperlink, IconSet, NoteShape, PageMargins, PageSetup, Pane, Print, PrintOptions, Selection, SheetView, notes_vml, parse_sqref, range_ref,
|
||||
Hyperlink, IconSet, NoteShape, PageMargins, PageSetup, Pane, Print, PrintOptions, Selection, SheetView, Table, TableColumn, TableStyleInfo,
|
||||
notes_vml, parse_sqref, range_ref,
|
||||
};
|
||||
pub use package::{Package, is_xlsb};
|
||||
pub use sheet::{Cell, CellFormula, CellValue, Column, FormulaKind, Row, SheetPart, iso_date_serial};
|
||||
|
||||
@@ -7,7 +7,7 @@ use craft_drawingml::chart::Chart;
|
||||
use craft_drawingml::theme::Theme;
|
||||
|
||||
use crate::drawing::Drawing;
|
||||
use crate::features::{Comments, NoteShape};
|
||||
use crate::features::{Comments, NoteShape, Table};
|
||||
use crate::sheet::{Row, RowReader, SheetPart};
|
||||
use crate::strings::StringItem;
|
||||
use crate::styles::Styles;
|
||||
@@ -284,6 +284,22 @@ impl<'a> Package<'a> {
|
||||
Ok(Some((part, Drawing::read(&root), rels)))
|
||||
}
|
||||
|
||||
/// The tables of a worksheet read by [`Package::sheet`], in the order of its `tableParts`,
|
||||
/// each with its part name. A table part that does not read is skipped with a warning.
|
||||
pub fn tables(&mut self, sheet: &SheetPart) -> Result<Vec<(String, Table)>, Error> {
|
||||
let mut out = Vec::new();
|
||||
for id in &sheet.features.table_parts {
|
||||
let Some(rel) = sheet.rels.by_id(id).filter(|r| !r.external) else { continue };
|
||||
let part = rel.target.clone();
|
||||
match self.optional_xml(&part)?.map(|t| Table::read(&t)) {
|
||||
Some(Some(t)) => out.push((part, t)),
|
||||
Some(None) => self.warn(format!("skipped {part}: the table has no range")),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// A chart part (`c:chartSpace`, or `cx:chartSpace` of an Office 2016 chart) through
|
||||
/// `craft-drawingml`. `None` when the part is missing or holds no chart it can read.
|
||||
pub fn chart(&mut self, part: &str) -> Result<Option<Chart>, Error> {
|
||||
|
||||
@@ -31,11 +31,11 @@ fn excel_like() -> Vec<u8> {
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<worksheet {ns} xr:uid="{{00000000-0001-0000-0000-000000000000}}"><sheetPr><tabColor rgb="FFFF0000"/></sheetPr><dimension ref="A1:C3"/><sheetViews><sheetView tabSelected="1" workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews><sheetFormatPr defaultRowHeight="15" x14ac:dyDescent="0.25"/><cols><col min="1" max="2" width="12.5" customWidth="1"/><col min="3" max="3" width="9" hidden="1" style="1"/></cols>
|
||||
<sheetData><row r="1" spans="1:3" x14ac:dyDescent="0.25"><c r="A1" t="s"><v>0</v></c><c r="B1" t="s"><v>1</v></c><c r="C1" s="1"><v>0.1</v></c></row><row r="2" ht="30" customHeight="1"><c r="A2"><f>SUM(C1:C3)</f><v>0.30000000000000004</v></c><c r="B2" t="b"><v>1</v></c><c r="C2" s="1"><f t="shared" ref="C2:C3" si="0">C1*2</f><v>0.2</v></c></row><row r="3"><c r="A3" t="e"><v>#N/A</v></c><c r="B3" t="inlineStr"><is><t xml:space="preserve"> inline </t></is></c><c r="C3" s="1"><f t="shared" si="0"/><v>0.4</v></c></row></sheetData>
|
||||
<mergeCells count="1"><mergeCell ref="A5:B6"/></mergeCells><conditionalFormatting sqref="C1:C3"><cfRule type="cellIs" dxfId="0" priority="1" operator="greaterThan"><formula>0.25</formula></cfRule></conditionalFormatting><dataValidations count="1"><dataValidation type="list" allowBlank="1" showInputMessage="1" sqref="A1:A3 C:C" xr:uid="{{00000000-0002-0000-0000-000000000000}}"><formula1>"a,b"</formula1></dataValidation></dataValidations><hyperlinks><hyperlink ref="A1" r:id="rId2" tooltip="t"/><hyperlink ref="B1" location="Data!A1" display="here"/></hyperlinks><printOptions gridLines="1"/><pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3"/><pageSetup orientation="landscape" fitToWidth="1"/><headerFooter differentFirst="1"><oddHeader>&C&P</oddHeader><firstFooter xml:space="preserve"> x </firstFooter></headerFooter><rowBreaks count="1" manualBreakCount="1"><brk id="10" max="16383" man="1"/></rowBreaks><drawing r:id="rId1"/><legacyDrawing r:id="rId3"/>
|
||||
<mergeCells count="1"><mergeCell ref="A5:B6"/></mergeCells><conditionalFormatting sqref="C1:C3"><cfRule type="cellIs" dxfId="0" priority="1" operator="greaterThan"><formula>0.25</formula></cfRule></conditionalFormatting><dataValidations count="1"><dataValidation type="list" allowBlank="1" showInputMessage="1" sqref="A1:A3 C:C" xr:uid="{{00000000-0002-0000-0000-000000000000}}"><formula1>"a,b"</formula1></dataValidation></dataValidations><hyperlinks><hyperlink ref="A1" r:id="rId2" tooltip="t"/><hyperlink ref="B1" location="Data!A1" display="here"/></hyperlinks><printOptions gridLines="1"/><pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3"/><pageSetup orientation="landscape" fitToWidth="1"/><headerFooter differentFirst="1"><oddHeader>&C&P</oddHeader><firstFooter xml:space="preserve"> x </firstFooter></headerFooter><rowBreaks count="1" manualBreakCount="1"><brk id="10" max="16383" man="1"/></rowBreaks><drawing r:id="rId1"/><legacyDrawing r:id="rId3"/><tableParts count="1"><tablePart r:id="rId5"/></tableParts>
|
||||
<extLst><ext uri="{{05C60535-1F16-4fd2-B633-F4F36F0B64E0}}" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"><x14:sparklineGroups xmlns:xm="http://schemas.microsoft.com/office/excel/2006/main"><x14:sparklineGroup><x14:sparklines><x14:sparkline><xm:f>Data!C1:C3</xm:f><xm:sqref>D1</xm:sqref></x14:sparkline></x14:sparklines></x14:sparklineGroup></x14:sparklineGroups></ext></extLst></worksheet>"#
|
||||
);
|
||||
let sheet1_rels = format!(
|
||||
r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="{RT}/drawing" Target="../drawings/drawing1.xml"/><Relationship Id="rId2" Type="{RT}/hyperlink" Target="https://example.com/a?b&c" TargetMode="External"/><Relationship Id="rId3" Type="{RT}/vmlDrawing" Target="../drawings/vmlDrawing1.vml"/><Relationship Id="rId4" Type="{RT}/comments" Target="../comments1.xml"/></Relationships>"#
|
||||
r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="{RT}/drawing" Target="../drawings/drawing1.xml"/><Relationship Id="rId2" Type="{RT}/hyperlink" Target="https://example.com/a?b&c" TargetMode="External"/><Relationship Id="rId3" Type="{RT}/vmlDrawing" Target="../drawings/vmlDrawing1.vml"/><Relationship Id="rId4" Type="{RT}/comments" Target="../comments1.xml"/><Relationship Id="rId5" Type="{RT}/table" Target="../tables/table1.xml"/></Relationships>"#
|
||||
);
|
||||
let styles = format!(
|
||||
r#"<styleSheet {ns}><numFmts count="1"><numFmt numFmtId="164" formatCode="0.0%"/></numFmts><fonts count="2" x14ac:knownFonts="1"><font><sz val="11"/><color theme="1"/><name val="Calibri"/><family val="2"/><scheme val="minor"/></font><font><b/><i val="0"/><u val="double"/><vertAlign val="superscript"/><sz val="9.5"/><color rgb="FF0000FF" tint="-0.25"/><name val="Arial"/></font></fonts>
|
||||
@@ -51,7 +51,7 @@ fn excel_like() -> Vec<u8> {
|
||||
let package_rels = format!(
|
||||
r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="{RT}/officeDocument" Target="xl/workbook.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/></Relationships>"#
|
||||
);
|
||||
let types = r#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Default Extension="bin" ContentType="application/vnd.ms-office.vbaProject"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.ms-excel.sheet.macroEnabled.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/worksheets/sheet2.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/chartsheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"/><Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/drawings/drawing1.xml" ContentType="application/vnd.openxmlformats-officedocument.drawing+xml"/><Override PartName="/xl/charts/chart1.xml" ContentType="application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/><Default Extension="png" ContentType="image/png"/><Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/><Override PartName="/xl/comments1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>"#;
|
||||
let types = r#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Default Extension="bin" ContentType="application/vnd.ms-office.vbaProject"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.ms-excel.sheet.macroEnabled.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/worksheets/sheet2.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/chartsheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"/><Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/drawings/drawing1.xml" ContentType="application/vnd.openxmlformats-officedocument.drawing+xml"/><Override PartName="/xl/charts/chart1.xml" ContentType="application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/><Default Extension="png" ContentType="image/png"/><Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/><Override PartName="/xl/tables/table1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"/><Override PartName="/xl/comments1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>"#;
|
||||
zip(&[
|
||||
("[Content_Types].xml", types.as_bytes()),
|
||||
("_rels/.rels", package_rels.as_bytes()),
|
||||
@@ -71,6 +71,7 @@ fn excel_like() -> Vec<u8> {
|
||||
("xl/sharedStrings.xml", strings.as_bytes()),
|
||||
("xl/calcChain.xml", b"<calcChain/>"),
|
||||
("xl/comments1.xml", COMMENTS.as_bytes()),
|
||||
("xl/tables/table1.xml", TABLE.as_bytes()),
|
||||
("xl/drawings/vmlDrawing1.vml", VML.as_bytes()),
|
||||
("xl/vbaProject.bin", &[0xD0, 0xCF, 0x11, 0xE0, 1, 2, 3]),
|
||||
])
|
||||
@@ -83,6 +84,8 @@ const DRAWING: &str = r#"<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/
|
||||
const DRAWING_RELS: &str = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" Target="../charts/chart1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image1.png"/></Relationships>"#;
|
||||
const CHART: &str = r#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><c:chart><c:plotArea><c:barChart><c:barDir val="col"/><c:grouping val="clustered"/><c:ser><c:idx val="0"/><c:order val="0"/><c:tx><c:v>S1</c:v></c:tx><c:cat><c:strRef><c:f>Data!$A$1:$A$2</c:f><c:strCache><c:ptCount val="2"/><c:pt idx="0"><c:v>a</c:v></c:pt><c:pt idx="1"><c:v>b</c:v></c:pt></c:strCache></c:strRef></c:cat><c:val><c:numRef><c:f>Data!$C$1:$C$2</c:f><c:numCache><c:ptCount val="2"/><c:pt idx="0"><c:v>1</c:v></c:pt><c:pt idx="1"><c:v>2</c:v></c:pt></c:numCache></c:numRef></c:val></c:ser></c:barChart></c:plotArea></c:chart></c:chartSpace>"#;
|
||||
|
||||
const TABLE: &str = r#"<table xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" id="1" xr:uid="{T}" name="Prices" displayName="Prices" ref="A1:C4" totalsRowCount="1"><autoFilter ref="A1:C3"/><tableColumns count="3"><tableColumn id="1" name="Name" totalsRowLabel="Total"/><tableColumn id="2" name="Flag"/><tableColumn id="3" name="Value" totalsRowFunction="sum" dataDxfId="0"><calculatedColumnFormula>Prices[[#This Row],[Flag]]*2</calculatedColumnFormula></tableColumn></tableColumns><tableStyleInfo name="TableStyleMedium2" showFirstColumn="0" showLastColumn="0" showRowStripes="1" showColumnStripes="0"/></table>"#;
|
||||
|
||||
fn read(bytes: &[u8]) -> Document {
|
||||
Document::read(bytes, &Limits::default()).unwrap()
|
||||
}
|
||||
@@ -130,6 +133,7 @@ fn assert_same(a: &Document, b: &Document) {
|
||||
assert_eq!(x.part.features, y.part.features);
|
||||
assert_eq!(x.notes, y.notes);
|
||||
assert_eq!(x.drawing, y.drawing);
|
||||
assert_eq!(x.tables, y.tables);
|
||||
assert_eq!(
|
||||
x.comments.as_ref().map(|c| (&c.authors, &c.comments, c.to_xml())),
|
||||
y.comments.as_ref().map(|c| (&c.authors, &c.comments, c.to_xml()))
|
||||
@@ -373,6 +377,58 @@ fn charts_and_pictures() {
|
||||
assert!(sheet.rels.first("drawing").is_none() && sheet.features.drawing.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tables() {
|
||||
let mut doc = read(&excel_like());
|
||||
let ws = doc.worksheets[0].clone().unwrap();
|
||||
assert_eq!(ws.part.features.table_parts, ["rId5"]);
|
||||
let t = &ws.tables[0];
|
||||
assert_eq!(
|
||||
(t.part.as_str(), t.table.name.as_deref(), t.table.range.a1(), t.table.totals_row_count),
|
||||
("xl/tables/table1.xml", Some("Prices"), "A1:C4".to_string(), Some(1))
|
||||
);
|
||||
assert_eq!(t.table.attrs, [("xr:uid".to_string(), "{T}".to_string())]);
|
||||
let value = &t.table.columns[2];
|
||||
assert_eq!(
|
||||
(value.totals_row_function.as_deref(), value.data_dxf_id, value.calculated_formula.clone()),
|
||||
(Some("sum"), Some(0), Some(("Prices[[#This Row],[Flag]]*2".to_string(), false)))
|
||||
);
|
||||
assert_eq!(t.table.style.as_ref().and_then(|s| s.show_row_stripes), Some(true));
|
||||
assert!(t.table.auto_filter.is_some());
|
||||
assert!(!doc.parts.iter().any(|p| p.name.contains("tables/")));
|
||||
|
||||
// A new table on the third sheet takes a name no other table has.
|
||||
let mut new = t.table.clone();
|
||||
new.id = Some(2);
|
||||
new.name = Some("Other".into());
|
||||
new.display_name = Some("Other".into());
|
||||
doc.worksheets[2].as_mut().unwrap().tables.push(craft_xlsx::TablePart { part: String::new(), table: new });
|
||||
let again = read(&doc.write().unwrap());
|
||||
assert_same(&doc_after(&doc), &again);
|
||||
let t2 = again.worksheets[2].clone().unwrap().tables.remove(0);
|
||||
assert_eq!((t2.part.as_str(), t2.table.name.as_deref()), ("xl/tables/table2.xml", Some("Other")));
|
||||
assert_eq!(again.worksheets[0].as_ref().unwrap().tables[0].table, t.table);
|
||||
|
||||
// A table removed takes its relationship and its tableParts entry with it.
|
||||
let mut doc = again;
|
||||
doc.worksheets[0].as_mut().unwrap().tables.clear();
|
||||
let bytes = doc.write().unwrap();
|
||||
let mut p = Package::open(&bytes, &Limits::default()).unwrap();
|
||||
let wb = p.workbook().unwrap();
|
||||
let sheet = p.sheet(&wb.sheets[0], |_| Ok(())).unwrap();
|
||||
assert!(sheet.rels.first("table").is_none() && sheet.features.table_parts.is_empty() && sheet.root.child("tableParts").is_none());
|
||||
assert!(p.opc().read("xl/tables/table1.xml").unwrap().is_none());
|
||||
|
||||
// A new part on an earlier sheet never takes the name a later sheet's part keeps.
|
||||
let mut doc = read(&excel_like());
|
||||
let kept = doc.worksheets[0].as_mut().unwrap().tables.remove(0);
|
||||
doc.worksheets[2].as_mut().unwrap().tables.push(kept);
|
||||
doc.worksheets[0].as_mut().unwrap().tables.push(craft_xlsx::TablePart { part: String::new(), table: t2.table.clone() });
|
||||
let again = read(&doc.write().unwrap());
|
||||
let names = |i: usize| again.worksheets[i].as_ref().unwrap().tables.iter().map(|t| t.part.clone()).collect::<Vec<_>>();
|
||||
assert_eq!((names(0), names(2)), (vec!["xl/tables/table2.xml".to_string()], vec!["xl/tables/table1.xml".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_workbook() {
|
||||
let cell = |at: &str, value| Cell { at: CellRef::parse(at).unwrap(), value, ..Cell::default() };
|
||||
|
||||
Reference in new issue
Block a user