1use crate::mce::children;
7use crate::slide::parse_text_body;
8use crate::xml::{unescape_attr, Event, Ns, Reader, XmlError};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct CommentAuthor {
13 pub id: String,
15 pub name: String,
16 pub initials: Option<String>,
17}
18
19#[derive(Clone, Debug, Default, PartialEq, Eq)]
21pub struct Comment {
22 pub author: Option<String>,
24 pub initials: Option<String>,
25 pub date: Option<String>,
27 pub text: String,
28 pub reply: bool,
29}
30
31pub fn parse_authors(xml: &[u8]) -> Result<Vec<CommentAuthor>, XmlError> {
33 let mut reader = Reader::new(xml);
34 if !skip_to_root(&mut reader)? {
35 return Ok(Vec::new());
36 }
37 let mut authors = Vec::new();
38 children(&mut reader, &mut |reader, child| {
39 let is_author = child.name.is(Ns::Pml, b"cmAuthor") || child.name.is(Ns::P188, b"author");
40 if !is_author {
41 return reader.skip_element();
42 }
43 let id = reader.attr(&child, Ns::None, b"id").map(unescape_attr);
44 let name = reader.attr(&child, Ns::None, b"name").map(unescape_attr);
45 if let (Some(id), Some(name)) = (id, name) {
46 authors.push(CommentAuthor {
47 id,
48 name,
49 initials: reader
50 .attr(&child, Ns::None, b"initials")
51 .map(unescape_attr)
52 .filter(|initials| !initials.is_empty()),
53 });
54 }
55 reader.skip_element()
56 })?;
57 Ok(authors)
58}
59
60pub fn parse_comments(xml: &[u8], authors: &[CommentAuthor]) -> Result<Vec<Comment>, XmlError> {
62 let mut reader = Reader::new(xml);
63 if !skip_to_root(&mut reader)? {
64 return Ok(Vec::new());
65 }
66 let mut comments = Vec::new();
67 children(&mut reader, &mut |reader, child| {
68 if child.name.is(Ns::Pml, b"cm") {
69 let mut comment = header(reader, &child, b"dt", authors);
70 children(reader, &mut |reader, item| {
71 if !item.name.is(Ns::Pml, b"text") {
72 return reader.skip_element();
73 }
74 reader.text_content(&mut comment.text)
75 })?;
76 comments.push(comment);
77 return Ok(());
78 }
79 if child.name.is(Ns::P188, b"cm") {
80 let mut comment = header(reader, &child, b"created", authors);
81 let mut replies = Vec::new();
82 children(reader, &mut |reader, item| {
83 if item.name.is(Ns::P188, b"txBody") {
84 comment.text = parse_text_body(reader)?.text();
85 return Ok(());
86 }
87 if item.name.is(Ns::P188, b"replyLst") {
88 return children(reader, &mut |reader, reply| {
89 if !reply.name.is(Ns::P188, b"reply") {
90 return reader.skip_element();
91 }
92 let mut comment = header(reader, &reply, b"created", authors);
93 comment.reply = true;
94 children(reader, &mut |reader, part| {
95 if !part.name.is(Ns::P188, b"txBody") {
96 return reader.skip_element();
97 }
98 comment.text = parse_text_body(reader)?.text();
99 Ok(())
100 })?;
101 replies.push(comment);
102 Ok(())
103 });
104 }
105 reader.skip_element()
106 })?;
107 comments.push(comment);
108 comments.append(&mut replies);
109 return Ok(());
110 }
111 reader.skip_element()
112 })?;
113 Ok(comments)
114}
115
116fn header(
117 reader: &Reader<'_>,
118 start: &crate::xml::Start<'_>,
119 date_attr: &[u8],
120 authors: &[CommentAuthor],
121) -> Comment {
122 let author_id = reader.attr(start, Ns::None, b"authorId").map(unescape_attr);
123 let author = author_id
124 .as_deref()
125 .and_then(|id| authors.iter().find(|author| author.id == id));
126 Comment {
127 author: author.map(|author| author.name.clone()).or(author_id),
128 initials: author.and_then(|author| author.initials.clone()),
129 date: reader.attr(start, Ns::None, date_attr).map(unescape_attr),
130 text: String::new(),
131 reply: false,
132 }
133}
134
135fn skip_to_root(reader: &mut Reader<'_>) -> Result<bool, XmlError> {
137 loop {
138 match reader.next()? {
139 Event::Start(_) => return Ok(true),
140 Event::Eof => return Ok(false),
141 _ => {}
142 }
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 const AUTHORS_2006: &[u8] = br#"<p:cmAuthorLst xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cmAuthor id="0" name="Ada Lovelace" initials="AL" lastIdx="2" clrIdx="0"/><p:cmAuthor id="1" name="Bob" lastIdx="1" clrIdx="1"/></p:cmAuthorLst>"#;
151
152 #[test]
153 fn legacy_comments_resolve_authors_and_keep_order() {
154 let authors = parse_authors(AUTHORS_2006).unwrap();
155 assert_eq!(authors.len(), 2);
156 assert_eq!(authors[0].initials.as_deref(), Some("AL"));
157 let xml = br#"<p:cmLst xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cm authorId="0" dt="2024-05-01T10:00:00.000" idx="1"><p:pos x="10" y="20"/><p:text>First & foremost</p:text></p:cm><p:cm authorId="9" idx="2"><p:text>Orphan</p:text></p:cm></p:cmLst>"#;
158 let comments = parse_comments(xml, &authors).unwrap();
159 assert_eq!(comments.len(), 2);
160 assert_eq!(comments[0].author.as_deref(), Some("Ada Lovelace"));
161 assert_eq!(comments[0].date.as_deref(), Some("2024-05-01T10:00:00.000"));
162 assert_eq!(comments[0].text, "First & foremost");
163 assert_eq!(comments[1].author.as_deref(), Some("9"));
164 assert!(!comments[1].reply);
165 }
166
167 #[test]
168 fn modern_comments_carry_rich_text_and_replies() {
169 let authors = parse_authors(br#"<p188:authorLst xmlns:p188="http://schemas.microsoft.com/office/powerpoint/2018/8/main"><p188:author id="{AAAA-1}" name="Ada" initials="A" userId="ada" providerId="AD"/></p188:authorLst>"#).unwrap();
170 let xml = br#"<p188:cmLst xmlns:p188="http://schemas.microsoft.com/office/powerpoint/2018/8/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><p188:cm id="{C1}" authorId="{AAAA-1}" created="2024-05-01T10:00:00.000"><p188:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:rPr lang="en-US"/><a:t>Please </a:t></a:r><a:r><a:t>fix</a:t></a:r></a:p></p188:txBody><p188:replyLst><p188:reply id="{R1}" authorId="{ZZZ}" created="2024-05-02T09:00:00.000"><p188:txBody><a:bodyPr/><a:p><a:r><a:t>Done</a:t></a:r></a:p></p188:txBody></p188:reply></p188:replyLst></p188:cm></p188:cmLst>"#;
171 let comments = parse_comments(xml, &authors).unwrap();
172 assert_eq!(comments.len(), 2);
173 assert_eq!(comments[0].author.as_deref(), Some("Ada"));
174 assert_eq!(comments[0].text, "Please fix");
175 assert!(!comments[0].reply);
176 assert_eq!(comments[1].author.as_deref(), Some("{ZZZ}"));
177 assert_eq!(comments[1].text, "Done");
178 assert!(comments[1].reply);
179 }
180}