From 2797099c60482aeafa99335f4af54dfeb61871dc Mon Sep 17 00:00:00 2001 From: "louis.fancelli" Date: Thu, 9 Jan 2025 16:27:58 +0100 Subject: [PATCH] Add WIP feature support for w:moveTo and w:moveFrom This should be a good start for w:moveTo and w:moveFrom operations support. Currently, it allows to display moved elements using Paragraph::raw_text() without duplicating the text --- .../documents/elements/delete_instr_text.rs | 2 +- .../src/documents/elements/instr_pageref.rs | 2 +- .../src/documents/elements/instr_text.rs | 4 +- docx-core/src/documents/elements/instr_toc.rs | 10 +- docx-core/src/documents/elements/mod.rs | 4 + docx-core/src/documents/elements/move_from.rs | 133 +++++++++++++++++ docx-core/src/documents/elements/move_to.rs | 134 ++++++++++++++++++ docx-core/src/documents/elements/paragraph.rs | 37 +++++ docx-core/src/reader/mod.rs | 2 + docx-core/src/reader/move_from.rs | 64 +++++++++ docx-core/src/reader/move_to.rs | 64 +++++++++ docx-core/src/reader/paragraph.rs | 10 ++ docx-core/src/reader/xml_element.rs | 4 + docx-core/src/xml_builder/elements.rs | 2 + 14 files changed, 463 insertions(+), 9 deletions(-) create mode 100644 docx-core/src/documents/elements/move_from.rs create mode 100644 docx-core/src/documents/elements/move_to.rs create mode 100644 docx-core/src/reader/move_from.rs create mode 100644 docx-core/src/reader/move_to.rs diff --git a/docx-core/src/documents/elements/delete_instr_text.rs b/docx-core/src/documents/elements/delete_instr_text.rs index 5de4a63f2..5f79875e4 100644 --- a/docx-core/src/documents/elements/delete_instr_text.rs +++ b/docx-core/src/documents/elements/delete_instr_text.rs @@ -86,7 +86,7 @@ mod tests { let b = DeleteInstrText::TOC(InstrToC::new().heading_styles_range(1, 3)).build(); assert_eq!( str::from_utf8(&b).unwrap(), - r#"TOC \o "1-3""# + r"TOC \o "1-3"" ); } } diff --git a/docx-core/src/documents/elements/instr_pageref.rs b/docx-core/src/documents/elements/instr_pageref.rs index 4aa881f03..b39aee3ec 100644 --- a/docx-core/src/documents/elements/instr_pageref.rs +++ b/docx-core/src/documents/elements/instr_pageref.rs @@ -78,6 +78,6 @@ mod tests { #[test] fn test_page_ref() { let b = InstrPAGEREF::new("_Toc00000000").hyperlink().build(); - assert_eq!(str::from_utf8(&b).unwrap(), r#"PAGEREF _Toc00000000 \h"#); + assert_eq!(str::from_utf8(&b).unwrap(), r"PAGEREF _Toc00000000 \h"); } } diff --git a/docx-core/src/documents/elements/instr_text.rs b/docx-core/src/documents/elements/instr_text.rs index 62e39ad9d..edc6c463f 100644 --- a/docx-core/src/documents/elements/instr_text.rs +++ b/docx-core/src/documents/elements/instr_text.rs @@ -104,7 +104,7 @@ mod tests { let b = Box::new(InstrText::TOC(InstrToC::new().heading_styles_range(1, 3))).build(); assert_eq!( str::from_utf8(&b).unwrap(), - r#"TOC \o "1-3""# + r"TOC \o "1-3"" ); } @@ -117,7 +117,7 @@ mod tests { .build(); assert_eq!( str::from_utf8(&b).unwrap(), - r#"PAGEREF _Toc90425847 \h"# + r"PAGEREF _Toc90425847 \h" ); } } diff --git a/docx-core/src/documents/elements/instr_toc.rs b/docx-core/src/documents/elements/instr_toc.rs index 2bcaa48c9..9bf986cd4 100644 --- a/docx-core/src/documents/elements/instr_toc.rs +++ b/docx-core/src/documents/elements/instr_toc.rs @@ -392,7 +392,7 @@ mod tests { #[test] fn test_toc() { let b = InstrToC::new().heading_styles_range(1, 3).build(); - assert_eq!(str::from_utf8(&b).unwrap(), r#"TOC \o "1-3""#); + assert_eq!(str::from_utf8(&b).unwrap(), r"TOC \o "1-3""); } #[test] @@ -404,20 +404,20 @@ mod tests { .build(); assert_eq!( str::from_utf8(&b).unwrap(), - r#"TOC \o "1-3" \t "style1,2,style2,3""# + r"TOC \o "1-3" \t "style1,2,style2,3"" ); } #[test] fn read_toc_with_o_and_h() { - let i = r#"TOC \o "1-3" \h"#; + let i = r"TOC \o "1-3" \h"; let i = InstrToC::from_str(i).unwrap(); assert_eq!(i, InstrToC::new().heading_styles_range(1, 3).hyperlink()); } #[test] fn read_toc_with_l_and_n() { - let i = r#"TOC \o "1-3" \l "4-5" \n "1-4" \h"#; + let i = r"TOC \o "1-3" \l "4-5" \n "1-4" \h"; let i = InstrToC::from_str(i).unwrap(); assert_eq!( i, @@ -431,7 +431,7 @@ mod tests { #[test] fn read_toc_with_a_and_b_and_t() { - let i = r#"TOC \a "hoge" \b "test" \o "1-3" \t "MySpectacularStyle,1,MySpectacularStyle2,4""#; + let i = r"TOC \a "hoge" \b "test" \o "1-3" \t "MySpectacularStyle,1,MySpectacularStyle2,4""; let i = InstrToC::from_str(i).unwrap(); assert_eq!( i, diff --git a/docx-core/src/documents/elements/mod.rs b/docx-core/src/documents/elements/mod.rs index 808ded439..9727d7410 100644 --- a/docx-core/src/documents/elements/mod.rs +++ b/docx-core/src/documents/elements/mod.rs @@ -61,6 +61,8 @@ mod level_text; mod line_spacing; mod link; mod mc_fallback; +mod move_from; +mod move_to; mod name; mod next; mod num_pages; @@ -199,6 +201,8 @@ pub use level_text::*; pub use line_spacing::*; pub use link::*; pub use mc_fallback::*; +pub use move_from::*; +pub use move_to::*; pub use name::*; pub use next::*; pub use num_pages::*; diff --git a/docx-core/src/documents/elements/move_from.rs b/docx-core/src/documents/elements/move_from.rs new file mode 100644 index 000000000..1a6a1cd9c --- /dev/null +++ b/docx-core/src/documents/elements/move_from.rs @@ -0,0 +1,133 @@ +use serde::ser::{SerializeStruct, Serializer}; +use serde::Serialize; +use std::io::Write; + +use crate::xml_builder::*; +use crate::{documents::*, escape}; + +#[derive(Serialize, Debug, Clone, PartialEq)] +pub struct MoveFrom { + pub author: String, + pub date: String, + pub children: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum MoveFromChild { + Run(Run), + CommentStart(Box), + CommentEnd(CommentRangeEnd), +} + +impl Serialize for MoveFromChild { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + MoveFromChild::Run(ref r) => { + let mut t = serializer.serialize_struct("Run", 2)?; + t.serialize_field("type", "run")?; + t.serialize_field("data", r)?; + t.end() + } + MoveFromChild::CommentStart(ref r) => { + let mut t = serializer.serialize_struct("CommentRangeStart", 2)?; + t.serialize_field("type", "commentRangeStart")?; + t.serialize_field("data", r)?; + t.end() + } + MoveFromChild::CommentEnd(ref r) => { + let mut t = serializer.serialize_struct("CommentRangeEnd", 2)?; + t.serialize_field("type", "commentRangeEnd")?; + t.serialize_field("data", r)?; + t.end() + } + } + } +} + +impl Default for MoveFrom { + fn default() -> Self { + Self { + author: "unnamed".to_owned(), + date: "1970-01-01T00:00:00Z".to_owned(), + children: vec![], + } + } +} + +impl MoveFrom { + pub fn new() -> Self { + Self { + children: vec![], + ..Default::default() + } + } + + pub fn add_run(mut self, run: Run) -> Self { + self.children.push(MoveFromChild::Run(run)); + self + } + + pub fn add_comment_start(mut self, comment: Comment) -> Self { + self.children.push(MoveFromChild::CommentStart(Box::new( + CommentRangeStart::new(comment), + ))); + self + } + + pub fn add_comment_end(mut self, id: usize) -> Self { + self.children + .push(MoveFromChild::CommentEnd(CommentRangeEnd::new(id))); + self + } + + pub fn author(mut self, author: impl Into) -> Self { + self.author = escape::escape(&author.into()); + self + } + + pub fn date(mut self, date: impl Into) -> Self { + self.date = date.into(); + self + } +} + +impl HistoryId for MoveFrom {} + +impl BuildXML for MoveFrom { + fn build_to( + &self, + stream: xml::writer::EventWriter, + ) -> xml::writer::Result> { + let id = self.generate(); + XMLBuilder::from(stream) + .open_move_from(&id, &self.author, &self.date)? + .apply_each(&self.children, |ch, b| match ch { + MoveFromChild::Run(t) => b.add_child(t), + MoveFromChild::CommentStart(c) => b.add_child(&c), + MoveFromChild::CommentEnd(c) => b.add_child(c), + })? + .close()? + .into_inner() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[cfg(test)] + use pretty_assertions::assert_eq; + use std::str; + + #[test] + fn test_move_from_default() { + let b = MoveFrom::new().add_run(Run::new()).build(); + assert_eq!( + str::from_utf8(&b).unwrap(), + r#""# + ); + } +} diff --git a/docx-core/src/documents/elements/move_to.rs b/docx-core/src/documents/elements/move_to.rs new file mode 100644 index 000000000..307a5105d --- /dev/null +++ b/docx-core/src/documents/elements/move_to.rs @@ -0,0 +1,134 @@ +use serde::ser::{SerializeStruct, Serializer}; +use serde::Serialize; +use std::io::Write; + +use crate::xml_builder::*; +use crate::{documents::*, escape}; + +#[derive(Serialize, Debug, Clone, PartialEq)] +pub struct MoveTo { + pub author: String, + pub date: String, + pub children: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum MoveToChild { + Run(Run), + CommentStart(Box), + CommentEnd(CommentRangeEnd), +} + +impl Serialize for MoveToChild { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + MoveToChild::Run(ref r) => { + let mut t = serializer.serialize_struct("Run", 2)?; + t.serialize_field("type", "run")?; + t.serialize_field("data", r)?; + t.end() + } + MoveToChild::CommentStart(ref r) => { + let mut t = serializer.serialize_struct("CommentRangeStart", 2)?; + t.serialize_field("type", "commentRangeStart")?; + t.serialize_field("data", r)?; + t.end() + } + MoveToChild::CommentEnd(ref r) => { + let mut t = serializer.serialize_struct("CommentRangeEnd", 2)?; + t.serialize_field("type", "commentRangeEnd")?; + t.serialize_field("data", r)?; + t.end() + } + } + } +} + +impl Default for MoveTo { + fn default() -> Self { + Self { + author: "unnamed".to_owned(), + date: "1970-01-01T00:00:00Z".to_owned(), + children: vec![], + } + } +} + +impl MoveTo { + pub fn new() -> Self { + Self { + children: vec![], + ..Default::default() + } + } + + pub fn add_run(mut self, run: Run) -> Self { + self.children.push(MoveToChild::Run(run)); + self + } + + pub fn add_comment_start(mut self, comment: Comment) -> Self { + self.children + .push(MoveToChild::CommentStart(Box::new(CommentRangeStart::new( + comment, + )))); + self + } + + pub fn add_comment_end(mut self, id: usize) -> Self { + self.children + .push(MoveToChild::CommentEnd(CommentRangeEnd::new(id))); + self + } + + pub fn author(mut self, author: impl Into) -> Self { + self.author = escape::escape(&author.into()); + self + } + + pub fn date(mut self, date: impl Into) -> Self { + self.date = date.into(); + self + } +} + +impl HistoryId for MoveTo {} + +impl BuildXML for MoveTo { + fn build_to( + &self, + stream: xml::writer::EventWriter, + ) -> xml::writer::Result> { + let id = self.generate(); + XMLBuilder::from(stream) + .open_move_to(&id, &self.author, &self.date)? + .apply_each(&self.children, |ch, b| match ch { + MoveToChild::Run(t) => b.add_child(t), + MoveToChild::CommentStart(c) => b.add_child(&c), + MoveToChild::CommentEnd(c) => b.add_child(c), + })? + .close()? + .into_inner() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[cfg(test)] + use pretty_assertions::assert_eq; + use std::str; + + #[test] + fn test_move_to_default() { + let b = MoveTo::new().add_run(Run::new()).build(); + assert_eq!( + str::from_utf8(&b).unwrap(), + r#""# + ); + } +} diff --git a/docx-core/src/documents/elements/paragraph.rs b/docx-core/src/documents/elements/paragraph.rs index f22bc8fff..dde3a33d3 100644 --- a/docx-core/src/documents/elements/paragraph.rs +++ b/docx-core/src/documents/elements/paragraph.rs @@ -32,6 +32,8 @@ pub enum ParagraphChild { Run(Box), Insert(Insert), Delete(Delete), + MoveTo(MoveTo), + MoveFrom(MoveFrom), BookmarkStart(BookmarkStart), Hyperlink(Hyperlink), BookmarkEnd(BookmarkEnd), @@ -51,6 +53,8 @@ impl BuildXML for ParagraphChild { ParagraphChild::Run(v) => v.build_to(stream), ParagraphChild::Insert(v) => v.build_to(stream), ParagraphChild::Delete(v) => v.build_to(stream), + ParagraphChild::MoveTo(v) => v.build_to(stream), + ParagraphChild::MoveFrom(v) => v.build_to(stream), ParagraphChild::Hyperlink(v) => v.build_to(stream), ParagraphChild::BookmarkStart(v) => v.build_to(stream), ParagraphChild::BookmarkEnd(v) => v.build_to(stream), @@ -87,6 +91,18 @@ impl Serialize for ParagraphChild { t.serialize_field("data", r)?; t.end() } + ParagraphChild::MoveTo(ref r) => { + let mut t = serializer.serialize_struct("MoveTo", 2)?; + t.serialize_field("type", "moveTo")?; + t.serialize_field("data", r)?; + t.end() + } + ParagraphChild::MoveFrom(ref r) => { + let mut t = serializer.serialize_struct("MoveFrom", 2)?; + t.serialize_field("type", "moveFrom")?; + t.serialize_field("data", r)?; + t.end() + } ParagraphChild::Hyperlink(ref r) => { let mut t = serializer.serialize_struct("hyperlink", 2)?; t.serialize_field("type", "hyperlink")?; @@ -195,6 +211,16 @@ impl Paragraph { self } + pub fn add_move_to(mut self, move_to: MoveTo) -> Paragraph { + self.children.push(ParagraphChild::MoveTo(move_to)); + self + } + + pub fn add_move_from(mut self, move_from: MoveFrom) -> Paragraph { + self.children.push(ParagraphChild::MoveFrom(move_from)); + self + } + pub fn add_bookmark_start(mut self, id: usize, name: impl Into) -> Paragraph { self.children .push(ParagraphChild::BookmarkStart(BookmarkStart::new(id, name))); @@ -382,6 +408,17 @@ impl Paragraph { } } } + ParagraphChild::MoveTo(m) => { + for c in m.children.iter() { + if let MoveToChild::Run(r) = c { + for c in r.children.iter() { + if let RunChild::Text(t) = c { + s.push_str(&t.text); + } + } + } + } + } _ => {} } } diff --git a/docx-core/src/reader/mod.rs b/docx-core/src/reader/mod.rs index fa30d6b91..ee0f94006 100644 --- a/docx-core/src/reader/mod.rs +++ b/docx-core/src/reader/mod.rs @@ -30,6 +30,8 @@ mod insert; mod level; mod level_override; mod mc_fallback; +mod move_from; +mod move_to; mod numbering_property; mod numberings; mod page_num_type; diff --git a/docx-core/src/reader/move_from.rs b/docx-core/src/reader/move_from.rs new file mode 100644 index 000000000..5cee8cb2e --- /dev/null +++ b/docx-core/src/reader/move_from.rs @@ -0,0 +1,64 @@ +use std::io::Read; +use std::str::FromStr; + +use xml::attribute::OwnedAttribute; +use xml::reader::{EventReader, XmlEvent}; + +use super::*; + +impl ElementReader for MoveFrom { + fn read( + r: &mut EventReader, + attrs: &[OwnedAttribute], + ) -> Result { + let mut del = Self::new(); + loop { + let e = r.next(); + match e { + Ok(XmlEvent::StartElement { + name, attributes, .. + }) => { + let e = XMLElement::from_str(&name.local_name) + .expect("should convert to XMLElement"); + match e { + XMLElement::Run => { + del = del.add_run(Run::read(r, attrs)?); + } + XMLElement::CommentRangeStart => { + if let Some(id) = read(&attributes, "id") { + if let Ok(id) = usize::from_str(&id) { + let comment = Comment::new(id); + del = del.add_comment_start(comment); + } + } + } + XMLElement::CommentRangeEnd => { + if let Some(id) = read(&attributes, "id") { + if let Ok(id) = usize::from_str(&id) { + del = del.add_comment_end(id); + } + } + } + _ => {} + } + } + Ok(XmlEvent::EndElement { name, .. }) => { + let e = XMLElement::from_str(&name.local_name).unwrap(); + if e == XMLElement::MoveFrom { + for attr in attrs { + let local_name = &attr.name.local_name; + if local_name == "author" { + del = del.author(&attr.value); + } else if local_name == "date" { + del = del.date(&attr.value); + } + } + return Ok(del); + } + } + Err(_) => return Err(ReaderError::XMLReadError), + _ => {} + } + } + } +} diff --git a/docx-core/src/reader/move_to.rs b/docx-core/src/reader/move_to.rs new file mode 100644 index 000000000..240d6be06 --- /dev/null +++ b/docx-core/src/reader/move_to.rs @@ -0,0 +1,64 @@ +use std::io::Read; +use std::str::FromStr; + +use xml::attribute::OwnedAttribute; +use xml::reader::{EventReader, XmlEvent}; + +use super::*; + +impl ElementReader for MoveTo { + fn read( + r: &mut EventReader, + attrs: &[OwnedAttribute], + ) -> Result { + let mut del = Self::new(); + loop { + let e = r.next(); + match e { + Ok(XmlEvent::StartElement { + name, attributes, .. + }) => { + let e = XMLElement::from_str(&name.local_name) + .expect("should convert to XMLElement"); + match e { + XMLElement::Run => { + del = del.add_run(Run::read(r, attrs)?); + } + XMLElement::CommentRangeStart => { + if let Some(id) = read(&attributes, "id") { + if let Ok(id) = usize::from_str(&id) { + let comment = Comment::new(id); + del = del.add_comment_start(comment); + } + } + } + XMLElement::CommentRangeEnd => { + if let Some(id) = read(&attributes, "id") { + if let Ok(id) = usize::from_str(&id) { + del = del.add_comment_end(id); + } + } + } + _ => {} + } + } + Ok(XmlEvent::EndElement { name, .. }) => { + let e = XMLElement::from_str(&name.local_name).unwrap(); + if e == XMLElement::MoveTo { + for attr in attrs { + let local_name = &attr.name.local_name; + if local_name == "author" { + del = del.author(&attr.value); + } else if local_name == "date" { + del = del.date(&attr.value); + } + } + return Ok(del); + } + } + Err(_) => return Err(ReaderError::XMLReadError), + _ => {} + } + } + } +} diff --git a/docx-core/src/reader/paragraph.rs b/docx-core/src/reader/paragraph.rs index daf03c4ce..d72bd6056 100644 --- a/docx-core/src/reader/paragraph.rs +++ b/docx-core/src/reader/paragraph.rs @@ -46,6 +46,16 @@ impl ElementReader for Paragraph { p = p.add_delete(del); continue; } + XMLElement::MoveTo => { + let move_to = MoveTo::read(r, &attributes)?; + p = p.add_move_to(move_to); + continue; + } + XMLElement::MoveFrom => { + let move_from = MoveFrom::read(r, &attributes)?; + p = p.add_move_from(move_from); + continue; + } XMLElement::BookmarkStart => { let s = BookmarkStart::read(r, &attributes)?; p = p.add_bookmark_start(s.id, s.name); diff --git a/docx-core/src/reader/xml_element.rs b/docx-core/src/reader/xml_element.rs index 4a965e0bb..ccd768d18 100644 --- a/docx-core/src/reader/xml_element.rs +++ b/docx-core/src/reader/xml_element.rs @@ -53,6 +53,8 @@ pub enum XMLElement { Justification, OutlineLvl, Insert, + MoveTo, + MoveFrom, SnapToGrid, KeepNext, KeepLines, @@ -299,6 +301,8 @@ impl FromStr for XMLElement { "jc" => Ok(XMLElement::Justification), "ins" => Ok(XMLElement::Insert), "del" => Ok(XMLElement::Delete), + "moveFrom" => Ok(XMLElement::MoveTo), + "moveTo" => Ok(XMLElement::MoveFrom), "delText" => Ok(XMLElement::DeleteText), "bookmarkStart" => Ok(XMLElement::BookmarkStart), "bookmarkEnd" => Ok(XMLElement::BookmarkEnd), diff --git a/docx-core/src/xml_builder/elements.rs b/docx-core/src/xml_builder/elements.rs index 24b34f0e5..ee5c61514 100644 --- a/docx-core/src/xml_builder/elements.rs +++ b/docx-core/src/xml_builder/elements.rs @@ -389,6 +389,8 @@ impl XMLBuilder { open!(open_insert, "w:ins", "w:id", "w:author", "w:date"); open!(open_delete, "w:del", "w:id", "w:author", "w:date"); + open!(open_move_to, "w:moveTo", "w:id", "w:author", "w:date"); + open!(open_move_from, "w:moveFrom", "w:id", "w:author", "w:date"); open!( open_paragraph_property_change, "w:pPrChange",