1use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[serde(rename = "slicers")]
18pub struct SlicerDefinitions {
19 #[serde(rename = "@xmlns")]
20 pub xmlns: String,
21
22 #[serde(rename = "@xmlns:mc", skip_serializing_if = "Option::is_none")]
23 pub xmlns_mc: Option<String>,
24
25 #[serde(rename = "slicer", default)]
26 pub slicers: Vec<SlicerDefinition>,
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct SlicerDefinition {
32 #[serde(rename = "@name")]
33 pub name: String,
34
35 #[serde(rename = "@cache")]
36 pub cache: String,
37
38 #[serde(rename = "@caption", skip_serializing_if = "Option::is_none")]
39 pub caption: Option<String>,
40
41 #[serde(rename = "@startItem", skip_serializing_if = "Option::is_none")]
42 pub start_item: Option<u32>,
43
44 #[serde(rename = "@columnCount", skip_serializing_if = "Option::is_none")]
45 pub column_count: Option<u32>,
46
47 #[serde(rename = "@showCaption", skip_serializing_if = "Option::is_none")]
48 pub show_caption: Option<bool>,
49
50 #[serde(rename = "@style", skip_serializing_if = "Option::is_none")]
51 pub style: Option<String>,
52
53 #[serde(rename = "@lockedPosition", skip_serializing_if = "Option::is_none")]
54 pub locked_position: Option<bool>,
55
56 #[serde(rename = "@rowHeight")]
57 pub row_height: u32,
58}
59
60#[derive(Debug, Clone, PartialEq)]
65pub struct SlicerCacheDefinition {
66 pub name: String,
67 pub source_name: String,
68 pub table_slicer_cache: Option<TableSlicerCache>,
69}
70
71#[derive(Debug, Clone, PartialEq)]
73pub struct TableSlicerCache {
74 pub table_id: u32,
75 pub column: u32,
76}
77
78pub fn serialize_slicer_cache(def: &SlicerCacheDefinition) -> String {
83 use std::fmt::Write;
84
85 let ns_x14 = crate::namespaces::SLICER_2009;
86 let ns_x15 = crate::namespaces::SLICER_2010;
87 let ns_mc = crate::namespaces::MC;
88
89 let mut xml = String::new();
90 let _ = write!(
91 xml,
92 "<slicerCacheDefinition \
93 xmlns=\"{ns_x14}\" \
94 xmlns:mc=\"{ns_mc}\" \
95 name=\"{}\" \
96 sourceName=\"{}\"",
97 escape_xml_attr(&def.name),
98 escape_xml_attr(&def.source_name),
99 );
100
101 if let Some(ref tsc) = def.table_slicer_cache {
102 let _ = write!(xml, ">");
103 let _ = write!(
104 xml,
105 "<extLst>\
106 <ext xmlns:x15=\"{ns_x15}\" \
107 uri=\"{{2F2917AC-EB37-4324-AD4E-5DD8C200BD13}}\">\
108 <x15:tableSlicerCache tableId=\"{}\" column=\"{}\"/>\
109 </ext>\
110 </extLst>",
111 tsc.table_id, tsc.column,
112 );
113 let _ = write!(xml, "</slicerCacheDefinition>");
114 } else {
115 let _ = write!(xml, "/>");
116 }
117
118 xml
119}
120
121pub fn parse_slicer_cache(xml: &str) -> Option<SlicerCacheDefinition> {
126 let name = extract_attr(xml, "name")?;
127 let source_name = extract_attr(xml, "sourceName")?;
128
129 let table_slicer_cache = if let Some(tsc_start) = xml.find("tableSlicerCache") {
130 let remainder = &xml[tsc_start..];
131 let table_id = extract_attr(remainder, "tableId").and_then(|s| s.parse::<u32>().ok());
132 let column = extract_attr(remainder, "column").and_then(|s| s.parse::<u32>().ok());
133 match (table_id, column) {
134 (Some(tid), Some(col)) => Some(TableSlicerCache {
135 table_id: tid,
136 column: col,
137 }),
138 _ => None,
139 }
140 } else {
141 None
142 };
143
144 Some(SlicerCacheDefinition {
145 name,
146 source_name,
147 table_slicer_cache,
148 })
149}
150
151fn extract_attr(xml: &str, attr_name: &str) -> Option<String> {
153 let pattern = format!("{}=\"", attr_name);
154 let start = xml.find(&pattern)?;
155 let after_eq = start + pattern.len();
156 let end = xml[after_eq..].find('"')?;
157 Some(xml[after_eq..after_eq + end].to_string())
158}
159
160fn escape_xml_attr(s: &str) -> String {
162 s.replace('&', "&")
163 .replace('<', "<")
164 .replace('>', ">")
165 .replace('"', """)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_slicer_definition_roundtrip() {
174 let defs = SlicerDefinitions {
175 xmlns: "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main".to_string(),
176 xmlns_mc: None,
177 slicers: vec![SlicerDefinition {
178 name: "Slicer_Category".to_string(),
179 cache: "Slicer_Category".to_string(),
180 caption: Some("Category".to_string()),
181 start_item: None,
182 column_count: None,
183 show_caption: Some(true),
184 style: Some("SlicerStyleLight1".to_string()),
185 locked_position: None,
186 row_height: 241300,
187 }],
188 };
189
190 let xml = quick_xml::se::to_string(&defs).unwrap();
191 assert!(xml.contains("Slicer_Category"));
192 assert!(xml.contains("SlicerStyleLight1"));
193
194 let parsed: SlicerDefinitions = quick_xml::de::from_str(&xml).unwrap();
195 assert_eq!(parsed.slicers.len(), 1);
196 assert_eq!(parsed.slicers[0].name, "Slicer_Category");
197 assert_eq!(parsed.slicers[0].row_height, 241300);
198 }
199
200 #[test]
201 fn test_slicer_cache_serialize_and_parse() {
202 let cache = SlicerCacheDefinition {
203 name: "Slicer_Category".to_string(),
204 source_name: "Category".to_string(),
205 table_slicer_cache: Some(TableSlicerCache {
206 table_id: 1,
207 column: 2,
208 }),
209 };
210
211 let xml = serialize_slicer_cache(&cache);
212 assert!(xml.contains("Slicer_Category"));
213 assert!(xml.contains("sourceName=\"Category\""));
214 assert!(xml.contains("x15:tableSlicerCache"));
215 assert!(xml.contains("tableId=\"1\""));
216 assert!(xml.contains("column=\"2\""));
217
218 let parsed = parse_slicer_cache(&xml).unwrap();
219 assert_eq!(parsed.name, "Slicer_Category");
220 assert_eq!(parsed.source_name, "Category");
221 let tsc = parsed.table_slicer_cache.unwrap();
222 assert_eq!(tsc.table_id, 1);
223 assert_eq!(tsc.column, 2);
224 }
225
226 #[test]
227 fn test_slicer_definition_minimal() {
228 let def = SlicerDefinition {
229 name: "S1".to_string(),
230 cache: "S1".to_string(),
231 caption: None,
232 start_item: None,
233 column_count: None,
234 show_caption: None,
235 style: None,
236 locked_position: None,
237 row_height: 241300,
238 };
239
240 let xml = quick_xml::se::to_string(&def).unwrap();
241 assert!(!xml.contains("caption="));
242 assert!(!xml.contains("style="));
243 assert!(xml.contains("rowHeight=\"241300\""));
244 }
245
246 #[test]
247 fn test_multiple_slicers_in_definitions() {
248 let defs = SlicerDefinitions {
249 xmlns: "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main".to_string(),
250 xmlns_mc: None,
251 slicers: vec![
252 SlicerDefinition {
253 name: "Slicer_A".to_string(),
254 cache: "Slicer_A".to_string(),
255 caption: Some("Column A".to_string()),
256 start_item: None,
257 column_count: Some(2),
258 show_caption: Some(true),
259 style: Some("SlicerStyleDark1".to_string()),
260 locked_position: None,
261 row_height: 241300,
262 },
263 SlicerDefinition {
264 name: "Slicer_B".to_string(),
265 cache: "Slicer_B".to_string(),
266 caption: Some("Column B".to_string()),
267 start_item: None,
268 column_count: None,
269 show_caption: Some(false),
270 style: None,
271 locked_position: Some(true),
272 row_height: 241300,
273 },
274 ],
275 };
276
277 let xml = quick_xml::se::to_string(&defs).unwrap();
278 let parsed: SlicerDefinitions = quick_xml::de::from_str(&xml).unwrap();
279 assert_eq!(parsed.slicers.len(), 2);
280 assert_eq!(parsed.slicers[0].name, "Slicer_A");
281 assert_eq!(parsed.slicers[1].name, "Slicer_B");
282 }
283
284 #[test]
285 fn test_slicer_cache_without_table_cache() {
286 let cache = SlicerCacheDefinition {
287 name: "Slicer_X".to_string(),
288 source_name: "ColumnX".to_string(),
289 table_slicer_cache: None,
290 };
291
292 let xml = serialize_slicer_cache(&cache);
293 assert!(!xml.contains("tableSlicerCache"));
294 assert!(xml.contains("Slicer_X"));
295
296 let parsed = parse_slicer_cache(&xml).unwrap();
297 assert_eq!(parsed.name, "Slicer_X");
298 assert!(parsed.table_slicer_cache.is_none());
299 }
300
301 #[test]
302 fn test_slicer_cache_escapes_special_chars() {
303 let cache = SlicerCacheDefinition {
304 name: "Slicer_A&B".to_string(),
305 source_name: "Col<1>".to_string(),
306 table_slicer_cache: None,
307 };
308
309 let xml = serialize_slicer_cache(&cache);
310 assert!(xml.contains("Slicer_A&B"));
311 assert!(xml.contains("Col<1>"));
312 }
313
314 #[test]
315 fn test_extract_attr() {
316 let xml = r#"<elem name="hello" id="42">"#;
317 assert_eq!(extract_attr(xml, "name"), Some("hello".to_string()));
318 assert_eq!(extract_attr(xml, "id"), Some("42".to_string()));
319 assert_eq!(extract_attr(xml, "missing"), None);
320 }
321}