linch_docx_rs/document/
comments.rs1use crate::document::Paragraph;
4use crate::error::Result;
5use crate::xml::{get_attr, RawXmlElement, RawXmlNode};
6use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
7use quick_xml::{Reader, Writer};
8use std::io::Cursor;
9
10#[derive(Clone, Debug)]
12pub struct Comment {
13 pub id: u32,
15 pub author: String,
17 pub initials: Option<String>,
19 pub date: Option<String>,
21 pub paragraphs: Vec<Paragraph>,
23 pub unknown_children: Vec<RawXmlNode>,
25}
26
27impl Comment {
28 pub fn new(id: u32, author: impl Into<String>, text: impl Into<String>) -> Self {
30 Comment {
31 id,
32 author: author.into(),
33 initials: None,
34 date: None,
35 paragraphs: vec![Paragraph::new(text)],
36 unknown_children: Vec::new(),
37 }
38 }
39
40 pub fn text(&self) -> String {
42 self.paragraphs
43 .iter()
44 .map(|p| p.text())
45 .collect::<Vec<_>>()
46 .join("\n")
47 }
48}
49
50#[derive(Clone, Debug, Default)]
52pub struct Comments {
53 pub comments: Vec<Comment>,
54 pub unknown_children: Vec<RawXmlNode>,
55}
56
57impl Comments {
58 pub fn from_xml(xml: &str) -> Result<Self> {
60 let mut reader = Reader::from_str(xml);
61 reader.config_mut().trim_text(true);
62
63 let mut comments = Comments::default();
64 let mut buf = Vec::new();
65
66 loop {
67 match reader.read_event_into(&mut buf)? {
68 Event::Start(e) => {
69 let local = e.name().local_name();
70 if local.as_ref() == b"comment" {
71 comments.comments.push(parse_comment(&mut reader, &e)?);
72 } else if local.as_ref() != b"comments" {
73 let raw = RawXmlElement::from_reader(&mut reader, &e)?;
74 comments.unknown_children.push(RawXmlNode::Element(raw));
75 }
76 }
77 Event::Eof => break,
78 _ => {}
79 }
80 buf.clear();
81 }
82
83 Ok(comments)
84 }
85
86 pub fn to_xml(&self) -> Result<String> {
88 let mut buffer = Cursor::new(Vec::new());
89 let mut writer = Writer::new(&mut buffer);
90
91 writer.write_event(Event::Decl(BytesDecl::new(
92 "1.0",
93 Some("UTF-8"),
94 Some("yes"),
95 )))?;
96
97 let mut start = BytesStart::new("w:comments");
98 start.push_attribute(("xmlns:w", crate::xml::W));
99 start.push_attribute(("xmlns:r", crate::xml::R));
100 writer.write_event(Event::Start(start))?;
101
102 for comment in &self.comments {
103 let mut cs = BytesStart::new("w:comment");
104 cs.push_attribute(("w:id", comment.id.to_string().as_str()));
105 cs.push_attribute(("w:author", comment.author.as_str()));
106 if let Some(ref initials) = comment.initials {
107 cs.push_attribute(("w:initials", initials.as_str()));
108 }
109 if let Some(ref date) = comment.date {
110 cs.push_attribute(("w:date", date.as_str()));
111 }
112 writer.write_event(Event::Start(cs))?;
113
114 for para in &comment.paragraphs {
115 para.write_to(&mut writer)?;
116 }
117 for child in &comment.unknown_children {
118 child.write_to(&mut writer)?;
119 }
120
121 writer.write_event(Event::End(BytesEnd::new("w:comment")))?;
122 }
123
124 for child in &self.unknown_children {
125 child.write_to(&mut writer)?;
126 }
127
128 writer.write_event(Event::End(BytesEnd::new("w:comments")))?;
129
130 let xml_bytes = buffer.into_inner();
131 String::from_utf8(xml_bytes)
132 .map_err(|e| crate::error::Error::InvalidDocument(e.to_string()))
133 }
134
135 pub fn get(&self, id: u32) -> Option<&Comment> {
137 self.comments.iter().find(|c| c.id == id)
138 }
139
140 pub fn next_id(&self) -> u32 {
142 self.comments.iter().map(|c| c.id).max().unwrap_or(0) + 1
143 }
144
145 pub fn add(&mut self, author: impl Into<String>, text: impl Into<String>) -> u32 {
147 let id = self.next_id();
148 self.comments.push(Comment::new(id, author, text));
149 id
150 }
151}
152
153fn parse_comment<R: std::io::BufRead>(
154 reader: &mut Reader<R>,
155 start: &BytesStart,
156) -> Result<Comment> {
157 let id = get_attr(start, "w:id")
158 .and_then(|v| v.parse().ok())
159 .unwrap_or(0);
160 let author = get_attr(start, "w:author").unwrap_or_default();
161 let initials = get_attr(start, "w:initials");
162 let date = get_attr(start, "w:date");
163
164 let mut comment = Comment {
165 id,
166 author,
167 initials,
168 date,
169 paragraphs: Vec::new(),
170 unknown_children: Vec::new(),
171 };
172
173 let mut buf = Vec::new();
174 loop {
175 match reader.read_event_into(&mut buf)? {
176 Event::Start(e) => {
177 let local = e.name().local_name();
178 if local.as_ref() == b"p" {
179 comment.paragraphs.push(Paragraph::from_reader(reader, &e)?);
180 } else {
181 let raw = RawXmlElement::from_reader(reader, &e)?;
182 comment.unknown_children.push(RawXmlNode::Element(raw));
183 }
184 }
185 Event::Empty(e) => {
186 if e.name().local_name().as_ref() == b"p" {
187 comment.paragraphs.push(Paragraph::from_empty(&e)?);
188 }
189 }
190 Event::End(e) if e.name().local_name().as_ref() == b"comment" => break,
191 Event::Eof => break,
192 _ => {}
193 }
194 buf.clear();
195 }
196
197 Ok(comment)
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_parse_comments() {
206 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
207<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
208 <w:comment w:id="1" w:author="Alice" w:date="2024-01-15T10:30:00Z" w:initials="A">
209 <w:p><w:r><w:t>Great point!</w:t></w:r></w:p>
210 </w:comment>
211 <w:comment w:id="2" w:author="Bob">
212 <w:p><w:r><w:t>Need to revise this.</w:t></w:r></w:p>
213 </w:comment>
214</w:comments>"#;
215
216 let comments = Comments::from_xml(xml).unwrap();
217 assert_eq!(comments.comments.len(), 2);
218
219 let c1 = comments.get(1).unwrap();
220 assert_eq!(c1.author, "Alice");
221 assert_eq!(c1.text(), "Great point!");
222 assert_eq!(c1.initials.as_deref(), Some("A"));
223 assert_eq!(c1.date.as_deref(), Some("2024-01-15T10:30:00Z"));
224
225 let c2 = comments.get(2).unwrap();
226 assert_eq!(c2.author, "Bob");
227 assert_eq!(c2.text(), "Need to revise this.");
228 }
229
230 #[test]
231 fn test_comments_roundtrip() {
232 let mut comments = Comments::default();
233 comments.add("Alice", "First comment");
234 comments.add("Bob", "Second comment");
235
236 let xml = comments.to_xml().unwrap();
237 let comments2 = Comments::from_xml(&xml).unwrap();
238
239 assert_eq!(comments2.comments.len(), 2);
240 assert_eq!(comments2.get(1).unwrap().text(), "First comment");
241 assert_eq!(comments2.get(2).unwrap().author, "Bob");
242 }
243}