Skip to content

Commit 2d55d69

Browse files
fix: exclude MoveFrom ghost text from paragraph.raw_text() (#879)
Add first-class MoveFrom/MoveTo tracked-change support following the existing Insert/Delete pattern. When text is cut-and-pasted in Word with Track Changes, both moveFrom (ghost) and moveTo (live) contain the same text. raw_text() now skips MoveFrom and includes MoveTo, preventing moved text from appearing twice in output. Also prevent nested MoveFrom subtrees from flattening into parent containers (Insert, Delete, Hyperlink, MoveTo) via ignore_element(). Fixes #794
1 parent 2ae569f commit 2d55d69

18 files changed

Lines changed: 710 additions & 4 deletions

File tree

docx-core/src/documents/elements/fit_text.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ impl BuildXML for FitText {
2727
&self,
2828
stream: crate::xml::writer::EventWriter<W>,
2929
) -> crate::xml::writer::Result<crate::xml::writer::EventWriter<W>> {
30-
XMLBuilder::from(stream).fit_text(self.val, self.id)?.into_inner()
30+
XMLBuilder::from(stream)
31+
.fit_text(self.val, self.id)?
32+
.into_inner()
3133
}
3234
}
3335

docx-core/src/documents/elements/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ mod doc_id;
3030
mod doc_var;
3131
mod drawing;
3232
mod dstrike;
33-
mod fld_char;
3433
mod fit_text;
34+
mod fld_char;
3535
mod font;
3636
mod font_scheme;
3737
mod footer_reference;
@@ -64,6 +64,8 @@ mod level_text;
6464
mod line_spacing;
6565
mod link;
6666
mod mc_fallback;
67+
mod move_from;
68+
mod move_to;
6769
mod name;
6870
mod next;
6971
mod num_pages;
@@ -172,8 +174,8 @@ pub use doc_id::*;
172174
pub use doc_var::*;
173175
pub use drawing::*;
174176
pub use dstrike::*;
175-
pub use fld_char::*;
176177
pub use fit_text::*;
178+
pub use fld_char::*;
177179
pub use font::*;
178180
pub use font_scheme::*;
179181
pub use footer_reference::*;
@@ -206,6 +208,8 @@ pub use level_text::*;
206208
pub use line_spacing::*;
207209
pub use link::*;
208210
pub use mc_fallback::*;
211+
pub use move_from::*;
212+
pub use move_to::*;
209213
pub use name::*;
210214
pub use next::*;
211215
pub use num_pages::*;
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
use serde::ser::{SerializeStruct, Serializer};
2+
use serde::Serialize;
3+
use std::io::Write;
4+
5+
use crate::xml_builder::*;
6+
use crate::{documents::*, escape};
7+
8+
#[derive(Serialize, Debug, Clone, PartialEq)]
9+
pub struct MoveFrom {
10+
pub author: String,
11+
pub date: String,
12+
pub children: Vec<MoveFromChild>,
13+
}
14+
15+
#[derive(Debug, Clone, PartialEq)]
16+
pub enum MoveFromChild {
17+
Run(Box<Run>),
18+
CommentStart(Box<CommentRangeStart>),
19+
CommentEnd(CommentRangeEnd),
20+
}
21+
22+
impl Serialize for MoveFromChild {
23+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
24+
where
25+
S: Serializer,
26+
{
27+
match *self {
28+
MoveFromChild::Run(ref r) => {
29+
let mut t = serializer.serialize_struct("Run", 2)?;
30+
t.serialize_field("type", "run")?;
31+
t.serialize_field("data", r)?;
32+
t.end()
33+
}
34+
MoveFromChild::CommentStart(ref r) => {
35+
let mut t = serializer.serialize_struct("CommentRangeStart", 2)?;
36+
t.serialize_field("type", "commentRangeStart")?;
37+
t.serialize_field("data", r)?;
38+
t.end()
39+
}
40+
MoveFromChild::CommentEnd(ref r) => {
41+
let mut t = serializer.serialize_struct("CommentRangeEnd", 2)?;
42+
t.serialize_field("type", "commentRangeEnd")?;
43+
t.serialize_field("data", r)?;
44+
t.end()
45+
}
46+
}
47+
}
48+
}
49+
50+
impl Default for MoveFrom {
51+
fn default() -> MoveFrom {
52+
MoveFrom {
53+
author: "unnamed".to_owned(),
54+
date: "1970-01-01T00:00:00Z".to_owned(),
55+
children: vec![],
56+
}
57+
}
58+
}
59+
60+
impl MoveFrom {
61+
pub fn new() -> MoveFrom {
62+
Self {
63+
children: vec![],
64+
..Default::default()
65+
}
66+
}
67+
68+
pub fn add_run(mut self, run: Run) -> MoveFrom {
69+
self.children.push(MoveFromChild::Run(Box::new(run)));
70+
self
71+
}
72+
73+
pub fn add_comment_start(mut self, comment: Comment) -> MoveFrom {
74+
self.children.push(MoveFromChild::CommentStart(Box::new(
75+
CommentRangeStart::new(comment),
76+
)));
77+
self
78+
}
79+
80+
pub fn add_comment_end(mut self, id: usize) -> MoveFrom {
81+
self.children
82+
.push(MoveFromChild::CommentEnd(CommentRangeEnd::new(id)));
83+
self
84+
}
85+
86+
pub fn author(mut self, author: impl Into<String>) -> MoveFrom {
87+
self.author = escape::escape(&author.into());
88+
self
89+
}
90+
91+
pub fn date(mut self, date: impl Into<String>) -> MoveFrom {
92+
self.date = date.into();
93+
self
94+
}
95+
}
96+
97+
impl HistoryId for MoveFrom {}
98+
99+
impl BuildXML for MoveFrom {
100+
fn build_to<W: Write>(
101+
&self,
102+
stream: crate::xml::writer::EventWriter<W>,
103+
) -> crate::xml::writer::Result<crate::xml::writer::EventWriter<W>> {
104+
let id = self.generate();
105+
XMLBuilder::from(stream)
106+
.open_move_from(&id, &self.author, &self.date)?
107+
.apply_each(&self.children, |ch, b| match ch {
108+
MoveFromChild::Run(t) => b.add_child(&**t),
109+
MoveFromChild::CommentStart(c) => b.add_child(&**c),
110+
MoveFromChild::CommentEnd(c) => b.add_child(c),
111+
})?
112+
.close()?
113+
.into_inner()
114+
}
115+
}
116+
117+
#[cfg(test)]
118+
mod tests {
119+
120+
use super::*;
121+
#[cfg(test)]
122+
use pretty_assertions::assert_eq;
123+
use std::str;
124+
125+
#[test]
126+
fn test_move_from_default() {
127+
let b = MoveFrom::new().add_run(Run::new()).build();
128+
assert_eq!(
129+
str::from_utf8(&b).unwrap(),
130+
r#"<w:moveFrom w:id="123" w:author="unnamed" w:date="1970-01-01T00:00:00Z"><w:r><w:rPr /></w:r></w:moveFrom>"#
131+
);
132+
}
133+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
use serde::ser::{SerializeStruct, Serializer};
2+
use serde::Serialize;
3+
use std::io::Write;
4+
5+
use super::*;
6+
7+
use crate::documents::{BuildXML, HistoryId, Run};
8+
use crate::{escape, xml_builder::*};
9+
10+
#[derive(Debug, Clone, PartialEq)]
11+
pub enum MoveToChild {
12+
Run(Box<Run>),
13+
Delete(Delete),
14+
CommentStart(Box<CommentRangeStart>),
15+
CommentEnd(CommentRangeEnd),
16+
}
17+
18+
impl BuildXML for MoveToChild {
19+
fn build_to<W: Write>(
20+
&self,
21+
stream: crate::xml::writer::EventWriter<W>,
22+
) -> crate::xml::writer::Result<crate::xml::writer::EventWriter<W>> {
23+
match self {
24+
MoveToChild::Run(v) => v.build_to(stream),
25+
MoveToChild::Delete(v) => v.build_to(stream),
26+
MoveToChild::CommentStart(v) => v.build_to(stream),
27+
MoveToChild::CommentEnd(v) => v.build_to(stream),
28+
}
29+
}
30+
}
31+
32+
impl Serialize for MoveToChild {
33+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
34+
where
35+
S: Serializer,
36+
{
37+
match *self {
38+
MoveToChild::Run(ref r) => {
39+
let mut t = serializer.serialize_struct("Run", 2)?;
40+
t.serialize_field("type", "run")?;
41+
t.serialize_field("data", r)?;
42+
t.end()
43+
}
44+
MoveToChild::Delete(ref r) => {
45+
let mut t = serializer.serialize_struct("Delete", 2)?;
46+
t.serialize_field("type", "delete")?;
47+
t.serialize_field("data", r)?;
48+
t.end()
49+
}
50+
MoveToChild::CommentStart(ref r) => {
51+
let mut t = serializer.serialize_struct("CommentRangeStart", 2)?;
52+
t.serialize_field("type", "commentRangeStart")?;
53+
t.serialize_field("data", r)?;
54+
t.end()
55+
}
56+
MoveToChild::CommentEnd(ref r) => {
57+
let mut t = serializer.serialize_struct("CommentRangeEnd", 2)?;
58+
t.serialize_field("type", "commentRangeEnd")?;
59+
t.serialize_field("data", r)?;
60+
t.end()
61+
}
62+
}
63+
}
64+
}
65+
66+
#[derive(Serialize, Debug, Clone, PartialEq)]
67+
pub struct MoveTo {
68+
pub children: Vec<MoveToChild>,
69+
pub author: String,
70+
pub date: String,
71+
}
72+
73+
impl Default for MoveTo {
74+
fn default() -> MoveTo {
75+
MoveTo {
76+
author: "unnamed".to_owned(),
77+
date: "1970-01-01T00:00:00Z".to_owned(),
78+
children: vec![],
79+
}
80+
}
81+
}
82+
83+
impl MoveTo {
84+
pub fn new(run: Run) -> MoveTo {
85+
Self {
86+
children: vec![MoveToChild::Run(Box::new(run))],
87+
..Default::default()
88+
}
89+
}
90+
91+
pub fn new_with_empty() -> MoveTo {
92+
Self {
93+
..Default::default()
94+
}
95+
}
96+
97+
pub fn add_run(mut self, run: Run) -> MoveTo {
98+
self.children.push(MoveToChild::Run(Box::new(run)));
99+
self
100+
}
101+
102+
pub fn add_delete(mut self, del: Delete) -> MoveTo {
103+
self.children.push(MoveToChild::Delete(del));
104+
self
105+
}
106+
107+
pub fn add_child(mut self, c: MoveToChild) -> MoveTo {
108+
self.children.push(c);
109+
self
110+
}
111+
112+
pub fn add_comment_start(mut self, comment: Comment) -> Self {
113+
self.children
114+
.push(MoveToChild::CommentStart(Box::new(CommentRangeStart::new(
115+
comment,
116+
))));
117+
self
118+
}
119+
120+
pub fn add_comment_end(mut self, id: usize) -> Self {
121+
self.children
122+
.push(MoveToChild::CommentEnd(CommentRangeEnd::new(id)));
123+
self
124+
}
125+
126+
pub fn author(mut self, author: impl Into<String>) -> MoveTo {
127+
self.author = escape::escape(&author.into());
128+
self
129+
}
130+
131+
pub fn date(mut self, date: impl Into<String>) -> MoveTo {
132+
self.date = date.into();
133+
self
134+
}
135+
}
136+
137+
impl HistoryId for MoveTo {}
138+
139+
impl BuildXML for MoveTo {
140+
fn build_to<W: Write>(
141+
&self,
142+
stream: crate::xml::writer::EventWriter<W>,
143+
) -> crate::xml::writer::Result<crate::xml::writer::EventWriter<W>> {
144+
XMLBuilder::from(stream)
145+
.open_move_to(&self.generate(), &self.author, &self.date)?
146+
.add_children(&self.children)?
147+
.close()?
148+
.into_inner()
149+
}
150+
}
151+
152+
#[cfg(test)]
153+
mod tests {
154+
155+
use super::*;
156+
#[cfg(test)]
157+
use pretty_assertions::assert_eq;
158+
use std::str;
159+
160+
#[test]
161+
fn test_move_to_default() {
162+
let b = MoveTo::new(Run::new()).build();
163+
assert_eq!(
164+
str::from_utf8(&b).unwrap(),
165+
r#"<w:moveTo w:id="123" w:author="unnamed" w:date="1970-01-01T00:00:00Z"><w:r><w:rPr /></w:r></w:moveTo>"#
166+
);
167+
}
168+
}

0 commit comments

Comments
 (0)