1use crate::{
3 BlockDetails, DocumentBlock, DocumentRevision, ParagraphInline, RevisionKind, RevisionScope,
4};
5use serde::{Deserialize, Serialize};
6use std::collections::{BTreeMap, HashMap, HashSet};
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum InlineChange {
11 #[default]
12 Unchanged,
13 Deletion,
14 Insertion,
15}
16
17#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
18pub struct RevisionInline {
19 pub inline: ParagraphInline,
20 pub change: InlineChange,
21}
22
23#[derive(Default)]
24struct Boundary {
25 delta: i64,
26 removed: Vec<String>,
27}
28struct Edit {
29 start: usize,
30 end: usize,
31 removed: Option<String>,
32}
33
34fn paragraph_inlines(
35 block: &DocumentBlock,
36 edits: &[Edit],
37 inherited: InlineChange,
38) -> Vec<RevisionInline> {
39 let mut length = 0;
40 let mut offsets = HashSet::from([0]);
41 for c in block.content.chars() {
42 length += c.len_utf16();
43 offsets.insert(length);
44 }
45 let mut boundaries: BTreeMap<usize, Boundary> = BTreeMap::new();
46 for edit in edits {
47 if edit.start > edit.end || !offsets.contains(&edit.start) || !offsets.contains(&edit.end) {
48 continue;
49 }
50 if let Some(text) = &edit.removed {
51 boundaries
52 .entry(edit.start)
53 .or_default()
54 .removed
55 .push(text.clone());
56 } else {
57 boundaries.entry(edit.start).or_default().delta += 1;
58 boundaries.entry(edit.end).or_default().delta -= 1;
59 }
60 }
61 let inlines = match &block.details {
62 BlockDetails::Paragraph(d) => d.inlines.clone(),
63 BlockDetails::Raw(d) => d
64 .get("inlines")
65 .and_then(|v| serde_json::from_value(v.clone()).ok())
66 .unwrap_or_else(|| {
67 vec![ParagraphInline::Text {
68 start: 0,
69 end: length,
70 text: block.content.clone(),
71 style: None,
72 link: None,
73 note_reference: None,
74 }]
75 }),
76 _ => Vec::new(),
77 };
78 let mut events = boundaries.into_iter().peekable();
79 let mut position = 0;
80 let mut inserted = 0i64;
81 let mut output = Vec::new();
82 fn flush(
83 events: &mut std::iter::Peekable<std::collections::btree_map::IntoIter<usize, Boundary>>,
84 position: usize,
85 inserted: &mut i64,
86 output: &mut Vec<RevisionInline>,
87 ) {
88 if events.peek().is_some_and(|(offset, _)| *offset == position) {
89 let (_, event) = events.next().unwrap();
90 for text in event.removed {
91 output.push(RevisionInline {
92 inline: ParagraphInline::Text {
93 start: position,
94 end: position,
95 text,
96 style: None,
97 link: None,
98 note_reference: None,
99 },
100 change: InlineChange::Deletion,
101 });
102 }
103 *inserted += event.delta;
104 }
105 }
106 for inline in inlines {
107 flush(&mut events, position, &mut inserted, &mut output);
108 let change = |n: i64| {
109 if inherited != InlineChange::Unchanged {
110 inherited
111 } else if n > 0 {
112 InlineChange::Insertion
113 } else {
114 InlineChange::Unchanged
115 }
116 };
117 let text = match &inline {
118 ParagraphInline::Block { block_id, .. } => {
119 output.push(RevisionInline {
120 inline: ParagraphInline::Block {
121 block_id: block_id.clone(),
122 offset: position,
123 },
124 change: change(inserted),
125 });
126 continue;
127 }
128 ParagraphInline::Text { text, .. } | ParagraphInline::FormField { text, .. } => text,
129 };
130 let start = position;
131 let utf16: Vec<_> = text.encode_utf16().collect();
132 let end = start + utf16.len();
133 if start == end && matches!(inline, ParagraphInline::FormField { .. }) {
134 output.push(RevisionInline {
135 inline: inline.clone(),
136 change: change(inserted),
137 });
138 }
139 while position < end {
140 flush(&mut events, position, &mut inserted, &mut output);
141 let next = end.min(events.peek().map(|(offset, _)| *offset).unwrap_or(end));
142 let Ok(text) = String::from_utf16(&utf16[position - start..next - start]) else {
143 events.next();
145 continue;
146 };
147 let piece = match &inline {
148 ParagraphInline::Text {
149 style,
150 link,
151 note_reference,
152 ..
153 } => ParagraphInline::Text {
154 start: position,
155 end: next,
156 text,
157 style: style.clone(),
158 link: link.clone(),
159 note_reference: note_reference.clone(),
160 },
161 ParagraphInline::FormField { block_id, .. } => ParagraphInline::FormField {
162 block_id: block_id.clone(),
163 start: position,
164 end: next,
165 text,
166 },
167 _ => unreachable!(),
168 };
169 output.push(RevisionInline {
170 inline: piece,
171 change: change(inserted),
172 });
173 position = next;
174 }
175 }
176 flush(&mut events, position, &mut inserted, &mut output);
177 output
178}
179
180pub fn revision_inlines(
183 blocks: &[DocumentBlock],
184 revisions: &[DocumentRevision],
185) -> BTreeMap<String, Vec<RevisionInline>> {
186 let mut edits: HashMap<&str, Vec<Edit>> = HashMap::new();
187 let mut containers = HashMap::new();
188 for revision in revisions {
189 let deletion = matches!(
190 revision.kind,
191 RevisionKind::Deletion | RevisionKind::MoveFrom
192 );
193 if !deletion
194 && !matches!(
195 revision.kind,
196 RevisionKind::Insertion | RevisionKind::MoveTo
197 )
198 {
199 continue;
200 }
201 for source in &revision.source_refs {
202 if matches!(
203 revision.scope,
204 RevisionScope::TableRow | RevisionScope::TableCell
205 ) {
206 containers.insert(
207 source.node_id.as_str(),
208 if deletion {
209 InlineChange::Deletion
210 } else {
211 InlineChange::Insertion
212 },
213 );
214 } else if revision.scope == RevisionScope::Text {
215 if let (Some(start), Some(end)) = (source.start, source.end) {
216 if if deletion {
217 start != end || revision.text.as_ref().is_none_or(String::is_empty)
218 } else {
219 start >= end
220 } {
221 continue;
222 }
223 edits.entry(&source.node_id).or_default().push(Edit {
224 start,
225 end,
226 removed: if deletion {
227 revision.text.clone()
228 } else {
229 None
230 },
231 });
232 }
233 }
234 }
235 }
236 fn visit(
237 block: &DocumentBlock,
238 inherited: InlineChange,
239 edits: &HashMap<&str, Vec<Edit>>,
240 containers: &HashMap<&str, InlineChange>,
241 output: &mut BTreeMap<String, Vec<RevisionInline>>,
242 ) {
243 let state = containers
244 .get(block.id.as_str())
245 .copied()
246 .unwrap_or(inherited);
247 let changes = edits.get(block.id.as_str());
248 let paragraph = match &block.details {
249 BlockDetails::Paragraph(_) => true,
250 BlockDetails::Raw(d) => d["kind"] == "paragraph",
251 _ => false,
252 };
253 if paragraph && (changes.is_some() || state != InlineChange::Unchanged) {
254 output.insert(
255 block.id.clone(),
256 paragraph_inlines(block, changes.map(Vec::as_slice).unwrap_or_default(), state),
257 );
258 }
259 for child in &block.children {
260 visit(child, state, edits, containers, output);
261 }
262 }
263 let mut output = BTreeMap::new();
264 if !edits.is_empty() || !containers.is_empty() {
265 for block in blocks {
266 visit(
267 block,
268 InlineChange::Unchanged,
269 &edits,
270 &containers,
271 &mut output,
272 );
273 }
274 }
275 output
276}