Skip to main content

linch_docx_rs/document/
comments.rs

1//! Comments (comments.xml)
2
3use 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/// A single comment
11#[derive(Clone, Debug)]
12pub struct Comment {
13    /// Comment ID
14    pub id: u32,
15    /// Author name
16    pub author: String,
17    /// Author initials
18    pub initials: Option<String>,
19    /// Date (ISO 8601)
20    pub date: Option<String>,
21    /// Comment content paragraphs
22    pub paragraphs: Vec<Paragraph>,
23    /// Unknown children (preserved)
24    pub unknown_children: Vec<RawXmlNode>,
25}
26
27impl Comment {
28    /// Create a new comment
29    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    /// Get comment text
41    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/// Collection of comments from comments.xml
51#[derive(Clone, Debug, Default)]
52pub struct Comments {
53    pub comments: Vec<Comment>,
54    pub unknown_children: Vec<RawXmlNode>,
55}
56
57impl Comments {
58    /// Parse from XML string
59    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    /// Serialize to XML string
87    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    /// Get a comment by ID
136    pub fn get(&self, id: u32) -> Option<&Comment> {
137        self.comments.iter().find(|c| c.id == id)
138    }
139
140    /// Next available ID
141    pub fn next_id(&self) -> u32 {
142        self.comments.iter().map(|c| c.id).max().unwrap_or(0) + 1
143    }
144
145    /// Add a comment, returns the assigned ID
146    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}