Skip to main content

rs_docx/document/
track_changes.rs

1//! Track Changes support
2//!
3//! Handles w:ins (insertions) and w:del (deletions) elements for revision tracking.
4
5use hard_xml::{XmlRead, XmlWrite};
6use std::borrow::Cow;
7
8use super::Run;
9
10/// Insertion (w:ins) - Represents inserted content in track changes
11///
12/// Contains runs of text that were inserted during revision tracking.
13#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
14#[cfg_attr(test, derive(PartialEq))]
15#[xml(tag = "w:ins")]
16pub struct Insertion<'a> {
17    /// Unique identifier for this revision
18    #[xml(attr = "w:id")]
19    pub id: Option<Cow<'a, str>>,
20    /// Author of the revision
21    #[xml(attr = "w:author")]
22    pub author: Option<Cow<'a, str>>,
23    /// Date/time of the revision
24    #[xml(attr = "w:date")]
25    pub date: Option<Cow<'a, str>>,
26    /// Runs containing the inserted content
27    #[xml(child = "w:r")]
28    pub runs: Vec<Run<'a>>,
29}
30
31/// Deletion (w:del) - Represents deleted content in track changes
32///
33/// Contains runs of text that were deleted during revision tracking.
34/// The actual deleted text is stored in w:delText elements within the runs.
35#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
36#[cfg_attr(test, derive(PartialEq))]
37#[xml(tag = "w:del")]
38pub struct Deletion<'a> {
39    /// Unique identifier for this revision
40    #[xml(attr = "w:id")]
41    pub id: Option<Cow<'a, str>>,
42    /// Author of the revision
43    #[xml(attr = "w:author")]
44    pub author: Option<Cow<'a, str>>,
45    /// Date/time of the revision
46    #[xml(attr = "w:date")]
47    pub date: Option<Cow<'a, str>>,
48    /// Runs containing the deleted content (with w:delText)
49    #[xml(child = "w:r")]
50    pub runs: Vec<Run<'a>>,
51}
52
53impl<'a> Insertion<'a> {
54    /// Extracts all text from the insertion's runs
55    pub fn text(&self) -> String {
56        self.runs
57            .iter()
58            .flat_map(|run| run.iter_text())
59            .map(|cow| cow.to_string())
60            .collect()
61    }
62}
63
64impl<'a> Deletion<'a> {
65    /// Extracts all deleted text from the deletion's runs
66    pub fn text(&self) -> String {
67        self.runs
68            .iter()
69            .flat_map(|run| {
70                run.content.iter().filter_map(|content| {
71                    if let super::RunContent::DelText(del_text) = content {
72                        Some(del_text.text.to_string())
73                    } else {
74                        None
75                    }
76                })
77            })
78            .collect()
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use hard_xml::XmlRead;
86
87    #[test]
88    fn test_parse_insertion() {
89        let xml = r#"<w:ins w:id="1" w:author="test" w:date="2024-01-01T00:00:00Z">
90            <w:r><w:t>inserted text</w:t></w:r>
91        </w:ins>"#;
92        let ins = Insertion::from_str(xml).unwrap();
93        assert_eq!(ins.id.as_deref(), Some("1"));
94        assert_eq!(ins.author.as_deref(), Some("test"));
95        assert_eq!(ins.text(), "inserted text");
96    }
97
98    #[test]
99    fn test_parse_deletion() {
100        let xml = r#"<w:del w:id="1" w:author="test" w:date="2024-01-01T00:00:00Z">
101            <w:r><w:delText>deleted text</w:delText></w:r>
102        </w:del>"#;
103        let del = Deletion::from_str(xml).unwrap();
104        assert_eq!(del.id.as_deref(), Some("1"));
105        assert_eq!(del.author.as_deref(), Some("test"));
106        assert_eq!(del.text(), "deleted text");
107    }
108}