1use std::collections::HashMap;
4use std::io::{Seek, Write};
5
6use opc::{Package, Part, Relationship, Relationships, TargetMode};
7use xml_core::{BytesDecl, BytesEnd, BytesStart, BytesText, Event, Writer};
8
9use crate::error::Result;
10use crate::model::{
11 AutoFilterColumn, Border, BorderEdge, BorderStyle, CalculationMode, CalculationProperties,
12 Cell, CellAnchorPoint, CellComment, CellFormat, CellHyperlink, CellValue, CfvoPosition,
13 ColumnSetting, ComparisonOperator, ConditionalFormattingCondition, ConditionalFormattingRule,
14 CustomPropertyValue, DataValidationKind, DataValidationRule, DefinedName, DocumentProperties,
15 DrawingObject, ExcelDateTime, ExcelTable, ExternalWorkbookLink, GradientFill,
16 HorizontalAlignment, HyperlinkTarget, IconSetType, IgnoredError, NamedCellStyle,
17 PageOrientation, PatternFill, PatternType, PrintSettings, ProtectedRange, RichTextRun,
18 Scenario, Sheet, SheetChart, SheetDrawing, SheetPicture, SheetProtection, SheetVisibility,
19 SortState, TableColumn, TableStyle, TableStyleElementType, TextComparisonOperator,
20 TotalsRowFunction, VerticalAlignment, Workbook, WorkbookProtection, WriteProtection,
21};
22
23const SPREADSHEETML_NAMESPACE: &str = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
24const RELATIONSHIPS_NAMESPACE: &str =
25 "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
26
27const MAIN_WORKBOOK_CONTENT_TYPE: &str =
28 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml";
29const MAIN_WORKBOOK_RELATIONSHIP_TYPE: &str =
30 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
31const MAIN_WORKBOOK_PART_NAME: &str = "/xl/workbook.xml";
32const MAIN_WORKBOOK_ENTRY_NAME: &str = "xl/workbook.xml";
33
34const WORKSHEET_CONTENT_TYPE: &str =
35 "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml";
36const WORKSHEET_RELATIONSHIP_TYPE: &str =
37 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet";
38
39const STYLES_CONTENT_TYPE: &str =
48 "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml";
49const STYLES_RELATIONSHIP_TYPE: &str =
50 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
51const STYLES_PART_NAME: &str = "/xl/styles.xml";
52const STYLES_ENTRY_NAME: &str = "styles.xml";
53
54const SHARED_STRINGS_CONTENT_TYPE: &str =
55 "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml";
56const SHARED_STRINGS_RELATIONSHIP_TYPE: &str =
57 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings";
58const SHARED_STRINGS_PART_NAME: &str = "/xl/sharedStrings.xml";
59const SHARED_STRINGS_ENTRY_NAME: &str = "sharedStrings.xml";
60
61const CORE_PROPERTIES_CONTENT_TYPE: &str =
66 "application/vnd.openxmlformats-package.core-properties+xml";
67const CORE_PROPERTIES_RELATIONSHIP_TYPE: &str =
68 "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
69const CORE_PROPERTIES_PART_NAME: &str = "/docProps/core.xml";
70const CORE_PROPERTIES_ENTRY_NAME: &str = "docProps/core.xml";
71const CORE_PROPERTIES_NAMESPACE: &str =
72 "http://schemas.openxmlformats.org/package/2006/metadata/core-properties";
73const DC_NAMESPACE: &str = "http://purl.org/dc/elements/1.1/";
74const DCTERMS_NAMESPACE: &str = "http://purl.org/dc/terms/";
75const XSI_NAMESPACE: &str = "http://www.w3.org/2001/XMLSchema-instance";
76
77const EXTENDED_PROPERTIES_CONTENT_TYPE: &str =
78 "application/vnd.openxmlformats-officedocument.extended-properties+xml";
79const EXTENDED_PROPERTIES_RELATIONSHIP_TYPE: &str =
80 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties";
81const EXTENDED_PROPERTIES_PART_NAME: &str = "/docProps/app.xml";
82const EXTENDED_PROPERTIES_ENTRY_NAME: &str = "docProps/app.xml";
83const EXTENDED_PROPERTIES_NAMESPACE: &str =
84 "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties";
85
86const CUSTOM_PROPERTIES_CONTENT_TYPE: &str =
90 "application/vnd.openxmlformats-officedocument.custom-properties+xml";
91const CUSTOM_PROPERTIES_RELATIONSHIP_TYPE: &str =
92 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
93const CUSTOM_PROPERTIES_PART_NAME: &str = "/docProps/custom.xml";
94const CUSTOM_PROPERTIES_ENTRY_NAME: &str = "docProps/custom.xml";
95const CUSTOM_PROPERTIES_NAMESPACE: &str =
96 "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties";
97const CUSTOM_PROPERTIES_VT_NAMESPACE: &str =
98 "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes";
99const CUSTOM_PROPERTY_FMTID: &str = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";
103const FIRST_CUSTOM_PROPERTY_PID: i32 = 2;
106
107const HYPERLINK_RELATIONSHIP_TYPE: &str =
110 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
111
112const TABLE_CONTENT_TYPE: &str =
113 "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml";
114const TABLE_RELATIONSHIP_TYPE: &str =
115 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table";
116
117const COMMENTS_CONTENT_TYPE: &str =
118 "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml";
119const COMMENTS_RELATIONSHIP_TYPE: &str =
120 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
121
122const DRAWING_CONTENT_TYPE: &str = "application/vnd.openxmlformats-officedocument.drawing+xml";
124const DRAWING_RELATIONSHIP_TYPE: &str =
125 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing";
126const IMAGE_RELATIONSHIP_TYPE: &str =
127 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
128const CHART_CONTENT_TYPE: &str =
129 "application/vnd.openxmlformats-officedocument.drawingml.chart+xml";
130const CHART_RELATIONSHIP_TYPE: &str =
131 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
132const DRAWINGML_NAMESPACE: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
133const SPREADSHEET_DRAWING_NAMESPACE: &str =
134 "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing";
135const CHART_NAMESPACE: &str = "http://schemas.openxmlformats.org/drawingml/2006/chart";
136
137const VML_DRAWING_CONTENT_TYPE: &str = "application/vnd.openxmlformats-officedocument.vmlDrawing";
140const VML_DRAWING_RELATIONSHIP_TYPE: &str =
141 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing";
142
143const THEME_CONTENT_TYPE: &str = "application/vnd.openxmlformats-officedocument.theme+xml";
144const THEME_RELATIONSHIP_TYPE: &str =
145 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
146const THEME_PART_NAME: &str = "/xl/theme/theme1.xml";
147const THEME_ENTRY_NAME: &str = "theme/theme1.xml";
148
149const MACRO_ENABLED_WORKBOOK_CONTENT_TYPE: &str =
153 "application/vnd.ms-excel.sheet.macroEnabled.main+xml";
154const VBA_PROJECT_CONTENT_TYPE: &str = "application/vnd.ms-office.vbaProject";
155const VBA_PROJECT_RELATIONSHIP_TYPE: &str =
156 "http://schemas.microsoft.com/office/2006/relationships/vbaProject";
157const VBA_PROJECT_PART_NAME: &str = "/xl/vbaProject.bin";
158const VBA_PROJECT_ENTRY_NAME: &str = "vbaProject.bin";
159
160const EXTERNAL_LINK_CONTENT_TYPE: &str =
161 "application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml";
162const EXTERNAL_LINK_RELATIONSHIP_TYPE: &str =
163 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink";
164const EXTERNAL_LINK_TARGET_RELATIONSHIP_TYPE: &str =
165 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLinkPath";
166
167const FIRST_CUSTOM_NUM_FMT_ID: u32 = 164;
172
173impl Workbook {
174 pub fn write_to<W: Write + Seek>(&self, writer: W) -> Result<W> {
185 let mut package = Package::new();
186
187 let workbook_content_type = if self.vba_project.is_some() {
188 MACRO_ENABLED_WORKBOOK_CONTENT_TYPE
189 } else {
190 MAIN_WORKBOOK_CONTENT_TYPE
191 };
192
193 package.add_relationship(Relationship {
194 id: "rId1".to_string(),
195 rel_type: MAIN_WORKBOOK_RELATIONSHIP_TYPE.to_string(),
196 target: MAIN_WORKBOOK_ENTRY_NAME.to_string(),
197 target_mode: TargetMode::Internal,
198 });
199 package.add_relationship(Relationship {
200 id: "rId2".to_string(),
201 rel_type: CORE_PROPERTIES_RELATIONSHIP_TYPE.to_string(),
202 target: CORE_PROPERTIES_ENTRY_NAME.to_string(),
203 target_mode: TargetMode::Internal,
204 });
205 package.add_relationship(Relationship {
206 id: "rId3".to_string(),
207 rel_type: EXTENDED_PROPERTIES_RELATIONSHIP_TYPE.to_string(),
208 target: EXTENDED_PROPERTIES_ENTRY_NAME.to_string(),
209 target_mode: TargetMode::Internal,
210 });
211 if !self.properties.custom_properties.is_empty() {
216 package.add_relationship(Relationship {
217 id: "rId4".to_string(),
218 rel_type: CUSTOM_PROPERTIES_RELATIONSHIP_TYPE.to_string(),
219 target: CUSTOM_PROPERTIES_ENTRY_NAME.to_string(),
220 target_mode: TargetMode::Internal,
221 });
222 }
223
224 let shared_strings = collect_shared_strings(self);
225 let cell_formats = collect_cell_formats(self);
226 let dxfs = collect_dxfs(self);
227
228 let mut workbook_relationships = Relationships::new();
233 let sheet_relationship_ids: Vec<String> = (0..self.sheets.len())
234 .map(|index| format!("rId{}", index + 1))
235 .collect();
236 for (index, relationship_id) in sheet_relationship_ids.iter().enumerate() {
237 workbook_relationships.add(Relationship {
238 id: relationship_id.clone(),
239 rel_type: WORKSHEET_RELATIONSHIP_TYPE.to_string(),
240 target: format!("worksheets/sheet{}.xml", index + 1),
241 target_mode: TargetMode::Internal,
242 });
243 }
244 let mut next_workbook_relationship_id = self.sheets.len() + 1;
245
246 let styles_relationship_id = format!("rId{next_workbook_relationship_id}");
247 next_workbook_relationship_id += 1;
248 workbook_relationships.add(Relationship {
249 id: styles_relationship_id,
250 rel_type: STYLES_RELATIONSHIP_TYPE.to_string(),
251 target: STYLES_ENTRY_NAME.to_string(),
252 target_mode: TargetMode::Internal,
253 });
254 let shared_strings_relationship_id = format!("rId{next_workbook_relationship_id}");
255 next_workbook_relationship_id += 1;
256 workbook_relationships.add(Relationship {
257 id: shared_strings_relationship_id,
258 rel_type: SHARED_STRINGS_RELATIONSHIP_TYPE.to_string(),
259 target: SHARED_STRINGS_ENTRY_NAME.to_string(),
260 target_mode: TargetMode::Internal,
261 });
262 let theme_relationship_id = format!("rId{next_workbook_relationship_id}");
263 next_workbook_relationship_id += 1;
264 workbook_relationships.add(Relationship {
265 id: theme_relationship_id,
266 rel_type: THEME_RELATIONSHIP_TYPE.to_string(),
267 target: THEME_ENTRY_NAME.to_string(),
268 target_mode: TargetMode::Internal,
269 });
270
271 let mut external_link_relationship_ids: Vec<String> = Vec::new();
272 for (index, _link) in self.external_links.iter().enumerate() {
273 let id = format!("rId{next_workbook_relationship_id}");
274 next_workbook_relationship_id += 1;
275 workbook_relationships.add(Relationship {
276 id: id.clone(),
277 rel_type: EXTERNAL_LINK_RELATIONSHIP_TYPE.to_string(),
278 target: format!("externalLinks/externalLink{}.xml", index + 1),
279 target_mode: TargetMode::Internal,
280 });
281 external_link_relationship_ids.push(id);
282 }
283
284 if self.vba_project.is_some() {
285 let id = format!("rId{next_workbook_relationship_id}");
286 next_workbook_relationship_id += 1;
287 workbook_relationships.add(Relationship {
288 id,
289 rel_type: VBA_PROJECT_RELATIONSHIP_TYPE.to_string(),
290 target: VBA_PROJECT_ENTRY_NAME.to_string(),
291 target_mode: TargetMode::Internal,
292 });
293 }
294 let _ = next_workbook_relationship_id; let mut all_defined_names = self.defined_names.clone();
301 all_defined_names.extend(synthesized_print_defined_names(self));
302
303 let workbook_xml = to_workbook_xml(
304 self,
305 &sheet_relationship_ids,
306 &all_defined_names,
307 &external_link_relationship_ids,
308 )?;
309 let mut workbook_part =
310 Part::new(MAIN_WORKBOOK_PART_NAME, workbook_content_type, workbook_xml);
311 workbook_part.relationships = workbook_relationships;
312 package.add_part(workbook_part);
313
314 package.add_part(Part::new(
315 THEME_PART_NAME,
316 THEME_CONTENT_TYPE,
317 to_theme_xml(),
318 ));
319
320 if let Some(vba_project) = &self.vba_project {
321 package.add_part(Part::new(
322 VBA_PROJECT_PART_NAME,
323 VBA_PROJECT_CONTENT_TYPE,
324 vba_project.clone(),
325 ));
326 }
327
328 for (index, link) in self.external_links.iter().enumerate() {
329 let mut link_relationships = Relationships::new();
330 link_relationships.add(Relationship {
331 id: "rId1".to_string(),
332 rel_type: EXTERNAL_LINK_TARGET_RELATIONSHIP_TYPE.to_string(),
333 target: link.target.clone(),
334 target_mode: TargetMode::External,
335 });
336 let mut link_part = Part::new(
337 format!("/xl/externalLinks/externalLink{}.xml", index + 1),
338 EXTERNAL_LINK_CONTENT_TYPE,
339 to_external_link_xml(link)?,
340 );
341 link_part.relationships = link_relationships;
342 package.add_part(link_part);
343 }
344
345 let mut next_media_index = 1usize;
350 let mut next_chart_index = 1usize;
351
352 for (index, sheet) in self.sheets.iter().enumerate() {
353 let mut sheet_relationships = Relationships::new();
357 let mut next_sheet_relationship_id = 1usize;
358
359 let hyperlink_ids: Vec<Option<String>> = sheet
360 .hyperlinks
361 .iter()
362 .map(|hyperlink| match &hyperlink.target {
363 HyperlinkTarget::External(url) => {
364 let id = format!("rId{next_sheet_relationship_id}");
365 next_sheet_relationship_id += 1;
366 sheet_relationships.add(Relationship {
367 id: id.clone(),
368 rel_type: HYPERLINK_RELATIONSHIP_TYPE.to_string(),
369 target: url.clone(),
370 target_mode: TargetMode::External,
371 });
372 Some(id)
373 }
374 HyperlinkTarget::Internal(_) => None,
375 })
376 .collect();
377
378 let table_id = if sheet.table.is_some() {
379 let id = format!("rId{next_sheet_relationship_id}");
380 next_sheet_relationship_id += 1;
381 sheet_relationships.add(Relationship {
382 id: id.clone(),
383 rel_type: TABLE_RELATIONSHIP_TYPE.to_string(),
384 target: format!("../tables/table{}.xml", index + 1),
385 target_mode: TargetMode::Internal,
386 });
387 Some(id)
388 } else {
389 None
390 };
391
392 let legacy_drawing_id = if !sheet.comments.is_empty() {
393 let id = format!("rId{next_sheet_relationship_id}");
394 next_sheet_relationship_id += 1;
395 sheet_relationships.add(Relationship {
396 id: id.clone(),
397 rel_type: VML_DRAWING_RELATIONSHIP_TYPE.to_string(),
398 target: format!("../drawings/vmlDrawing{}.vml", index + 1),
399 target_mode: TargetMode::Internal,
400 });
401 let comments_id = format!("rId{next_sheet_relationship_id}");
406 sheet_relationships.add(Relationship {
407 id: comments_id,
408 rel_type: COMMENTS_RELATIONSHIP_TYPE.to_string(),
409 target: format!("../comments{}.xml", index + 1),
410 target_mode: TargetMode::Internal,
411 });
412 Some(id)
413 } else {
414 None
415 };
416
417 let drawing_id = if sheet.drawing.is_some() {
424 let id = format!("rId{next_sheet_relationship_id}");
429 sheet_relationships.add(Relationship {
430 id: id.clone(),
431 rel_type: DRAWING_RELATIONSHIP_TYPE.to_string(),
432 target: format!("../drawings/drawing{}.xml", index + 1),
433 target_mode: TargetMode::Internal,
434 });
435 Some(id)
436 } else {
437 None
438 };
439
440 let relationship_ids = WorksheetRelationshipIds {
441 hyperlinks: hyperlink_ids,
442 table: table_id,
443 legacy_drawing: legacy_drawing_id,
444 drawing: drawing_id,
445 };
446
447 let worksheet_xml = to_worksheet_xml(
448 sheet,
449 &shared_strings,
450 &cell_formats,
451 &dxfs,
452 &relationship_ids,
453 )?;
454 let mut worksheet_part = Part::new(
455 format!("/xl/worksheets/sheet{}.xml", index + 1),
456 WORKSHEET_CONTENT_TYPE,
457 worksheet_xml,
458 );
459 worksheet_part.relationships = sheet_relationships;
460 package.add_part(worksheet_part);
461
462 if let Some(sheet_drawing) = &sheet.drawing {
463 let mut drawing_relationships = Relationships::new();
464 let drawing_xml = to_drawing_xml(
465 sheet_drawing,
466 &mut drawing_relationships,
467 &mut next_media_index,
468 &mut next_chart_index,
469 &mut package,
470 )?;
471 let mut drawing_part = Part::new(
472 format!("/xl/drawings/drawing{}.xml", index + 1),
473 DRAWING_CONTENT_TYPE,
474 drawing_xml,
475 );
476 drawing_part.relationships = drawing_relationships;
477 package.add_part(drawing_part);
478 }
479
480 if let Some(table) = &sheet.table {
481 package.add_part(Part::new(
482 format!("/xl/tables/table{}.xml", index + 1),
483 TABLE_CONTENT_TYPE,
484 to_table_xml(table, index + 1)?,
485 ));
486 }
487
488 if !sheet.comments.is_empty() {
489 package.add_part(Part::new(
490 format!("/xl/comments{}.xml", index + 1),
491 COMMENTS_CONTENT_TYPE,
492 to_comments_xml(&sheet.comments)?,
493 ));
494 package.add_part(Part::new(
495 format!("/xl/drawings/vmlDrawing{}.vml", index + 1),
496 VML_DRAWING_CONTENT_TYPE,
497 to_comments_vml(&sheet.comments),
498 ));
499 }
500 }
501
502 package.add_part(Part::new(
503 STYLES_PART_NAME,
504 STYLES_CONTENT_TYPE,
505 to_styles_xml(
506 &cell_formats,
507 &dxfs,
508 &self.table_styles,
509 &self.named_cell_styles,
510 )?,
511 ));
512 package.add_part(Part::new(
513 SHARED_STRINGS_PART_NAME,
514 SHARED_STRINGS_CONTENT_TYPE,
515 to_shared_strings_xml(&shared_strings)?,
516 ));
517 package.add_part(Part::new(
518 CORE_PROPERTIES_PART_NAME,
519 CORE_PROPERTIES_CONTENT_TYPE,
520 to_core_properties_xml(&self.properties)?,
521 ));
522 package.add_part(Part::new(
523 EXTENDED_PROPERTIES_PART_NAME,
524 EXTENDED_PROPERTIES_CONTENT_TYPE,
525 to_extended_properties_xml(&self.properties)?,
526 ));
527 if !self.properties.custom_properties.is_empty() {
528 package.add_part(Part::new(
529 CUSTOM_PROPERTIES_PART_NAME,
530 CUSTOM_PROPERTIES_CONTENT_TYPE,
531 to_custom_properties_xml(&self.properties.custom_properties)?,
532 ));
533 }
534
535 Ok(package.write_to(writer)?)
536 }
537}
538
539#[derive(Clone, PartialEq)]
545enum SharedStringEntry {
546 Plain(String),
547 Rich(Vec<RichTextRun>),
548}
549
550struct SharedStrings {
561 entries: Vec<SharedStringEntry>,
562 plain_indices: HashMap<String, usize>,
563 total_count: usize,
564}
565
566impl SharedStrings {
567 fn index_of_rich(&self, runs: &[RichTextRun]) -> Option<usize> {
568 self.entries.iter().position(
569 |entry| matches!(entry, SharedStringEntry::Rich(existing) if existing == runs),
570 )
571 }
572}
573
574fn collect_shared_strings(workbook: &Workbook) -> SharedStrings {
575 let mut entries = Vec::new();
576 let mut plain_indices = HashMap::new();
577 let mut total_count = 0usize;
578
579 for sheet in &workbook.sheets {
580 for row in &sheet.rows {
581 for cell in &row.cells {
582 match &cell.value {
583 CellValue::Text(text) => {
584 total_count += 1;
585 if !plain_indices.contains_key(text) {
586 plain_indices.insert(text.clone(), entries.len());
587 entries.push(SharedStringEntry::Plain(text.clone()));
588 }
589 }
590 CellValue::RichText(runs) => {
591 total_count += 1;
592 let already_present =
593 entries.iter().any(|entry| matches!(entry, SharedStringEntry::Rich(existing) if existing == runs));
594 if !already_present {
595 entries.push(SharedStringEntry::Rich(runs.clone()));
596 }
597 }
598 _ => {}
599 }
600 }
601 }
602 }
603
604 SharedStrings {
605 entries,
606 plain_indices,
607 total_count,
608 }
609}
610
611fn collect_cell_formats(workbook: &Workbook) -> Vec<CellFormat> {
618 let mut formats: Vec<CellFormat> = Vec::new();
619
620 for sheet in &workbook.sheets {
621 for row in &sheet.rows {
622 if let Some(format) = &row.default_format {
623 if !formats.contains(format) {
624 formats.push(format.clone());
625 }
626 }
627 for cell in &row.cells {
628 if let Some(format) = &cell.format {
629 if !formats.contains(format) {
630 formats.push(format.clone());
631 }
632 }
633 }
634 }
635 for setting in &sheet.column_settings {
638 if let Some(format) = &setting.style {
639 if !formats.contains(format) {
640 formats.push(format.clone());
641 }
642 }
643 }
644 }
645
646 formats
647}
648
649fn collect_dxfs(workbook: &Workbook) -> Vec<CellFormat> {
659 let mut dxfs: Vec<CellFormat> = Vec::new();
660
661 for sheet in &workbook.sheets {
662 for rule in &sheet.conditional_formatting_rules {
663 if !dxfs.contains(&rule.format) {
664 dxfs.push(rule.format.clone());
665 }
666 }
667 }
668
669 for table_style in &workbook.table_styles {
670 for element in &table_style.elements {
671 if !dxfs.contains(&element.format) {
672 dxfs.push(element.format.clone());
673 }
674 }
675 }
676
677 dxfs
678}
679
680fn to_workbook_xml(
685 workbook: &Workbook,
686 sheet_relationship_ids: &[String],
687 defined_names: &[DefinedName],
688 external_link_relationship_ids: &[String],
689) -> Result<Vec<u8>> {
690 let mut writer = Writer::new(Vec::new());
691
692 writer.write_event(Event::Decl(BytesDecl::new(
693 "1.0",
694 Some("UTF-8"),
695 Some("yes"),
696 )))?;
697
698 let mut root = BytesStart::new("workbook");
699 root.push_attribute(("xmlns", SPREADSHEETML_NAMESPACE));
700 root.push_attribute(("xmlns:r", RELATIONSHIPS_NAMESPACE));
701 writer.write_event(Event::Start(root))?;
702
703 if let Some(write_protection) = &workbook.write_protection {
708 write_file_sharing(&mut writer, write_protection)?;
709 }
710
711 if let Some(protection) = &workbook.protection {
712 write_workbook_protection(&mut writer, protection)?;
713 }
714
715 if workbook.active_tab.is_some() {
716 writer.write_event(Event::Start(BytesStart::new("bookViews")))?;
717 let mut workbook_view = BytesStart::new("workbookView");
718 if let Some(active_tab) = workbook.active_tab {
719 let active_tab_text = active_tab.to_string();
720 workbook_view.push_attribute(("activeTab", active_tab_text.as_str()));
721 }
722 writer.write_event(Event::Empty(workbook_view))?;
723 writer.write_event(Event::End(BytesEnd::new("bookViews")))?;
724 }
725
726 writer.write_event(Event::Start(BytesStart::new("sheets")))?;
727 for (index, sheet) in workbook.sheets.iter().enumerate() {
728 let sheet_id = (index + 1).to_string();
729 let mut sheet_start = BytesStart::new("sheet");
730 sheet_start.push_attribute(("name", sheet.name.as_str()));
731 sheet_start.push_attribute(("sheetId", sheet_id.as_str()));
732 match sheet.visibility {
734 SheetVisibility::Visible => {}
735 SheetVisibility::Hidden => sheet_start.push_attribute(("state", "hidden")),
736 SheetVisibility::VeryHidden => sheet_start.push_attribute(("state", "veryHidden")),
737 }
738 sheet_start.push_attribute(("r:id", sheet_relationship_ids[index].as_str()));
739 writer.write_event(Event::Empty(sheet_start))?;
740 }
741 writer.write_event(Event::End(BytesEnd::new("sheets")))?;
742
743 if !workbook.external_links.is_empty() {
746 writer.write_event(Event::Start(BytesStart::new("externalReferences")))?;
747 for relationship_id in external_link_relationship_ids {
748 let mut external_reference = BytesStart::new("externalReference");
749 external_reference.push_attribute(("r:id", relationship_id.as_str()));
750 writer.write_event(Event::Empty(external_reference))?;
751 }
752 writer.write_event(Event::End(BytesEnd::new("externalReferences")))?;
753 }
754
755 if !defined_names.is_empty() {
762 write_defined_names(&mut writer, defined_names)?;
763 }
764
765 if let Some(calculation) = &workbook.calculation {
767 write_calculation_properties(&mut writer, calculation)?;
768 }
769
770 writer.write_event(Event::End(BytesEnd::new("workbook")))?;
771
772 Ok(writer.into_inner())
773}
774
775fn write_calculation_properties<W: Write>(
777 writer: &mut Writer<W>,
778 calculation: &CalculationProperties,
779) -> Result<()> {
780 let mut start = BytesStart::new("calcPr");
781 if let Some(mode) = calculation.mode {
782 start.push_attribute(("calcMode", calculation_mode_str(mode)));
783 }
784 if calculation.iterate {
785 start.push_attribute(("iterate", "1"));
786 }
787 writer.write_event(Event::Empty(start))?;
788 Ok(())
789}
790
791fn calculation_mode_str(mode: CalculationMode) -> &'static str {
792 match mode {
793 CalculationMode::Automatic => "auto",
794 CalculationMode::AutomaticExceptTables => "autoNoTable",
795 CalculationMode::Manual => "manual",
796 }
797}
798
799fn write_workbook_protection<W: Write>(
803 writer: &mut Writer<W>,
804 protection: &WorkbookProtection,
805) -> Result<()> {
806 let password_text = protection.password.as_deref().map(legacy_password_hash_hex);
807 let mut start = BytesStart::new("workbookProtection");
808 if protection.lock_structure {
809 start.push_attribute(("lockStructure", "1"));
810 }
811 if protection.lock_windows {
812 start.push_attribute(("lockWindows", "1"));
813 }
814 if let Some(password_text) = &password_text {
815 start.push_attribute(("workbookPassword", password_text.as_str()));
816 }
817 writer.write_event(Event::Empty(start))?;
818 Ok(())
819}
820
821fn write_file_sharing<W: Write>(
825 writer: &mut Writer<W>,
826 write_protection: &WriteProtection,
827) -> Result<()> {
828 let password_text = write_protection
829 .password
830 .as_deref()
831 .map(legacy_password_hash_hex);
832 let mut start = BytesStart::new("fileSharing");
833 if write_protection.read_only_recommended {
834 start.push_attribute(("readOnlyRecommended", "1"));
835 }
836 if let Some(user_name) = &write_protection.user_name {
837 start.push_attribute(("userName", user_name.as_str()));
838 }
839 if let Some(password_text) = &password_text {
840 start.push_attribute(("reservationPassword", password_text.as_str()));
841 }
842 writer.write_event(Event::Empty(start))?;
843 Ok(())
844}
845
846fn synthesized_print_defined_names(workbook: &Workbook) -> Vec<DefinedName> {
850 let mut names = Vec::new();
851
852 for (index, sheet) in workbook.sheets.iter().enumerate() {
853 if let Some(print_area) = &sheet.print_settings.print_area {
854 let refers_to = qualify_sheet_reference(&sheet.name, print_area);
855 names.push(
856 DefinedName::new("_xlnm.Print_Area", refers_to)
857 .with_local_sheet_id(index)
858 .with_hidden(true),
859 );
860 }
861
862 let repeat_rows = sheet.print_settings.repeat_rows.as_deref();
863 let repeat_columns = sheet.print_settings.repeat_columns.as_deref();
864 if repeat_rows.is_some() || repeat_columns.is_some() {
865 let mut parts = Vec::new();
866 if let Some(columns) = repeat_columns {
867 parts.push(qualify_sheet_reference(&sheet.name, columns));
868 }
869 if let Some(rows) = repeat_rows {
870 parts.push(qualify_sheet_reference(&sheet.name, rows));
871 }
872 names.push(
873 DefinedName::new("_xlnm.Print_Titles", parts.join(","))
874 .with_local_sheet_id(index)
875 .with_hidden(true),
876 );
877 }
878 }
879
880 names
881}
882
883fn qualify_sheet_reference(sheet_name: &str, range: &str) -> String {
892 format!("{sheet_name}!{range}")
893}
894
895fn write_defined_names<W: Write>(
901 writer: &mut Writer<W>,
902 defined_names: &[DefinedName],
903) -> Result<()> {
904 writer.write_event(Event::Start(BytesStart::new("definedNames")))?;
905
906 for defined_name in defined_names {
907 let mut start = BytesStart::new("definedName");
908 start.push_attribute(("name", defined_name.name.as_str()));
909 let local_sheet_id_text = defined_name.local_sheet_id.map(|id| id.to_string());
910 if let Some(local_sheet_id_text) = &local_sheet_id_text {
911 start.push_attribute(("localSheetId", local_sheet_id_text.as_str()));
912 }
913 if defined_name.hidden {
914 start.push_attribute(("hidden", "1"));
915 }
916 if let Some(comment) = &defined_name.comment {
917 start.push_attribute(("comment", comment.as_str()));
918 }
919 writer.write_event(Event::Start(start))?;
920 writer.write_event(Event::Text(BytesText::new(&defined_name.refers_to)))?;
921 writer.write_event(Event::End(BytesEnd::new("definedName")))?;
922 }
923
924 writer.write_event(Event::End(BytesEnd::new("definedNames")))?;
925
926 Ok(())
927}
928
929struct WorksheetRelationshipIds {
935 hyperlinks: Vec<Option<String>>,
939 table: Option<String>,
941 legacy_drawing: Option<String>,
944 drawing: Option<String>,
948}
949
950fn to_worksheet_xml(
966 sheet: &Sheet,
967 shared_strings: &SharedStrings,
968 cell_formats: &[CellFormat],
969 dxfs: &[CellFormat],
970 relationship_ids: &WorksheetRelationshipIds,
971) -> Result<Vec<u8>> {
972 let mut writer = Writer::new(Vec::new());
973
974 writer.write_event(Event::Decl(BytesDecl::new(
975 "1.0",
976 Some("UTF-8"),
977 Some("yes"),
978 )))?;
979
980 let mut root = BytesStart::new("worksheet");
981 root.push_attribute(("xmlns", SPREADSHEETML_NAMESPACE));
982 root.push_attribute(("xmlns:r", RELATIONSHIPS_NAMESPACE));
983 writer.write_event(Event::Start(root))?;
984
985 let fits_to_page =
993 sheet.print_settings.fit_to_width.is_some() || sheet.print_settings.fit_to_height.is_some();
994 let has_outline_pr =
995 sheet.outline_summary_below.is_some() || sheet.outline_summary_right.is_some();
996 if fits_to_page || sheet.tab_color.is_some() || has_outline_pr {
997 writer.write_event(Event::Start(BytesStart::new("sheetPr")))?;
998 if let Some(tab_color) = &sheet.tab_color {
999 let mut tab_color_start = BytesStart::new("tabColor");
1000 tab_color_start.push_attribute(("rgb", tab_color.as_str()));
1001 writer.write_event(Event::Empty(tab_color_start))?;
1002 }
1003 if has_outline_pr {
1004 let mut outline_pr = BytesStart::new("outlinePr");
1005 if let Some(summary_below) = sheet.outline_summary_below {
1006 outline_pr.push_attribute(("summaryBelow", if summary_below { "1" } else { "0" }));
1007 }
1008 if let Some(summary_right) = sheet.outline_summary_right {
1009 outline_pr.push_attribute(("summaryRight", if summary_right { "1" } else { "0" }));
1010 }
1011 writer.write_event(Event::Empty(outline_pr))?;
1012 }
1013 if fits_to_page {
1014 let mut page_setup_pr = BytesStart::new("pageSetUpPr");
1015 page_setup_pr.push_attribute(("fitToPage", "1"));
1016 writer.write_event(Event::Empty(page_setup_pr))?;
1017 }
1018 writer.write_event(Event::End(BytesEnd::new("sheetPr")))?;
1019 }
1020
1021 let mut dimension = BytesStart::new("dimension");
1022 let dimension_ref = compute_dimension_ref(sheet);
1023 dimension.push_attribute(("ref", dimension_ref.as_str()));
1024 writer.write_event(Event::Empty(dimension))?;
1025
1026 if sheet.freeze_panes.is_some()
1029 || sheet.zoom_scale.is_some()
1030 || sheet.show_grid_lines.is_some()
1031 || sheet.show_row_col_headers.is_some()
1032 || sheet.show_zeros.is_some()
1033 || sheet.right_to_left.is_some()
1034 || sheet.active_cell.is_some()
1035 {
1036 write_sheet_views(&mut writer, sheet)?;
1037 }
1038
1039 if sheet.default_column_width.is_some() || sheet.default_row_height.is_some() {
1044 let mut sheet_format_pr = BytesStart::new("sheetFormatPr");
1045 if let Some(width) = sheet.default_column_width {
1046 let width_text = width.to_string();
1047 sheet_format_pr.push_attribute(("defaultColWidth", width_text.as_str()));
1048 }
1049 let row_height_text = sheet.default_row_height.unwrap_or(15.0).to_string();
1050 sheet_format_pr.push_attribute(("defaultRowHeight", row_height_text.as_str()));
1051 writer.write_event(Event::Empty(sheet_format_pr))?;
1052 }
1053
1054 if !sheet.column_settings.is_empty() {
1055 write_cols(&mut writer, &sheet.column_settings, cell_formats)?;
1056 }
1057
1058 writer.write_event(Event::Start(BytesStart::new("sheetData")))?;
1059 for (row_index, row) in sheet.rows.iter().enumerate() {
1060 let row_number = row_index + 1;
1061 let row_number_text = row_number.to_string();
1062 let mut row_start = BytesStart::new("row");
1063 row_start.push_attribute(("r", row_number_text.as_str()));
1064 let height_text = row.height.map(|height| height.to_string());
1065 if let Some(height_text) = &height_text {
1066 row_start.push_attribute(("ht", height_text.as_str()));
1067 row_start.push_attribute(("customHeight", "1"));
1068 }
1069 if row.hidden {
1070 row_start.push_attribute(("hidden", "1"));
1071 }
1072 if row.outline_level != 0 {
1073 let outline_level_text = row.outline_level.to_string();
1074 row_start.push_attribute(("outlineLevel", outline_level_text.as_str()));
1075 }
1076 if row.collapsed {
1077 row_start.push_attribute(("collapsed", "1"));
1078 }
1079 let row_style_text =
1083 style_index(&row.default_format, cell_formats).map(|index| index.to_string());
1084 if let Some(row_style_text) = &row_style_text {
1085 row_start.push_attribute(("s", row_style_text.as_str()));
1086 row_start.push_attribute(("customFormat", "1"));
1087 }
1088 writer.write_event(Event::Start(row_start))?;
1089
1090 for (column_index, cell) in row.cells.iter().enumerate() {
1091 write_cell(
1092 &mut writer,
1093 column_index,
1094 row_number,
1095 cell,
1096 shared_strings,
1097 cell_formats,
1098 )?;
1099 }
1100
1101 writer.write_event(Event::End(BytesEnd::new("row")))?;
1102 }
1103 writer.write_event(Event::End(BytesEnd::new("sheetData")))?;
1104
1105 if let Some(protection) = &sheet.protection {
1106 write_sheet_protection(&mut writer, protection)?;
1107 }
1108
1109 if !sheet.protected_ranges.is_empty() {
1113 write_protected_ranges(&mut writer, &sheet.protected_ranges)?;
1114 }
1115
1116 if !sheet.scenarios.is_empty() {
1119 write_scenarios(&mut writer, &sheet.scenarios)?;
1120 }
1121
1122 if let Some(range) = &sheet.auto_filter_range {
1123 if sheet.auto_filter_columns.is_empty() && sheet.sort_state.is_none() {
1127 let mut auto_filter = BytesStart::new("autoFilter");
1128 auto_filter.push_attribute(("ref", range.as_str()));
1129 writer.write_event(Event::Empty(auto_filter))?;
1130 } else {
1131 let mut auto_filter = BytesStart::new("autoFilter");
1132 auto_filter.push_attribute(("ref", range.as_str()));
1133 writer.write_event(Event::Start(auto_filter))?;
1134 write_filter_columns(&mut writer, &sheet.auto_filter_columns)?;
1135 if let Some(sort_state) = &sheet.sort_state {
1136 write_sort_state(&mut writer, sort_state)?;
1137 }
1138 writer.write_event(Event::End(BytesEnd::new("autoFilter")))?;
1139 }
1140 }
1141
1142 if !sheet.merged_ranges.is_empty() {
1143 let count_text = sheet.merged_ranges.len().to_string();
1144 let mut merge_cells_start = BytesStart::new("mergeCells");
1145 merge_cells_start.push_attribute(("count", count_text.as_str()));
1146 writer.write_event(Event::Start(merge_cells_start))?;
1147 for range in &sheet.merged_ranges {
1148 let mut merge_cell = BytesStart::new("mergeCell");
1149 merge_cell.push_attribute(("ref", range.as_str()));
1150 writer.write_event(Event::Empty(merge_cell))?;
1151 }
1152 writer.write_event(Event::End(BytesEnd::new("mergeCells")))?;
1153 }
1154
1155 for (index, rule) in sheet.conditional_formatting_rules.iter().enumerate() {
1156 write_conditional_formatting(&mut writer, index, rule, dxfs)?;
1157 }
1158 if !sheet.data_validation_rules.is_empty() {
1159 write_data_validations(&mut writer, &sheet.data_validation_rules)?;
1160 }
1161
1162 if !sheet.hyperlinks.is_empty() {
1163 write_hyperlinks(&mut writer, &sheet.hyperlinks, &relationship_ids.hyperlinks)?;
1164 }
1165
1166 if sheet.print_settings.orientation.is_some()
1167 || sheet.print_settings.fit_to_width.is_some()
1168 || sheet.print_settings.fit_to_height.is_some()
1169 || sheet.print_settings.scale.is_some()
1170 {
1171 write_page_setup(&mut writer, &sheet.print_settings)?;
1172 }
1173
1174 if sheet.print_settings.header.is_some()
1175 || sheet.print_settings.footer.is_some()
1176 || sheet.print_settings.header_first.is_some()
1177 || sheet.print_settings.footer_first.is_some()
1178 || sheet.print_settings.header_even.is_some()
1179 || sheet.print_settings.footer_even.is_some()
1180 {
1181 write_header_footer(&mut writer, &sheet.print_settings)?;
1182 }
1183
1184 if !sheet.row_breaks.is_empty() {
1186 write_page_breaks(&mut writer, "rowBreaks", &sheet.row_breaks, 16383)?;
1187 }
1188 if !sheet.column_breaks.is_empty() {
1189 write_page_breaks(&mut writer, "colBreaks", &sheet.column_breaks, 1048575)?;
1190 }
1191
1192 if !sheet.ignored_errors.is_empty() {
1195 write_ignored_errors(&mut writer, &sheet.ignored_errors)?;
1196 }
1197
1198 if let Some(drawing_id) = &relationship_ids.drawing {
1200 let mut drawing_start = BytesStart::new("drawing");
1201 drawing_start.push_attribute(("r:id", drawing_id.as_str()));
1202 writer.write_event(Event::Empty(drawing_start))?;
1203 }
1204
1205 if let Some(legacy_drawing_id) = &relationship_ids.legacy_drawing {
1206 let mut legacy_drawing = BytesStart::new("legacyDrawing");
1207 legacy_drawing.push_attribute(("r:id", legacy_drawing_id.as_str()));
1208 writer.write_event(Event::Empty(legacy_drawing))?;
1209 }
1210
1211 if let (Some(_table), Some(table_id)) = (&sheet.table, &relationship_ids.table) {
1212 let mut table_parts_start = BytesStart::new("tableParts");
1215 table_parts_start.push_attribute(("count", "1"));
1216 writer.write_event(Event::Start(table_parts_start))?;
1217 let mut table_part = BytesStart::new("tablePart");
1218 table_part.push_attribute(("r:id", table_id.as_str()));
1219 writer.write_event(Event::Empty(table_part))?;
1220 writer.write_event(Event::End(BytesEnd::new("tableParts")))?;
1221 }
1222
1223 writer.write_event(Event::End(BytesEnd::new("worksheet")))?;
1224
1225 Ok(writer.into_inner())
1226}
1227
1228fn write_sheet_views<W: Write>(writer: &mut Writer<W>, sheet: &Sheet) -> Result<()> {
1238 writer.write_event(Event::Start(BytesStart::new("sheetViews")))?;
1239 let mut sheet_view = BytesStart::new("sheetView");
1240 if let Some(show_grid_lines) = sheet.show_grid_lines {
1241 sheet_view.push_attribute(("showGridLines", if show_grid_lines { "1" } else { "0" }));
1242 }
1243 if let Some(show_row_col_headers) = sheet.show_row_col_headers {
1244 sheet_view.push_attribute((
1245 "showRowColHeaders",
1246 if show_row_col_headers { "1" } else { "0" },
1247 ));
1248 }
1249 if let Some(show_zeros) = sheet.show_zeros {
1250 sheet_view.push_attribute(("showZeros", if show_zeros { "1" } else { "0" }));
1251 }
1252 if let Some(right_to_left) = sheet.right_to_left {
1253 sheet_view.push_attribute(("rightToLeft", if right_to_left { "1" } else { "0" }));
1254 }
1255 let zoom_scale_text = sheet.zoom_scale.map(|value| value.to_string());
1256 if let Some(zoom_scale_text) = &zoom_scale_text {
1257 sheet_view.push_attribute(("zoomScale", zoom_scale_text.as_str()));
1258 }
1259 sheet_view.push_attribute(("workbookViewId", "0"));
1260
1261 if sheet.freeze_panes.is_none() && sheet.active_cell.is_none() {
1262 writer.write_event(Event::Empty(sheet_view))?;
1263 writer.write_event(Event::End(BytesEnd::new("sheetViews")))?;
1264 return Ok(());
1265 }
1266 writer.write_event(Event::Start(sheet_view))?;
1267
1268 if let Some(freeze_panes) = &sheet.freeze_panes {
1269 let x_split_text = freeze_panes.frozen_columns.to_string();
1270 let y_split_text = freeze_panes.frozen_rows.to_string();
1271 let top_left_cell =
1272 cell_reference(freeze_panes.frozen_columns, freeze_panes.frozen_rows + 1);
1273 let active_pane = match (
1274 freeze_panes.frozen_columns > 0,
1275 freeze_panes.frozen_rows > 0,
1276 ) {
1277 (true, true) => "bottomRight",
1278 (true, false) => "topRight",
1279 (false, true) => "bottomLeft",
1280 (false, false) => "topLeft",
1281 };
1282
1283 let mut pane = BytesStart::new("pane");
1284 if freeze_panes.frozen_columns > 0 {
1285 pane.push_attribute(("xSplit", x_split_text.as_str()));
1286 }
1287 if freeze_panes.frozen_rows > 0 {
1288 pane.push_attribute(("ySplit", y_split_text.as_str()));
1289 }
1290 pane.push_attribute(("topLeftCell", top_left_cell.as_str()));
1291 pane.push_attribute(("activePane", active_pane));
1292 pane.push_attribute(("state", "frozen"));
1293 writer.write_event(Event::Empty(pane))?;
1294 }
1295
1296 if let Some(active_cell) = &sheet.active_cell {
1297 let mut selection = BytesStart::new("selection");
1298 selection.push_attribute(("activeCell", active_cell.as_str()));
1299 selection.push_attribute(("sqref", active_cell.as_str()));
1300 writer.write_event(Event::Empty(selection))?;
1301 }
1302
1303 writer.write_event(Event::End(BytesEnd::new("sheetView")))?;
1304 writer.write_event(Event::End(BytesEnd::new("sheetViews")))?;
1305 Ok(())
1306}
1307
1308fn write_cols<W: Write>(
1312 writer: &mut Writer<W>,
1313 column_settings: &[ColumnSetting],
1314 cell_formats: &[CellFormat],
1315) -> Result<()> {
1316 writer.write_event(Event::Start(BytesStart::new("cols")))?;
1317 for setting in column_settings {
1318 let index_text = (setting.column + 1).to_string();
1319 let width_text = setting.width.map(|width| width.to_string());
1320 let style_text = style_index(&setting.style, cell_formats).map(|index| index.to_string());
1323
1324 let mut col = BytesStart::new("col");
1325 col.push_attribute(("min", index_text.as_str()));
1326 col.push_attribute(("max", index_text.as_str()));
1327 if let Some(width_text) = &width_text {
1328 col.push_attribute(("width", width_text.as_str()));
1329 col.push_attribute(("customWidth", "1"));
1330 }
1331 if let Some(style_text) = &style_text {
1332 col.push_attribute(("style", style_text.as_str()));
1333 }
1334 if setting.hidden {
1335 col.push_attribute(("hidden", "1"));
1336 }
1337 if setting.outline_level != 0 {
1338 let outline_level_text = setting.outline_level.to_string();
1339 col.push_attribute(("outlineLevel", outline_level_text.as_str()));
1340 }
1341 if setting.collapsed {
1342 col.push_attribute(("collapsed", "1"));
1343 }
1344 writer.write_event(Event::Empty(col))?;
1345 }
1346 writer.write_event(Event::End(BytesEnd::new("cols")))?;
1347 Ok(())
1348}
1349
1350fn write_sheet_protection<W: Write>(
1353 writer: &mut Writer<W>,
1354 protection: &SheetProtection,
1355) -> Result<()> {
1356 let password_text = protection.password.as_deref().map(legacy_password_hash_hex);
1357 let mut start = BytesStart::new("sheetProtection");
1358 start.push_attribute(("sheet", "1"));
1359 if let Some(password_text) = &password_text {
1360 start.push_attribute(("password", password_text.as_str()));
1361 }
1362 writer.write_event(Event::Empty(start))?;
1363 Ok(())
1364}
1365
1366fn write_protected_ranges<W: Write>(
1370 writer: &mut Writer<W>,
1371 protected_ranges: &[ProtectedRange],
1372) -> Result<()> {
1373 writer.write_event(Event::Start(BytesStart::new("protectedRanges")))?;
1374 for range in protected_ranges {
1375 let password_text = range.password.as_deref().map(legacy_password_hash_hex);
1376 let mut start = BytesStart::new("protectedRange");
1377 start.push_attribute(("name", range.name.as_str()));
1378 start.push_attribute(("sqref", range.range.as_str()));
1379 if let Some(password_text) = &password_text {
1380 start.push_attribute(("password", password_text.as_str()));
1381 }
1382 writer.write_event(Event::Empty(start))?;
1383 }
1384 writer.write_event(Event::End(BytesEnd::new("protectedRanges")))?;
1385 Ok(())
1386}
1387
1388fn write_scenarios<W: Write>(writer: &mut Writer<W>, scenarios: &[Scenario]) -> Result<()> {
1394 writer.write_event(Event::Start(BytesStart::new("scenarios")))?;
1395 for scenario in scenarios {
1396 let count_text = scenario.inputs.len().to_string();
1397 let mut scenario_start = BytesStart::new("scenario");
1398 scenario_start.push_attribute(("name", scenario.name.as_str()));
1399 scenario_start.push_attribute(("locked", "1"));
1400 scenario_start.push_attribute(("count", count_text.as_str()));
1401 if let Some(comment) = &scenario.comment {
1402 scenario_start.push_attribute(("comment", comment.as_str()));
1403 }
1404 writer.write_event(Event::Start(scenario_start))?;
1405 for (cell, value) in &scenario.inputs {
1406 let mut input_cell = BytesStart::new("inputCells");
1407 input_cell.push_attribute(("r", cell.as_str()));
1408 input_cell.push_attribute(("val", value.as_str()));
1409 writer.write_event(Event::Empty(input_cell))?;
1410 }
1411 writer.write_event(Event::End(BytesEnd::new("scenario")))?;
1412 }
1413 writer.write_event(Event::End(BytesEnd::new("scenarios")))?;
1414 Ok(())
1415}
1416
1417fn write_ignored_errors<W: Write>(
1424 writer: &mut Writer<W>,
1425 ignored_errors: &[IgnoredError],
1426) -> Result<()> {
1427 writer.write_event(Event::Start(BytesStart::new("ignoredErrors")))?;
1428 for entry in ignored_errors {
1429 let mut start = BytesStart::new("ignoredError");
1430 start.push_attribute(("sqref", entry.range.as_str()));
1431 if entry.eval_error {
1432 start.push_attribute(("evalError", "1"));
1433 }
1434 if entry.two_digit_text_year {
1435 start.push_attribute(("twoDigitTextYear", "1"));
1436 }
1437 if entry.number_stored_as_text {
1438 start.push_attribute(("numberStoredAsText", "1"));
1439 }
1440 if entry.formula {
1441 start.push_attribute(("formula", "1"));
1442 }
1443 if entry.formula_range {
1444 start.push_attribute(("formulaRange", "1"));
1445 }
1446 if entry.unlocked_formula {
1447 start.push_attribute(("unlockedFormula", "1"));
1448 }
1449 if entry.empty_cell_reference {
1450 start.push_attribute(("emptyCellReference", "1"));
1451 }
1452 if entry.list_data_validation {
1453 start.push_attribute(("listDataValidation", "1"));
1454 }
1455 if entry.calculated_column {
1456 start.push_attribute(("calculatedColumn", "1"));
1457 }
1458 writer.write_event(Event::Empty(start))?;
1459 }
1460 writer.write_event(Event::End(BytesEnd::new("ignoredErrors")))?;
1461 Ok(())
1462}
1463
1464fn write_filter_columns<W: Write>(
1469 writer: &mut Writer<W>,
1470 columns: &[AutoFilterColumn],
1471) -> Result<()> {
1472 for column in columns {
1473 let col_id_text = column.column_offset.to_string();
1474 let mut filter_column = BytesStart::new("filterColumn");
1475 filter_column.push_attribute(("colId", col_id_text.as_str()));
1476 writer.write_event(Event::Start(filter_column))?;
1477 writer.write_event(Event::Start(BytesStart::new("filters")))?;
1478 for value in &column.visible_values {
1479 let mut filter = BytesStart::new("filter");
1480 filter.push_attribute(("val", value.as_str()));
1481 writer.write_event(Event::Empty(filter))?;
1482 }
1483 writer.write_event(Event::End(BytesEnd::new("filters")))?;
1484 writer.write_event(Event::End(BytesEnd::new("filterColumn")))?;
1485 }
1486 Ok(())
1487}
1488
1489fn write_sort_state<W: Write>(writer: &mut Writer<W>, sort_state: &SortState) -> Result<()> {
1495 let mut start = BytesStart::new("sortState");
1496 start.push_attribute(("ref", sort_state.range.as_str()));
1497 if sort_state.conditions.is_empty() {
1498 writer.write_event(Event::Empty(start))?;
1499 } else {
1500 writer.write_event(Event::Start(start))?;
1501 for condition in &sort_state.conditions {
1502 let mut condition_start = BytesStart::new("sortCondition");
1503 condition_start.push_attribute(("ref", condition.range.as_str()));
1504 if condition.descending {
1505 condition_start.push_attribute(("descending", "1"));
1506 }
1507 writer.write_event(Event::Empty(condition_start))?;
1508 }
1509 writer.write_event(Event::End(BytesEnd::new("sortState")))?;
1510 }
1511 Ok(())
1512}
1513
1514fn write_page_breaks<W: Write>(
1521 writer: &mut Writer<W>,
1522 element_name: &str,
1523 breaks: &[u32],
1524 max: u32,
1525) -> Result<()> {
1526 let count_text = breaks.len().to_string();
1527 let max_text = max.to_string();
1528 let mut start = BytesStart::new(element_name);
1529 start.push_attribute(("count", count_text.as_str()));
1530 start.push_attribute(("manualBreakCount", count_text.as_str()));
1531 writer.write_event(Event::Start(start))?;
1532 for id in breaks {
1533 let id_text = id.to_string();
1534 let mut brk = BytesStart::new("brk");
1535 brk.push_attribute(("id", id_text.as_str()));
1536 brk.push_attribute(("max", max_text.as_str()));
1537 brk.push_attribute(("man", "1"));
1538 writer.write_event(Event::Empty(brk))?;
1539 }
1540 writer.write_event(Event::End(BytesEnd::new(element_name)))?;
1541 Ok(())
1542}
1543
1544fn legacy_password_hash(password: &str) -> u16 {
1556 let bytes = password.as_bytes();
1557 let mut hash: u16 = 0;
1558 for &byte in bytes.iter().rev() {
1559 hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff);
1560 hash ^= byte as u16;
1561 }
1562 hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff);
1563 hash ^= 0xCE4B; hash ^= bytes.len() as u16;
1565 hash
1566}
1567
1568fn legacy_password_hash_hex(password: &str) -> String {
1569 format!("{:04X}", legacy_password_hash(password))
1570}
1571
1572fn write_hyperlinks<W: Write>(
1577 writer: &mut Writer<W>,
1578 hyperlinks: &[CellHyperlink],
1579 relationship_ids: &[Option<String>],
1580) -> Result<()> {
1581 writer.write_event(Event::Start(BytesStart::new("hyperlinks")))?;
1582 for (hyperlink, relationship_id) in hyperlinks.iter().zip(relationship_ids) {
1583 let mut start = BytesStart::new("hyperlink");
1584 start.push_attribute(("ref", hyperlink.cell.as_str()));
1585 match (&hyperlink.target, relationship_id) {
1586 (HyperlinkTarget::External(_), Some(relationship_id)) => {
1587 start.push_attribute(("r:id", relationship_id.as_str()));
1588 }
1589 (HyperlinkTarget::Internal(location), _) => {
1590 start.push_attribute(("location", location.as_str()));
1591 }
1592 (HyperlinkTarget::External(_), None) => {}
1596 }
1597 if let Some(tooltip) = &hyperlink.tooltip {
1598 start.push_attribute(("tooltip", tooltip.as_str()));
1599 }
1600 writer.write_event(Event::Empty(start))?;
1601 }
1602 writer.write_event(Event::End(BytesEnd::new("hyperlinks")))?;
1603 Ok(())
1604}
1605
1606fn write_page_setup<W: Write>(
1610 writer: &mut Writer<W>,
1611 print_settings: &PrintSettings,
1612) -> Result<()> {
1613 let mut start = BytesStart::new("pageSetup");
1614 if let Some(orientation) = print_settings.orientation {
1615 start.push_attribute(("orientation", page_orientation_str(orientation)));
1616 }
1617 let scale_text = print_settings.scale.map(|value| value.to_string());
1620 if let Some(scale_text) = &scale_text {
1621 start.push_attribute(("scale", scale_text.as_str()));
1622 }
1623 let fit_to_width_text = print_settings.fit_to_width.unwrap_or(0).to_string();
1624 let fit_to_height_text = print_settings.fit_to_height.unwrap_or(0).to_string();
1625 if print_settings.fit_to_width.is_some() || print_settings.fit_to_height.is_some() {
1626 start.push_attribute(("fitToWidth", fit_to_width_text.as_str()));
1627 start.push_attribute(("fitToHeight", fit_to_height_text.as_str()));
1628 }
1629 writer.write_event(Event::Empty(start))?;
1630 Ok(())
1631}
1632
1633fn page_orientation_str(orientation: PageOrientation) -> &'static str {
1634 match orientation {
1635 PageOrientation::Portrait => "portrait",
1636 PageOrientation::Landscape => "landscape",
1637 }
1638}
1639
1640fn write_header_footer<W: Write>(
1646 writer: &mut Writer<W>,
1647 print_settings: &PrintSettings,
1648) -> Result<()> {
1649 let mut start = BytesStart::new("headerFooter");
1650 if print_settings.header_first.is_some() || print_settings.footer_first.is_some() {
1651 start.push_attribute(("differentFirst", "1"));
1652 }
1653 if print_settings.header_even.is_some() || print_settings.footer_even.is_some() {
1654 start.push_attribute(("differentOddEven", "1"));
1655 }
1656 writer.write_event(Event::Start(start))?;
1657 if let Some(header) = &print_settings.header {
1658 write_element_text(writer, "oddHeader", header)?;
1659 }
1660 if let Some(footer) = &print_settings.footer {
1661 write_element_text(writer, "oddFooter", footer)?;
1662 }
1663 if let Some(header) = &print_settings.header_even {
1664 write_element_text(writer, "evenHeader", header)?;
1665 }
1666 if let Some(footer) = &print_settings.footer_even {
1667 write_element_text(writer, "evenFooter", footer)?;
1668 }
1669 if let Some(header) = &print_settings.header_first {
1670 write_element_text(writer, "firstHeader", header)?;
1671 }
1672 if let Some(footer) = &print_settings.footer_first {
1673 write_element_text(writer, "firstFooter", footer)?;
1674 }
1675 writer.write_event(Event::End(BytesEnd::new("headerFooter")))?;
1676 Ok(())
1677}
1678
1679fn write_conditional_formatting<W: Write>(
1685 writer: &mut Writer<W>,
1686 index: usize,
1687 rule: &ConditionalFormattingRule,
1688 dxfs: &[CellFormat],
1689) -> Result<()> {
1690 let mut conditional_formatting_start = BytesStart::new("conditionalFormatting");
1691 conditional_formatting_start.push_attribute(("sqref", rule.range.as_str()));
1692 writer.write_event(Event::Start(conditional_formatting_start))?;
1693
1694 let dxf_id = dxfs
1698 .iter()
1699 .position(|format| format == &rule.format)
1700 .unwrap_or(0);
1701 let dxf_id_text = dxf_id.to_string();
1702 let priority_text = (index + 1).to_string();
1703 let anchor_cell = range_anchor_cell(&rule.range);
1704
1705 let mut cf_rule_start = BytesStart::new("cfRule");
1706 match &rule.condition {
1707 ConditionalFormattingCondition::CellIs { operator, .. } => {
1708 cf_rule_start.push_attribute(("type", "cellIs"));
1709 cf_rule_start.push_attribute(("dxfId", dxf_id_text.as_str()));
1710 cf_rule_start.push_attribute(("priority", priority_text.as_str()));
1711 cf_rule_start.push_attribute(("operator", comparison_operator_str(*operator)));
1712 }
1713 ConditionalFormattingCondition::Expression { .. } => {
1714 cf_rule_start.push_attribute(("type", "expression"));
1715 cf_rule_start.push_attribute(("dxfId", dxf_id_text.as_str()));
1716 cf_rule_start.push_attribute(("priority", priority_text.as_str()));
1717 }
1718 ConditionalFormattingCondition::ColorScale(_) => {
1719 cf_rule_start.push_attribute(("type", "colorScale"));
1720 cf_rule_start.push_attribute(("priority", priority_text.as_str()));
1721 }
1722 ConditionalFormattingCondition::DataBar { .. } => {
1723 cf_rule_start.push_attribute(("type", "dataBar"));
1724 cf_rule_start.push_attribute(("priority", priority_text.as_str()));
1725 }
1726 ConditionalFormattingCondition::IconSet(_) => {
1727 cf_rule_start.push_attribute(("type", "iconSet"));
1728 cf_rule_start.push_attribute(("priority", priority_text.as_str()));
1729 }
1730 ConditionalFormattingCondition::Top10 {
1731 rank,
1732 percent,
1733 bottom,
1734 } => {
1735 cf_rule_start.push_attribute(("type", "top10"));
1736 cf_rule_start.push_attribute(("dxfId", dxf_id_text.as_str()));
1737 cf_rule_start.push_attribute(("priority", priority_text.as_str()));
1738 let rank_text = rank.to_string();
1739 cf_rule_start.push_attribute(("rank", rank_text.as_str()));
1740 if *percent {
1741 cf_rule_start.push_attribute(("percent", "1"));
1742 }
1743 if *bottom {
1744 cf_rule_start.push_attribute(("bottom", "1"));
1745 }
1746 writer.write_event(Event::Empty(cf_rule_start))?;
1747 writer.write_event(Event::End(BytesEnd::new("conditionalFormatting")))?;
1748 return Ok(());
1749 }
1750 ConditionalFormattingCondition::AboveAverage {
1751 above,
1752 equal_average,
1753 } => {
1754 cf_rule_start.push_attribute(("type", "aboveAverage"));
1755 cf_rule_start.push_attribute(("dxfId", dxf_id_text.as_str()));
1756 cf_rule_start.push_attribute(("priority", priority_text.as_str()));
1757 if !*above {
1758 cf_rule_start.push_attribute(("aboveAverage", "0"));
1759 }
1760 if *equal_average {
1761 cf_rule_start.push_attribute(("equalAverage", "1"));
1762 }
1763 writer.write_event(Event::Empty(cf_rule_start))?;
1764 writer.write_event(Event::End(BytesEnd::new("conditionalFormatting")))?;
1765 return Ok(());
1766 }
1767 ConditionalFormattingCondition::DuplicateValues => {
1768 cf_rule_start.push_attribute(("type", "duplicateValues"));
1769 cf_rule_start.push_attribute(("dxfId", dxf_id_text.as_str()));
1770 cf_rule_start.push_attribute(("priority", priority_text.as_str()));
1771 writer.write_event(Event::Empty(cf_rule_start))?;
1772 writer.write_event(Event::End(BytesEnd::new("conditionalFormatting")))?;
1773 return Ok(());
1774 }
1775 ConditionalFormattingCondition::UniqueValues => {
1776 cf_rule_start.push_attribute(("type", "uniqueValues"));
1777 cf_rule_start.push_attribute(("dxfId", dxf_id_text.as_str()));
1778 cf_rule_start.push_attribute(("priority", priority_text.as_str()));
1779 writer.write_event(Event::Empty(cf_rule_start))?;
1780 writer.write_event(Event::End(BytesEnd::new("conditionalFormatting")))?;
1781 return Ok(());
1782 }
1783 ConditionalFormattingCondition::ContainsText { operator, .. } => {
1784 cf_rule_start.push_attribute(("type", text_comparison_operator_type_str(*operator)));
1785 cf_rule_start.push_attribute(("dxfId", dxf_id_text.as_str()));
1786 cf_rule_start.push_attribute(("priority", priority_text.as_str()));
1787 cf_rule_start
1788 .push_attribute(("operator", text_comparison_operator_type_str(*operator)));
1789 }
1790 }
1791 let text_for_attribute;
1792 if let ConditionalFormattingCondition::ContainsText { text, .. } = &rule.condition {
1793 text_for_attribute = text.clone();
1794 cf_rule_start.push_attribute(("text", text_for_attribute.as_str()));
1795 }
1796 writer.write_event(Event::Start(cf_rule_start))?;
1797
1798 match &rule.condition {
1799 ConditionalFormattingCondition::CellIs {
1800 formula1, formula2, ..
1801 } => {
1802 write_element_text(writer, "formula", formula1)?;
1803 if let Some(formula2) = formula2 {
1804 write_element_text(writer, "formula", formula2)?;
1805 }
1806 }
1807 ConditionalFormattingCondition::Expression { formula } => {
1808 write_element_text(writer, "formula", formula)?;
1809 }
1810 ConditionalFormattingCondition::ColorScale(stops) => {
1811 writer.write_event(Event::Start(BytesStart::new("colorScale")))?;
1812 for stop in stops {
1813 write_cfvo(writer, &stop.position)?;
1814 }
1815 for stop in stops {
1816 let mut color = BytesStart::new("color");
1817 color.push_attribute(("rgb", stop.color.as_str()));
1818 writer.write_event(Event::Empty(color))?;
1819 }
1820 writer.write_event(Event::End(BytesEnd::new("colorScale")))?;
1821 }
1822 ConditionalFormattingCondition::DataBar { min, max, color } => {
1823 writer.write_event(Event::Start(BytesStart::new("dataBar")))?;
1824 write_cfvo(writer, min)?;
1825 write_cfvo(writer, max)?;
1826 let mut color_start = BytesStart::new("color");
1827 color_start.push_attribute(("rgb", color.as_str()));
1828 writer.write_event(Event::Empty(color_start))?;
1829 writer.write_event(Event::End(BytesEnd::new("dataBar")))?;
1830 }
1831 ConditionalFormattingCondition::IconSet(icon_set) => {
1832 let mut icon_set_start = BytesStart::new("iconSet");
1833 icon_set_start.push_attribute(("iconSet", icon_set_type_str(*icon_set)));
1834 writer.write_event(Event::Start(icon_set_start))?;
1835 for percent in [0, 33, 67] {
1836 write_cfvo(writer, &CfvoPosition::Percent(percent as f64))?;
1837 }
1838 writer.write_event(Event::End(BytesEnd::new("iconSet")))?;
1839 }
1840 ConditionalFormattingCondition::ContainsText { operator, text } => {
1841 write_element_text(
1842 writer,
1843 "formula",
1844 &text_comparison_formula(*operator, anchor_cell, text),
1845 )?;
1846 }
1847 ConditionalFormattingCondition::Top10 { .. }
1848 | ConditionalFormattingCondition::AboveAverage { .. }
1849 | ConditionalFormattingCondition::DuplicateValues
1850 | ConditionalFormattingCondition::UniqueValues => unreachable!("returned early above"),
1851 }
1852
1853 writer.write_event(Event::End(BytesEnd::new("cfRule")))?;
1854 writer.write_event(Event::End(BytesEnd::new("conditionalFormatting")))?;
1855
1856 Ok(())
1857}
1858
1859fn range_anchor_cell(range: &str) -> &str {
1863 range.split(':').next().unwrap_or(range)
1864}
1865
1866fn escape_formula_string(text: &str) -> String {
1869 text.replace('"', "\"\"")
1870}
1871
1872fn text_comparison_formula(
1873 operator: TextComparisonOperator,
1874 anchor_cell: &str,
1875 text: &str,
1876) -> String {
1877 let escaped = escape_formula_string(text);
1878 match operator {
1879 TextComparisonOperator::Contains => {
1880 format!(r#"NOT(ISERROR(SEARCH("{escaped}",{anchor_cell})))"#)
1881 }
1882 TextComparisonOperator::NotContains => {
1883 format!(r#"ISERROR(SEARCH("{escaped}",{anchor_cell}))"#)
1884 }
1885 TextComparisonOperator::BeginsWith => {
1886 format!(r#"LEFT({anchor_cell},LEN("{escaped}"))="{escaped}""#)
1887 }
1888 TextComparisonOperator::EndsWith => {
1889 format!(r#"RIGHT({anchor_cell},LEN("{escaped}"))="{escaped}""#)
1890 }
1891 }
1892}
1893
1894fn text_comparison_operator_type_str(operator: TextComparisonOperator) -> &'static str {
1895 match operator {
1896 TextComparisonOperator::Contains => "containsText",
1897 TextComparisonOperator::NotContains => "notContainsText",
1898 TextComparisonOperator::BeginsWith => "beginsWith",
1899 TextComparisonOperator::EndsWith => "endsWith",
1900 }
1901}
1902
1903fn icon_set_type_str(icon_set: IconSetType) -> &'static str {
1904 match icon_set {
1905 IconSetType::ThreeTrafficLights => "3TrafficLights1",
1906 IconSetType::ThreeArrows => "3Arrows",
1907 IconSetType::ThreeFlags => "3Flags",
1908 IconSetType::ThreeSymbols => "3Symbols",
1909 }
1910}
1911
1912fn write_cfvo<W: Write>(writer: &mut Writer<W>, position: &CfvoPosition) -> Result<()> {
1915 let mut cfvo = BytesStart::new("cfvo");
1916 match position {
1917 CfvoPosition::Min => {
1918 cfvo.push_attribute(("type", "min"));
1919 }
1920 CfvoPosition::Max => {
1921 cfvo.push_attribute(("type", "max"));
1922 }
1923 CfvoPosition::Percent(value) => {
1924 cfvo.push_attribute(("type", "percent"));
1925 let value_text = value.to_string();
1926 cfvo.push_attribute(("val", value_text.as_str()));
1927 writer.write_event(Event::Empty(cfvo))?;
1928 return Ok(());
1929 }
1930 CfvoPosition::Number(value) => {
1931 cfvo.push_attribute(("type", "num"));
1932 let value_text = value.to_string();
1933 cfvo.push_attribute(("val", value_text.as_str()));
1934 writer.write_event(Event::Empty(cfvo))?;
1935 return Ok(());
1936 }
1937 }
1938 writer.write_event(Event::Empty(cfvo))?;
1939 Ok(())
1940}
1941
1942fn write_data_validations<W: Write>(
1945 writer: &mut Writer<W>,
1946 rules: &[DataValidationRule],
1947) -> Result<()> {
1948 let count_text = rules.len().to_string();
1949 let mut start = BytesStart::new("dataValidations");
1950 start.push_attribute(("count", count_text.as_str()));
1951 writer.write_event(Event::Start(start))?;
1952
1953 for rule in rules {
1954 let (type_str, operator, formula1, formula2): (
1955 &str,
1956 Option<ComparisonOperator>,
1957 &str,
1958 Option<&str>,
1959 ) = match &rule.kind {
1960 DataValidationKind::List { source } => ("list", None, source.as_str(), None),
1961 DataValidationKind::WholeNumber {
1962 operator,
1963 formula1,
1964 formula2,
1965 } => (
1966 "whole",
1967 Some(*operator),
1968 formula1.as_str(),
1969 formula2.as_deref(),
1970 ),
1971 DataValidationKind::Decimal {
1972 operator,
1973 formula1,
1974 formula2,
1975 } => (
1976 "decimal",
1977 Some(*operator),
1978 formula1.as_str(),
1979 formula2.as_deref(),
1980 ),
1981 DataValidationKind::Date {
1982 operator,
1983 formula1,
1984 formula2,
1985 } => (
1986 "date",
1987 Some(*operator),
1988 formula1.as_str(),
1989 formula2.as_deref(),
1990 ),
1991 DataValidationKind::Time {
1992 operator,
1993 formula1,
1994 formula2,
1995 } => (
1996 "time",
1997 Some(*operator),
1998 formula1.as_str(),
1999 formula2.as_deref(),
2000 ),
2001 DataValidationKind::TextLength {
2002 operator,
2003 formula1,
2004 formula2,
2005 } => (
2006 "textLength",
2007 Some(*operator),
2008 formula1.as_str(),
2009 formula2.as_deref(),
2010 ),
2011 DataValidationKind::Custom { formula } => ("custom", None, formula.as_str(), None),
2012 };
2013
2014 let mut data_validation_start = BytesStart::new("dataValidation");
2015 data_validation_start.push_attribute(("type", type_str));
2016 if let Some(operator) = operator {
2017 data_validation_start.push_attribute(("operator", comparison_operator_str(operator)));
2018 }
2019 if rule.allow_blank {
2020 data_validation_start.push_attribute(("allowBlank", "1"));
2021 }
2022 if rule.input_message.is_some() {
2023 data_validation_start.push_attribute(("showInputMessage", "1"));
2024 }
2025 if rule.error_message.is_some() {
2026 data_validation_start.push_attribute(("showErrorMessage", "1"));
2027 }
2028 if let Some(message) = &rule.input_message {
2029 if let Some(title) = &message.title {
2030 data_validation_start.push_attribute(("promptTitle", title.as_str()));
2031 }
2032 data_validation_start.push_attribute(("prompt", message.text.as_str()));
2033 }
2034 if let Some(message) = &rule.error_message {
2035 if let Some(title) = &message.title {
2036 data_validation_start.push_attribute(("errorTitle", title.as_str()));
2037 }
2038 data_validation_start.push_attribute(("error", message.text.as_str()));
2039 }
2040 data_validation_start.push_attribute(("sqref", rule.range.as_str()));
2041 writer.write_event(Event::Start(data_validation_start))?;
2042
2043 write_element_text(writer, "formula1", formula1)?;
2044 if let Some(formula2) = formula2 {
2045 write_element_text(writer, "formula2", formula2)?;
2046 }
2047
2048 writer.write_event(Event::End(BytesEnd::new("dataValidation")))?;
2049 }
2050
2051 writer.write_event(Event::End(BytesEnd::new("dataValidations")))?;
2052
2053 Ok(())
2054}
2055
2056fn write_element_text<W: Write>(writer: &mut Writer<W>, name: &str, text: &str) -> Result<()> {
2059 writer.write_event(Event::Start(BytesStart::new(name)))?;
2060 writer.write_event(Event::Text(BytesText::new(text)))?;
2061 writer.write_event(Event::End(BytesEnd::new(name)))?;
2062 Ok(())
2063}
2064
2065fn comparison_operator_str(operator: ComparisonOperator) -> &'static str {
2069 match operator {
2070 ComparisonOperator::LessThan => "lessThan",
2071 ComparisonOperator::LessThanOrEqual => "lessThanOrEqual",
2072 ComparisonOperator::Equal => "equal",
2073 ComparisonOperator::NotEqual => "notEqual",
2074 ComparisonOperator::GreaterThanOrEqual => "greaterThanOrEqual",
2075 ComparisonOperator::GreaterThan => "greaterThan",
2076 ComparisonOperator::Between => "between",
2077 ComparisonOperator::NotBetween => "notBetween",
2078 }
2079}
2080
2081fn style_index(format: &Option<CellFormat>, cell_formats: &[CellFormat]) -> Option<usize> {
2086 format
2087 .as_ref()
2088 .and_then(|format| {
2089 cell_formats
2090 .iter()
2091 .position(|candidate| candidate == format)
2092 })
2093 .map(|index| index + 1)
2094}
2095
2096fn write_cell<W: Write>(
2101 writer: &mut Writer<W>,
2102 column_index: usize,
2103 row_number: usize,
2104 cell: &Cell,
2105 shared_strings: &SharedStrings,
2106 cell_formats: &[CellFormat],
2107) -> Result<()> {
2108 let style = style_index(&cell.format, cell_formats);
2109
2110 match &cell.value {
2111 CellValue::Empty => {
2112 if let Some(style) = style {
2113 let reference = cell_reference(column_index, row_number);
2114 let style_text = style.to_string();
2115 let mut start = BytesStart::new("c");
2116 start.push_attribute(("r", reference.as_str()));
2117 start.push_attribute(("s", style_text.as_str()));
2118 writer.write_event(Event::Empty(start))?;
2119 }
2120 }
2121
2122 CellValue::Text(text) => {
2123 let reference = cell_reference(column_index, row_number);
2124 let index = shared_strings.plain_indices.get(text).copied().unwrap_or(0);
2125 let index_text = index.to_string();
2126 let style_text = style.map(|value| value.to_string());
2127
2128 let mut start = BytesStart::new("c");
2129 start.push_attribute(("r", reference.as_str()));
2130 if let Some(style_text) = &style_text {
2131 start.push_attribute(("s", style_text.as_str()));
2132 }
2133 start.push_attribute(("t", "s"));
2134 writer.write_event(Event::Start(start))?;
2135 writer.write_event(Event::Start(BytesStart::new("v")))?;
2136 writer.write_event(Event::Text(BytesText::new(&index_text)))?;
2137 writer.write_event(Event::End(BytesEnd::new("v")))?;
2138 writer.write_event(Event::End(BytesEnd::new("c")))?;
2139 }
2140
2141 CellValue::RichText(runs) => {
2145 let reference = cell_reference(column_index, row_number);
2146 let index = shared_strings.index_of_rich(runs).unwrap_or(0);
2147 let index_text = index.to_string();
2148 let style_text = style.map(|value| value.to_string());
2149
2150 let mut start = BytesStart::new("c");
2151 start.push_attribute(("r", reference.as_str()));
2152 if let Some(style_text) = &style_text {
2153 start.push_attribute(("s", style_text.as_str()));
2154 }
2155 start.push_attribute(("t", "s"));
2156 writer.write_event(Event::Start(start))?;
2157 writer.write_event(Event::Start(BytesStart::new("v")))?;
2158 writer.write_event(Event::Text(BytesText::new(&index_text)))?;
2159 writer.write_event(Event::End(BytesEnd::new("v")))?;
2160 writer.write_event(Event::End(BytesEnd::new("c")))?;
2161 }
2162
2163 CellValue::Boolean(value) => {
2165 let reference = cell_reference(column_index, row_number);
2166 let value_text = if *value { "1" } else { "0" };
2167 let style_text = style.map(|value| value.to_string());
2168
2169 let mut start = BytesStart::new("c");
2170 start.push_attribute(("r", reference.as_str()));
2171 if let Some(style_text) = &style_text {
2172 start.push_attribute(("s", style_text.as_str()));
2173 }
2174 start.push_attribute(("t", "b"));
2175 writer.write_event(Event::Start(start))?;
2176 writer.write_event(Event::Start(BytesStart::new("v")))?;
2177 writer.write_event(Event::Text(BytesText::new(value_text)))?;
2178 writer.write_event(Event::End(BytesEnd::new("v")))?;
2179 writer.write_event(Event::End(BytesEnd::new("c")))?;
2180 }
2181
2182 CellValue::Date(date) => {
2185 let reference = cell_reference(column_index, row_number);
2186 let serial = excel_serial_datetime(date);
2187 let number_text = serial.to_string();
2188 let style_text = style.map(|value| value.to_string());
2189
2190 let mut start = BytesStart::new("c");
2191 start.push_attribute(("r", reference.as_str()));
2192 if let Some(style_text) = &style_text {
2193 start.push_attribute(("s", style_text.as_str()));
2194 }
2195 writer.write_event(Event::Start(start))?;
2196 writer.write_event(Event::Start(BytesStart::new("v")))?;
2197 writer.write_event(Event::Text(BytesText::new(&number_text)))?;
2198 writer.write_event(Event::End(BytesEnd::new("v")))?;
2199 writer.write_event(Event::End(BytesEnd::new("c")))?;
2200 }
2201
2202 CellValue::Number(number) => {
2203 let reference = cell_reference(column_index, row_number);
2204 let number_text = number.to_string();
2205 let style_text = style.map(|value| value.to_string());
2206
2207 let mut start = BytesStart::new("c");
2208 start.push_attribute(("r", reference.as_str()));
2209 if let Some(style_text) = &style_text {
2210 start.push_attribute(("s", style_text.as_str()));
2211 }
2212 writer.write_event(Event::Start(start))?;
2213 writer.write_event(Event::Start(BytesStart::new("v")))?;
2214 writer.write_event(Event::Text(BytesText::new(&number_text)))?;
2215 writer.write_event(Event::End(BytesEnd::new("v")))?;
2216 writer.write_event(Event::End(BytesEnd::new("c")))?;
2217 }
2218
2219 CellValue::Formula {
2220 formula,
2221 cached_value,
2222 } => {
2223 let reference = cell_reference(column_index, row_number);
2224 let style_text = style.map(|value| value.to_string());
2225
2226 let mut start = BytesStart::new("c");
2227 start.push_attribute(("r", reference.as_str()));
2228 if let Some(style_text) = &style_text {
2229 start.push_attribute(("s", style_text.as_str()));
2230 }
2231 writer.write_event(Event::Start(start))?;
2232
2233 if let Some(formula_text) = formula {
2237 writer.write_event(Event::Start(BytesStart::new("f")))?;
2238 writer.write_event(Event::Text(BytesText::new(formula_text)))?;
2239 writer.write_event(Event::End(BytesEnd::new("f")))?;
2240 }
2241 if let Some(value) = cached_value {
2242 let value_text = value.to_string();
2243 writer.write_event(Event::Start(BytesStart::new("v")))?;
2244 writer.write_event(Event::Text(BytesText::new(&value_text)))?;
2245 writer.write_event(Event::End(BytesEnd::new("v")))?;
2246 }
2247
2248 writer.write_event(Event::End(BytesEnd::new("c")))?;
2249 }
2250
2251 CellValue::ArrayFormula {
2255 formula,
2256 range,
2257 cached_value,
2258 } => {
2259 let reference = cell_reference(column_index, row_number);
2260 let style_text = style.map(|value| value.to_string());
2261
2262 let mut start = BytesStart::new("c");
2263 start.push_attribute(("r", reference.as_str()));
2264 if let Some(style_text) = &style_text {
2265 start.push_attribute(("s", style_text.as_str()));
2266 }
2267 writer.write_event(Event::Start(start))?;
2268
2269 let mut f_start = BytesStart::new("f");
2270 f_start.push_attribute(("t", "array"));
2271 f_start.push_attribute(("ref", range.as_str()));
2272 writer.write_event(Event::Start(f_start))?;
2273 writer.write_event(Event::Text(BytesText::new(formula)))?;
2274 writer.write_event(Event::End(BytesEnd::new("f")))?;
2275
2276 if let Some(value) = cached_value {
2277 let value_text = value.to_string();
2278 writer.write_event(Event::Start(BytesStart::new("v")))?;
2279 writer.write_event(Event::Text(BytesText::new(&value_text)))?;
2280 writer.write_event(Event::End(BytesEnd::new("v")))?;
2281 }
2282
2283 writer.write_event(Event::End(BytesEnd::new("c")))?;
2284 }
2285
2286 CellValue::SharedFormula {
2290 formula,
2291 group_index,
2292 range,
2293 cached_value,
2294 } => {
2295 let reference = cell_reference(column_index, row_number);
2296 let style_text = style.map(|value| value.to_string());
2297 let group_index_text = group_index.to_string();
2298
2299 let mut start = BytesStart::new("c");
2300 start.push_attribute(("r", reference.as_str()));
2301 if let Some(style_text) = &style_text {
2302 start.push_attribute(("s", style_text.as_str()));
2303 }
2304 writer.write_event(Event::Start(start))?;
2305
2306 let mut f_start = BytesStart::new("f");
2307 f_start.push_attribute(("t", "shared"));
2308 f_start.push_attribute(("si", group_index_text.as_str()));
2309 if let Some(range) = range {
2310 f_start.push_attribute(("ref", range.as_str()));
2311 }
2312 if let Some(formula_text) = formula {
2313 writer.write_event(Event::Start(f_start))?;
2314 writer.write_event(Event::Text(BytesText::new(formula_text)))?;
2315 writer.write_event(Event::End(BytesEnd::new("f")))?;
2316 } else {
2317 writer.write_event(Event::Empty(f_start))?;
2318 }
2319
2320 if let Some(value) = cached_value {
2321 let value_text = value.to_string();
2322 writer.write_event(Event::Start(BytesStart::new("v")))?;
2323 writer.write_event(Event::Text(BytesText::new(&value_text)))?;
2324 writer.write_event(Event::End(BytesEnd::new("v")))?;
2325 }
2326
2327 writer.write_event(Event::End(BytesEnd::new("c")))?;
2328 }
2329 }
2330
2331 Ok(())
2332}
2333
2334fn cell_reference(column_index: usize, row_number: usize) -> String {
2336 format!("{}{row_number}", column_letters(column_index))
2337}
2338
2339fn compute_dimension_ref(sheet: &Sheet) -> String {
2346 let mut max_row = 0usize;
2347 let mut max_column_count = 0usize;
2348
2349 for (row_index, row) in sheet.rows.iter().enumerate() {
2350 if !row.cells.is_empty() {
2351 max_row = max_row.max(row_index + 1);
2352 max_column_count = max_column_count.max(row.cells.len());
2353 }
2354 }
2355
2356 if let Some(table) = &sheet.table {
2357 if let Some((end_column_index, end_row)) = parse_range_end(&table.range) {
2358 max_row = max_row.max(end_row);
2359 max_column_count = max_column_count.max(end_column_index + 1);
2360 }
2361 }
2362
2363 if max_row == 0 || max_column_count == 0 {
2364 "A1".to_string()
2365 } else {
2366 format!("A1:{}{max_row}", column_letters(max_column_count - 1))
2367 }
2368}
2369
2370fn parse_range_end(range: &str) -> Option<(usize, usize)> {
2374 let (_, end) = range.split_once(':')?;
2375 let split_at = end.find(|character: char| character.is_ascii_digit())?;
2376 let (letters, digits) = end.split_at(split_at);
2377 if letters.is_empty() {
2378 return None;
2379 }
2380
2381 let mut column_index: usize = 0;
2382 for character in letters.chars() {
2383 if !character.is_ascii_alphabetic() {
2384 return None;
2385 }
2386 let digit = (character.to_ascii_uppercase() as u8 - b'A' + 1) as usize;
2387 column_index = column_index * 26 + digit;
2388 }
2389
2390 let row = digits.parse::<usize>().ok()?;
2391 Some((column_index - 1, row))
2392}
2393
2394fn excel_serial_datetime(date: &ExcelDateTime) -> f64 {
2407 let days_since_unix_epoch =
2408 days_from_civil(date.year as i64, date.month as i64, date.day as i64);
2409 let serial_day = days_since_unix_epoch + 25569;
2410 let fraction_of_day =
2411 (date.hour as f64 * 3600.0 + date.minute as f64 * 60.0 + date.second as f64) / 86400.0;
2412 serial_day as f64 + fraction_of_day
2413}
2414
2415fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
2419 let y = if m <= 2 { y - 1 } else { y };
2420 let era = if y >= 0 { y } else { y - 399 } / 400;
2421 let yoe = y - era * 400; let mp = (m + 9) % 12; let doy = (153 * mp + 2) / 5 + d - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146097 + doe - 719468
2426}
2427
2428fn column_letters(mut index: usize) -> String {
2431 let mut letters = Vec::new();
2432 loop {
2433 let remainder = index % 26;
2434 letters.push((b'A' + remainder as u8) as char);
2435 if index < 26 {
2436 break;
2437 }
2438 index = index / 26 - 1;
2439 }
2440 letters.iter().rev().collect()
2441}
2442
2443struct FontEntry {
2446 bold: bool,
2447 italic: bool,
2448 size: f64,
2449 color: String,
2450 name: String,
2451 underline: bool,
2452 strike: bool,
2453}
2454
2455enum FillEntry {
2461 None,
2462 Gray125,
2463 Solid(String),
2464 Pattern(PatternFill),
2465 Gradient(GradientFill),
2466}
2467
2468#[derive(Clone, PartialEq)]
2472struct BorderEntry {
2473 top: Option<(BorderStyle, String)>,
2474 bottom: Option<(BorderStyle, String)>,
2475 left: Option<(BorderStyle, String)>,
2476 right: Option<(BorderStyle, String)>,
2477 diagonal: Option<(BorderStyle, String)>,
2478 diagonal_up: bool,
2479 diagonal_down: bool,
2480}
2481
2482struct XfEntry {
2485 num_fmt_id: u32,
2486 font_id: usize,
2487 fill_id: usize,
2488 border_id: usize,
2489 xf_id: usize,
2493 horizontal_alignment: Option<HorizontalAlignment>,
2494 vertical_alignment: Option<VerticalAlignment>,
2495 wrap_text: bool,
2496 indent: Option<u32>,
2497 text_rotation: Option<u16>,
2498 shrink_to_fit: bool,
2499 locked: Option<bool>,
2500 formula_hidden: Option<bool>,
2501}
2502
2503struct DxfFontEntry {
2508 bold: bool,
2509 italic: bool,
2510 color: Option<String>,
2511}
2512
2513struct DxfEntry {
2517 font: Option<DxfFontEntry>,
2518 num_fmt: Option<(u32, String)>,
2519 fill_color: Option<String>,
2520 horizontal_alignment: Option<HorizontalAlignment>,
2521 vertical_alignment: Option<VerticalAlignment>,
2522}
2523
2524struct StylesBuild {
2533 num_fmts: Vec<(u32, String)>,
2534 fonts: Vec<FontEntry>,
2535 fills: Vec<FillEntry>,
2536 borders: Vec<BorderEntry>,
2537 cell_xfs: Vec<XfEntry>,
2538 cell_style_xfs: Vec<XfEntry>,
2543 dxfs: Vec<DxfEntry>,
2544}
2545
2546fn resolve_border_edge(edge: &BorderEdge) -> (BorderStyle, String) {
2549 (
2550 edge.style,
2551 edge.color.clone().unwrap_or_else(|| "FF000000".to_string()),
2552 )
2553}
2554
2555fn resolve_format_ids(
2562 format: &CellFormat,
2563 num_fmts: &mut Vec<(u32, String)>,
2564 next_num_fmt_id: &mut u32,
2565 fonts: &mut Vec<FontEntry>,
2566 fills: &mut Vec<FillEntry>,
2567 borders: &mut Vec<BorderEntry>,
2568) -> (u32, usize, usize, usize) {
2569 let num_fmt_id = match &format.number_format {
2570 None => 0,
2571 Some(code) => match num_fmts.iter().find(|(_, existing)| existing == code) {
2572 Some((id, _)) => *id,
2573 None => {
2574 let id = *next_num_fmt_id;
2575 *next_num_fmt_id += 1;
2576 num_fmts.push((id, code.clone()));
2577 id
2578 }
2579 },
2580 };
2581
2582 let font_id = if !format.bold
2583 && !format.italic
2584 && format.font_size.is_none()
2585 && format.font_color.is_none()
2586 && format.font_name.is_none()
2587 && !format.underline
2588 && !format.strike
2589 {
2590 0
2591 } else {
2592 let size = format.font_size.unwrap_or(11.0);
2593 let color = format
2594 .font_color
2595 .clone()
2596 .unwrap_or_else(|| "FF000000".to_string());
2597 let name = format
2598 .font_name
2599 .clone()
2600 .unwrap_or_else(|| "Calibri".to_string());
2601 match fonts.iter().position(|font| {
2602 font.bold == format.bold
2603 && font.italic == format.italic
2604 && font.size == size
2605 && font.color == color
2606 && font.name == name
2607 && font.underline == format.underline
2608 && font.strike == format.strike
2609 }) {
2610 Some(index) => index,
2611 None => {
2612 fonts.push(FontEntry {
2613 bold: format.bold,
2614 italic: format.italic,
2615 size,
2616 color,
2617 name,
2618 underline: format.underline,
2619 strike: format.strike,
2620 });
2621 fonts.len() - 1
2622 }
2623 }
2624 };
2625
2626 let fill_id =
2629 if let Some(gradient) = &format.gradient_fill {
2630 match fills.iter().position(
2631 |fill| matches!(fill, FillEntry::Gradient(existing) if existing == gradient),
2632 ) {
2633 Some(index) => index,
2634 None => {
2635 fills.push(FillEntry::Gradient(gradient.clone()));
2636 fills.len() - 1
2637 }
2638 }
2639 } else if let Some(pattern) = &format.pattern_fill {
2640 match fills.iter().position(
2641 |fill| matches!(fill, FillEntry::Pattern(existing) if existing == pattern),
2642 ) {
2643 Some(index) => index,
2644 None => {
2645 fills.push(FillEntry::Pattern(pattern.clone()));
2646 fills.len() - 1
2647 }
2648 }
2649 } else {
2650 match &format.fill_color {
2651 None => 0,
2652 Some(color) => {
2653 match fills.iter().position(
2654 |fill| matches!(fill, FillEntry::Solid(existing) if existing == color),
2655 ) {
2656 Some(index) => index,
2657 None => {
2658 fills.push(FillEntry::Solid(color.clone()));
2659 fills.len() - 1
2660 }
2661 }
2662 }
2663 }
2664 };
2665
2666 let border_id = if format.border == Border::default() {
2667 0
2668 } else {
2669 let candidate = BorderEntry {
2670 top: format.border.top.as_ref().map(resolve_border_edge),
2671 bottom: format.border.bottom.as_ref().map(resolve_border_edge),
2672 left: format.border.left.as_ref().map(resolve_border_edge),
2673 right: format.border.right.as_ref().map(resolve_border_edge),
2674 diagonal: format.border.diagonal.as_ref().map(resolve_border_edge),
2675 diagonal_up: format.border.diagonal_up,
2676 diagonal_down: format.border.diagonal_down,
2677 };
2678 match borders.iter().position(|existing| existing == &candidate) {
2679 Some(index) => index,
2680 None => {
2681 borders.push(candidate);
2682 borders.len() - 1
2683 }
2684 }
2685 };
2686
2687 (num_fmt_id, font_id, fill_id, border_id)
2688}
2689
2690fn build_styles(
2691 cell_formats: &[CellFormat],
2692 dxf_formats: &[CellFormat],
2693 named_cell_styles: &[NamedCellStyle],
2694) -> StylesBuild {
2695 let mut num_fmts: Vec<(u32, String)> = Vec::new();
2696 let mut fonts = vec![FontEntry {
2697 bold: false,
2698 italic: false,
2699 size: 11.0,
2700 color: "FF000000".to_string(),
2701 name: "Calibri".to_string(),
2702 underline: false,
2703 strike: false,
2704 }];
2705 let mut fills = vec![FillEntry::None, FillEntry::Gray125];
2706 let mut borders = vec![BorderEntry {
2707 top: None,
2708 bottom: None,
2709 left: None,
2710 right: None,
2711 diagonal: None,
2712 diagonal_up: false,
2713 diagonal_down: false,
2714 }];
2715 let mut cell_xfs = vec![XfEntry {
2716 num_fmt_id: 0,
2717 font_id: 0,
2718 fill_id: 0,
2719 border_id: 0,
2720 xf_id: 0,
2721 horizontal_alignment: None,
2722 vertical_alignment: None,
2723 wrap_text: false,
2724 indent: None,
2725 text_rotation: None,
2726 shrink_to_fit: false,
2727 locked: None,
2728 formula_hidden: None,
2729 }];
2730
2731 let mut next_num_fmt_id = FIRST_CUSTOM_NUM_FMT_ID;
2732
2733 for format in cell_formats {
2734 let (num_fmt_id, font_id, fill_id, border_id) = resolve_format_ids(
2735 format,
2736 &mut num_fmts,
2737 &mut next_num_fmt_id,
2738 &mut fonts,
2739 &mut fills,
2740 &mut borders,
2741 );
2742
2743 let xf_id = format
2749 .named_style
2750 .as_ref()
2751 .and_then(|name| {
2752 named_cell_styles
2753 .iter()
2754 .position(|style| &style.name == name)
2755 })
2756 .map(|index| index + 1)
2757 .unwrap_or(0);
2758
2759 cell_xfs.push(XfEntry {
2760 num_fmt_id,
2761 font_id,
2762 fill_id,
2763 border_id,
2764 xf_id,
2765 horizontal_alignment: format.horizontal_alignment,
2766 vertical_alignment: format.vertical_alignment,
2767 wrap_text: format.wrap_text,
2768 indent: format.indent,
2769 text_rotation: format.text_rotation,
2770 shrink_to_fit: format.shrink_to_fit,
2771 locked: format.locked,
2772 formula_hidden: format.formula_hidden,
2773 });
2774 }
2775
2776 let mut cell_style_xfs: Vec<XfEntry> = Vec::new();
2782 for style in named_cell_styles {
2783 let (num_fmt_id, font_id, fill_id, border_id) = resolve_format_ids(
2784 &style.format,
2785 &mut num_fmts,
2786 &mut next_num_fmt_id,
2787 &mut fonts,
2788 &mut fills,
2789 &mut borders,
2790 );
2791 cell_style_xfs.push(XfEntry {
2792 num_fmt_id,
2793 font_id,
2794 fill_id,
2795 border_id,
2796 xf_id: 0,
2797 horizontal_alignment: style.format.horizontal_alignment,
2798 vertical_alignment: style.format.vertical_alignment,
2799 wrap_text: style.format.wrap_text,
2800 indent: style.format.indent,
2801 text_rotation: style.format.text_rotation,
2802 shrink_to_fit: style.format.shrink_to_fit,
2803 locked: style.format.locked,
2804 formula_hidden: style.format.formula_hidden,
2805 });
2806 }
2807
2808 let mut dxfs: Vec<DxfEntry> = Vec::new();
2813 for format in dxf_formats {
2814 let font = if format.bold || format.italic || format.font_color.is_some() {
2815 Some(DxfFontEntry {
2816 bold: format.bold,
2817 italic: format.italic,
2818 color: format.font_color.clone(),
2819 })
2820 } else {
2821 None
2822 };
2823
2824 let num_fmt = format.number_format.as_ref().map(|code| {
2825 let id = match num_fmts.iter().find(|(_, existing)| existing == code) {
2826 Some((id, _)) => *id,
2827 None => {
2828 let id = next_num_fmt_id;
2829 next_num_fmt_id += 1;
2830 id
2831 }
2832 };
2833 (id, code.clone())
2834 });
2835
2836 dxfs.push(DxfEntry {
2837 font,
2838 num_fmt,
2839 fill_color: format.fill_color.clone(),
2840 horizontal_alignment: format.horizontal_alignment,
2841 vertical_alignment: format.vertical_alignment,
2842 });
2843 }
2844
2845 StylesBuild {
2846 num_fmts,
2847 fonts,
2848 fills,
2849 borders,
2850 cell_xfs,
2851 cell_style_xfs,
2852 dxfs,
2853 }
2854}
2855
2856fn border_style_str(style: BorderStyle) -> &'static str {
2857 match style {
2858 BorderStyle::Thin => "thin",
2859 BorderStyle::Medium => "medium",
2860 BorderStyle::Thick => "thick",
2861 BorderStyle::Dashed => "dashed",
2862 BorderStyle::Dotted => "dotted",
2863 BorderStyle::Double => "double",
2864 BorderStyle::Hair => "hair",
2865 }
2866}
2867
2868fn write_border_edge<W: Write>(
2872 writer: &mut Writer<W>,
2873 name: &str,
2874 edge: &Option<(BorderStyle, String)>,
2875) -> Result<()> {
2876 match edge {
2877 None => {
2878 writer.write_event(Event::Empty(BytesStart::new(name)))?;
2879 }
2880 Some((style, color)) => {
2881 let mut start = BytesStart::new(name);
2882 start.push_attribute(("style", border_style_str(*style)));
2883 writer.write_event(Event::Start(start))?;
2884 let mut color_start = BytesStart::new("color");
2885 color_start.push_attribute(("rgb", color.as_str()));
2886 writer.write_event(Event::Empty(color_start))?;
2887 writer.write_event(Event::End(BytesEnd::new(name)))?;
2888 }
2889 }
2890 Ok(())
2891}
2892
2893fn pattern_type_str(pattern_type: PatternType) -> &'static str {
2895 match pattern_type {
2896 PatternType::Gray125 => "gray125",
2897 PatternType::Gray0625 => "gray0625",
2898 PatternType::DarkGray => "darkGray",
2899 PatternType::MediumGray => "mediumGray",
2900 PatternType::LightGray => "lightGray",
2901 PatternType::DarkHorizontal => "darkHorizontal",
2902 PatternType::DarkVertical => "darkVertical",
2903 PatternType::DarkDown => "darkDown",
2904 PatternType::DarkUp => "darkUp",
2905 PatternType::DarkGrid => "darkGrid",
2906 PatternType::DarkTrellis => "darkTrellis",
2907 PatternType::LightHorizontal => "lightHorizontal",
2908 PatternType::LightVertical => "lightVertical",
2909 PatternType::LightDown => "lightDown",
2910 PatternType::LightUp => "lightUp",
2911 PatternType::LightGrid => "lightGrid",
2912 PatternType::LightTrellis => "lightTrellis",
2913 }
2914}
2915
2916fn horizontal_alignment_str(alignment: HorizontalAlignment) -> &'static str {
2917 match alignment {
2918 HorizontalAlignment::Left => "left",
2919 HorizontalAlignment::Center => "center",
2920 HorizontalAlignment::Right => "right",
2921 }
2922}
2923
2924fn vertical_alignment_str(alignment: VerticalAlignment) -> &'static str {
2925 match alignment {
2926 VerticalAlignment::Top => "top",
2927 VerticalAlignment::Center => "center",
2928 VerticalAlignment::Bottom => "bottom",
2929 }
2930}
2931
2932fn write_xf<W: Write>(writer: &mut Writer<W>, xf: &XfEntry) -> Result<()> {
2939 let num_fmt_id_text = xf.num_fmt_id.to_string();
2940 let font_id_text = xf.font_id.to_string();
2941 let fill_id_text = xf.fill_id.to_string();
2942 let border_id_text = xf.border_id.to_string();
2943 let xf_id_text = xf.xf_id.to_string();
2944
2945 let mut start = BytesStart::new("xf");
2946 start.push_attribute(("numFmtId", num_fmt_id_text.as_str()));
2947 start.push_attribute(("fontId", font_id_text.as_str()));
2948 start.push_attribute(("fillId", fill_id_text.as_str()));
2949 start.push_attribute(("borderId", border_id_text.as_str()));
2950 start.push_attribute(("xfId", xf_id_text.as_str()));
2951 if xf.num_fmt_id != 0 {
2952 start.push_attribute(("applyNumberFormat", "1"));
2953 }
2954 if xf.border_id != 0 {
2955 start.push_attribute(("applyBorder", "1"));
2956 }
2957 if xf.font_id != 0 {
2958 start.push_attribute(("applyFont", "1"));
2959 }
2960 if xf.fill_id != 0 {
2961 start.push_attribute(("applyFill", "1"));
2962 }
2963
2964 let needs_alignment = xf.horizontal_alignment.is_some()
2967 || xf.vertical_alignment.is_some()
2968 || xf.wrap_text
2969 || xf.indent.is_some()
2970 || xf.text_rotation.is_some()
2971 || xf.shrink_to_fit;
2972 let needs_protection = xf.locked.is_some() || xf.formula_hidden.is_some();
2973
2974 if needs_alignment {
2975 start.push_attribute(("applyAlignment", "1"));
2976 }
2977 if needs_protection {
2978 start.push_attribute(("applyProtection", "1"));
2979 }
2980
2981 if !needs_alignment && !needs_protection {
2982 writer.write_event(Event::Empty(start))?;
2983 return Ok(());
2984 }
2985 writer.write_event(Event::Start(start))?;
2986
2987 if needs_alignment {
2988 let indent_text = xf.indent.map(|value| value.to_string());
2989 let text_rotation_text = xf.text_rotation.map(|value| value.to_string());
2990 let mut alignment = BytesStart::new("alignment");
2991 if let Some(horizontal) = xf.horizontal_alignment {
2992 alignment.push_attribute(("horizontal", horizontal_alignment_str(horizontal)));
2993 }
2994 if let Some(vertical) = xf.vertical_alignment {
2995 alignment.push_attribute(("vertical", vertical_alignment_str(vertical)));
2996 }
2997 if xf.wrap_text {
2998 alignment.push_attribute(("wrapText", "1"));
2999 }
3000 if let Some(indent_text) = &indent_text {
3001 alignment.push_attribute(("indent", indent_text.as_str()));
3002 }
3003 if let Some(text_rotation_text) = &text_rotation_text {
3004 alignment.push_attribute(("textRotation", text_rotation_text.as_str()));
3005 }
3006 if xf.shrink_to_fit {
3007 alignment.push_attribute(("shrinkToFit", "1"));
3008 }
3009 writer.write_event(Event::Empty(alignment))?;
3010 }
3011
3012 if needs_protection {
3013 let mut protection = BytesStart::new("protection");
3014 if let Some(locked) = xf.locked {
3015 protection.push_attribute(("locked", if locked { "1" } else { "0" }));
3016 }
3017 if let Some(formula_hidden) = xf.formula_hidden {
3018 protection.push_attribute(("hidden", if formula_hidden { "1" } else { "0" }));
3019 }
3020 writer.write_event(Event::Empty(protection))?;
3021 }
3022
3023 writer.write_event(Event::End(BytesEnd::new("xf")))?;
3024 Ok(())
3025}
3026
3027fn to_styles_xml(
3037 cell_formats: &[CellFormat],
3038 dxf_formats: &[CellFormat],
3039 table_styles: &[TableStyle],
3040 named_cell_styles: &[NamedCellStyle],
3041) -> Result<Vec<u8>> {
3042 let built = build_styles(cell_formats, dxf_formats, named_cell_styles);
3043 let mut writer = Writer::new(Vec::new());
3044
3045 writer.write_event(Event::Decl(BytesDecl::new(
3046 "1.0",
3047 Some("UTF-8"),
3048 Some("yes"),
3049 )))?;
3050
3051 let mut root = BytesStart::new("styleSheet");
3052 root.push_attribute(("xmlns", SPREADSHEETML_NAMESPACE));
3053 writer.write_event(Event::Start(root))?;
3054
3055 if !built.num_fmts.is_empty() {
3056 let count = built.num_fmts.len().to_string();
3057 let mut start = BytesStart::new("numFmts");
3058 start.push_attribute(("count", count.as_str()));
3059 writer.write_event(Event::Start(start))?;
3060 for (id, code) in &built.num_fmts {
3061 let id_text = id.to_string();
3062 let mut num_fmt = BytesStart::new("numFmt");
3063 num_fmt.push_attribute(("numFmtId", id_text.as_str()));
3064 num_fmt.push_attribute(("formatCode", code.as_str()));
3065 writer.write_event(Event::Empty(num_fmt))?;
3066 }
3067 writer.write_event(Event::End(BytesEnd::new("numFmts")))?;
3068 }
3069
3070 let fonts_count = built.fonts.len().to_string();
3071 let mut fonts_start = BytesStart::new("fonts");
3072 fonts_start.push_attribute(("count", fonts_count.as_str()));
3073 writer.write_event(Event::Start(fonts_start))?;
3074 for font in &built.fonts {
3075 writer.write_event(Event::Start(BytesStart::new("font")))?;
3076 if font.bold {
3077 writer.write_event(Event::Empty(BytesStart::new("b")))?;
3078 }
3079 if font.italic {
3080 writer.write_event(Event::Empty(BytesStart::new("i")))?;
3081 }
3082 if font.strike {
3086 writer.write_event(Event::Empty(BytesStart::new("strike")))?;
3087 }
3088 let size_text = font.size.to_string();
3089 let mut size = BytesStart::new("sz");
3090 size.push_attribute(("val", size_text.as_str()));
3091 writer.write_event(Event::Empty(size))?;
3092 let mut color = BytesStart::new("color");
3093 color.push_attribute(("rgb", font.color.as_str()));
3094 writer.write_event(Event::Empty(color))?;
3095 if font.underline {
3096 writer.write_event(Event::Empty(BytesStart::new("u")))?;
3097 }
3098 let mut name = BytesStart::new("name");
3099 name.push_attribute(("val", font.name.as_str()));
3100 writer.write_event(Event::Empty(name))?;
3101 let mut family = BytesStart::new("family");
3102 family.push_attribute(("val", "2"));
3103 writer.write_event(Event::Empty(family))?;
3104 writer.write_event(Event::End(BytesEnd::new("font")))?;
3105 }
3106 writer.write_event(Event::End(BytesEnd::new("fonts")))?;
3107
3108 let fills_count = built.fills.len().to_string();
3109 let mut fills_start = BytesStart::new("fills");
3110 fills_start.push_attribute(("count", fills_count.as_str()));
3111 writer.write_event(Event::Start(fills_start))?;
3112 for fill in &built.fills {
3113 writer.write_event(Event::Start(BytesStart::new("fill")))?;
3114 match fill {
3115 FillEntry::None => {
3116 let mut pattern = BytesStart::new("patternFill");
3117 pattern.push_attribute(("patternType", "none"));
3118 writer.write_event(Event::Empty(pattern))?;
3119 }
3120 FillEntry::Gray125 => {
3121 let mut pattern = BytesStart::new("patternFill");
3122 pattern.push_attribute(("patternType", "gray125"));
3123 writer.write_event(Event::Empty(pattern))?;
3124 }
3125 FillEntry::Solid(color) => {
3126 let mut pattern = BytesStart::new("patternFill");
3127 pattern.push_attribute(("patternType", "solid"));
3128 writer.write_event(Event::Start(pattern))?;
3129 let mut fg_color = BytesStart::new("fgColor");
3130 fg_color.push_attribute(("rgb", color.as_str()));
3131 writer.write_event(Event::Empty(fg_color))?;
3132 writer.write_event(Event::End(BytesEnd::new("patternFill")))?;
3133 }
3134 FillEntry::Pattern(pattern_fill) => {
3136 let mut pattern = BytesStart::new("patternFill");
3137 pattern
3138 .push_attribute(("patternType", pattern_type_str(pattern_fill.pattern_type)));
3139 writer.write_event(Event::Start(pattern))?;
3140 if let Some(fg_color) = &pattern_fill.fg_color {
3141 let mut fg = BytesStart::new("fgColor");
3142 fg.push_attribute(("rgb", fg_color.as_str()));
3143 writer.write_event(Event::Empty(fg))?;
3144 }
3145 if let Some(bg_color) = &pattern_fill.bg_color {
3146 let mut bg = BytesStart::new("bgColor");
3147 bg.push_attribute(("rgb", bg_color.as_str()));
3148 writer.write_event(Event::Empty(bg))?;
3149 }
3150 writer.write_event(Event::End(BytesEnd::new("patternFill")))?;
3151 }
3152 FillEntry::Gradient(gradient) => {
3155 let degree_text = gradient.angle.to_string();
3156 let mut gradient_start = BytesStart::new("gradientFill");
3157 gradient_start.push_attribute(("degree", degree_text.as_str()));
3158 writer.write_event(Event::Start(gradient_start))?;
3159 for stop in &gradient.stops {
3160 let position_text = stop.position.to_string();
3161 let mut gs = BytesStart::new("stop");
3162 gs.push_attribute(("position", position_text.as_str()));
3163 writer.write_event(Event::Start(gs))?;
3164 let mut color = BytesStart::new("color");
3165 color.push_attribute(("rgb", stop.color.as_str()));
3166 writer.write_event(Event::Empty(color))?;
3167 writer.write_event(Event::End(BytesEnd::new("stop")))?;
3168 }
3169 writer.write_event(Event::End(BytesEnd::new("gradientFill")))?;
3170 }
3171 }
3172 writer.write_event(Event::End(BytesEnd::new("fill")))?;
3173 }
3174 writer.write_event(Event::End(BytesEnd::new("fills")))?;
3175
3176 let borders_count = built.borders.len().to_string();
3182 let mut borders_start = BytesStart::new("borders");
3183 borders_start.push_attribute(("count", borders_count.as_str()));
3184 writer.write_event(Event::Start(borders_start))?;
3185 for border in &built.borders {
3186 let mut border_start = BytesStart::new("border");
3187 if border.diagonal_up {
3188 border_start.push_attribute(("diagonalUp", "1"));
3189 }
3190 if border.diagonal_down {
3191 border_start.push_attribute(("diagonalDown", "1"));
3192 }
3193 writer.write_event(Event::Start(border_start))?;
3194 write_border_edge(&mut writer, "left", &border.left)?;
3195 write_border_edge(&mut writer, "right", &border.right)?;
3196 write_border_edge(&mut writer, "top", &border.top)?;
3197 write_border_edge(&mut writer, "bottom", &border.bottom)?;
3198 write_border_edge(&mut writer, "diagonal", &border.diagonal)?;
3199 writer.write_event(Event::End(BytesEnd::new("border")))?;
3200 }
3201 writer.write_event(Event::End(BytesEnd::new("borders")))?;
3202
3203 let cell_style_xfs_count = (1 + built.cell_style_xfs.len()).to_string();
3207 let mut cell_style_xfs_start = BytesStart::new("cellStyleXfs");
3208 cell_style_xfs_start.push_attribute(("count", cell_style_xfs_count.as_str()));
3209 writer.write_event(Event::Start(cell_style_xfs_start))?;
3210 writer.write_raw(r#"<xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>"#)?;
3211 for xf in &built.cell_style_xfs {
3212 write_xf(&mut writer, xf)?;
3213 }
3214 writer.write_event(Event::End(BytesEnd::new("cellStyleXfs")))?;
3215
3216 let cell_xfs_count = built.cell_xfs.len().to_string();
3217 let mut cell_xfs_start = BytesStart::new("cellXfs");
3218 cell_xfs_start.push_attribute(("count", cell_xfs_count.as_str()));
3219 writer.write_event(Event::Start(cell_xfs_start))?;
3220 for xf in &built.cell_xfs {
3221 write_xf(&mut writer, xf)?;
3222 }
3223 writer.write_event(Event::End(BytesEnd::new("cellXfs")))?;
3224
3225 let cell_styles_count = (1 + named_cell_styles.len()).to_string();
3228 let mut cell_styles_start = BytesStart::new("cellStyles");
3229 cell_styles_start.push_attribute(("count", cell_styles_count.as_str()));
3230 writer.write_event(Event::Start(cell_styles_start))?;
3231 writer.write_raw(r#"<cellStyle name="Normal" xfId="0" builtinId="0"/>"#)?;
3232 for (index, style) in named_cell_styles.iter().enumerate() {
3233 let xf_id_text = (index + 1).to_string();
3234 let mut cell_style = BytesStart::new("cellStyle");
3235 cell_style.push_attribute(("name", style.name.as_str()));
3236 cell_style.push_attribute(("xfId", xf_id_text.as_str()));
3237 if let Some(builtin_id) = style.builtin_id {
3238 let builtin_id_text = builtin_id.to_string();
3239 cell_style.push_attribute(("builtinId", builtin_id_text.as_str()));
3240 }
3241 writer.write_event(Event::Empty(cell_style))?;
3242 }
3243 writer.write_event(Event::End(BytesEnd::new("cellStyles")))?;
3244
3245 if !built.dxfs.is_empty() {
3246 let dxfs_count = built.dxfs.len().to_string();
3247 let mut dxfs_start = BytesStart::new("dxfs");
3248 dxfs_start.push_attribute(("count", dxfs_count.as_str()));
3249 writer.write_event(Event::Start(dxfs_start))?;
3250 for dxf in &built.dxfs {
3251 writer.write_event(Event::Start(BytesStart::new("dxf")))?;
3252
3253 if let Some(font) = &dxf.font {
3256 writer.write_event(Event::Start(BytesStart::new("font")))?;
3257 if font.bold {
3258 writer.write_event(Event::Empty(BytesStart::new("b")))?;
3259 }
3260 if font.italic {
3261 writer.write_event(Event::Empty(BytesStart::new("i")))?;
3262 }
3263 if let Some(color) = &font.color {
3264 let mut color_start = BytesStart::new("color");
3265 color_start.push_attribute(("rgb", color.as_str()));
3266 writer.write_event(Event::Empty(color_start))?;
3267 }
3268 writer.write_event(Event::End(BytesEnd::new("font")))?;
3269 }
3270
3271 if let Some((id, code)) = &dxf.num_fmt {
3272 let id_text = id.to_string();
3273 let mut num_fmt = BytesStart::new("numFmt");
3274 num_fmt.push_attribute(("numFmtId", id_text.as_str()));
3275 num_fmt.push_attribute(("formatCode", code.as_str()));
3276 writer.write_event(Event::Empty(num_fmt))?;
3277 }
3278
3279 if let Some(color) = &dxf.fill_color {
3280 writer.write_event(Event::Start(BytesStart::new("fill")))?;
3281 let mut pattern = BytesStart::new("patternFill");
3282 pattern.push_attribute(("patternType", "solid"));
3283 writer.write_event(Event::Start(pattern))?;
3284 let mut bg_color = BytesStart::new("bgColor");
3287 bg_color.push_attribute(("rgb", color.as_str()));
3288 writer.write_event(Event::Empty(bg_color))?;
3289 writer.write_event(Event::End(BytesEnd::new("patternFill")))?;
3290 writer.write_event(Event::End(BytesEnd::new("fill")))?;
3291 }
3292
3293 if dxf.horizontal_alignment.is_some() || dxf.vertical_alignment.is_some() {
3294 let mut alignment = BytesStart::new("alignment");
3295 if let Some(horizontal) = dxf.horizontal_alignment {
3296 alignment.push_attribute(("horizontal", horizontal_alignment_str(horizontal)));
3297 }
3298 if let Some(vertical) = dxf.vertical_alignment {
3299 alignment.push_attribute(("vertical", vertical_alignment_str(vertical)));
3300 }
3301 writer.write_event(Event::Empty(alignment))?;
3302 }
3303
3304 writer.write_event(Event::End(BytesEnd::new("dxf")))?;
3305 }
3306 writer.write_event(Event::End(BytesEnd::new("dxfs")))?;
3307 }
3308
3309 if !table_styles.is_empty() {
3315 let count_text = table_styles.len().to_string();
3316 let mut table_styles_start = BytesStart::new("tableStyles");
3317 table_styles_start.push_attribute(("count", count_text.as_str()));
3318 writer.write_event(Event::Start(table_styles_start))?;
3319 for style in table_styles {
3320 let element_count_text = style.elements.len().to_string();
3321 let mut table_style_start = BytesStart::new("tableStyle");
3322 table_style_start.push_attribute(("name", style.name.as_str()));
3323 table_style_start.push_attribute(("pivot", "0"));
3324 table_style_start.push_attribute(("count", element_count_text.as_str()));
3325 writer.write_event(Event::Start(table_style_start))?;
3326 for element in &style.elements {
3327 let dxf_id = dxf_formats
3328 .iter()
3329 .position(|format| format == &element.format)
3330 .unwrap_or(0);
3331 let dxf_id_text = dxf_id.to_string();
3332 let mut element_start = BytesStart::new("tableStyleElement");
3333 element_start
3334 .push_attribute(("type", table_style_element_type_str(element.element_type)));
3335 element_start.push_attribute(("dxfId", dxf_id_text.as_str()));
3336 writer.write_event(Event::Empty(element_start))?;
3337 }
3338 writer.write_event(Event::End(BytesEnd::new("tableStyle")))?;
3339 }
3340 writer.write_event(Event::End(BytesEnd::new("tableStyles")))?;
3341 }
3342
3343 writer.write_event(Event::End(BytesEnd::new("styleSheet")))?;
3344
3345 Ok(writer.into_inner())
3346}
3347
3348fn table_style_element_type_str(element_type: TableStyleElementType) -> &'static str {
3351 match element_type {
3352 TableStyleElementType::WholeTable => "wholeTable",
3353 TableStyleElementType::HeaderRow => "headerRow",
3354 TableStyleElementType::TotalRow => "totalRow",
3355 TableStyleElementType::FirstRowStripe => "firstRowStripe",
3356 TableStyleElementType::SecondRowStripe => "secondRowStripe",
3357 TableStyleElementType::FirstColumnStripe => "firstColumnStripe",
3358 TableStyleElementType::SecondColumnStripe => "secondColumnStripe",
3359 TableStyleElementType::FirstColumn => "firstColumn",
3360 TableStyleElementType::LastColumn => "lastColumn",
3361 }
3362}
3363
3364fn to_shared_strings_xml(shared_strings: &SharedStrings) -> Result<Vec<u8>> {
3366 let mut writer = Writer::new(Vec::new());
3367
3368 writer.write_event(Event::Decl(BytesDecl::new(
3369 "1.0",
3370 Some("UTF-8"),
3371 Some("yes"),
3372 )))?;
3373
3374 let mut root = BytesStart::new("sst");
3375 root.push_attribute(("xmlns", SPREADSHEETML_NAMESPACE));
3376 let count = shared_strings.total_count.to_string();
3377 let unique_count = shared_strings.entries.len().to_string();
3378 root.push_attribute(("count", count.as_str()));
3379 root.push_attribute(("uniqueCount", unique_count.as_str()));
3380 writer.write_event(Event::Start(root))?;
3381
3382 for entry in &shared_strings.entries {
3383 match entry {
3384 SharedStringEntry::Plain(text) => {
3385 writer.write_event(Event::Start(BytesStart::new("si")))?;
3386 writer.write_event(Event::Start(BytesStart::new("t")))?;
3387 writer.write_event(Event::Text(BytesText::new(text)))?;
3388 writer.write_event(Event::End(BytesEnd::new("t")))?;
3389 writer.write_event(Event::End(BytesEnd::new("si")))?;
3390 }
3391 SharedStringEntry::Rich(runs) => {
3396 writer.write_event(Event::Start(BytesStart::new("si")))?;
3397 for run in runs {
3398 write_rich_text_run(&mut writer, run)?;
3399 }
3400 writer.write_event(Event::End(BytesEnd::new("si")))?;
3401 }
3402 }
3403 }
3404
3405 writer.write_event(Event::End(BytesEnd::new("sst")))?;
3406
3407 Ok(writer.into_inner())
3408}
3409
3410fn write_rich_text_run<W: Write>(writer: &mut Writer<W>, run: &RichTextRun) -> Result<()> {
3416 writer.write_event(Event::Start(BytesStart::new("r")))?;
3417 let has_run_properties = run.bold
3418 || run.italic
3419 || run.font_size.is_some()
3420 || run.font_color.is_some()
3421 || run.font_name.is_some()
3422 || run.underline
3423 || run.strike;
3424 if has_run_properties {
3425 writer.write_event(Event::Start(BytesStart::new("rPr")))?;
3426 if let Some(font_name) = &run.font_name {
3427 let mut r_font = BytesStart::new("rFont");
3428 r_font.push_attribute(("val", font_name.as_str()));
3429 writer.write_event(Event::Empty(r_font))?;
3430 }
3431 if run.bold {
3432 writer.write_event(Event::Empty(BytesStart::new("b")))?;
3433 }
3434 if run.italic {
3435 writer.write_event(Event::Empty(BytesStart::new("i")))?;
3436 }
3437 if run.strike {
3438 writer.write_event(Event::Empty(BytesStart::new("strike")))?;
3439 }
3440 if let Some(size) = run.font_size {
3441 let size_text = size.to_string();
3442 let mut sz = BytesStart::new("sz");
3443 sz.push_attribute(("val", size_text.as_str()));
3444 writer.write_event(Event::Empty(sz))?;
3445 }
3446 if let Some(color) = &run.font_color {
3447 let mut color_start = BytesStart::new("color");
3448 color_start.push_attribute(("rgb", color.as_str()));
3449 writer.write_event(Event::Empty(color_start))?;
3450 }
3451 if run.underline {
3452 writer.write_event(Event::Empty(BytesStart::new("u")))?;
3453 }
3454 writer.write_event(Event::End(BytesEnd::new("rPr")))?;
3455 }
3456 writer.write_event(Event::Start(BytesStart::new("t")))?;
3457 writer.write_event(Event::Text(BytesText::new(&run.text)))?;
3458 writer.write_event(Event::End(BytesEnd::new("t")))?;
3459 writer.write_event(Event::End(BytesEnd::new("r")))?;
3460 Ok(())
3461}
3462
3463fn to_core_properties_xml(properties: &DocumentProperties) -> Result<Vec<u8>> {
3468 let mut writer = Writer::new(Vec::new());
3469
3470 writer.write_event(Event::Decl(BytesDecl::new(
3471 "1.0",
3472 Some("UTF-8"),
3473 Some("yes"),
3474 )))?;
3475
3476 let mut root = BytesStart::new("cp:coreProperties");
3477 root.push_attribute(("xmlns:cp", CORE_PROPERTIES_NAMESPACE));
3478 root.push_attribute(("xmlns:dc", DC_NAMESPACE));
3479 root.push_attribute(("xmlns:dcterms", DCTERMS_NAMESPACE));
3480 root.push_attribute(("xmlns:xsi", XSI_NAMESPACE));
3481
3482 let has_any = properties.title.is_some()
3483 || properties.author.is_some()
3484 || properties.subject.is_some()
3485 || properties.keywords.is_some();
3486 if !has_any {
3487 writer.write_event(Event::Empty(root))?;
3488 return Ok(writer.into_inner());
3489 }
3490 writer.write_event(Event::Start(root))?;
3491
3492 if let Some(title) = &properties.title {
3493 write_element_text(&mut writer, "dc:title", title)?;
3494 }
3495 if let Some(author) = &properties.author {
3496 write_element_text(&mut writer, "dc:creator", author)?;
3497 }
3498 if let Some(subject) = &properties.subject {
3499 write_element_text(&mut writer, "dc:subject", subject)?;
3500 }
3501 if let Some(keywords) = &properties.keywords {
3502 write_element_text(&mut writer, "cp:keywords", keywords)?;
3503 }
3504
3505 writer.write_event(Event::End(BytesEnd::new("cp:coreProperties")))?;
3506
3507 Ok(writer.into_inner())
3508}
3509
3510fn to_extended_properties_xml(properties: &DocumentProperties) -> Result<Vec<u8>> {
3516 let mut writer = Writer::new(Vec::new());
3517
3518 writer.write_event(Event::Decl(BytesDecl::new(
3519 "1.0",
3520 Some("UTF-8"),
3521 Some("yes"),
3522 )))?;
3523
3524 let mut root = BytesStart::new("Properties");
3525 root.push_attribute(("xmlns", EXTENDED_PROPERTIES_NAMESPACE));
3526 writer.write_event(Event::Start(root))?;
3527
3528 writer.write_event(Event::Start(BytesStart::new("Application")))?;
3529 writer.write_event(Event::Text(BytesText::new("office-toolkit")))?;
3530 writer.write_event(Event::End(BytesEnd::new("Application")))?;
3531
3532 if let Some(company) = &properties.company {
3533 write_element_text(&mut writer, "Company", company)?;
3534 }
3535 if let Some(manager) = &properties.manager {
3536 write_element_text(&mut writer, "Manager", manager)?;
3537 }
3538 if let Some(hyperlink_base) = &properties.hyperlink_base {
3539 write_element_text(&mut writer, "HyperlinkBase", hyperlink_base)?;
3540 }
3541
3542 writer.write_event(Event::End(BytesEnd::new("Properties")))?;
3543
3544 Ok(writer.into_inner())
3545}
3546
3547fn to_custom_properties_xml(
3555 custom_properties: &[(String, CustomPropertyValue)],
3556) -> Result<Vec<u8>> {
3557 let mut writer = Writer::new(Vec::new());
3558
3559 writer.write_event(Event::Decl(BytesDecl::new(
3560 "1.0",
3561 Some("UTF-8"),
3562 Some("yes"),
3563 )))?;
3564
3565 let mut root = BytesStart::new("Properties");
3566 root.push_attribute(("xmlns", CUSTOM_PROPERTIES_NAMESPACE));
3567 root.push_attribute(("xmlns:vt", CUSTOM_PROPERTIES_VT_NAMESPACE));
3568 writer.write_event(Event::Start(root))?;
3569
3570 for (index, (name, value)) in custom_properties.iter().enumerate() {
3571 let pid = FIRST_CUSTOM_PROPERTY_PID + index as i32;
3572
3573 let mut property = BytesStart::new("property");
3574 property.push_attribute(("fmtid", CUSTOM_PROPERTY_FMTID));
3575 property.push_attribute(("pid", pid.to_string().as_str()));
3576 property.push_attribute(("name", name.as_str()));
3577 writer.write_event(Event::Start(property))?;
3578
3579 let (variant_element, text) = match value {
3580 CustomPropertyValue::Text(text) => ("vt:lpwstr", text.clone()),
3581 CustomPropertyValue::Bool(value) => (
3582 "vt:bool",
3583 if *value {
3584 "true".to_string()
3585 } else {
3586 "false".to_string()
3587 },
3588 ),
3589 CustomPropertyValue::Int(value) => ("vt:i4", value.to_string()),
3590 CustomPropertyValue::Number(value) => ("vt:r8", value.to_string()),
3591 };
3592 writer.write_event(Event::Start(BytesStart::new(variant_element)))?;
3593 writer.write_event(Event::Text(BytesText::new(&text)))?;
3594 writer.write_event(Event::End(BytesEnd::new(variant_element)))?;
3595
3596 writer.write_event(Event::End(BytesEnd::new("property")))?;
3597 }
3598
3599 writer.write_event(Event::End(BytesEnd::new("Properties")))?;
3600
3601 Ok(writer.into_inner())
3602}
3603
3604fn to_theme_xml() -> Vec<u8> {
3615 const THEME_XML: &str = concat!(
3616 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#,
3617 r#"<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office">"#,
3618 "<a:themeElements>",
3619 r#"<a:clrScheme name="Office">"#,
3620 r#"<a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>"#,
3621 r#"<a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>"#,
3622 r#"<a:dk2><a:srgbClr val="44546A"/></a:dk2>"#,
3623 r#"<a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>"#,
3624 r#"<a:accent1><a:srgbClr val="4472C4"/></a:accent1>"#,
3625 r#"<a:accent2><a:srgbClr val="ED7D31"/></a:accent2>"#,
3626 r#"<a:accent3><a:srgbClr val="A5A5A5"/></a:accent3>"#,
3627 r#"<a:accent4><a:srgbClr val="FFC000"/></a:accent4>"#,
3628 r#"<a:accent5><a:srgbClr val="5B9BD5"/></a:accent5>"#,
3629 r#"<a:accent6><a:srgbClr val="70AD47"/></a:accent6>"#,
3630 r#"<a:hlink><a:srgbClr val="0563C1"/></a:hlink>"#,
3631 r#"<a:folHlink><a:srgbClr val="954F72"/></a:folHlink>"#,
3632 "</a:clrScheme>",
3633 r#"<a:fontScheme name="Office">"#,
3634 r#"<a:majorFont><a:latin typeface="Calibri Light"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>"#,
3635 r#"<a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>"#,
3636 "</a:fontScheme>",
3637 r#"<a:fmtScheme name="Office">"#,
3638 "<a:fillStyleLst>",
3639 r#"<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>"#,
3640 r#"<a:gradFill rotWithShape="1"><a:gsLst>"#,
3641 r#"<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="50000"/><a:satMod val="300000"/></a:schemeClr></a:gs>"#,
3642 r#"<a:gs pos="35000"><a:schemeClr val="phClr"><a:tint val="37000"/><a:satMod val="300000"/></a:schemeClr></a:gs>"#,
3643 r#"<a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="15000"/><a:satMod val="350000"/></a:schemeClr></a:gs>"#,
3644 r#"</a:gsLst><a:lin ang="16200000" scaled="1"/></a:gradFill>"#,
3645 r#"<a:gradFill rotWithShape="1"><a:gsLst>"#,
3646 r#"<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="100000"/><a:shade val="100000"/><a:satMod val="130000"/></a:schemeClr></a:gs>"#,
3647 r#"<a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="50000"/><a:shade val="100000"/><a:satMod val="350000"/></a:schemeClr></a:gs>"#,
3648 r#"</a:gsLst><a:lin ang="16200000" scaled="0"/></a:gradFill>"#,
3649 "</a:fillStyleLst>",
3650 "<a:lnStyleLst>",
3651 r#"<a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"><a:shade val="95000"/><a:satMod val="105000"/></a:schemeClr></a:solidFill><a:prstDash val="solid"/></a:ln>"#,
3652 r#"<a:ln w="25400" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln>"#,
3653 r#"<a:ln w="38100" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln>"#,
3654 "</a:lnStyleLst>",
3655 "<a:effectStyleLst>",
3656 r#"<a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="20000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="38000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle>"#,
3657 r#"<a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle>"#,
3658 "<a:effectStyle>",
3659 r#"<a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst>"#,
3660 r#"<a:scene3d><a:camera prst="orthographicFront"><a:rot lat="0" lon="0" rev="0"/></a:camera><a:lightRig rig="threePt" dir="t"><a:rot lat="0" lon="0" rev="1200000"/></a:lightRig></a:scene3d>"#,
3661 r#"<a:sp3d><a:bevelT w="63500" h="25400"/></a:sp3d>"#,
3662 "</a:effectStyle>",
3663 "</a:effectStyleLst>",
3664 "<a:bgFillStyleLst>",
3665 r#"<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>"#,
3666 r#"<a:gradFill rotWithShape="1"><a:gsLst>"#,
3667 r#"<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="40000"/><a:satMod val="350000"/></a:schemeClr></a:gs>"#,
3668 r#"<a:gs pos="40000"><a:schemeClr val="phClr"><a:tint val="45000"/><a:shade val="99000"/><a:satMod val="350000"/></a:schemeClr></a:gs>"#,
3669 r#"<a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="20000"/><a:satMod val="255000"/></a:schemeClr></a:gs>"#,
3670 r#"</a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="-80000" r="50000" b="180000"/></a:path></a:gradFill>"#,
3671 r#"<a:gradFill rotWithShape="1"><a:gsLst>"#,
3672 r#"<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="80000"/><a:satMod val="300000"/></a:schemeClr></a:gs>"#,
3673 r#"<a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="30000"/><a:satMod val="200000"/></a:schemeClr></a:gs>"#,
3674 r#"</a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path></a:gradFill>"#,
3675 "</a:bgFillStyleLst>",
3676 "</a:fmtScheme>",
3677 "</a:themeElements>",
3678 "</a:theme>",
3679 );
3680 THEME_XML.as_bytes().to_vec()
3681}
3682
3683fn to_external_link_xml(link: &ExternalWorkbookLink) -> Result<Vec<u8>> {
3689 let mut writer = Writer::new(Vec::new());
3690
3691 writer.write_event(Event::Decl(BytesDecl::new(
3692 "1.0",
3693 Some("UTF-8"),
3694 Some("yes"),
3695 )))?;
3696
3697 let mut root = BytesStart::new("externalLink");
3698 root.push_attribute(("xmlns", SPREADSHEETML_NAMESPACE));
3699 writer.write_event(Event::Start(root))?;
3700
3701 let mut external_book = BytesStart::new("externalBook");
3702 external_book.push_attribute(("xmlns:r", RELATIONSHIPS_NAMESPACE));
3703 external_book.push_attribute(("r:id", "rId1"));
3704 writer.write_event(Event::Start(external_book))?;
3705
3706 if !link.sheet_names.is_empty() {
3707 writer.write_event(Event::Start(BytesStart::new("sheetNames")))?;
3708 for name in &link.sheet_names {
3709 let mut sheet_name = BytesStart::new("sheetName");
3710 sheet_name.push_attribute(("val", name.as_str()));
3711 writer.write_event(Event::Empty(sheet_name))?;
3712 }
3713 writer.write_event(Event::End(BytesEnd::new("sheetNames")))?;
3714 }
3715
3716 if !link.defined_names.is_empty() {
3717 writer.write_event(Event::Start(BytesStart::new("definedNames")))?;
3718 for (name, refers_to) in &link.defined_names {
3719 let mut defined_name = BytesStart::new("definedName");
3720 defined_name.push_attribute(("name", name.as_str()));
3721 defined_name.push_attribute(("refersTo", refers_to.as_str()));
3722 writer.write_event(Event::Empty(defined_name))?;
3723 }
3724 writer.write_event(Event::End(BytesEnd::new("definedNames")))?;
3725 }
3726
3727 writer.write_event(Event::End(BytesEnd::new("externalBook")))?;
3728 writer.write_event(Event::End(BytesEnd::new("externalLink")))?;
3729
3730 Ok(writer.into_inner())
3731}
3732
3733fn to_table_xml(table: &ExcelTable, table_id: usize) -> Result<Vec<u8>> {
3740 let mut writer = Writer::new(Vec::new());
3741
3742 writer.write_event(Event::Decl(BytesDecl::new(
3743 "1.0",
3744 Some("UTF-8"),
3745 Some("yes"),
3746 )))?;
3747
3748 let table_id_text = table_id.to_string();
3749 let mut root = BytesStart::new("table");
3750 root.push_attribute(("xmlns", SPREADSHEETML_NAMESPACE));
3751 root.push_attribute(("id", table_id_text.as_str()));
3752 root.push_attribute(("name", table.name.as_str()));
3753 root.push_attribute(("displayName", table.name.as_str()));
3754 root.push_attribute(("ref", table.range.as_str()));
3755 if table.show_totals_row {
3761 root.push_attribute(("totalsRowShown", "1"));
3762 root.push_attribute(("totalsRowCount", "1"));
3763 } else {
3764 root.push_attribute(("totalsRowShown", "0"));
3765 }
3766 writer.write_event(Event::Start(root))?;
3767
3768 let auto_filter_ref = if table.show_totals_row {
3771 shrink_range_by_one_row(&table.range)
3772 } else {
3773 table.range.clone()
3774 };
3775 let mut auto_filter = BytesStart::new("autoFilter");
3776 auto_filter.push_attribute(("ref", auto_filter_ref.as_str()));
3777 writer.write_event(Event::Empty(auto_filter))?;
3778
3779 if let Some(sort_state) = &table.sort_state {
3783 write_sort_state(&mut writer, sort_state)?;
3784 }
3785
3786 let columns_count = table.columns.len().to_string();
3787 let mut table_columns_start = BytesStart::new("tableColumns");
3788 table_columns_start.push_attribute(("count", columns_count.as_str()));
3789 writer.write_event(Event::Start(table_columns_start))?;
3790 for (index, column) in table.columns.iter().enumerate() {
3791 write_table_column(&mut writer, index, column)?;
3792 }
3793 writer.write_event(Event::End(BytesEnd::new("tableColumns")))?;
3794
3795 let table_style_name = table
3796 .table_style_name
3797 .as_deref()
3798 .unwrap_or("TableStyleMedium2");
3799 let mut table_style_info = BytesStart::new("tableStyleInfo");
3800 table_style_info.push_attribute(("name", table_style_name));
3801 table_style_info.push_attribute(("showFirstColumn", "0"));
3802 table_style_info.push_attribute(("showLastColumn", "0"));
3803 table_style_info.push_attribute(("showRowStripes", "1"));
3804 table_style_info.push_attribute(("showColumnStripes", "0"));
3805 writer.write_event(Event::Empty(table_style_info))?;
3806
3807 writer.write_event(Event::End(BytesEnd::new("table")))?;
3808
3809 Ok(writer.into_inner())
3810}
3811
3812fn write_table_column<W: Write>(
3825 writer: &mut Writer<W>,
3826 index: usize,
3827 column: &TableColumn,
3828) -> Result<()> {
3829 let column_id_text = (index + 1).to_string();
3830 let mut table_column = BytesStart::new("tableColumn");
3831 table_column.push_attribute(("id", column_id_text.as_str()));
3832 table_column.push_attribute(("name", column.name.as_str()));
3833
3834 let (totals_row_function, totals_row_formula) = if column.totals_row_label.is_some() {
3839 (None, None)
3840 } else {
3841 let formula = match column.totals_row_function {
3842 Some(TotalsRowFunction::Custom) => column.totals_row_formula.clone(),
3843 _ => None,
3844 };
3845 (column.totals_row_function, formula)
3846 };
3847 if let Some(function) = totals_row_function {
3848 table_column.push_attribute(("totalsRowFunction", totals_row_function_str(function)));
3849 }
3850 if let Some(label) = &column.totals_row_label {
3851 table_column.push_attribute(("totalsRowLabel", label.as_str()));
3852 }
3853
3854 if column.calculated_formula.is_none() && totals_row_formula.is_none() {
3855 writer.write_event(Event::Empty(table_column))?;
3856 return Ok(());
3857 }
3858
3859 writer.write_event(Event::Start(table_column))?;
3860 if let Some(formula) = &column.calculated_formula {
3861 write_element_text(writer, "calculatedColumnFormula", formula)?;
3862 }
3863 if let Some(formula) = &totals_row_formula {
3864 write_element_text(writer, "totalsRowFormula", formula)?;
3865 }
3866 writer.write_event(Event::End(BytesEnd::new("tableColumn")))?;
3867 Ok(())
3868}
3869
3870fn totals_row_function_str(function: TotalsRowFunction) -> &'static str {
3871 match function {
3872 TotalsRowFunction::Sum => "sum",
3873 TotalsRowFunction::Average => "average",
3874 TotalsRowFunction::Count => "count",
3875 TotalsRowFunction::CountNums => "countNums",
3876 TotalsRowFunction::Max => "max",
3877 TotalsRowFunction::Min => "min",
3878 TotalsRowFunction::StdDev => "stdDev",
3879 TotalsRowFunction::Var => "var",
3880 TotalsRowFunction::Custom => "custom",
3881 }
3882}
3883
3884fn shrink_range_by_one_row(range: &str) -> String {
3888 let Some((start, end)) = range.split_once(':') else {
3889 return range.to_string();
3890 };
3891 let Some(split_at) = end.find(|character: char| character.is_ascii_digit()) else {
3892 return range.to_string();
3893 };
3894 let (letters, digits) = end.split_at(split_at);
3895 match digits.parse::<usize>() {
3896 Ok(row) if row > 1 => format!("{start}:{letters}{}", row - 1),
3897 _ => range.to_string(),
3898 }
3899}
3900
3901fn to_comments_xml(comments: &[CellComment]) -> Result<Vec<u8>> {
3906 let mut writer = Writer::new(Vec::new());
3907
3908 writer.write_event(Event::Decl(BytesDecl::new(
3909 "1.0",
3910 Some("UTF-8"),
3911 Some("yes"),
3912 )))?;
3913
3914 let mut root = BytesStart::new("comments");
3915 root.push_attribute(("xmlns", SPREADSHEETML_NAMESPACE));
3916 writer.write_event(Event::Start(root))?;
3917
3918 let mut authors: Vec<String> = Vec::new();
3919 for comment in comments {
3920 if !authors.contains(&comment.author) {
3921 authors.push(comment.author.clone());
3922 }
3923 }
3924
3925 writer.write_event(Event::Start(BytesStart::new("authors")))?;
3926 for author in &authors {
3927 write_element_text(&mut writer, "author", author)?;
3928 }
3929 writer.write_event(Event::End(BytesEnd::new("authors")))?;
3930
3931 writer.write_event(Event::Start(BytesStart::new("commentList")))?;
3932 for comment in comments {
3933 let author_id = authors
3934 .iter()
3935 .position(|author| author == &comment.author)
3936 .unwrap_or(0);
3937 let author_id_text = author_id.to_string();
3938 let mut comment_start = BytesStart::new("comment");
3939 comment_start.push_attribute(("ref", comment.cell.as_str()));
3940 comment_start.push_attribute(("authorId", author_id_text.as_str()));
3941 writer.write_event(Event::Start(comment_start))?;
3942 writer.write_event(Event::Start(BytesStart::new("text")))?;
3943 match &comment.rich_text {
3947 Some(runs) => {
3948 for run in runs {
3949 write_rich_text_run(&mut writer, run)?;
3950 }
3951 }
3952 None => {
3953 writer.write_event(Event::Start(BytesStart::new("r")))?;
3954 writer.write_event(Event::Start(BytesStart::new("t")))?;
3955 writer.write_event(Event::Text(BytesText::new(&comment.text)))?;
3956 writer.write_event(Event::End(BytesEnd::new("t")))?;
3957 writer.write_event(Event::End(BytesEnd::new("r")))?;
3958 }
3959 }
3960 writer.write_event(Event::End(BytesEnd::new("text")))?;
3961 writer.write_event(Event::End(BytesEnd::new("comment")))?;
3962 }
3963 writer.write_event(Event::End(BytesEnd::new("commentList")))?;
3964
3965 writer.write_event(Event::End(BytesEnd::new("comments")))?;
3966
3967 Ok(writer.into_inner())
3968}
3969
3970fn to_comments_vml(comments: &[CellComment]) -> Vec<u8> {
3981 let mut vml = String::new();
3982 vml.push_str(
3983 r#"<xml xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">
3984<o:shapelayout v:ext="edit"><o:idmap v:ext="edit" data="1"/></o:shapelayout>
3985<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe">
3986<v:stroke joinstyle="miter"/>
3987<v:path gradientshapeok="t" o:connecttype="rect"/>
3988</v:shapetype>
3989"#,
3990 );
3991
3992 for (index, comment) in comments.iter().enumerate() {
3993 let shape_id = 1025 + index;
3994 let (column_index, row_number) = cell_column_and_row(&comment.cell);
3995 let row_index = row_number.saturating_sub(1);
3996 vml.push_str(&format!(
3997 r##"<v:shape id="_x0000_s{shape_id}" type="#_x0000_t202" style='position:absolute;margin-left:59.25pt;margin-top:1.5pt;width:108pt;height:59.25pt;z-index:{index_plus_one};visibility:hidden' fillcolor="#ffffe1" o:insetmode="auto">
3998<v:fill color2="#ffffe1"/>
3999<v:shadow on="t" color="black" obscured="t"/>
4000<v:path o:connecttype="none"/>
4001<x:ClientData ObjectType="Note">
4002<x:MoveWithCells/>
4003<x:SizeWithCells/>
4004<x:Anchor>{col1}, 15, {row1}, 2, {col2}, 31, {row2}, 4</x:Anchor>
4005<x:AutoFill>False</x:AutoFill>
4006<x:Row>{row_index}</x:Row>
4007<x:Column>{column_index}</x:Column>
4008</x:ClientData>
4009</v:shape>
4010"##,
4011 shape_id = shape_id,
4012 index_plus_one = index + 1,
4013 col1 = column_index,
4014 row1 = row_index,
4015 col2 = column_index + 2,
4016 row2 = row_index + 4,
4017 row_index = row_index,
4018 column_index = column_index,
4019 ));
4020 }
4021
4022 vml.push_str("</xml>");
4023 vml.into_bytes()
4024}
4025
4026fn to_drawing_xml(
4039 drawing: &SheetDrawing,
4040 relationships: &mut Relationships,
4041 next_media_index: &mut usize,
4042 next_chart_index: &mut usize,
4043 package: &mut Package,
4044) -> Result<Vec<u8>> {
4045 let mut writer = Writer::new(Vec::new());
4046
4047 writer.write_event(Event::Decl(BytesDecl::new(
4048 "1.0",
4049 Some("UTF-8"),
4050 Some("yes"),
4051 )))?;
4052
4053 let mut root = BytesStart::new("xdr:wsDr");
4054 root.push_attribute(("xmlns:xdr", SPREADSHEET_DRAWING_NAMESPACE));
4055 root.push_attribute(("xmlns:a", DRAWINGML_NAMESPACE));
4056 root.push_attribute(("xmlns:r", RELATIONSHIPS_NAMESPACE));
4057 writer.write_event(Event::Start(root))?;
4058
4059 let mut next_relationship_id = 1usize;
4060
4061 for (anchor_index, anchor) in drawing.anchors.iter().enumerate() {
4062 writer.write_event(Event::Start(BytesStart::new("xdr:twoCellAnchor")))?;
4063 write_cell_anchor_point(&mut writer, "xdr:from", &anchor.from)?;
4064 write_cell_anchor_point(&mut writer, "xdr:to", &anchor.to)?;
4065
4066 let shape_id = (anchor_index + 1) as u32;
4069
4070 match &anchor.object {
4071 DrawingObject::Picture(picture) => {
4072 let relationship_id = format!("rId{next_relationship_id}");
4073 next_relationship_id += 1;
4074 let media_index = *next_media_index;
4075 *next_media_index += 1;
4076 let entry_name = format!("image{}.{}", media_index, picture.format.extension());
4077 relationships.add(Relationship {
4078 id: relationship_id.clone(),
4079 rel_type: IMAGE_RELATIONSHIP_TYPE.to_string(),
4080 target: format!("../media/{entry_name}"),
4081 target_mode: TargetMode::Internal,
4082 });
4083 package.add_part(Part::new(
4084 format!("/xl/media/{entry_name}"),
4085 picture.format.content_type(),
4086 picture.data.clone(),
4087 ));
4088 write_sheet_picture(&mut writer, picture, &relationship_id, shape_id)?;
4089 }
4090 DrawingObject::Chart(sheet_chart) => {
4091 let relationship_id = format!("rId{next_relationship_id}");
4092 next_relationship_id += 1;
4093 let chart_index = *next_chart_index;
4094 *next_chart_index += 1;
4095 relationships.add(Relationship {
4096 id: relationship_id.clone(),
4097 rel_type: CHART_RELATIONSHIP_TYPE.to_string(),
4098 target: format!("../charts/chart{chart_index}.xml"),
4099 target_mode: TargetMode::Internal,
4100 });
4101 let mut chart_writer = Writer::new(Vec::new());
4102 chart::write_chart_space(&mut chart_writer, &sheet_chart.chart_space)?;
4103 package.add_part(Part::new(
4104 format!("/xl/charts/chart{chart_index}.xml"),
4105 CHART_CONTENT_TYPE,
4106 chart_writer.into_inner(),
4107 ));
4108 write_sheet_chart(&mut writer, sheet_chart, &relationship_id, shape_id)?;
4109 }
4110 }
4111
4112 writer.write_event(Event::Empty(BytesStart::new("xdr:clientData")))?;
4117
4118 writer.write_event(Event::End(BytesEnd::new("xdr:twoCellAnchor")))?;
4119 }
4120
4121 writer.write_event(Event::End(BytesEnd::new("xdr:wsDr")))?;
4122
4123 Ok(writer.into_inner())
4124}
4125
4126fn write_cell_anchor_point<W: Write>(
4129 writer: &mut Writer<W>,
4130 element_name: &str,
4131 point: &CellAnchorPoint,
4132) -> Result<()> {
4133 writer.write_event(Event::Start(BytesStart::new(element_name)))?;
4134
4135 writer.write_event(Event::Start(BytesStart::new("xdr:col")))?;
4136 writer.write_event(Event::Text(BytesText::new(&point.column.to_string())))?;
4137 writer.write_event(Event::End(BytesEnd::new("xdr:col")))?;
4138
4139 writer.write_event(Event::Start(BytesStart::new("xdr:colOff")))?;
4140 writer.write_event(Event::Text(BytesText::new(
4141 &point.column_offset_emu.to_string(),
4142 )))?;
4143 writer.write_event(Event::End(BytesEnd::new("xdr:colOff")))?;
4144
4145 writer.write_event(Event::Start(BytesStart::new("xdr:row")))?;
4146 writer.write_event(Event::Text(BytesText::new(&point.row.to_string())))?;
4147 writer.write_event(Event::End(BytesEnd::new("xdr:row")))?;
4148
4149 writer.write_event(Event::Start(BytesStart::new("xdr:rowOff")))?;
4150 writer.write_event(Event::Text(BytesText::new(
4151 &point.row_offset_emu.to_string(),
4152 )))?;
4153 writer.write_event(Event::End(BytesEnd::new("xdr:rowOff")))?;
4154
4155 writer.write_event(Event::End(BytesEnd::new(element_name)))?;
4156 Ok(())
4157}
4158
4159fn write_sheet_picture<W: Write>(
4171 writer: &mut Writer<W>,
4172 picture: &SheetPicture,
4173 relationship_id: &str,
4174 id: u32,
4175) -> Result<()> {
4176 writer.write_event(Event::Start(BytesStart::new("xdr:pic")))?;
4177
4178 writer.write_event(Event::Start(BytesStart::new("xdr:nvPicPr")))?;
4179 let mut c_nv_pr = BytesStart::new("xdr:cNvPr");
4180 c_nv_pr.push_attribute(("id", id.to_string().as_str()));
4181 c_nv_pr.push_attribute(("name", picture.name.as_str()));
4182 writer.write_event(Event::Empty(c_nv_pr))?;
4183 writer.write_event(Event::Empty(BytesStart::new("xdr:cNvPicPr")))?;
4184 writer.write_event(Event::End(BytesEnd::new("xdr:nvPicPr")))?;
4185
4186 writer.write_event(Event::Start(BytesStart::new("xdr:blipFill")))?;
4187 let mut blip = BytesStart::new("a:blip");
4188 blip.push_attribute(("r:embed", relationship_id));
4189 writer.write_event(Event::Empty(blip))?;
4190 writer.write_event(Event::Start(BytesStart::new("a:stretch")))?;
4191 writer.write_event(Event::Empty(BytesStart::new("a:fillRect")))?;
4192 writer.write_event(Event::End(BytesEnd::new("a:stretch")))?;
4193 writer.write_event(Event::End(BytesEnd::new("xdr:blipFill")))?;
4194
4195 writer.write_event(Event::Start(BytesStart::new("xdr:spPr")))?;
4196 writer.write_event(Event::Start(BytesStart::new("a:xfrm")))?;
4210 let mut off = BytesStart::new("a:off");
4211 off.push_attribute(("x", "0"));
4212 off.push_attribute(("y", "0"));
4213 writer.write_event(Event::Empty(off))?;
4214 let mut ext = BytesStart::new("a:ext");
4215 ext.push_attribute(("cx", "0"));
4216 ext.push_attribute(("cy", "0"));
4217 writer.write_event(Event::Empty(ext))?;
4218 writer.write_event(Event::End(BytesEnd::new("a:xfrm")))?;
4219 let empty_properties = drawing::ShapeProperties::default();
4227 let shape_properties = picture
4228 .shape_properties
4229 .as_ref()
4230 .unwrap_or(&empty_properties);
4231 drawing::write_geometry_fill_line_effects(writer, shape_properties, "rect")?;
4232 writer.write_event(Event::End(BytesEnd::new("xdr:spPr")))?;
4233
4234 writer.write_event(Event::End(BytesEnd::new("xdr:pic")))?;
4235 Ok(())
4236}
4237
4238fn write_sheet_chart<W: Write>(
4247 writer: &mut Writer<W>,
4248 sheet_chart: &SheetChart,
4249 relationship_id: &str,
4250 id: u32,
4251) -> Result<()> {
4252 writer.write_event(Event::Start(BytesStart::new("xdr:graphicFrame")))?;
4253
4254 writer.write_event(Event::Start(BytesStart::new("xdr:nvGraphicFramePr")))?;
4255 let mut c_nv_pr = BytesStart::new("xdr:cNvPr");
4256 c_nv_pr.push_attribute(("id", id.to_string().as_str()));
4257 c_nv_pr.push_attribute(("name", sheet_chart.name.as_str()));
4258 writer.write_event(Event::Empty(c_nv_pr))?;
4259 writer.write_event(Event::Empty(BytesStart::new("xdr:cNvGraphicFramePr")))?;
4260 writer.write_event(Event::End(BytesEnd::new("xdr:nvGraphicFramePr")))?;
4261
4262 writer.write_event(Event::Start(BytesStart::new("xdr:xfrm")))?;
4263 let mut off = BytesStart::new("a:off");
4264 off.push_attribute(("x", "0"));
4265 off.push_attribute(("y", "0"));
4266 writer.write_event(Event::Empty(off))?;
4267 let mut ext = BytesStart::new("a:ext");
4268 ext.push_attribute(("cx", "0"));
4269 ext.push_attribute(("cy", "0"));
4270 writer.write_event(Event::Empty(ext))?;
4271 writer.write_event(Event::End(BytesEnd::new("xdr:xfrm")))?;
4272
4273 writer.write_event(Event::Start(BytesStart::new("a:graphic")))?;
4274 let mut graphic_data = BytesStart::new("a:graphicData");
4275 graphic_data.push_attribute(("uri", CHART_NAMESPACE));
4276 writer.write_event(Event::Start(graphic_data))?;
4277 let mut c_chart = BytesStart::new("c:chart");
4278 c_chart.push_attribute(("xmlns:c", CHART_NAMESPACE));
4279 c_chart.push_attribute(("r:id", relationship_id));
4280 writer.write_event(Event::Empty(c_chart))?;
4281 writer.write_event(Event::End(BytesEnd::new("a:graphicData")))?;
4282 writer.write_event(Event::End(BytesEnd::new("a:graphic")))?;
4283
4284 writer.write_event(Event::End(BytesEnd::new("xdr:graphicFrame")))?;
4285 Ok(())
4286}
4287
4288fn cell_column_and_row(reference: &str) -> (usize, usize) {
4293 let letters: String = reference
4294 .chars()
4295 .take_while(|character| character.is_ascii_alphabetic())
4296 .collect();
4297 let digits: String = reference
4298 .chars()
4299 .skip_while(|character| character.is_ascii_alphabetic())
4300 .collect();
4301 let row_number = digits.parse::<usize>().unwrap_or(1);
4302 if letters.is_empty() {
4303 return (0, row_number);
4304 }
4305 let mut index: usize = 0;
4306 for character in letters.chars() {
4307 let digit = (character.to_ascii_uppercase() as u8 - b'A' + 1) as usize;
4308 index = index * 26 + digit;
4309 }
4310 (index - 1, row_number)
4311}
4312
4313#[cfg(test)]
4314mod tests {
4315 use super::*;
4316 use crate::model::Row;
4317
4318 #[test]
4319 fn writes_column_letters_correctly() {
4320 assert_eq!(column_letters(0), "A");
4321 assert_eq!(column_letters(1), "B");
4322 assert_eq!(column_letters(25), "Z");
4323 assert_eq!(column_letters(26), "AA");
4324 assert_eq!(column_letters(27), "AB");
4325 assert_eq!(column_letters(51), "AZ");
4326 assert_eq!(column_letters(52), "BA");
4327 assert_eq!(column_letters(701), "ZZ");
4328 assert_eq!(column_letters(702), "AAA");
4329 }
4330
4331 #[test]
4332 fn writes_a_cell_reference_from_column_and_row() {
4333 assert_eq!(cell_reference(0, 1), "A1");
4334 assert_eq!(cell_reference(2, 5), "C5");
4335 assert_eq!(cell_reference(26, 11), "AA11");
4336 }
4337
4338 #[test]
4339 fn writes_workbook_with_worksheet_styles_and_shared_strings_parts() {
4340 let workbook = Workbook::new().with_sheet(
4341 Sheet::new("Feuil1")
4342 .with_row(Row::new().with_text("Nombre_1").with_text("Nombre_2"))
4343 .with_row(Row::new().with_number(10.0).with_number(1.0)),
4344 );
4345
4346 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4347 let package = opc::Package::read_from(buffer).unwrap();
4348
4349 assert!(package.part(MAIN_WORKBOOK_PART_NAME).is_some());
4350 assert!(package.part("/xl/worksheets/sheet1.xml").is_some());
4351 assert!(package.part(STYLES_PART_NAME).is_some());
4352 assert!(package.part(SHARED_STRINGS_PART_NAME).is_some());
4353 assert!(package.part(CORE_PROPERTIES_PART_NAME).is_some());
4354 assert!(package.part(EXTENDED_PROPERTIES_PART_NAME).is_some());
4355
4356 let workbook_xml =
4357 std::str::from_utf8(&package.part(MAIN_WORKBOOK_PART_NAME).unwrap().data).unwrap();
4358 assert!(
4359 workbook_xml.contains(r#"<sheet name="Feuil1" sheetId="1" r:id="rId1"/>"#),
4360 "{workbook_xml}"
4361 );
4362
4363 let worksheet_xml =
4364 std::str::from_utf8(&package.part("/xl/worksheets/sheet1.xml").unwrap().data).unwrap();
4365 assert!(
4366 worksheet_xml.contains(r#"<c r="A1" t="s"><v>0</v></c>"#),
4367 "{worksheet_xml}"
4368 );
4369 assert!(
4370 worksheet_xml.contains(r#"<c r="B1" t="s"><v>1</v></c>"#),
4371 "{worksheet_xml}"
4372 );
4373 assert!(
4374 worksheet_xml.contains(r#"<c r="A2"><v>10</v></c>"#),
4375 "{worksheet_xml}"
4376 );
4377 assert!(
4378 worksheet_xml.contains(r#"<c r="B2"><v>1</v></c>"#),
4379 "{worksheet_xml}"
4380 );
4381
4382 let shared_strings_xml =
4383 std::str::from_utf8(&package.part(SHARED_STRINGS_PART_NAME).unwrap().data).unwrap();
4384 assert!(
4385 shared_strings_xml.contains(r#"count="2" uniqueCount="2""#),
4386 "{shared_strings_xml}"
4387 );
4388 assert!(
4389 shared_strings_xml.contains("<t>Nombre_1</t>"),
4390 "{shared_strings_xml}"
4391 );
4392 assert!(
4393 shared_strings_xml.contains("<t>Nombre_2</t>"),
4394 "{shared_strings_xml}"
4395 );
4396
4397 let styles_xml =
4398 std::str::from_utf8(&package.part(STYLES_PART_NAME).unwrap().data).unwrap();
4399 assert!(styles_xml.contains(r#"<fills count="2">"#), "{styles_xml}");
4400 assert!(
4401 styles_xml.contains(r#"<patternFill patternType="none"/>"#),
4402 "{styles_xml}"
4403 );
4404 assert!(
4405 styles_xml.contains(r#"<patternFill patternType="gray125"/>"#),
4406 "{styles_xml}"
4407 );
4408 assert!(
4409 styles_xml.contains(r#"<cellXfs count="1">"#),
4410 "{styles_xml}"
4411 );
4412 }
4413
4414 #[test]
4415 fn deduplicates_repeated_text_values_across_the_shared_strings_table() {
4416 let workbook = Workbook::new().with_sheet(
4417 Sheet::new("Feuil1")
4418 .with_row(Row::new().with_text("Répété"))
4419 .with_row(Row::new().with_text("Répété"))
4420 .with_row(Row::new().with_text("Unique")),
4421 );
4422
4423 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4424 let package = opc::Package::read_from(buffer).unwrap();
4425
4426 let shared_strings_xml =
4427 std::str::from_utf8(&package.part(SHARED_STRINGS_PART_NAME).unwrap().data).unwrap();
4428 assert!(
4429 shared_strings_xml.contains(r#"count="3" uniqueCount="2""#),
4430 "{shared_strings_xml}"
4431 );
4432
4433 let worksheet_xml =
4434 std::str::from_utf8(&package.part("/xl/worksheets/sheet1.xml").unwrap().data).unwrap();
4435 assert!(
4436 worksheet_xml.contains(r#"<c r="A1" t="s"><v>0</v></c>"#),
4437 "{worksheet_xml}"
4438 );
4439 assert!(
4440 worksheet_xml.contains(r#"<c r="A2" t="s"><v>0</v></c>"#),
4441 "{worksheet_xml}"
4442 );
4443 assert!(
4444 worksheet_xml.contains(r#"<c r="A3" t="s"><v>1</v></c>"#),
4445 "{worksheet_xml}"
4446 );
4447 }
4448
4449 #[test]
4450 fn omits_a_c_element_entirely_for_an_empty_cell() {
4451 let workbook = Workbook::new().with_sheet(
4452 Sheet::new("Feuil1").with_row(
4453 Row::new()
4454 .with_text("A")
4455 .with_cell(Cell::new())
4456 .with_text("C"),
4457 ),
4458 );
4459
4460 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4461 let package = opc::Package::read_from(buffer).unwrap();
4462
4463 let worksheet_xml =
4464 std::str::from_utf8(&package.part("/xl/worksheets/sheet1.xml").unwrap().data).unwrap();
4465 assert!(
4466 worksheet_xml.contains(r#"<c r="A1" t="s""#),
4467 "{worksheet_xml}"
4468 );
4469 assert!(!worksheet_xml.contains(r#"r="B1""#), "{worksheet_xml}");
4470 assert!(
4471 worksheet_xml.contains(r#"<c r="C1" t="s""#),
4472 "{worksheet_xml}"
4473 );
4474 }
4475
4476 #[test]
4477 fn writes_a_dynamic_style_with_number_format_font_fill_and_alignment() {
4478 let format = CellFormat::new()
4479 .with_number_format("0.00")
4480 .with_bold(true)
4481 .with_fill_color("FFFFFF00")
4482 .with_horizontal_alignment(HorizontalAlignment::Center);
4483
4484 let workbook = Workbook::new().with_sheet(
4485 Sheet::new("Feuil1")
4486 .with_row(Row::new().with_cell(Cell::number(3.5).with_format(format))),
4487 );
4488
4489 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4490 let package = opc::Package::read_from(buffer).unwrap();
4491
4492 let styles_xml =
4493 std::str::from_utf8(&package.part(STYLES_PART_NAME).unwrap().data).unwrap();
4494 assert!(
4495 styles_xml.contains(r#"<numFmt numFmtId="164" formatCode="0.00"/>"#),
4496 "{styles_xml}"
4497 );
4498 assert!(styles_xml.contains("<b/>"), "{styles_xml}");
4499 assert!(
4500 styles_xml.contains(r#"<fgColor rgb="FFFFFF00"/>"#),
4501 "{styles_xml}"
4502 );
4503 assert!(
4504 styles_xml.contains(r#"<alignment horizontal="center"/>"#),
4505 "{styles_xml}"
4506 );
4507 assert!(
4508 styles_xml.contains(r#"<cellXfs count="2">"#),
4509 "{styles_xml}"
4510 );
4511
4512 let worksheet_xml =
4513 std::str::from_utf8(&package.part("/xl/worksheets/sheet1.xml").unwrap().data).unwrap();
4514 assert!(
4515 worksheet_xml.contains(r#"<c r="A1" s="1"><v>3.5</v></c>"#),
4516 "{worksheet_xml}"
4517 );
4518 }
4519
4520 #[test]
4521 fn writes_a_styled_but_empty_cell_as_a_self_closing_element() {
4522 let workbook = Workbook::new().with_sheet(Sheet::new("Feuil1").with_row(
4523 Row::new().with_cell(Cell::new().with_format(CellFormat::new().with_bold(true))),
4524 ));
4525
4526 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4527 let package = opc::Package::read_from(buffer).unwrap();
4528
4529 let worksheet_xml =
4530 std::str::from_utf8(&package.part("/xl/worksheets/sheet1.xml").unwrap().data).unwrap();
4531 assert!(
4532 worksheet_xml.contains(r#"<c r="A1" s="1"/>"#),
4533 "{worksheet_xml}"
4534 );
4535 }
4536
4537 #[test]
4538 fn writes_a_formula_cell_with_text_and_cached_value() {
4539 let workbook = Workbook::new().with_sheet(
4540 Sheet::new("Feuil1")
4541 .with_row(Row::new().with_cell(Cell::formula("A1*2").with_cached_value(20.0))),
4542 );
4543
4544 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4545 let package = opc::Package::read_from(buffer).unwrap();
4546
4547 let worksheet_xml =
4548 std::str::from_utf8(&package.part("/xl/worksheets/sheet1.xml").unwrap().data).unwrap();
4549 assert!(
4550 worksheet_xml.contains(r#"<c r="A1"><f>A1*2</f><v>20</v></c>"#),
4551 "{worksheet_xml}"
4552 );
4553 }
4554
4555 #[test]
4556 fn writes_a_formula_cell_with_no_cached_value_and_no_v_element() {
4557 let workbook = Workbook::new()
4558 .with_sheet(Sheet::new("Feuil1").with_row(Row::new().with_formula("A1+1")));
4559
4560 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4561 let package = opc::Package::read_from(buffer).unwrap();
4562
4563 let worksheet_xml =
4564 std::str::from_utf8(&package.part("/xl/worksheets/sheet1.xml").unwrap().data).unwrap();
4565 assert!(
4566 worksheet_xml.contains(r#"<c r="A1"><f>A1+1</f></c>"#),
4567 "{worksheet_xml}"
4568 );
4569 }
4570
4571 #[test]
4572 fn writes_a_cell_is_conditional_formatting_rule_with_its_dxf() {
4573 use crate::model::ConditionalFormattingRule;
4574
4575 let format = CellFormat::new()
4576 .with_bold(true)
4577 .with_fill_color("FFFFFF00");
4578 let rule = ConditionalFormattingRule::cell_is(
4579 "E3:E9",
4580 ComparisonOperator::GreaterThan,
4581 "0.5",
4582 format,
4583 );
4584
4585 let workbook =
4586 Workbook::new().with_sheet(Sheet::new("Feuil1").with_conditional_formatting_rule(rule));
4587
4588 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4589 let package = opc::Package::read_from(buffer).unwrap();
4590
4591 let worksheet_xml =
4592 std::str::from_utf8(&package.part("/xl/worksheets/sheet1.xml").unwrap().data).unwrap();
4593 assert!(
4594 worksheet_xml.contains(r#"<conditionalFormatting sqref="E3:E9">"#),
4595 "{worksheet_xml}"
4596 );
4597 assert!(
4598 worksheet_xml.contains(
4599 r#"<cfRule type="cellIs" dxfId="0" priority="1" operator="greaterThan">"#
4600 ),
4601 "{worksheet_xml}"
4602 );
4603 assert!(
4604 worksheet_xml.contains("<formula>0.5</formula>"),
4605 "{worksheet_xml}"
4606 );
4607
4608 let styles_xml =
4609 std::str::from_utf8(&package.part(STYLES_PART_NAME).unwrap().data).unwrap();
4610 assert!(styles_xml.contains(r#"<dxfs count="1">"#), "{styles_xml}");
4611 assert!(styles_xml.contains("<b/>"), "{styles_xml}");
4612 assert!(
4613 styles_xml.contains(r#"<bgColor rgb="FFFFFF00"/>"#),
4614 "{styles_xml}"
4615 );
4616 }
4617
4618 #[test]
4619 fn writes_an_expression_conditional_formatting_rule_with_two_formulas_for_between() {
4620 use crate::model::ConditionalFormattingRule;
4621
4622 let format = CellFormat::new().with_fill_color("FFFF0000");
4623 let rule =
4624 ConditionalFormattingRule::cell_is("A1:A10", ComparisonOperator::Between, "1", format)
4625 .with_second_formula("10");
4626
4627 let workbook =
4628 Workbook::new().with_sheet(Sheet::new("Feuil1").with_conditional_formatting_rule(rule));
4629
4630 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4631 let package = opc::Package::read_from(buffer).unwrap();
4632
4633 let worksheet_xml =
4634 std::str::from_utf8(&package.part("/xl/worksheets/sheet1.xml").unwrap().data).unwrap();
4635 assert!(
4636 worksheet_xml.contains(r#"operator="between""#),
4637 "{worksheet_xml}"
4638 );
4639 assert!(
4640 worksheet_xml.contains("<formula>1</formula><formula>10</formula>"),
4641 "{worksheet_xml}"
4642 );
4643 }
4644
4645 #[test]
4646 fn writes_a_data_validation_list_rule() {
4647 use crate::model::DataValidationRule;
4648
4649 let rule = DataValidationRule::list("B2:B20", "\"Oui,Non\"");
4650
4651 let workbook =
4652 Workbook::new().with_sheet(Sheet::new("Feuil1").with_data_validation_rule(rule));
4653
4654 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4655 let package = opc::Package::read_from(buffer).unwrap();
4656
4657 let worksheet_xml =
4658 std::str::from_utf8(&package.part("/xl/worksheets/sheet1.xml").unwrap().data).unwrap();
4659 assert!(
4660 worksheet_xml.contains(r#"<dataValidations count="1">"#),
4661 "{worksheet_xml}"
4662 );
4663 assert!(
4664 worksheet_xml.contains(r#"<dataValidation type="list" allowBlank="1" sqref="B2:B20">"#),
4665 "{worksheet_xml}"
4666 );
4667 assert!(
4671 worksheet_xml.contains("<formula1>"Oui,Non"</formula1>"),
4672 "{worksheet_xml}"
4673 );
4674 }
4675
4676 #[test]
4677 fn writes_a_whole_number_data_validation_rule_with_messages() {
4678 use crate::model::{DataValidationRule, Message};
4679
4680 let rule =
4681 DataValidationRule::whole_number("C2:C20", ComparisonOperator::GreaterThanOrEqual, "0")
4682 .with_input_message(Message::new("Entrez un nombre positif").with_title("Saisie"))
4683 .with_error_message(Message::new("Valeur invalide").with_title("Erreur"));
4684
4685 let workbook =
4686 Workbook::new().with_sheet(Sheet::new("Feuil1").with_data_validation_rule(rule));
4687
4688 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4689 let package = opc::Package::read_from(buffer).unwrap();
4690
4691 let worksheet_xml =
4692 std::str::from_utf8(&package.part("/xl/worksheets/sheet1.xml").unwrap().data).unwrap();
4693 assert!(worksheet_xml.contains(r#"type="whole""#), "{worksheet_xml}");
4694 assert!(
4695 worksheet_xml.contains(r#"operator="greaterThanOrEqual""#),
4696 "{worksheet_xml}"
4697 );
4698 assert!(
4699 worksheet_xml.contains(r#"showInputMessage="1""#),
4700 "{worksheet_xml}"
4701 );
4702 assert!(
4703 worksheet_xml.contains(r#"showErrorMessage="1""#),
4704 "{worksheet_xml}"
4705 );
4706 assert!(
4707 worksheet_xml.contains(r#"promptTitle="Saisie""#),
4708 "{worksheet_xml}"
4709 );
4710 assert!(
4711 worksheet_xml.contains(r#"prompt="Entrez un nombre positif""#),
4712 "{worksheet_xml}"
4713 );
4714 assert!(
4715 worksheet_xml.contains(r#"errorTitle="Erreur""#),
4716 "{worksheet_xml}"
4717 );
4718 assert!(
4719 worksheet_xml.contains(r#"error="Valeur invalide""#),
4720 "{worksheet_xml}"
4721 );
4722 assert!(
4723 worksheet_xml.contains("<formula1>0</formula1>"),
4724 "{worksheet_xml}"
4725 );
4726 }
4727
4728 #[test]
4729 fn writes_a_workbook_scoped_defined_name() {
4730 use crate::model::DefinedName;
4731
4732 let workbook = Workbook::new()
4733 .with_sheet(Sheet::new("Feuil1"))
4734 .with_defined_name(DefinedName::new("Plage_Ventes", "Feuil1!$A$1:$C$12"));
4735
4736 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4737 let package = opc::Package::read_from(buffer).unwrap();
4738
4739 let workbook_xml =
4740 std::str::from_utf8(&package.part(MAIN_WORKBOOK_PART_NAME).unwrap().data).unwrap();
4741 assert!(workbook_xml.contains(r#"<definedNames><definedName name="Plage_Ventes">Feuil1!$A$1:$C$12</definedName></definedNames>"#), "{workbook_xml}");
4742 }
4743
4744 #[test]
4745 fn writes_a_sheet_scoped_hidden_defined_name_with_a_comment() {
4746 use crate::model::DefinedName;
4747
4748 let defined_name = DefinedName::new("Northwind_Database", "Feuil2!$A$1:$T$47")
4749 .with_local_sheet_id(1)
4750 .with_hidden(true)
4751 .with_comment("Source de donnees externe");
4752
4753 let workbook = Workbook::new()
4754 .with_sheet(Sheet::new("Feuil1"))
4755 .with_sheet(Sheet::new("Feuil2"))
4756 .with_defined_name(defined_name);
4757
4758 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4759 let package = opc::Package::read_from(buffer).unwrap();
4760
4761 let workbook_xml =
4762 std::str::from_utf8(&package.part(MAIN_WORKBOOK_PART_NAME).unwrap().data).unwrap();
4763 assert!(
4764 workbook_xml.contains(
4765 r#"<definedName name="Northwind_Database" localSheetId="1" hidden="1" comment="Source de donnees externe">Feuil2!$A$1:$T$47</definedName>"#
4766 ),
4767 "{workbook_xml}"
4768 );
4769 }
4770
4771 #[test]
4772 fn omits_defined_names_entirely_when_none_are_set() {
4773 let workbook = Workbook::new().with_sheet(Sheet::new("Feuil1"));
4774
4775 let buffer = workbook.write_to(std::io::Cursor::new(Vec::new())).unwrap();
4776 let package = opc::Package::read_from(buffer).unwrap();
4777
4778 let workbook_xml =
4779 std::str::from_utf8(&package.part(MAIN_WORKBOOK_PART_NAME).unwrap().data).unwrap();
4780 assert!(!workbook_xml.contains("definedNames"), "{workbook_xml}");
4781 }
4782}