ppt_rs/generator/slide_content/
sections.rs1#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct SlideSection {
9 pub name: String,
10 pub first_slide: usize,
11 pub slide_count: usize,
12}
13
14impl SlideSection {
15 pub fn new(name: &str, first_slide: usize, slide_count: usize) -> Self {
17 Self {
18 name: name.to_string(),
19 first_slide,
20 slide_count,
21 }
22 }
23
24 pub fn last_slide(&self) -> usize {
26 if self.slide_count == 0 {
27 self.first_slide
28 } else {
29 self.first_slide + self.slide_count - 1
30 }
31 }
32
33 pub fn contains_slide(&self, slide_index: usize) -> bool {
35 slide_index >= self.first_slide && slide_index < self.first_slide + self.slide_count
36 }
37}
38
39#[derive(Clone, Debug, Default)]
41pub struct SectionManager {
42 sections: Vec<SlideSection>,
43}
44
45impl SectionManager {
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn add_section(&mut self, name: &str, first_slide: usize, slide_count: usize) -> Result<(), String> {
52 let new_section = SlideSection::new(name, first_slide, slide_count);
53
54 for existing in &self.sections {
56 if sections_overlap(existing, &new_section) {
57 return Err(format!(
58 "Section '{}' (slides {}-{}) overlaps with '{}' (slides {}-{})",
59 name, first_slide, new_section.last_slide(),
60 existing.name, existing.first_slide, existing.last_slide(),
61 ));
62 }
63 }
64
65 self.sections.push(new_section);
66 self.sections.sort_by_key(|s| s.first_slide);
68 Ok(())
69 }
70
71 pub fn remove_section(&mut self, name: &str) -> bool {
73 let before = self.sections.len();
74 self.sections.retain(|s| s.name != name);
75 self.sections.len() < before
76 }
77
78 pub fn get_section(&self, name: &str) -> Option<&SlideSection> {
80 self.sections.iter().find(|s| s.name == name)
81 }
82
83 pub fn section_for_slide(&self, slide_index: usize) -> Option<&SlideSection> {
85 self.sections.iter().find(|s| s.contains_slide(slide_index))
86 }
87
88 pub fn sections(&self) -> &[SlideSection] {
90 &self.sections
91 }
92
93 pub fn len(&self) -> usize {
95 self.sections.len()
96 }
97
98 pub fn is_empty(&self) -> bool {
100 self.sections.is_empty()
101 }
102
103 pub fn clear(&mut self) {
105 self.sections.clear();
106 }
107
108 pub fn rename_section(&mut self, old_name: &str, new_name: &str) -> bool {
110 if let Some(section) = self.sections.iter_mut().find(|s| s.name == old_name) {
111 section.name = new_name.to_string();
112 true
113 } else {
114 false
115 }
116 }
117
118 pub fn to_xml(&self, total_slides: usize) -> String {
120 if self.sections.is_empty() {
121 return String::new();
122 }
123
124 let mut xml = String::from(
125 r#"<p:extLst><p:ext uri="{521415D9-36F7-43E2-AB2F-B90AF26B5E84}"><p14:sectionLst xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main">"#,
126 );
127
128 for section in &self.sections {
129 xml.push_str(&format!(
130 r#"<p14:section name="{}" id="{{{}}}">"#,
131 xml_escape(§ion.name),
132 generate_section_id(§ion.name),
133 ));
134 xml.push_str("<p14:sldIdLst>");
135 for i in 0..section.slide_count {
136 let slide_id = 256 + section.first_slide + i;
137 if section.first_slide + i < total_slides {
138 xml.push_str(&format!(r#"<p14:sldId id="{}"/>"#, slide_id));
139 }
140 }
141 xml.push_str("</p14:sldIdLst>");
142 xml.push_str("</p14:section>");
143 }
144
145 xml.push_str("</p14:sectionLst></p:ext></p:extLst>");
146 xml
147 }
148}
149
150fn sections_overlap(a: &SlideSection, b: &SlideSection) -> bool {
152 if a.slide_count == 0 || b.slide_count == 0 {
153 return false;
154 }
155 a.first_slide < b.first_slide + b.slide_count && b.first_slide < a.first_slide + a.slide_count
156}
157
158fn generate_section_id(name: &str) -> String {
160 let mut hash: u64 = 0xcbf29ce484222325;
161 for byte in name.bytes() {
162 hash ^= byte as u64;
163 hash = hash.wrapping_mul(0x100000001b3);
164 }
165 let a = (hash >> 32) as u32;
166 let b = (hash & 0xFFFF) as u16;
167 let c = ((hash >> 16) & 0xFFFF) as u16;
168 let d = hash.wrapping_mul(0x9e3779b97f4a7c15);
169 format!(
170 "{:08X}-{:04X}-{:04X}-{:04X}-{:012X}",
171 a,
172 b,
173 c,
174 (d & 0xFFFF) as u16,
175 d >> 16,
176 )
177}
178
179fn xml_escape(s: &str) -> String {
180 s.replace('&', "&")
181 .replace('<', "<")
182 .replace('>', ">")
183 .replace('"', """)
184 .replace('\'', "'")
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_slide_section_new() {
193 let section = SlideSection::new("Introduction", 0, 3);
194 assert_eq!(section.name, "Introduction");
195 assert_eq!(section.first_slide, 0);
196 assert_eq!(section.slide_count, 3);
197 }
198
199 #[test]
200 fn test_slide_section_last_slide() {
201 let section = SlideSection::new("Intro", 0, 3);
202 assert_eq!(section.last_slide(), 2);
203
204 let empty = SlideSection::new("Empty", 5, 0);
205 assert_eq!(empty.last_slide(), 5);
206 }
207
208 #[test]
209 fn test_slide_section_contains() {
210 let section = SlideSection::new("Body", 3, 5);
211 assert!(!section.contains_slide(2));
212 assert!(section.contains_slide(3));
213 assert!(section.contains_slide(7));
214 assert!(!section.contains_slide(8));
215 }
216
217 #[test]
218 fn test_section_manager_new() {
219 let mgr = SectionManager::new();
220 assert!(mgr.is_empty());
221 assert_eq!(mgr.len(), 0);
222 }
223
224 #[test]
225 fn test_section_manager_add() {
226 let mut mgr = SectionManager::new();
227 assert!(mgr.add_section("Intro", 0, 3).is_ok());
228 assert!(mgr.add_section("Body", 3, 5).is_ok());
229 assert_eq!(mgr.len(), 2);
230 }
231
232 #[test]
233 fn test_section_manager_overlap_detection() {
234 let mut mgr = SectionManager::new();
235 mgr.add_section("Intro", 0, 3).unwrap();
236 let result = mgr.add_section("Overlap", 2, 3);
237 assert!(result.is_err());
238 assert!(result.unwrap_err().contains("overlaps"));
239 }
240
241 #[test]
242 fn test_section_manager_no_overlap_adjacent() {
243 let mut mgr = SectionManager::new();
244 mgr.add_section("A", 0, 3).unwrap();
245 assert!(mgr.add_section("B", 3, 2).is_ok());
246 }
247
248 #[test]
249 fn test_section_manager_remove() {
250 let mut mgr = SectionManager::new();
251 mgr.add_section("Intro", 0, 3).unwrap();
252 assert!(mgr.remove_section("Intro"));
253 assert!(mgr.is_empty());
254 assert!(!mgr.remove_section("NonExistent"));
255 }
256
257 #[test]
258 fn test_section_manager_get_section() {
259 let mut mgr = SectionManager::new();
260 mgr.add_section("Intro", 0, 3).unwrap();
261 let section = mgr.get_section("Intro");
262 assert!(section.is_some());
263 assert_eq!(section.unwrap().first_slide, 0);
264 assert!(mgr.get_section("Missing").is_none());
265 }
266
267 #[test]
268 fn test_section_manager_section_for_slide() {
269 let mut mgr = SectionManager::new();
270 mgr.add_section("Intro", 0, 3).unwrap();
271 mgr.add_section("Body", 3, 5).unwrap();
272 assert_eq!(mgr.section_for_slide(0).unwrap().name, "Intro");
273 assert_eq!(mgr.section_for_slide(4).unwrap().name, "Body");
274 assert!(mgr.section_for_slide(10).is_none());
275 }
276
277 #[test]
278 fn test_section_manager_sorted() {
279 let mut mgr = SectionManager::new();
280 mgr.add_section("Body", 3, 5).unwrap();
281 mgr.add_section("Intro", 0, 3).unwrap();
282 assert_eq!(mgr.sections()[0].name, "Intro");
283 assert_eq!(mgr.sections()[1].name, "Body");
284 }
285
286 #[test]
287 fn test_section_manager_clear() {
288 let mut mgr = SectionManager::new();
289 mgr.add_section("A", 0, 2).unwrap();
290 mgr.clear();
291 assert!(mgr.is_empty());
292 }
293
294 #[test]
295 fn test_section_manager_rename() {
296 let mut mgr = SectionManager::new();
297 mgr.add_section("Old", 0, 3).unwrap();
298 assert!(mgr.rename_section("Old", "New"));
299 assert!(mgr.get_section("New").is_some());
300 assert!(mgr.get_section("Old").is_none());
301 assert!(!mgr.rename_section("Missing", "X"));
302 }
303
304 #[test]
305 fn test_section_manager_xml_empty() {
306 let mgr = SectionManager::new();
307 assert_eq!(mgr.to_xml(10), "");
308 }
309
310 #[test]
311 fn test_section_manager_xml() {
312 let mut mgr = SectionManager::new();
313 mgr.add_section("Intro", 0, 2).unwrap();
314 mgr.add_section("Body", 2, 3).unwrap();
315 let xml = mgr.to_xml(5);
316 assert!(xml.contains("<p:extLst>"));
317 assert!(xml.contains("p14:sectionLst"));
318 assert!(xml.contains("Intro"));
319 assert!(xml.contains("Body"));
320 assert!(xml.contains("p14:sldId"));
321 assert!(xml.contains("</p:extLst>"));
322 }
323
324 #[test]
325 fn test_section_manager_xml_slide_ids() {
326 let mut mgr = SectionManager::new();
327 mgr.add_section("Intro", 0, 2).unwrap();
328 let xml = mgr.to_xml(5);
329 assert!(xml.contains(r#"id="256""#));
331 assert!(xml.contains(r#"id="257""#));
332 }
333
334 #[test]
335 fn test_sections_overlap_fn() {
336 let a = SlideSection::new("A", 0, 3);
337 let b = SlideSection::new("B", 2, 3);
338 assert!(sections_overlap(&a, &b));
339
340 let c = SlideSection::new("C", 3, 2);
341 assert!(!sections_overlap(&a, &c));
342 }
343
344 #[test]
345 fn test_sections_overlap_empty() {
346 let a = SlideSection::new("A", 0, 0);
347 let b = SlideSection::new("B", 0, 3);
348 assert!(!sections_overlap(&a, &b));
349 }
350
351 #[test]
352 fn test_generate_section_id_deterministic() {
353 let id1 = generate_section_id("Test");
354 let id2 = generate_section_id("Test");
355 assert_eq!(id1, id2);
356 }
357
358 #[test]
359 fn test_generate_section_id_unique() {
360 let id1 = generate_section_id("Intro");
361 let id2 = generate_section_id("Body");
362 assert_ne!(id1, id2);
363 }
364
365 #[test]
366 fn test_section_xml_escaping() {
367 let mut mgr = SectionManager::new();
368 mgr.add_section("Q&A <Session>", 0, 1).unwrap();
369 let xml = mgr.to_xml(1);
370 assert!(xml.contains("Q&A <Session>"));
371 }
372}