ppt_rs/generator/slide_content/
comments.rs1use std::collections::HashMap;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct CommentAuthor {
11 pub id: u32,
12 pub name: String,
13 pub initials: String,
14 pub color_index: u32,
15}
16
17impl CommentAuthor {
18 pub fn new(id: u32, name: &str, initials: &str) -> Self {
19 Self {
20 id,
21 name: name.to_string(),
22 initials: initials.to_string(),
23 color_index: id,
24 }
25 }
26
27 pub fn color_index(mut self, idx: u32) -> Self {
28 self.color_index = idx;
29 self
30 }
31
32 pub fn to_xml(&self) -> String {
34 format!(
35 r#"<p:cmAuthor id="{}" name="{}" initials="{}" lastIdx="1" clrIdx="{}"/>"#,
36 self.id,
37 xml_escape(&self.name),
38 xml_escape(&self.initials),
39 self.color_index,
40 )
41 }
42}
43
44#[derive(Clone, Debug)]
46pub struct Comment {
47 pub author_id: u32,
48 pub text: String,
49 pub date: String,
50 pub x: u32,
51 pub y: u32,
52 pub index: u32,
53}
54
55impl Comment {
56 pub fn new(author_id: u32, text: &str) -> Self {
58 Self {
59 author_id,
60 text: text.to_string(),
61 date: "2025-01-01T00:00:00.000".to_string(),
62 x: 0,
63 y: 0,
64 index: 1,
65 }
66 }
67
68 pub fn position(mut self, x: u32, y: u32) -> Self {
70 self.x = x;
71 self.y = y;
72 self
73 }
74
75 pub fn date(mut self, date: &str) -> Self {
77 self.date = date.to_string();
78 self
79 }
80
81 pub fn index(mut self, idx: u32) -> Self {
83 self.index = idx;
84 self
85 }
86
87 pub fn to_xml(&self) -> String {
89 format!(
90 r#"<p:cm authorId="{}" dt="{}" idx="{}"><p:pos x="{}" y="{}"/><p:text>{}</p:text></p:cm>"#,
91 self.author_id,
92 xml_escape(&self.date),
93 self.index,
94 self.x,
95 self.y,
96 xml_escape(&self.text),
97 )
98 }
99}
100
101#[derive(Clone, Debug, Default)]
103pub struct CommentAuthorList {
104 authors: Vec<CommentAuthor>,
105 name_to_id: HashMap<String, u32>,
106 next_id: u32,
107}
108
109impl CommentAuthorList {
110 pub fn new() -> Self {
111 Self::default()
112 }
113
114 pub fn get_or_add(&mut self, name: &str, initials: &str) -> u32 {
116 if let Some(&id) = self.name_to_id.get(name) {
117 return id;
118 }
119 let id = self.next_id;
120 self.next_id += 1;
121 let author = CommentAuthor::new(id, name, initials);
122 self.authors.push(author);
123 self.name_to_id.insert(name.to_string(), id);
124 id
125 }
126
127 pub fn get_by_id(&self, id: u32) -> Option<&CommentAuthor> {
129 self.authors.iter().find(|a| a.id == id)
130 }
131
132 pub fn authors(&self) -> &[CommentAuthor] {
134 &self.authors
135 }
136
137 pub fn len(&self) -> usize {
139 self.authors.len()
140 }
141
142 pub fn is_empty(&self) -> bool {
144 self.authors.is_empty()
145 }
146
147 pub fn to_xml(&self) -> String {
149 let mut xml = String::from(
150 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#,
151 );
152 xml.push_str(
153 r#"<p:cmAuthorLst 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">"#,
154 );
155 for author in &self.authors {
156 xml.push_str(&author.to_xml());
157 }
158 xml.push_str("</p:cmAuthorLst>");
159 xml
160 }
161}
162
163#[derive(Clone, Debug, Default)]
165pub struct SlideComments {
166 comments: Vec<Comment>,
167}
168
169impl SlideComments {
170 pub fn new() -> Self {
171 Self::default()
172 }
173
174 pub fn add(&mut self, comment: Comment) {
176 self.comments.push(comment);
177 }
178
179 pub fn add_comment(&mut self, author_id: u32, text: &str, x: u32, y: u32) {
181 let idx = self.comments.len() as u32 + 1;
182 self.comments.push(
183 Comment::new(author_id, text)
184 .position(x, y)
185 .index(idx),
186 );
187 }
188
189 pub fn comments(&self) -> &[Comment] {
191 &self.comments
192 }
193
194 pub fn len(&self) -> usize {
196 self.comments.len()
197 }
198
199 pub fn is_empty(&self) -> bool {
201 self.comments.is_empty()
202 }
203
204 pub fn to_xml(&self) -> String {
206 let mut xml = String::from(
207 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#,
208 );
209 xml.push_str(
210 r#"<p:cmLst 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">"#,
211 );
212 for comment in &self.comments {
213 xml.push_str(&comment.to_xml());
214 }
215 xml.push_str("</p:cmLst>");
216 xml
217 }
218}
219
220fn xml_escape(s: &str) -> String {
221 s.replace('&', "&")
222 .replace('<', "<")
223 .replace('>', ">")
224 .replace('"', """)
225 .replace('\'', "'")
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_comment_author_new() {
234 let author = CommentAuthor::new(0, "John Doe", "JD");
235 assert_eq!(author.id, 0);
236 assert_eq!(author.name, "John Doe");
237 assert_eq!(author.initials, "JD");
238 assert_eq!(author.color_index, 0);
239 }
240
241 #[test]
242 fn test_comment_author_color_index() {
243 let author = CommentAuthor::new(0, "Jane", "J").color_index(5);
244 assert_eq!(author.color_index, 5);
245 }
246
247 #[test]
248 fn test_comment_author_xml() {
249 let author = CommentAuthor::new(0, "John Doe", "JD");
250 let xml = author.to_xml();
251 assert!(xml.contains(r#"id="0""#));
252 assert!(xml.contains(r#"name="John Doe""#));
253 assert!(xml.contains(r#"initials="JD""#));
254 }
255
256 #[test]
257 fn test_comment_new() {
258 let comment = Comment::new(0, "Review this slide");
259 assert_eq!(comment.author_id, 0);
260 assert_eq!(comment.text, "Review this slide");
261 assert_eq!(comment.x, 0);
262 assert_eq!(comment.y, 0);
263 }
264
265 #[test]
266 fn test_comment_position() {
267 let comment = Comment::new(0, "Note").position(100, 200);
268 assert_eq!(comment.x, 100);
269 assert_eq!(comment.y, 200);
270 }
271
272 #[test]
273 fn test_comment_date() {
274 let comment = Comment::new(0, "Note").date("2025-06-15T10:30:00.000");
275 assert_eq!(comment.date, "2025-06-15T10:30:00.000");
276 }
277
278 #[test]
279 fn test_comment_xml() {
280 let comment = Comment::new(0, "Fix this").position(100, 200).index(1);
281 let xml = comment.to_xml();
282 assert!(xml.contains(r#"authorId="0""#));
283 assert!(xml.contains(r#"idx="1""#));
284 assert!(xml.contains(r#"x="100""#));
285 assert!(xml.contains(r#"y="200""#));
286 assert!(xml.contains("Fix this"));
287 }
288
289 #[test]
290 fn test_comment_xml_escaping() {
291 let comment = Comment::new(0, "Use <b> & \"quotes\"");
292 let xml = comment.to_xml();
293 assert!(xml.contains("<b>"));
294 assert!(xml.contains("&"));
295 assert!(xml.contains(""quotes""));
296 }
297
298 #[test]
299 fn test_comment_author_list_new() {
300 let list = CommentAuthorList::new();
301 assert!(list.is_empty());
302 assert_eq!(list.len(), 0);
303 }
304
305 #[test]
306 fn test_comment_author_list_add() {
307 let mut list = CommentAuthorList::new();
308 let id1 = list.get_or_add("Alice", "A");
309 let id2 = list.get_or_add("Bob", "B");
310 assert_eq!(id1, 0);
311 assert_eq!(id2, 1);
312 assert_eq!(list.len(), 2);
313 }
314
315 #[test]
316 fn test_comment_author_list_dedup() {
317 let mut list = CommentAuthorList::new();
318 let id1 = list.get_or_add("Alice", "A");
319 let id2 = list.get_or_add("Alice", "A");
320 assert_eq!(id1, id2);
321 assert_eq!(list.len(), 1);
322 }
323
324 #[test]
325 fn test_comment_author_list_get_by_id() {
326 let mut list = CommentAuthorList::new();
327 list.get_or_add("Alice", "A");
328 let author = list.get_by_id(0);
329 assert!(author.is_some());
330 assert_eq!(author.unwrap().name, "Alice");
331 assert!(list.get_by_id(99).is_none());
332 }
333
334 #[test]
335 fn test_comment_author_list_xml() {
336 let mut list = CommentAuthorList::new();
337 list.get_or_add("Alice", "A");
338 let xml = list.to_xml();
339 assert!(xml.contains("<p:cmAuthorLst"));
340 assert!(xml.contains("Alice"));
341 assert!(xml.contains("</p:cmAuthorLst>"));
342 }
343
344 #[test]
345 fn test_slide_comments_new() {
346 let comments = SlideComments::new();
347 assert!(comments.is_empty());
348 assert_eq!(comments.len(), 0);
349 }
350
351 #[test]
352 fn test_slide_comments_add() {
353 let mut comments = SlideComments::new();
354 comments.add(Comment::new(0, "First comment").position(10, 20));
355 comments.add(Comment::new(1, "Second comment").position(30, 40));
356 assert_eq!(comments.len(), 2);
357 }
358
359 #[test]
360 fn test_slide_comments_add_comment() {
361 let mut comments = SlideComments::new();
362 comments.add_comment(0, "Auto-indexed", 100, 200);
363 comments.add_comment(0, "Second", 300, 400);
364 assert_eq!(comments.comments()[0].index, 1);
365 assert_eq!(comments.comments()[1].index, 2);
366 }
367
368 #[test]
369 fn test_slide_comments_xml() {
370 let mut comments = SlideComments::new();
371 comments.add_comment(0, "Review needed", 100, 200);
372 let xml = comments.to_xml();
373 assert!(xml.contains("<p:cmLst"));
374 assert!(xml.contains("Review needed"));
375 assert!(xml.contains("</p:cmLst>"));
376 }
377
378 #[test]
379 fn test_slide_comments_xml_empty() {
380 let comments = SlideComments::new();
381 let xml = comments.to_xml();
382 assert!(xml.contains("<p:cmLst"));
383 assert!(xml.contains("</p:cmLst>"));
384 }
385
386 #[test]
387 fn test_comment_author_list_xml_multiple() {
388 let mut list = CommentAuthorList::new();
389 list.get_or_add("Alice", "A");
390 list.get_or_add("Bob", "B");
391 let xml = list.to_xml();
392 assert!(xml.contains("Alice"));
393 assert!(xml.contains("Bob"));
394 assert!(xml.matches("<p:cmAuthor ").count() == 2);
395 }
396}