#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SlideSection {
pub name: String,
pub first_slide: usize,
pub slide_count: usize,
}
impl SlideSection {
pub fn new(name: &str, first_slide: usize, slide_count: usize) -> Self {
Self {
name: name.to_string(),
first_slide,
slide_count,
}
}
pub fn last_slide(&self) -> usize {
if self.slide_count == 0 {
self.first_slide
} else {
self.first_slide + self.slide_count - 1
}
}
pub fn contains_slide(&self, slide_index: usize) -> bool {
slide_index >= self.first_slide && slide_index < self.first_slide + self.slide_count
}
}
#[derive(Clone, Debug, Default)]
pub struct SectionManager {
sections: Vec<SlideSection>,
}
impl SectionManager {
pub fn new() -> Self {
Self::default()
}
pub fn add_section(&mut self, name: &str, first_slide: usize, slide_count: usize) -> Result<(), String> {
let new_section = SlideSection::new(name, first_slide, slide_count);
for existing in &self.sections {
if sections_overlap(existing, &new_section) {
return Err(format!(
"Section '{}' (slides {}-{}) overlaps with '{}' (slides {}-{})",
name, first_slide, new_section.last_slide(),
existing.name, existing.first_slide, existing.last_slide(),
));
}
}
self.sections.push(new_section);
self.sections.sort_by_key(|s| s.first_slide);
Ok(())
}
pub fn remove_section(&mut self, name: &str) -> bool {
let before = self.sections.len();
self.sections.retain(|s| s.name != name);
self.sections.len() < before
}
pub fn get_section(&self, name: &str) -> Option<&SlideSection> {
self.sections.iter().find(|s| s.name == name)
}
pub fn section_for_slide(&self, slide_index: usize) -> Option<&SlideSection> {
self.sections.iter().find(|s| s.contains_slide(slide_index))
}
pub fn sections(&self) -> &[SlideSection] {
&self.sections
}
pub fn len(&self) -> usize {
self.sections.len()
}
pub fn is_empty(&self) -> bool {
self.sections.is_empty()
}
pub fn clear(&mut self) {
self.sections.clear();
}
pub fn rename_section(&mut self, old_name: &str, new_name: &str) -> bool {
if let Some(section) = self.sections.iter_mut().find(|s| s.name == old_name) {
section.name = new_name.to_string();
true
} else {
false
}
}
pub fn to_xml(&self, total_slides: usize) -> String {
if self.sections.is_empty() {
return String::new();
}
let mut xml = String::from(
r#"<p:extLst><p:ext uri="{521415D9-36F7-43E2-AB2F-B90AF26B5E84}"><p14:sectionLst xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main">"#,
);
for section in &self.sections {
xml.push_str(&format!(
r#"<p14:section name="{}" id="{{{}}}">"#,
xml_escape(§ion.name),
generate_section_id(§ion.name),
));
xml.push_str("<p14:sldIdLst>");
for i in 0..section.slide_count {
let slide_id = 256 + section.first_slide + i;
if section.first_slide + i < total_slides {
xml.push_str(&format!(r#"<p14:sldId id="{}"/>"#, slide_id));
}
}
xml.push_str("</p14:sldIdLst>");
xml.push_str("</p14:section>");
}
xml.push_str("</p14:sectionLst></p:ext></p:extLst>");
xml
}
}
fn sections_overlap(a: &SlideSection, b: &SlideSection) -> bool {
if a.slide_count == 0 || b.slide_count == 0 {
return false;
}
a.first_slide < b.first_slide + b.slide_count && b.first_slide < a.first_slide + a.slide_count
}
fn generate_section_id(name: &str) -> String {
let mut hash: u64 = 0xcbf29ce484222325;
for byte in name.bytes() {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
let a = (hash >> 32) as u32;
let b = (hash & 0xFFFF) as u16;
let c = ((hash >> 16) & 0xFFFF) as u16;
let d = hash.wrapping_mul(0x9e3779b97f4a7c15);
format!(
"{:08X}-{:04X}-{:04X}-{:04X}-{:012X}",
a,
b,
c,
(d & 0xFFFF) as u16,
d >> 16,
)
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_slide_section_new() {
let section = SlideSection::new("Introduction", 0, 3);
assert_eq!(section.name, "Introduction");
assert_eq!(section.first_slide, 0);
assert_eq!(section.slide_count, 3);
}
#[test]
fn test_slide_section_last_slide() {
let section = SlideSection::new("Intro", 0, 3);
assert_eq!(section.last_slide(), 2);
let empty = SlideSection::new("Empty", 5, 0);
assert_eq!(empty.last_slide(), 5);
}
#[test]
fn test_slide_section_contains() {
let section = SlideSection::new("Body", 3, 5);
assert!(!section.contains_slide(2));
assert!(section.contains_slide(3));
assert!(section.contains_slide(7));
assert!(!section.contains_slide(8));
}
#[test]
fn test_section_manager_new() {
let mgr = SectionManager::new();
assert!(mgr.is_empty());
assert_eq!(mgr.len(), 0);
}
#[test]
fn test_section_manager_add() {
let mut mgr = SectionManager::new();
assert!(mgr.add_section("Intro", 0, 3).is_ok());
assert!(mgr.add_section("Body", 3, 5).is_ok());
assert_eq!(mgr.len(), 2);
}
#[test]
fn test_section_manager_overlap_detection() {
let mut mgr = SectionManager::new();
mgr.add_section("Intro", 0, 3).unwrap();
let result = mgr.add_section("Overlap", 2, 3);
assert!(result.is_err());
assert!(result.unwrap_err().contains("overlaps"));
}
#[test]
fn test_section_manager_no_overlap_adjacent() {
let mut mgr = SectionManager::new();
mgr.add_section("A", 0, 3).unwrap();
assert!(mgr.add_section("B", 3, 2).is_ok());
}
#[test]
fn test_section_manager_remove() {
let mut mgr = SectionManager::new();
mgr.add_section("Intro", 0, 3).unwrap();
assert!(mgr.remove_section("Intro"));
assert!(mgr.is_empty());
assert!(!mgr.remove_section("NonExistent"));
}
#[test]
fn test_section_manager_get_section() {
let mut mgr = SectionManager::new();
mgr.add_section("Intro", 0, 3).unwrap();
let section = mgr.get_section("Intro");
assert!(section.is_some());
assert_eq!(section.unwrap().first_slide, 0);
assert!(mgr.get_section("Missing").is_none());
}
#[test]
fn test_section_manager_section_for_slide() {
let mut mgr = SectionManager::new();
mgr.add_section("Intro", 0, 3).unwrap();
mgr.add_section("Body", 3, 5).unwrap();
assert_eq!(mgr.section_for_slide(0).unwrap().name, "Intro");
assert_eq!(mgr.section_for_slide(4).unwrap().name, "Body");
assert!(mgr.section_for_slide(10).is_none());
}
#[test]
fn test_section_manager_sorted() {
let mut mgr = SectionManager::new();
mgr.add_section("Body", 3, 5).unwrap();
mgr.add_section("Intro", 0, 3).unwrap();
assert_eq!(mgr.sections()[0].name, "Intro");
assert_eq!(mgr.sections()[1].name, "Body");
}
#[test]
fn test_section_manager_clear() {
let mut mgr = SectionManager::new();
mgr.add_section("A", 0, 2).unwrap();
mgr.clear();
assert!(mgr.is_empty());
}
#[test]
fn test_section_manager_rename() {
let mut mgr = SectionManager::new();
mgr.add_section("Old", 0, 3).unwrap();
assert!(mgr.rename_section("Old", "New"));
assert!(mgr.get_section("New").is_some());
assert!(mgr.get_section("Old").is_none());
assert!(!mgr.rename_section("Missing", "X"));
}
#[test]
fn test_section_manager_xml_empty() {
let mgr = SectionManager::new();
assert_eq!(mgr.to_xml(10), "");
}
#[test]
fn test_section_manager_xml() {
let mut mgr = SectionManager::new();
mgr.add_section("Intro", 0, 2).unwrap();
mgr.add_section("Body", 2, 3).unwrap();
let xml = mgr.to_xml(5);
assert!(xml.contains("<p:extLst>"));
assert!(xml.contains("p14:sectionLst"));
assert!(xml.contains("Intro"));
assert!(xml.contains("Body"));
assert!(xml.contains("p14:sldId"));
assert!(xml.contains("</p:extLst>"));
}
#[test]
fn test_section_manager_xml_slide_ids() {
let mut mgr = SectionManager::new();
mgr.add_section("Intro", 0, 2).unwrap();
let xml = mgr.to_xml(5);
assert!(xml.contains(r#"id="256""#));
assert!(xml.contains(r#"id="257""#));
}
#[test]
fn test_sections_overlap_fn() {
let a = SlideSection::new("A", 0, 3);
let b = SlideSection::new("B", 2, 3);
assert!(sections_overlap(&a, &b));
let c = SlideSection::new("C", 3, 2);
assert!(!sections_overlap(&a, &c));
}
#[test]
fn test_sections_overlap_empty() {
let a = SlideSection::new("A", 0, 0);
let b = SlideSection::new("B", 0, 3);
assert!(!sections_overlap(&a, &b));
}
#[test]
fn test_generate_section_id_deterministic() {
let id1 = generate_section_id("Test");
let id2 = generate_section_id("Test");
assert_eq!(id1, id2);
}
#[test]
fn test_generate_section_id_unique() {
let id1 = generate_section_id("Intro");
let id2 = generate_section_id("Body");
assert_ne!(id1, id2);
}
#[test]
fn test_section_xml_escaping() {
let mut mgr = SectionManager::new();
mgr.add_section("Q&A <Session>", 0, 1).unwrap();
let xml = mgr.to_xml(1);
assert!(xml.contains("Q&A <Session>"));
}
}