1use sheetkit_xml::doc_props::{
7 CoreProperties, CustomProperty, CustomPropertyValue as XmlCustomPropertyValue,
8 ExtendedProperties, CUSTOM_PROPERTY_FMTID,
9};
10use sheetkit_xml::namespaces;
11use sheetkit_xml::workbook::{CalcPr, WorkbookPr};
12
13#[derive(Debug, Clone, Default)]
15pub struct DocProperties {
16 pub title: Option<String>,
17 pub subject: Option<String>,
18 pub creator: Option<String>,
19 pub keywords: Option<String>,
20 pub description: Option<String>,
21 pub last_modified_by: Option<String>,
22 pub revision: Option<String>,
23 pub created: Option<String>,
24 pub modified: Option<String>,
25 pub category: Option<String>,
26 pub content_status: Option<String>,
27}
28
29impl From<&CoreProperties> for DocProperties {
30 fn from(props: &CoreProperties) -> Self {
31 Self {
32 title: props.title.clone(),
33 subject: props.subject.clone(),
34 creator: props.creator.clone(),
35 keywords: props.keywords.clone(),
36 description: props.description.clone(),
37 last_modified_by: props.last_modified_by.clone(),
38 revision: props.revision.clone(),
39 created: props.created.clone(),
40 modified: props.modified.clone(),
41 category: props.category.clone(),
42 content_status: props.content_status.clone(),
43 }
44 }
45}
46
47impl DocProperties {
48 pub fn to_core_properties(&self) -> CoreProperties {
50 CoreProperties {
51 title: self.title.clone(),
52 subject: self.subject.clone(),
53 creator: self.creator.clone(),
54 keywords: self.keywords.clone(),
55 description: self.description.clone(),
56 last_modified_by: self.last_modified_by.clone(),
57 revision: self.revision.clone(),
58 created: self.created.clone(),
59 modified: self.modified.clone(),
60 category: self.category.clone(),
61 content_status: self.content_status.clone(),
62 }
63 }
64}
65
66#[derive(Debug, Clone, Default)]
68pub struct AppProperties {
69 pub application: Option<String>,
70 pub doc_security: Option<u32>,
71 pub company: Option<String>,
72 pub app_version: Option<String>,
73 pub manager: Option<String>,
74 pub template: Option<String>,
75}
76
77impl From<&ExtendedProperties> for AppProperties {
78 fn from(props: &ExtendedProperties) -> Self {
79 Self {
80 application: props.application.clone(),
81 doc_security: props.doc_security,
82 company: props.company.clone(),
83 app_version: props.app_version.clone(),
84 manager: props.manager.clone(),
85 template: props.template.clone(),
86 }
87 }
88}
89
90impl AppProperties {
91 pub fn to_extended_properties(&self) -> ExtendedProperties {
93 ExtendedProperties {
94 xmlns: namespaces::EXTENDED_PROPERTIES.to_string(),
95 xmlns_vt: Some(namespaces::VT.to_string()),
96 application: self.application.clone(),
97 doc_security: self.doc_security,
98 scale_crop: None,
99 company: self.company.clone(),
100 links_up_to_date: None,
101 shared_doc: None,
102 hyperlinks_changed: None,
103 app_version: self.app_version.clone(),
104 template: self.template.clone(),
105 manager: self.manager.clone(),
106 }
107 }
108}
109
110#[derive(Debug, Clone, PartialEq)]
112pub enum CustomPropertyValue {
113 String(String),
114 Int(i32),
115 Float(f64),
116 Bool(bool),
117 DateTime(String),
118}
119
120impl CustomPropertyValue {
121 pub(crate) fn to_xml(&self) -> XmlCustomPropertyValue {
123 match self {
124 Self::String(s) => XmlCustomPropertyValue::String(s.clone()),
125 Self::Int(n) => XmlCustomPropertyValue::Int(*n),
126 Self::Float(f) => XmlCustomPropertyValue::Float(*f),
127 Self::Bool(b) => XmlCustomPropertyValue::Bool(*b),
128 Self::DateTime(dt) => XmlCustomPropertyValue::DateTime(dt.clone()),
129 }
130 }
131
132 pub(crate) fn from_xml(val: &XmlCustomPropertyValue) -> Self {
134 match val {
135 XmlCustomPropertyValue::String(s) => Self::String(s.clone()),
136 XmlCustomPropertyValue::Int(n) => Self::Int(*n),
137 XmlCustomPropertyValue::Float(f) => Self::Float(*f),
138 XmlCustomPropertyValue::Bool(b) => Self::Bool(*b),
139 XmlCustomPropertyValue::DateTime(dt) => Self::DateTime(dt.clone()),
140 }
141 }
142}
143
144pub(crate) fn find_custom_property(
146 props: &sheetkit_xml::doc_props::CustomProperties,
147 name: &str,
148) -> Option<CustomPropertyValue> {
149 props
150 .properties
151 .iter()
152 .find(|p| p.name == name)
153 .map(|p| CustomPropertyValue::from_xml(&p.value))
154}
155
156pub(crate) fn set_custom_property(
159 props: &mut sheetkit_xml::doc_props::CustomProperties,
160 name: &str,
161 value: CustomPropertyValue,
162) {
163 if let Some(existing) = props.properties.iter_mut().find(|p| p.name == name) {
164 existing.value = value.to_xml();
165 return;
166 }
167
168 let next_pid = props
169 .properties
170 .iter()
171 .map(|p| p.pid)
172 .max()
173 .map(|m| m + 1)
174 .unwrap_or(2);
175
176 props.properties.push(CustomProperty {
177 fmtid: CUSTOM_PROPERTY_FMTID.to_string(),
178 pid: next_pid,
179 name: name.to_string(),
180 value: value.to_xml(),
181 });
182}
183
184pub(crate) fn delete_custom_property(
186 props: &mut sheetkit_xml::doc_props::CustomProperties,
187 name: &str,
188) -> bool {
189 let before = props.properties.len();
190 props.properties.retain(|p| p.name != name);
191 props.properties.len() < before
192}
193
194#[derive(Debug, Clone, Default)]
198pub struct WorkbookSettings {
199 pub date1904: Option<bool>,
201 pub filter_privacy: Option<bool>,
203 pub default_theme_version: Option<u32>,
205 pub show_objects: Option<String>,
207 pub code_name: Option<String>,
209 pub check_compatibility: Option<bool>,
211 pub auto_compress_pictures: Option<bool>,
213 pub backup_file: Option<bool>,
215 pub save_external_link_values: Option<bool>,
217 pub update_links: Option<String>,
219 pub hide_pivot_field_list: Option<bool>,
221 pub show_pivot_chart_filter: Option<bool>,
223 pub allow_refresh_query: Option<bool>,
225 pub publish_items: Option<bool>,
227 pub show_border_unselected_tables: Option<bool>,
229 pub prompted_solutions: Option<bool>,
231 pub show_ink_annotation: Option<bool>,
233}
234
235impl From<&WorkbookPr> for WorkbookSettings {
236 fn from(pr: &WorkbookPr) -> Self {
237 Self {
238 date1904: pr.date1904,
239 filter_privacy: pr.filter_privacy,
240 default_theme_version: pr.default_theme_version,
241 show_objects: pr.show_objects.clone(),
242 code_name: pr.code_name.clone(),
243 check_compatibility: pr.check_compatibility,
244 auto_compress_pictures: pr.auto_compress_pictures,
245 backup_file: pr.backup_file,
246 save_external_link_values: pr.save_external_link_values,
247 update_links: pr.update_links.clone(),
248 hide_pivot_field_list: pr.hide_pivot_field_list,
249 show_pivot_chart_filter: pr.show_pivot_chart_filter,
250 allow_refresh_query: pr.allow_refresh_query,
251 publish_items: pr.publish_items,
252 show_border_unselected_tables: pr.show_border_unselected_tables,
253 prompted_solutions: pr.prompted_solutions,
254 show_ink_annotation: pr.show_ink_annotation,
255 }
256 }
257}
258
259impl WorkbookSettings {
260 pub fn to_workbook_pr(&self) -> WorkbookPr {
262 WorkbookPr {
263 date1904: self.date1904,
264 filter_privacy: self.filter_privacy,
265 default_theme_version: self.default_theme_version,
266 show_objects: self.show_objects.clone(),
267 code_name: self.code_name.clone(),
268 check_compatibility: self.check_compatibility,
269 auto_compress_pictures: self.auto_compress_pictures,
270 backup_file: self.backup_file,
271 save_external_link_values: self.save_external_link_values,
272 update_links: self.update_links.clone(),
273 hide_pivot_field_list: self.hide_pivot_field_list,
274 show_pivot_chart_filter: self.show_pivot_chart_filter,
275 allow_refresh_query: self.allow_refresh_query,
276 publish_items: self.publish_items,
277 show_border_unselected_tables: self.show_border_unselected_tables,
278 prompted_solutions: self.prompted_solutions,
279 show_ink_annotation: self.show_ink_annotation,
280 }
281 }
282}
283
284#[derive(Debug, Clone, Default)]
288pub struct CalcSettings {
289 pub calc_id: Option<u32>,
291 pub calc_mode: Option<String>,
293 pub full_calc_on_load: Option<bool>,
295 pub ref_mode: Option<String>,
297 pub iterate: Option<bool>,
299 pub iterate_count: Option<u32>,
301 pub iterate_delta: Option<f64>,
303 pub full_precision: Option<bool>,
305 pub calc_completed: Option<bool>,
307 pub calc_on_save: Option<bool>,
309 pub concurrent_calc: Option<bool>,
311 pub concurrent_manual_count: Option<u32>,
313 pub force_full_calc: Option<bool>,
315}
316
317impl From<&CalcPr> for CalcSettings {
318 fn from(pr: &CalcPr) -> Self {
319 Self {
320 calc_id: pr.calc_id,
321 calc_mode: pr.calc_mode.clone(),
322 full_calc_on_load: pr.full_calc_on_load,
323 ref_mode: pr.ref_mode.clone(),
324 iterate: pr.iterate,
325 iterate_count: pr.iterate_count,
326 iterate_delta: pr.iterate_delta,
327 full_precision: pr.full_precision,
328 calc_completed: pr.calc_completed,
329 calc_on_save: pr.calc_on_save,
330 concurrent_calc: pr.concurrent_calc,
331 concurrent_manual_count: pr.concurrent_manual_count,
332 force_full_calc: pr.force_full_calc,
333 }
334 }
335}
336
337impl CalcSettings {
338 pub fn to_calc_pr(&self) -> CalcPr {
340 CalcPr {
341 calc_id: self.calc_id,
342 calc_mode: self.calc_mode.clone(),
343 full_calc_on_load: self.full_calc_on_load,
344 ref_mode: self.ref_mode.clone(),
345 iterate: self.iterate,
346 iterate_count: self.iterate_count,
347 iterate_delta: self.iterate_delta,
348 full_precision: self.full_precision,
349 calc_completed: self.calc_completed,
350 calc_on_save: self.calc_on_save,
351 concurrent_calc: self.concurrent_calc,
352 concurrent_manual_count: self.concurrent_manual_count,
353 force_full_calc: self.force_full_calc,
354 }
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 #[test]
363 fn test_doc_properties_from_core_properties() {
364 let core = CoreProperties {
365 title: Some("T".to_string()),
366 creator: Some("C".to_string()),
367 ..Default::default()
368 };
369 let doc = DocProperties::from(&core);
370 assert_eq!(doc.title.as_deref(), Some("T"));
371 assert_eq!(doc.creator.as_deref(), Some("C"));
372 assert!(doc.subject.is_none());
373 }
374
375 #[test]
376 fn test_doc_properties_to_core_properties() {
377 let doc = DocProperties {
378 title: Some("T".to_string()),
379 subject: Some("S".to_string()),
380 ..Default::default()
381 };
382 let core = doc.to_core_properties();
383 assert_eq!(core.title.as_deref(), Some("T"));
384 assert_eq!(core.subject.as_deref(), Some("S"));
385 assert!(core.creator.is_none());
386 }
387
388 #[test]
389 fn test_app_properties_from_extended_properties() {
390 let ext = ExtendedProperties {
391 xmlns: namespaces::EXTENDED_PROPERTIES.to_string(),
392 xmlns_vt: None,
393 application: Some("TestApp".to_string()),
394 doc_security: Some(0),
395 company: Some("Corp".to_string()),
396 ..Default::default()
397 };
398 let app = AppProperties::from(&ext);
399 assert_eq!(app.application.as_deref(), Some("TestApp"));
400 assert_eq!(app.doc_security, Some(0));
401 assert_eq!(app.company.as_deref(), Some("Corp"));
402 }
403
404 #[test]
405 fn test_app_properties_to_extended_properties() {
406 let app = AppProperties {
407 application: Some("SheetKit".to_string()),
408 company: Some("Acme".to_string()),
409 ..Default::default()
410 };
411 let ext = app.to_extended_properties();
412 assert_eq!(ext.xmlns, namespaces::EXTENDED_PROPERTIES);
413 assert_eq!(ext.application.as_deref(), Some("SheetKit"));
414 assert_eq!(ext.company.as_deref(), Some("Acme"));
415 }
416
417 #[test]
418 fn test_custom_property_value_roundtrip() {
419 let vals = vec![
420 CustomPropertyValue::String("hello".to_string()),
421 CustomPropertyValue::Int(42),
422 CustomPropertyValue::Float(3.14),
423 CustomPropertyValue::Bool(true),
424 CustomPropertyValue::DateTime("2024-01-01T00:00:00Z".to_string()),
425 ];
426 for v in &vals {
427 let xml = v.to_xml();
428 let back = CustomPropertyValue::from_xml(&xml);
429 assert_eq!(*v, back);
430 }
431 }
432
433 #[test]
434 fn test_set_and_find_custom_property() {
435 let mut props = sheetkit_xml::doc_props::CustomProperties::default();
436 set_custom_property(
437 &mut props,
438 "Project",
439 CustomPropertyValue::String("SK".to_string()),
440 );
441 let found = find_custom_property(&props, "Project");
442 assert_eq!(found, Some(CustomPropertyValue::String("SK".to_string())));
443 assert_eq!(props.properties[0].pid, 2);
444 }
445
446 #[test]
447 fn test_set_custom_property_update_existing() {
448 let mut props = sheetkit_xml::doc_props::CustomProperties::default();
449 set_custom_property(
450 &mut props,
451 "Key",
452 CustomPropertyValue::String("old".to_string()),
453 );
454 set_custom_property(
455 &mut props,
456 "Key",
457 CustomPropertyValue::String("new".to_string()),
458 );
459 assert_eq!(props.properties.len(), 1);
460 assert_eq!(
461 find_custom_property(&props, "Key"),
462 Some(CustomPropertyValue::String("new".to_string()))
463 );
464 }
465
466 #[test]
467 fn test_delete_custom_property() {
468 let mut props = sheetkit_xml::doc_props::CustomProperties::default();
469 set_custom_property(&mut props, "Key", CustomPropertyValue::Int(1));
470 assert!(delete_custom_property(&mut props, "Key"));
471 assert!(!delete_custom_property(&mut props, "Key")); assert!(find_custom_property(&props, "Key").is_none());
473 }
474
475 #[test]
476 fn test_custom_property_pid_auto_increment() {
477 let mut props = sheetkit_xml::doc_props::CustomProperties::default();
478 set_custom_property(&mut props, "A", CustomPropertyValue::Int(1));
479 set_custom_property(&mut props, "B", CustomPropertyValue::Int(2));
480 set_custom_property(&mut props, "C", CustomPropertyValue::Int(3));
481 assert_eq!(props.properties[0].pid, 2);
482 assert_eq!(props.properties[1].pid, 3);
483 assert_eq!(props.properties[2].pid, 4);
484 }
485
486 #[test]
489 fn test_workbook_settings_default() {
490 let settings = WorkbookSettings::default();
491 assert!(settings.date1904.is_none());
492 assert!(settings.filter_privacy.is_none());
493 assert!(settings.default_theme_version.is_none());
494 assert!(settings.show_objects.is_none());
495 assert!(settings.code_name.is_none());
496 assert!(settings.check_compatibility.is_none());
497 assert!(settings.auto_compress_pictures.is_none());
498 assert!(settings.backup_file.is_none());
499 assert!(settings.save_external_link_values.is_none());
500 assert!(settings.update_links.is_none());
501 assert!(settings.hide_pivot_field_list.is_none());
502 assert!(settings.show_pivot_chart_filter.is_none());
503 assert!(settings.allow_refresh_query.is_none());
504 assert!(settings.publish_items.is_none());
505 assert!(settings.show_border_unselected_tables.is_none());
506 assert!(settings.prompted_solutions.is_none());
507 assert!(settings.show_ink_annotation.is_none());
508 }
509
510 #[test]
511 fn test_workbook_settings_to_xml_roundtrip() {
512 let settings = WorkbookSettings {
513 date1904: Some(false),
514 filter_privacy: Some(true),
515 default_theme_version: Some(166925),
516 show_objects: Some("all".to_string()),
517 code_name: Some("ThisWorkbook".to_string()),
518 check_compatibility: Some(true),
519 auto_compress_pictures: Some(false),
520 backup_file: Some(true),
521 save_external_link_values: Some(true),
522 update_links: Some("always".to_string()),
523 hide_pivot_field_list: Some(false),
524 show_pivot_chart_filter: Some(true),
525 allow_refresh_query: Some(true),
526 publish_items: Some(false),
527 show_border_unselected_tables: Some(true),
528 prompted_solutions: Some(false),
529 show_ink_annotation: Some(true),
530 };
531 let pr = settings.to_workbook_pr();
532 let back = WorkbookSettings::from(&pr);
533
534 assert_eq!(back.date1904, Some(false));
535 assert_eq!(back.filter_privacy, Some(true));
536 assert_eq!(back.default_theme_version, Some(166925));
537 assert_eq!(back.show_objects.as_deref(), Some("all"));
538 assert_eq!(back.code_name.as_deref(), Some("ThisWorkbook"));
539 assert_eq!(back.check_compatibility, Some(true));
540 assert_eq!(back.auto_compress_pictures, Some(false));
541 assert_eq!(back.backup_file, Some(true));
542 assert_eq!(back.save_external_link_values, Some(true));
543 assert_eq!(back.update_links.as_deref(), Some("always"));
544 assert_eq!(back.hide_pivot_field_list, Some(false));
545 assert_eq!(back.show_pivot_chart_filter, Some(true));
546 assert_eq!(back.allow_refresh_query, Some(true));
547 assert_eq!(back.publish_items, Some(false));
548 assert_eq!(back.show_border_unselected_tables, Some(true));
549 assert_eq!(back.prompted_solutions, Some(false));
550 assert_eq!(back.show_ink_annotation, Some(true));
551 }
552
553 #[test]
554 fn test_workbook_settings_date1904() {
555 let settings = WorkbookSettings {
556 date1904: Some(true),
557 ..Default::default()
558 };
559 let pr = settings.to_workbook_pr();
560 assert_eq!(pr.date1904, Some(true));
561 assert!(pr.filter_privacy.is_none());
563 assert!(pr.default_theme_version.is_none());
564 assert!(pr.code_name.is_none());
565
566 let back = WorkbookSettings::from(&pr);
567 assert_eq!(back.date1904, Some(true));
568 assert!(back.filter_privacy.is_none());
569 }
570
571 #[test]
574 fn test_calc_settings_default() {
575 let settings = CalcSettings::default();
576 assert!(settings.calc_id.is_none());
577 assert!(settings.calc_mode.is_none());
578 assert!(settings.full_calc_on_load.is_none());
579 assert!(settings.ref_mode.is_none());
580 assert!(settings.iterate.is_none());
581 assert!(settings.iterate_count.is_none());
582 assert!(settings.iterate_delta.is_none());
583 assert!(settings.full_precision.is_none());
584 assert!(settings.calc_completed.is_none());
585 assert!(settings.calc_on_save.is_none());
586 assert!(settings.concurrent_calc.is_none());
587 assert!(settings.concurrent_manual_count.is_none());
588 assert!(settings.force_full_calc.is_none());
589 }
590
591 #[test]
592 fn test_calc_settings_to_xml_roundtrip() {
593 let settings = CalcSettings {
594 calc_id: Some(191029),
595 calc_mode: Some("auto".to_string()),
596 full_calc_on_load: Some(true),
597 ref_mode: Some("A1".to_string()),
598 iterate: Some(true),
599 iterate_count: Some(100),
600 iterate_delta: Some(0.001),
601 full_precision: Some(true),
602 calc_completed: Some(true),
603 calc_on_save: Some(true),
604 concurrent_calc: Some(true),
605 concurrent_manual_count: Some(4),
606 force_full_calc: Some(false),
607 };
608 let pr = settings.to_calc_pr();
609 let back = CalcSettings::from(&pr);
610
611 assert_eq!(back.calc_id, Some(191029));
612 assert_eq!(back.calc_mode.as_deref(), Some("auto"));
613 assert_eq!(back.full_calc_on_load, Some(true));
614 assert_eq!(back.ref_mode.as_deref(), Some("A1"));
615 assert_eq!(back.iterate, Some(true));
616 assert_eq!(back.iterate_count, Some(100));
617 assert_eq!(back.iterate_delta, Some(0.001));
618 assert_eq!(back.full_precision, Some(true));
619 assert_eq!(back.calc_completed, Some(true));
620 assert_eq!(back.calc_on_save, Some(true));
621 assert_eq!(back.concurrent_calc, Some(true));
622 assert_eq!(back.concurrent_manual_count, Some(4));
623 assert_eq!(back.force_full_calc, Some(false));
624 }
625
626 #[test]
627 fn test_calc_settings_manual_mode() {
628 let settings = CalcSettings {
629 calc_mode: Some("manual".to_string()),
630 calc_on_save: Some(false),
631 ..Default::default()
632 };
633 let pr = settings.to_calc_pr();
634 assert_eq!(pr.calc_mode.as_deref(), Some("manual"));
635 assert_eq!(pr.calc_on_save, Some(false));
636 assert!(pr.calc_id.is_none());
638 assert!(pr.iterate.is_none());
639
640 let back = CalcSettings::from(&pr);
641 assert_eq!(back.calc_mode.as_deref(), Some("manual"));
642 assert_eq!(back.calc_on_save, Some(false));
643 assert!(back.calc_id.is_none());
644 }
645
646 #[test]
647 fn test_calc_settings_iterative() {
648 let settings = CalcSettings {
649 iterate: Some(true),
650 iterate_count: Some(200),
651 iterate_delta: Some(0.0001),
652 ..Default::default()
653 };
654 let pr = settings.to_calc_pr();
655 assert_eq!(pr.iterate, Some(true));
656 assert_eq!(pr.iterate_count, Some(200));
657 assert_eq!(pr.iterate_delta, Some(0.0001));
658 assert!(pr.calc_mode.is_none());
660 assert!(pr.ref_mode.is_none());
661
662 let back = CalcSettings::from(&pr);
663 assert_eq!(back.iterate, Some(true));
664 assert_eq!(back.iterate_count, Some(200));
665 assert_eq!(back.iterate_delta, Some(0.0001));
666 }
667}