Skip to main content

ppt_rs/parts/
notes_slide.rs

1//! Notes slide part
2//!
3//! Represents speaker notes for a slide (ppt/notesSlides/notesSlideN.xml).
4
5use super::base::{Part, PartType, ContentType};
6use crate::exc::PptxError;
7use crate::core::escape_xml;
8
9/// Notes slide part (ppt/notesSlides/notesSlideN.xml)
10#[derive(Debug, Clone)]
11pub struct NotesSlidePart {
12    path: String,
13    notes_number: usize,
14    slide_rel_id: String,
15    notes_text: String,
16    xml_content: Option<String>,
17}
18
19impl NotesSlidePart {
20    /// Create a new notes slide part
21    pub fn new(notes_number: usize) -> Self {
22        NotesSlidePart {
23            path: format!("ppt/notesSlides/notesSlide{}.xml", notes_number),
24            notes_number,
25            slide_rel_id: "rId1".to_string(),
26            notes_text: String::new(),
27            xml_content: None,
28        }
29    }
30
31    /// Create with notes text
32    pub fn with_text(notes_number: usize, text: impl Into<String>) -> Self {
33        let mut part = Self::new(notes_number);
34        part.notes_text = text.into();
35        part
36    }
37
38    /// Get notes number
39    pub fn notes_number(&self) -> usize {
40        self.notes_number
41    }
42
43    /// Get notes text
44    pub fn notes_text(&self) -> &str {
45        &self.notes_text
46    }
47
48    /// Set notes text
49    pub fn set_notes_text(&mut self, text: impl Into<String>) {
50        self.notes_text = text.into();
51        self.xml_content = None;
52    }
53
54    /// Set slide relationship ID
55    pub fn set_slide_rel_id(&mut self, rel_id: impl Into<String>) {
56        self.slide_rel_id = rel_id.into();
57    }
58
59    /// Get relative path for relationships
60    pub fn rel_target(&self) -> String {
61        format!("../notesSlides/notesSlide{}.xml", self.notes_number)
62    }
63
64    fn generate_xml(&self) -> String {
65        let paragraphs: String = if self.notes_text.is_empty() {
66            "<a:p><a:endParaRPr lang=\"en-US\"/></a:p>".to_string()
67        } else {
68            self.notes_text
69                .lines()
70                .map(|line| {
71                    format!(
72                        "<a:p><a:r><a:rPr lang=\"en-US\"/><a:t>{}</a:t></a:r></a:p>",
73                        escape_xml(line)
74                    )
75                })
76                .collect::<Vec<_>>()
77                .join("\n              ")
78        };
79
80        format!(
81            r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
82<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
83  <p:cSld>
84    <p:spTree>
85      <p:nvGrpSpPr>
86        <p:cNvPr id="1" name=""/>
87        <p:cNvGrpSpPr/>
88        <p:nvPr/>
89      </p:nvGrpSpPr>
90      <p:grpSpPr>
91        <a:xfrm>
92          <a:off x="0" y="0"/>
93          <a:ext cx="0" cy="0"/>
94          <a:chOff x="0" y="0"/>
95          <a:chExt cx="0" cy="0"/>
96        </a:xfrm>
97      </p:grpSpPr>
98      <p:sp>
99        <p:nvSpPr>
100          <p:cNvPr id="2" name="Slide Image Placeholder 1"/>
101          <p:cNvSpPr><a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/></p:cNvSpPr>
102          <p:nvPr><p:ph type="sldImg"/></p:nvPr>
103        </p:nvSpPr>
104        <p:spPr/>
105      </p:sp>
106      <p:sp>
107        <p:nvSpPr>
108          <p:cNvPr id="3" name="Notes Placeholder 2"/>
109          <p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
110          <p:nvPr><p:ph type="body" idx="1"/></p:nvPr>
111        </p:nvSpPr>
112        <p:spPr/>
113        <p:txBody>
114          <a:bodyPr/>
115          <a:lstStyle/>
116          {}
117        </p:txBody>
118      </p:sp>
119    </p:spTree>
120  </p:cSld>
121  <p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
122</p:notes>"#,
123            paragraphs
124        )
125    }
126}
127
128impl Part for NotesSlidePart {
129    fn path(&self) -> &str {
130        &self.path
131    }
132
133    fn part_type(&self) -> PartType {
134        PartType::Slide // Notes are associated with slides
135    }
136
137    fn content_type(&self) -> ContentType {
138        ContentType::Xml // Notes have their own content type
139    }
140
141    fn to_xml(&self) -> Result<String, PptxError> {
142        if let Some(ref xml) = self.xml_content {
143            return Ok(xml.clone());
144        }
145        Ok(self.generate_xml())
146    }
147
148    fn from_xml(xml: &str) -> Result<Self, PptxError> {
149        Ok(NotesSlidePart {
150            path: "ppt/notesSlides/notesSlide1.xml".to_string(),
151            notes_number: 1,
152            slide_rel_id: "rId1".to_string(),
153            notes_text: String::new(),
154            xml_content: Some(xml.to_string()),
155        })
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_notes_slide_new() {
165        let notes = NotesSlidePart::new(1);
166        assert_eq!(notes.notes_number(), 1);
167        assert_eq!(notes.path(), "ppt/notesSlides/notesSlide1.xml");
168    }
169
170    #[test]
171    fn test_notes_slide_with_text() {
172        let notes = NotesSlidePart::with_text(1, "Speaker notes here");
173        assert_eq!(notes.notes_text(), "Speaker notes here");
174    }
175
176    #[test]
177    fn test_notes_slide_set_text() {
178        let mut notes = NotesSlidePart::new(1);
179        notes.set_notes_text("Updated notes");
180        assert_eq!(notes.notes_text(), "Updated notes");
181    }
182
183    #[test]
184    fn test_notes_slide_to_xml() {
185        let notes = NotesSlidePart::with_text(1, "Test notes");
186        let xml = notes.to_xml().unwrap();
187        assert!(xml.contains("p:notes"));
188        assert!(xml.contains("Test notes"));
189    }
190
191    #[test]
192    fn test_notes_slide_multiline() {
193        let notes = NotesSlidePart::with_text(1, "Line 1\nLine 2\nLine 3");
194        let xml = notes.to_xml().unwrap();
195        assert!(xml.contains("Line 1"));
196        assert!(xml.contains("Line 2"));
197        assert!(xml.contains("Line 3"));
198    }
199
200    #[test]
201    fn test_notes_slide_rel_target() {
202        let notes = NotesSlidePart::new(3);
203        assert_eq!(notes.rel_target(), "../notesSlides/notesSlide3.xml");
204    }
205}