1use sheetkit_xml::content_types::{mime_types, ContentTypeOverride, ContentTypes};
7use sheetkit_xml::relationships::{rel_types, Relationship, Relationships};
8use sheetkit_xml::workbook::{SheetEntry, WorkbookXml};
9use sheetkit_xml::worksheet::{
10 Pane, Selection, SheetFormatPr, SheetPr, SheetProtection, SheetView, SheetViews, TabColor,
11 WorksheetXml,
12};
13
14use crate::error::{Error, Result};
15use crate::protection::legacy_password_hash;
16use crate::utils::cell_ref::cell_name_to_coordinates;
17use crate::utils::constants::{
18 DEFAULT_ROW_HEIGHT, MAX_COLUMN_WIDTH, MAX_ROW_HEIGHT, MAX_SHEET_NAME_LENGTH,
19 SHEET_NAME_INVALID_CHARS,
20};
21
22pub fn validate_sheet_name(name: &str) -> Result<()> {
30 if name.is_empty() {
31 return Err(Error::InvalidSheetName("sheet name cannot be empty".into()));
32 }
33 if name.len() > MAX_SHEET_NAME_LENGTH {
34 return Err(Error::InvalidSheetName(format!(
35 "sheet name '{}' exceeds {} characters",
36 name, MAX_SHEET_NAME_LENGTH
37 )));
38 }
39 for ch in SHEET_NAME_INVALID_CHARS {
40 if name.contains(*ch) {
41 return Err(Error::InvalidSheetName(format!(
42 "sheet name '{}' contains invalid character '{}'",
43 name, ch
44 )));
45 }
46 }
47 if name.starts_with('\'') || name.ends_with('\'') {
48 return Err(Error::InvalidSheetName(format!(
49 "sheet name '{}' cannot start or end with a single quote",
50 name
51 )));
52 }
53 Ok(())
54}
55
56pub fn next_rid(existing_rels: &[Relationship]) -> String {
60 let max = existing_rels
61 .iter()
62 .filter_map(|r| r.id.strip_prefix("rId").and_then(|n| n.parse::<u32>().ok()))
63 .max()
64 .unwrap_or(0);
65 format!("rId{}", max + 1)
66}
67
68pub fn next_sheet_id(existing_sheets: &[SheetEntry]) -> u32 {
73 existing_sheets
74 .iter()
75 .map(|s| s.sheet_id)
76 .max()
77 .unwrap_or(0)
78 + 1
79}
80
81pub fn find_sheet_index(worksheets: &[(String, WorksheetXml)], name: &str) -> Option<usize> {
83 worksheets.iter().position(|(n, _)| n == name)
84}
85
86pub fn add_sheet(
91 workbook_xml: &mut WorkbookXml,
92 workbook_rels: &mut Relationships,
93 content_types: &mut ContentTypes,
94 worksheets: &mut Vec<(String, WorksheetXml)>,
95 name: &str,
96 worksheet_data: WorksheetXml,
97) -> Result<usize> {
98 validate_sheet_name(name)?;
99
100 if worksheets.iter().any(|(n, _)| n == name) {
101 return Err(Error::SheetAlreadyExists {
102 name: name.to_string(),
103 });
104 }
105
106 let rid = next_rid(&workbook_rels.relationships);
107 let sheet_id = next_sheet_id(&workbook_xml.sheets.sheets);
108 let sheet_number = worksheets.len() + 1;
109 let target = format!("worksheets/sheet{}.xml", sheet_number);
110
111 workbook_xml.sheets.sheets.push(SheetEntry {
112 name: name.to_string(),
113 sheet_id,
114 state: None,
115 r_id: rid.clone(),
116 });
117
118 workbook_rels.relationships.push(Relationship {
119 id: rid,
120 rel_type: rel_types::WORKSHEET.to_string(),
121 target: target.clone(),
122 target_mode: None,
123 });
124
125 content_types.overrides.push(ContentTypeOverride {
126 part_name: format!("/xl/{}", target),
127 content_type: mime_types::WORKSHEET.to_string(),
128 });
129
130 worksheets.push((name.to_string(), worksheet_data));
131
132 Ok(worksheets.len() - 1)
133}
134
135pub fn delete_sheet(
139 workbook_xml: &mut WorkbookXml,
140 workbook_rels: &mut Relationships,
141 content_types: &mut ContentTypes,
142 worksheets: &mut Vec<(String, WorksheetXml)>,
143 name: &str,
144) -> Result<()> {
145 let idx = find_sheet_index(worksheets, name).ok_or_else(|| Error::SheetNotFound {
146 name: name.to_string(),
147 })?;
148
149 if worksheets.len() <= 1 {
150 return Err(Error::InvalidSheetName(
151 "cannot delete the last sheet in a workbook".into(),
152 ));
153 }
154
155 let r_id = workbook_xml.sheets.sheets[idx].r_id.clone();
156
157 worksheets.remove(idx);
158 workbook_xml.sheets.sheets.remove(idx);
159 workbook_rels.relationships.retain(|r| r.id != r_id);
160
161 rebuild_content_type_overrides(content_types, worksheets.len());
162 rebuild_worksheet_relationships(workbook_xml, workbook_rels);
163
164 Ok(())
165}
166
167pub fn rename_sheet(
169 workbook_xml: &mut WorkbookXml,
170 worksheets: &mut [(String, WorksheetXml)],
171 old_name: &str,
172 new_name: &str,
173) -> Result<()> {
174 validate_sheet_name(new_name)?;
175
176 let idx = find_sheet_index(worksheets, old_name).ok_or_else(|| Error::SheetNotFound {
177 name: old_name.to_string(),
178 })?;
179
180 if worksheets.iter().any(|(n, _)| n == new_name) {
181 return Err(Error::SheetAlreadyExists {
182 name: new_name.to_string(),
183 });
184 }
185
186 worksheets[idx].0 = new_name.to_string();
187 workbook_xml.sheets.sheets[idx].name = new_name.to_string();
188
189 Ok(())
190}
191
192pub fn copy_sheet(
194 workbook_xml: &mut WorkbookXml,
195 workbook_rels: &mut Relationships,
196 content_types: &mut ContentTypes,
197 worksheets: &mut Vec<(String, WorksheetXml)>,
198 source_name: &str,
199 target_name: &str,
200) -> Result<usize> {
201 let source_idx =
202 find_sheet_index(worksheets, source_name).ok_or_else(|| Error::SheetNotFound {
203 name: source_name.to_string(),
204 })?;
205
206 let cloned_data = worksheets[source_idx].1.clone();
207
208 add_sheet(
209 workbook_xml,
210 workbook_rels,
211 content_types,
212 worksheets,
213 target_name,
214 cloned_data,
215 )
216}
217
218pub fn active_sheet_index(workbook_xml: &WorkbookXml) -> usize {
220 workbook_xml
221 .book_views
222 .as_ref()
223 .and_then(|bv| bv.workbook_views.first())
224 .and_then(|v| v.active_tab)
225 .unwrap_or(0) as usize
226}
227
228pub fn set_active_sheet_index(workbook_xml: &mut WorkbookXml, index: u32) {
230 use sheetkit_xml::workbook::{BookViews, WorkbookView};
231
232 let book_views = workbook_xml.book_views.get_or_insert_with(|| BookViews {
233 workbook_views: vec![WorkbookView {
234 x_window: None,
235 y_window: None,
236 window_width: None,
237 window_height: None,
238 active_tab: Some(0),
239 }],
240 });
241
242 if let Some(view) = book_views.workbook_views.first_mut() {
243 view.active_tab = Some(index);
244 }
245}
246
247#[derive(Debug, Clone, Default)]
253pub struct SheetProtectionConfig {
254 pub password: Option<String>,
256 pub select_locked_cells: bool,
258 pub select_unlocked_cells: bool,
260 pub format_cells: bool,
262 pub format_columns: bool,
264 pub format_rows: bool,
266 pub insert_columns: bool,
268 pub insert_rows: bool,
270 pub insert_hyperlinks: bool,
272 pub delete_columns: bool,
274 pub delete_rows: bool,
276 pub sort: bool,
278 pub auto_filter: bool,
280 pub pivot_tables: bool,
282}
283
284pub fn protect_sheet(ws: &mut WorksheetXml, config: &SheetProtectionConfig) -> Result<()> {
290 let hashed = config.password.as_ref().map(|p| {
291 let h = legacy_password_hash(p);
292 format!("{:04X}", h)
293 });
294
295 let to_opt = |v: bool| if v { Some(true) } else { None };
296
297 ws.sheet_protection = Some(SheetProtection {
298 password: hashed,
299 sheet: Some(true),
300 objects: Some(true),
301 scenarios: Some(true),
302 select_locked_cells: to_opt(config.select_locked_cells),
303 select_unlocked_cells: to_opt(config.select_unlocked_cells),
304 format_cells: to_opt(config.format_cells),
305 format_columns: to_opt(config.format_columns),
306 format_rows: to_opt(config.format_rows),
307 insert_columns: to_opt(config.insert_columns),
308 insert_rows: to_opt(config.insert_rows),
309 insert_hyperlinks: to_opt(config.insert_hyperlinks),
310 delete_columns: to_opt(config.delete_columns),
311 delete_rows: to_opt(config.delete_rows),
312 sort: to_opt(config.sort),
313 auto_filter: to_opt(config.auto_filter),
314 pivot_tables: to_opt(config.pivot_tables),
315 });
316
317 Ok(())
318}
319
320pub fn unprotect_sheet(ws: &mut WorksheetXml) -> Result<()> {
322 ws.sheet_protection = None;
323 Ok(())
324}
325
326pub fn is_sheet_protected(ws: &WorksheetXml) -> bool {
328 ws.sheet_protection
329 .as_ref()
330 .and_then(|p| p.sheet)
331 .unwrap_or(false)
332}
333
334pub fn set_tab_color(ws: &mut WorksheetXml, rgb: &str) -> Result<()> {
336 let sheet_pr = ws.sheet_pr.get_or_insert_with(SheetPr::default);
337 sheet_pr.tab_color = Some(TabColor {
338 rgb: Some(rgb.to_string()),
339 theme: None,
340 indexed: None,
341 });
342 Ok(())
343}
344
345pub fn get_tab_color(ws: &WorksheetXml) -> Option<String> {
347 ws.sheet_pr
348 .as_ref()
349 .and_then(|pr| pr.tab_color.as_ref())
350 .and_then(|tc| tc.rgb.clone())
351}
352
353pub fn set_default_row_height(ws: &mut WorksheetXml, height: f64) -> Result<()> {
357 if height > MAX_ROW_HEIGHT {
358 return Err(Error::RowHeightExceeded {
359 height,
360 max: MAX_ROW_HEIGHT,
361 });
362 }
363 let fmt = ws.sheet_format_pr.get_or_insert(SheetFormatPr {
364 default_row_height: DEFAULT_ROW_HEIGHT,
365 default_col_width: None,
366 custom_height: None,
367 outline_level_row: None,
368 outline_level_col: None,
369 });
370 fmt.default_row_height = height;
371 Ok(())
372}
373
374pub fn get_default_row_height(ws: &WorksheetXml) -> f64 {
378 ws.sheet_format_pr
379 .as_ref()
380 .map(|f| f.default_row_height)
381 .unwrap_or(DEFAULT_ROW_HEIGHT)
382}
383
384pub fn set_default_col_width(ws: &mut WorksheetXml, width: f64) -> Result<()> {
388 if width > MAX_COLUMN_WIDTH {
389 return Err(Error::ColumnWidthExceeded {
390 width,
391 max: MAX_COLUMN_WIDTH,
392 });
393 }
394 let fmt = ws.sheet_format_pr.get_or_insert(SheetFormatPr {
395 default_row_height: DEFAULT_ROW_HEIGHT,
396 default_col_width: None,
397 custom_height: None,
398 outline_level_row: None,
399 outline_level_col: None,
400 });
401 fmt.default_col_width = Some(width);
402 Ok(())
403}
404
405pub fn get_default_col_width(ws: &WorksheetXml) -> Option<f64> {
409 ws.sheet_format_pr
410 .as_ref()
411 .and_then(|f| f.default_col_width)
412}
413
414pub fn set_panes(ws: &mut WorksheetXml, cell: &str) -> Result<()> {
423 let (col, row) = cell_name_to_coordinates(cell)?;
424
425 if col == 1 && row == 1 {
426 return Err(Error::InvalidCellReference(
427 "freeze pane at A1 has no effect".to_string(),
428 ));
429 }
430
431 let x_split = col - 1;
432 let y_split = row - 1;
433
434 let active_pane = match (x_split > 0, y_split > 0) {
435 (true, true) => "bottomRight",
436 (true, false) => "topRight",
437 (false, true) => "bottomLeft",
438 (false, false) => unreachable!(),
439 };
440
441 let pane = Pane {
442 x_split: if x_split > 0 { Some(x_split) } else { None },
443 y_split: if y_split > 0 { Some(y_split) } else { None },
444 top_left_cell: Some(cell.to_string()),
445 active_pane: Some(active_pane.to_string()),
446 state: Some("frozen".to_string()),
447 };
448
449 let selection = Selection {
450 pane: Some(active_pane.to_string()),
451 active_cell: Some(cell.to_string()),
452 sqref: Some(cell.to_string()),
453 };
454
455 let sheet_views = ws.sheet_views.get_or_insert_with(|| SheetViews {
456 sheet_views: vec![SheetView {
457 tab_selected: None,
458 zoom_scale: None,
459 workbook_view_id: 0,
460 pane: None,
461 selection: vec![],
462 }],
463 });
464
465 if let Some(view) = sheet_views.sheet_views.first_mut() {
466 view.pane = Some(pane);
467 view.selection = vec![selection];
468 }
469
470 Ok(())
471}
472
473pub fn unset_panes(ws: &mut WorksheetXml) {
475 if let Some(ref mut sheet_views) = ws.sheet_views {
476 for view in &mut sheet_views.sheet_views {
477 view.pane = None;
478 view.selection = vec![];
480 }
481 }
482}
483
484pub fn get_panes(ws: &WorksheetXml) -> Option<String> {
489 ws.sheet_views
490 .as_ref()
491 .and_then(|sv| sv.sheet_views.first())
492 .and_then(|view| view.pane.as_ref())
493 .and_then(|pane| pane.top_left_cell.clone())
494}
495
496fn rebuild_content_type_overrides(content_types: &mut ContentTypes, sheet_count: usize) {
499 content_types
500 .overrides
501 .retain(|o| o.content_type != mime_types::WORKSHEET);
502
503 for i in 1..=sheet_count {
504 content_types.overrides.push(ContentTypeOverride {
505 part_name: format!("/xl/worksheets/sheet{}.xml", i),
506 content_type: mime_types::WORKSHEET.to_string(),
507 });
508 }
509}
510
511fn rebuild_worksheet_relationships(
513 workbook_xml: &mut WorkbookXml,
514 workbook_rels: &mut Relationships,
515) {
516 let sheet_rids: Vec<String> = workbook_xml
517 .sheets
518 .sheets
519 .iter()
520 .map(|s| s.r_id.clone())
521 .collect();
522
523 for (i, rid) in sheet_rids.iter().enumerate() {
524 if let Some(rel) = workbook_rels
525 .relationships
526 .iter_mut()
527 .find(|r| r.id == *rid)
528 {
529 rel.target = format!("worksheets/sheet{}.xml", i + 1);
530 }
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537 use sheetkit_xml::content_types::ContentTypes;
538 use sheetkit_xml::relationships;
539 use sheetkit_xml::workbook::WorkbookXml;
540 use sheetkit_xml::worksheet::WorksheetXml;
541
542 #[test]
545 fn test_protect_sheet_no_password() {
546 let mut ws = WorksheetXml::default();
547 let config = SheetProtectionConfig::default();
548 protect_sheet(&mut ws, &config).unwrap();
549
550 assert!(ws.sheet_protection.is_some());
551 let prot = ws.sheet_protection.as_ref().unwrap();
552 assert_eq!(prot.sheet, Some(true));
553 assert_eq!(prot.objects, Some(true));
554 assert_eq!(prot.scenarios, Some(true));
555 assert!(prot.password.is_none());
556 }
557
558 #[test]
559 fn test_protect_sheet_with_password() {
560 let mut ws = WorksheetXml::default();
561 let config = SheetProtectionConfig {
562 password: Some("secret".to_string()),
563 ..SheetProtectionConfig::default()
564 };
565 protect_sheet(&mut ws, &config).unwrap();
566
567 let prot = ws.sheet_protection.as_ref().unwrap();
568 assert!(prot.password.is_some());
569 let pw = prot.password.as_ref().unwrap();
570 assert_eq!(pw.len(), 4);
572 assert!(pw.chars().all(|c| c.is_ascii_hexdigit()));
573 let expected = format!("{:04X}", legacy_password_hash("secret"));
575 assert_eq!(pw, &expected);
576 }
577
578 #[test]
579 fn test_unprotect_sheet() {
580 let mut ws = WorksheetXml::default();
581 let config = SheetProtectionConfig {
582 password: Some("test".to_string()),
583 ..SheetProtectionConfig::default()
584 };
585 protect_sheet(&mut ws, &config).unwrap();
586 assert!(ws.sheet_protection.is_some());
587
588 unprotect_sheet(&mut ws).unwrap();
589 assert!(ws.sheet_protection.is_none());
590 }
591
592 #[test]
593 fn test_is_sheet_protected() {
594 let mut ws = WorksheetXml::default();
595 assert!(!is_sheet_protected(&ws));
596
597 let config = SheetProtectionConfig::default();
598 protect_sheet(&mut ws, &config).unwrap();
599 assert!(is_sheet_protected(&ws));
600
601 unprotect_sheet(&mut ws).unwrap();
602 assert!(!is_sheet_protected(&ws));
603 }
604
605 #[test]
606 fn test_protect_sheet_with_permissions() {
607 let mut ws = WorksheetXml::default();
608 let config = SheetProtectionConfig {
609 password: None,
610 format_cells: true,
611 insert_rows: true,
612 delete_columns: true,
613 sort: true,
614 ..SheetProtectionConfig::default()
615 };
616 protect_sheet(&mut ws, &config).unwrap();
617
618 let prot = ws.sheet_protection.as_ref().unwrap();
619 assert_eq!(prot.format_cells, Some(true));
620 assert_eq!(prot.insert_rows, Some(true));
621 assert_eq!(prot.delete_columns, Some(true));
622 assert_eq!(prot.sort, Some(true));
623 assert!(prot.format_columns.is_none());
625 assert!(prot.format_rows.is_none());
626 assert!(prot.insert_columns.is_none());
627 assert!(prot.insert_hyperlinks.is_none());
628 assert!(prot.delete_rows.is_none());
629 assert!(prot.auto_filter.is_none());
630 assert!(prot.pivot_tables.is_none());
631 assert!(prot.select_locked_cells.is_none());
632 assert!(prot.select_unlocked_cells.is_none());
633 }
634
635 #[test]
638 fn test_set_tab_color() {
639 let mut ws = WorksheetXml::default();
640 set_tab_color(&mut ws, "FF0000").unwrap();
641
642 assert!(ws.sheet_pr.is_some());
643 let tab_color = ws.sheet_pr.as_ref().unwrap().tab_color.as_ref().unwrap();
644 assert_eq!(tab_color.rgb, Some("FF0000".to_string()));
645 }
646
647 #[test]
648 fn test_get_tab_color() {
649 let mut ws = WorksheetXml::default();
650 set_tab_color(&mut ws, "00FF00").unwrap();
651 assert_eq!(get_tab_color(&ws), Some("00FF00".to_string()));
652 }
653
654 #[test]
655 fn test_get_tab_color_none() {
656 let ws = WorksheetXml::default();
657 assert_eq!(get_tab_color(&ws), None);
658 }
659
660 #[test]
663 fn test_set_default_row_height() {
664 let mut ws = WorksheetXml::default();
665 set_default_row_height(&mut ws, 20.0).unwrap();
666
667 assert!(ws.sheet_format_pr.is_some());
668 assert_eq!(
669 ws.sheet_format_pr.as_ref().unwrap().default_row_height,
670 20.0
671 );
672 }
673
674 #[test]
675 fn test_get_default_row_height() {
676 let ws = WorksheetXml::default();
677 assert_eq!(get_default_row_height(&ws), DEFAULT_ROW_HEIGHT);
678
679 let mut ws2 = WorksheetXml::default();
680 set_default_row_height(&mut ws2, 25.0).unwrap();
681 assert_eq!(get_default_row_height(&ws2), 25.0);
682 }
683
684 #[test]
685 fn test_set_default_row_height_exceeds_max() {
686 let mut ws = WorksheetXml::default();
687 let result = set_default_row_height(&mut ws, 500.0);
688 assert!(result.is_err());
689 assert!(matches!(
690 result.unwrap_err(),
691 Error::RowHeightExceeded { .. }
692 ));
693 }
694
695 #[test]
698 fn test_set_default_col_width() {
699 let mut ws = WorksheetXml::default();
700 set_default_col_width(&mut ws, 12.0).unwrap();
701
702 assert!(ws.sheet_format_pr.is_some());
703 assert_eq!(
704 ws.sheet_format_pr.as_ref().unwrap().default_col_width,
705 Some(12.0)
706 );
707 }
708
709 #[test]
710 fn test_get_default_col_width() {
711 let ws = WorksheetXml::default();
712 assert_eq!(get_default_col_width(&ws), None);
713
714 let mut ws2 = WorksheetXml::default();
715 set_default_col_width(&mut ws2, 18.5).unwrap();
716 assert_eq!(get_default_col_width(&ws2), Some(18.5));
717 }
718
719 #[test]
720 fn test_set_default_col_width_exceeds_max() {
721 let mut ws = WorksheetXml::default();
722 let result = set_default_col_width(&mut ws, 300.0);
723 assert!(result.is_err());
724 assert!(matches!(
725 result.unwrap_err(),
726 Error::ColumnWidthExceeded { .. }
727 ));
728 }
729
730 #[test]
733 fn test_validate_empty_name() {
734 let result = validate_sheet_name("");
735 assert!(result.is_err());
736 let err_msg = result.unwrap_err().to_string();
737 assert!(
738 err_msg.contains("empty"),
739 "Error should mention empty: {err_msg}"
740 );
741 }
742
743 #[test]
744 fn test_validate_too_long_name() {
745 let long_name = "a".repeat(32);
746 let result = validate_sheet_name(&long_name);
747 assert!(result.is_err());
748 let err_msg = result.unwrap_err().to_string();
749 assert!(
750 err_msg.contains("exceeds"),
751 "Error should mention exceeds: {err_msg}"
752 );
753 }
754
755 #[test]
756 fn test_validate_exactly_max_length_is_ok() {
757 let name = "a".repeat(MAX_SHEET_NAME_LENGTH);
758 assert!(validate_sheet_name(&name).is_ok());
759 }
760
761 #[test]
762 fn test_validate_invalid_chars() {
763 for ch in SHEET_NAME_INVALID_CHARS {
764 let name = format!("Sheet{}", ch);
765 let result = validate_sheet_name(&name);
766 assert!(result.is_err(), "Name with '{}' should be invalid", ch);
767 }
768 }
769
770 #[test]
771 fn test_validate_single_quote_boundary() {
772 assert!(validate_sheet_name("'Sheet").is_err());
773 assert!(validate_sheet_name("Sheet'").is_err());
774 assert!(validate_sheet_name("'Sheet'").is_err());
775 assert!(validate_sheet_name("She'et").is_ok());
777 }
778
779 #[test]
780 fn test_validate_valid_name() {
781 assert!(validate_sheet_name("Sheet1").is_ok());
782 assert!(validate_sheet_name("My Data").is_ok());
783 assert!(validate_sheet_name("Q1-2024").is_ok());
784 assert!(validate_sheet_name("Sheet (2)").is_ok());
785 }
786
787 #[test]
788 fn test_next_rid() {
789 let rels = vec![
790 Relationship {
791 id: "rId1".to_string(),
792 rel_type: "".to_string(),
793 target: "".to_string(),
794 target_mode: None,
795 },
796 Relationship {
797 id: "rId3".to_string(),
798 rel_type: "".to_string(),
799 target: "".to_string(),
800 target_mode: None,
801 },
802 ];
803 assert_eq!(next_rid(&rels), "rId4");
804 }
805
806 #[test]
807 fn test_next_rid_empty() {
808 assert_eq!(next_rid(&[]), "rId1");
809 }
810
811 #[test]
812 fn test_next_sheet_id() {
813 let sheets = vec![
814 SheetEntry {
815 name: "Sheet1".to_string(),
816 sheet_id: 1,
817 state: None,
818 r_id: "rId1".to_string(),
819 },
820 SheetEntry {
821 name: "Sheet2".to_string(),
822 sheet_id: 5,
823 state: None,
824 r_id: "rId2".to_string(),
825 },
826 ];
827 assert_eq!(next_sheet_id(&sheets), 6);
828 }
829
830 #[test]
831 fn test_next_sheet_id_empty() {
832 assert_eq!(next_sheet_id(&[]), 1);
833 }
834
835 fn test_workbook_parts() -> (
837 WorkbookXml,
838 Relationships,
839 ContentTypes,
840 Vec<(String, WorksheetXml)>,
841 ) {
842 let workbook_xml = WorkbookXml::default();
843 let workbook_rels = relationships::workbook_rels();
844 let content_types = ContentTypes::default();
845 let worksheets = vec![("Sheet1".to_string(), WorksheetXml::default())];
846 (workbook_xml, workbook_rels, content_types, worksheets)
847 }
848
849 #[test]
850 fn test_add_sheet_basic() {
851 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
852
853 let idx = add_sheet(
854 &mut wb_xml,
855 &mut wb_rels,
856 &mut ct,
857 &mut ws,
858 "Sheet2",
859 WorksheetXml::default(),
860 )
861 .unwrap();
862
863 assert_eq!(idx, 1);
864 assert_eq!(ws.len(), 2);
865 assert_eq!(ws[1].0, "Sheet2");
866 assert_eq!(wb_xml.sheets.sheets.len(), 2);
867 assert_eq!(wb_xml.sheets.sheets[1].name, "Sheet2");
868
869 let ws_rels: Vec<_> = wb_rels
870 .relationships
871 .iter()
872 .filter(|r| r.rel_type == rel_types::WORKSHEET)
873 .collect();
874 assert_eq!(ws_rels.len(), 2);
875
876 let ws_overrides: Vec<_> = ct
877 .overrides
878 .iter()
879 .filter(|o| o.content_type == mime_types::WORKSHEET)
880 .collect();
881 assert_eq!(ws_overrides.len(), 2);
882 }
883
884 #[test]
885 fn test_add_sheet_duplicate_returns_error() {
886 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
887
888 let result = add_sheet(
889 &mut wb_xml,
890 &mut wb_rels,
891 &mut ct,
892 &mut ws,
893 "Sheet1",
894 WorksheetXml::default(),
895 );
896
897 assert!(result.is_err());
898 assert!(
899 matches!(result.unwrap_err(), Error::SheetAlreadyExists { name } if name == "Sheet1")
900 );
901 }
902
903 #[test]
904 fn test_add_sheet_invalid_name_returns_error() {
905 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
906
907 let result = add_sheet(
908 &mut wb_xml,
909 &mut wb_rels,
910 &mut ct,
911 &mut ws,
912 "Bad[Name",
913 WorksheetXml::default(),
914 );
915
916 assert!(result.is_err());
917 assert!(matches!(result.unwrap_err(), Error::InvalidSheetName(_)));
918 }
919
920 #[test]
921 fn test_delete_sheet_basic() {
922 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
923
924 add_sheet(
925 &mut wb_xml,
926 &mut wb_rels,
927 &mut ct,
928 &mut ws,
929 "Sheet2",
930 WorksheetXml::default(),
931 )
932 .unwrap();
933
934 assert_eq!(ws.len(), 2);
935
936 delete_sheet(&mut wb_xml, &mut wb_rels, &mut ct, &mut ws, "Sheet1").unwrap();
937
938 assert_eq!(ws.len(), 1);
939 assert_eq!(ws[0].0, "Sheet2");
940 assert_eq!(wb_xml.sheets.sheets.len(), 1);
941 assert_eq!(wb_xml.sheets.sheets[0].name, "Sheet2");
942
943 let ws_rels: Vec<_> = wb_rels
944 .relationships
945 .iter()
946 .filter(|r| r.rel_type == rel_types::WORKSHEET)
947 .collect();
948 assert_eq!(ws_rels.len(), 1);
949
950 let ws_overrides: Vec<_> = ct
951 .overrides
952 .iter()
953 .filter(|o| o.content_type == mime_types::WORKSHEET)
954 .collect();
955 assert_eq!(ws_overrides.len(), 1);
956 }
957
958 #[test]
959 fn test_delete_last_sheet_returns_error() {
960 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
961
962 let result = delete_sheet(&mut wb_xml, &mut wb_rels, &mut ct, &mut ws, "Sheet1");
963 assert!(result.is_err());
964 }
965
966 #[test]
967 fn test_delete_nonexistent_sheet_returns_error() {
968 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
969
970 let result = delete_sheet(&mut wb_xml, &mut wb_rels, &mut ct, &mut ws, "Nonexistent");
971 assert!(result.is_err());
972 assert!(
973 matches!(result.unwrap_err(), Error::SheetNotFound { name } if name == "Nonexistent")
974 );
975 }
976
977 #[test]
978 fn test_rename_sheet_basic() {
979 let (mut wb_xml, _, _, mut ws) = test_workbook_parts();
980
981 rename_sheet(&mut wb_xml, &mut ws, "Sheet1", "MySheet").unwrap();
982
983 assert_eq!(ws[0].0, "MySheet");
984 assert_eq!(wb_xml.sheets.sheets[0].name, "MySheet");
985 }
986
987 #[test]
988 fn test_rename_sheet_to_existing_returns_error() {
989 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
990
991 add_sheet(
992 &mut wb_xml,
993 &mut wb_rels,
994 &mut ct,
995 &mut ws,
996 "Sheet2",
997 WorksheetXml::default(),
998 )
999 .unwrap();
1000
1001 let result = rename_sheet(&mut wb_xml, &mut ws, "Sheet1", "Sheet2");
1002 assert!(result.is_err());
1003 assert!(
1004 matches!(result.unwrap_err(), Error::SheetAlreadyExists { name } if name == "Sheet2")
1005 );
1006 }
1007
1008 #[test]
1009 fn test_rename_nonexistent_sheet_returns_error() {
1010 let (mut wb_xml, _, _, mut ws) = test_workbook_parts();
1011
1012 let result = rename_sheet(&mut wb_xml, &mut ws, "Nope", "NewName");
1013 assert!(result.is_err());
1014 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { name } if name == "Nope"));
1015 }
1016
1017 #[test]
1018 fn test_copy_sheet_basic() {
1019 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1020
1021 let idx = copy_sheet(
1022 &mut wb_xml,
1023 &mut wb_rels,
1024 &mut ct,
1025 &mut ws,
1026 "Sheet1",
1027 "Sheet1 Copy",
1028 )
1029 .unwrap();
1030
1031 assert_eq!(idx, 1);
1032 assert_eq!(ws.len(), 2);
1033 assert_eq!(ws[1].0, "Sheet1 Copy");
1034 assert_eq!(ws[1].1, ws[0].1);
1036 }
1037
1038 #[test]
1039 fn test_copy_nonexistent_sheet_returns_error() {
1040 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1041
1042 let result = copy_sheet(
1043 &mut wb_xml,
1044 &mut wb_rels,
1045 &mut ct,
1046 &mut ws,
1047 "Nonexistent",
1048 "Copy",
1049 );
1050 assert!(result.is_err());
1051 }
1052
1053 #[test]
1054 fn test_copy_sheet_to_existing_name_returns_error() {
1055 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1056
1057 let result = copy_sheet(
1058 &mut wb_xml,
1059 &mut wb_rels,
1060 &mut ct,
1061 &mut ws,
1062 "Sheet1",
1063 "Sheet1",
1064 );
1065 assert!(result.is_err());
1066 }
1067
1068 #[test]
1069 fn test_find_sheet_index() {
1070 let ws: Vec<(String, WorksheetXml)> = vec![
1071 ("Sheet1".to_string(), WorksheetXml::default()),
1072 ("Sheet2".to_string(), WorksheetXml::default()),
1073 ];
1074
1075 assert_eq!(find_sheet_index(&ws, "Sheet1"), Some(0));
1076 assert_eq!(find_sheet_index(&ws, "Sheet2"), Some(1));
1077 assert_eq!(find_sheet_index(&ws, "Sheet3"), None);
1078 }
1079
1080 #[test]
1081 fn test_active_sheet_index_default() {
1082 let wb_xml = WorkbookXml::default();
1083 assert_eq!(active_sheet_index(&wb_xml), 0);
1084 }
1085
1086 #[test]
1087 fn test_set_active_sheet_index() {
1088 let mut wb_xml = WorkbookXml::default();
1089 set_active_sheet_index(&mut wb_xml, 2);
1090
1091 assert_eq!(active_sheet_index(&wb_xml), 2);
1092 }
1093
1094 #[test]
1095 fn test_multiple_add_delete_consistency() {
1096 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1097
1098 add_sheet(
1099 &mut wb_xml,
1100 &mut wb_rels,
1101 &mut ct,
1102 &mut ws,
1103 "A",
1104 WorksheetXml::default(),
1105 )
1106 .unwrap();
1107 add_sheet(
1108 &mut wb_xml,
1109 &mut wb_rels,
1110 &mut ct,
1111 &mut ws,
1112 "B",
1113 WorksheetXml::default(),
1114 )
1115 .unwrap();
1116 add_sheet(
1117 &mut wb_xml,
1118 &mut wb_rels,
1119 &mut ct,
1120 &mut ws,
1121 "C",
1122 WorksheetXml::default(),
1123 )
1124 .unwrap();
1125
1126 assert_eq!(ws.len(), 4);
1127
1128 delete_sheet(&mut wb_xml, &mut wb_rels, &mut ct, &mut ws, "B").unwrap();
1129
1130 assert_eq!(ws.len(), 3);
1131 let names: Vec<&str> = ws.iter().map(|(n, _)| n.as_str()).collect();
1132 assert_eq!(names, vec!["Sheet1", "A", "C"]);
1133
1134 assert_eq!(wb_xml.sheets.sheets.len(), 3);
1135 let ws_rels: Vec<_> = wb_rels
1136 .relationships
1137 .iter()
1138 .filter(|r| r.rel_type == rel_types::WORKSHEET)
1139 .collect();
1140 assert_eq!(ws_rels.len(), 3);
1141 let ws_overrides: Vec<_> = ct
1142 .overrides
1143 .iter()
1144 .filter(|o| o.content_type == mime_types::WORKSHEET)
1145 .collect();
1146 assert_eq!(ws_overrides.len(), 3);
1147 }
1148
1149 #[test]
1152 fn test_set_panes_freeze_row() {
1153 let mut ws = WorksheetXml::default();
1154 set_panes(&mut ws, "A2").unwrap();
1155
1156 let pane = ws.sheet_views.as_ref().unwrap().sheet_views[0]
1157 .pane
1158 .as_ref()
1159 .unwrap();
1160 assert_eq!(pane.y_split, Some(1));
1161 assert!(pane.x_split.is_none());
1162 assert_eq!(pane.top_left_cell, Some("A2".to_string()));
1163 assert_eq!(pane.active_pane, Some("bottomLeft".to_string()));
1164 assert_eq!(pane.state, Some("frozen".to_string()));
1165 }
1166
1167 #[test]
1168 fn test_set_panes_freeze_col() {
1169 let mut ws = WorksheetXml::default();
1170 set_panes(&mut ws, "B1").unwrap();
1171
1172 let pane = ws.sheet_views.as_ref().unwrap().sheet_views[0]
1173 .pane
1174 .as_ref()
1175 .unwrap();
1176 assert_eq!(pane.x_split, Some(1));
1177 assert!(pane.y_split.is_none());
1178 assert_eq!(pane.top_left_cell, Some("B1".to_string()));
1179 assert_eq!(pane.active_pane, Some("topRight".to_string()));
1180 assert_eq!(pane.state, Some("frozen".to_string()));
1181 }
1182
1183 #[test]
1184 fn test_set_panes_freeze_both() {
1185 let mut ws = WorksheetXml::default();
1186 set_panes(&mut ws, "B2").unwrap();
1187
1188 let pane = ws.sheet_views.as_ref().unwrap().sheet_views[0]
1189 .pane
1190 .as_ref()
1191 .unwrap();
1192 assert_eq!(pane.x_split, Some(1));
1193 assert_eq!(pane.y_split, Some(1));
1194 assert_eq!(pane.top_left_cell, Some("B2".to_string()));
1195 assert_eq!(pane.active_pane, Some("bottomRight".to_string()));
1196 assert_eq!(pane.state, Some("frozen".to_string()));
1197 }
1198
1199 #[test]
1200 fn test_set_panes_freeze_multiple_rows() {
1201 let mut ws = WorksheetXml::default();
1202 set_panes(&mut ws, "A4").unwrap();
1203
1204 let pane = ws.sheet_views.as_ref().unwrap().sheet_views[0]
1205 .pane
1206 .as_ref()
1207 .unwrap();
1208 assert_eq!(pane.y_split, Some(3));
1209 assert!(pane.x_split.is_none());
1210 assert_eq!(pane.top_left_cell, Some("A4".to_string()));
1211 assert_eq!(pane.active_pane, Some("bottomLeft".to_string()));
1212 }
1213
1214 #[test]
1215 fn test_set_panes_freeze_multiple_cols() {
1216 let mut ws = WorksheetXml::default();
1217 set_panes(&mut ws, "D1").unwrap();
1218
1219 let pane = ws.sheet_views.as_ref().unwrap().sheet_views[0]
1220 .pane
1221 .as_ref()
1222 .unwrap();
1223 assert_eq!(pane.x_split, Some(3));
1224 assert!(pane.y_split.is_none());
1225 assert_eq!(pane.top_left_cell, Some("D1".to_string()));
1226 assert_eq!(pane.active_pane, Some("topRight".to_string()));
1227 }
1228
1229 #[test]
1230 fn test_set_panes_a1_error() {
1231 let mut ws = WorksheetXml::default();
1232 let result = set_panes(&mut ws, "A1");
1233 assert!(result.is_err());
1234 assert!(matches!(
1235 result.unwrap_err(),
1236 Error::InvalidCellReference(_)
1237 ));
1238 }
1239
1240 #[test]
1241 fn test_set_panes_invalid_cell_error() {
1242 let mut ws = WorksheetXml::default();
1243 let result = set_panes(&mut ws, "ZZZZ1");
1244 assert!(result.is_err());
1245 }
1246
1247 #[test]
1248 fn test_unset_panes() {
1249 let mut ws = WorksheetXml::default();
1250 set_panes(&mut ws, "B2").unwrap();
1251 assert!(get_panes(&ws).is_some());
1252
1253 unset_panes(&mut ws);
1254 assert!(get_panes(&ws).is_none());
1255 let view = &ws.sheet_views.as_ref().unwrap().sheet_views[0];
1257 assert!(view.pane.is_none());
1258 assert!(view.selection.is_empty());
1259 }
1260
1261 #[test]
1262 fn test_get_panes_none_when_not_set() {
1263 let ws = WorksheetXml::default();
1264 assert!(get_panes(&ws).is_none());
1265 }
1266
1267 #[test]
1268 fn test_get_panes_returns_value_after_set() {
1269 let mut ws = WorksheetXml::default();
1270 set_panes(&mut ws, "C5").unwrap();
1271 assert_eq!(get_panes(&ws), Some("C5".to_string()));
1272 }
1273
1274 #[test]
1275 fn test_set_panes_selection_has_pane_attribute() {
1276 let mut ws = WorksheetXml::default();
1277 set_panes(&mut ws, "B2").unwrap();
1278
1279 let selection = &ws.sheet_views.as_ref().unwrap().sheet_views[0].selection[0];
1280 assert_eq!(selection.pane, Some("bottomRight".to_string()));
1281 assert_eq!(selection.active_cell, Some("B2".to_string()));
1282 assert_eq!(selection.sqref, Some("B2".to_string()));
1283 }
1284
1285 #[test]
1286 fn test_set_panes_overwrites_previous() {
1287 let mut ws = WorksheetXml::default();
1288 set_panes(&mut ws, "A2").unwrap();
1289 assert_eq!(get_panes(&ws), Some("A2".to_string()));
1290
1291 set_panes(&mut ws, "C3").unwrap();
1292 assert_eq!(get_panes(&ws), Some("C3".to_string()));
1293
1294 let pane = ws.sheet_views.as_ref().unwrap().sheet_views[0]
1295 .pane
1296 .as_ref()
1297 .unwrap();
1298 assert_eq!(pane.x_split, Some(2));
1299 assert_eq!(pane.y_split, Some(2));
1300 assert_eq!(pane.active_pane, Some("bottomRight".to_string()));
1301 }
1302
1303 #[test]
1304 fn test_unset_panes_noop_when_no_views() {
1305 let mut ws = WorksheetXml::default();
1306 unset_panes(&mut ws);
1308 assert!(get_panes(&ws).is_none());
1309 }
1310}