| 1 | use crate::{ |
| 2 | Error, ExGuid, Object, ObjectData, PropertySets, |
| 3 | active::{ActivePage, Changes}, |
| 4 | create::{current_timestamps, properties, string}, |
| 5 | document::{Element, Kind}, |
| 6 | edit::update_title, |
| 7 | write::{PropertyObject, fresh_guid}, |
| 8 | }; |
| 9 | use std::{ |
| 10 | collections::{BTreeMap, BTreeSet}, |
| 11 | ops::Range, |
| 12 | sync::Arc, |
| 13 | }; |
| 14 | |
| 15 | fn invalid(message: &'static str) -> Error { |
| 16 | Error { offset: 0, message } |
| 17 | } |
| 18 | |
| 19 | /// Splits ordinary paragraph text. The original paragraph/text remain on the left; nested |
| 20 | /// children move to the right. |
| 21 | #[derive(Debug, Clone, PartialEq)] |
| 22 | pub(crate) struct ParagraphSplit { |
| 23 | guid: [u8; 16], |
| 24 | text: ExGuid, |
| 25 | offset: u32, |
| 26 | author: String, |
| 27 | created: u32, |
| 28 | } |
| 29 | |
| 30 | impl ParagraphSplit { |
| 31 | /// The offset is measured in UTF-16 code units and must lie between Unicode scalars. |
| 32 | pub(crate) fn new(text: ExGuid, offset: u32, author: &str) -> Result<Self, Error> { |
| 33 | if text.guid == [0; 16] || author.contains('\0') { |
| 34 | return Err(invalid( |
| 35 | "Choose paragraph text and an author name without NUL", |
| 36 | )); |
| 37 | } |
| 38 | Ok(Self { |
| 39 | guid: fresh_guid()?, |
| 40 | text, |
| 41 | offset, |
| 42 | author: author.to_owned(), |
| 43 | created: current_timestamps()?.0, |
| 44 | }) |
| 45 | } |
| 46 | |
| 47 | /// `changes` creating the right paragraph `paragraph` with text `right`, and a copy of |
| 48 | /// the left paragraph's list nodes under `lists`, in order. |
| 49 | pub(crate) fn changes_as( |
| 50 | &self, |
| 51 | active: &ActivePage<'_>, |
| 52 | paragraph_id: ExGuid, |
| 53 | right_text: ExGuid, |
| 54 | list_ids: &[ExGuid], |
| 55 | ) -> Result<Changes, Error> { |
| 56 | let [_] = active.pages.as_slice() else { |
| 57 | return Err(invalid("Splitting requires a single active page")); |
| 58 | }; |
| 59 | let view = &active.view; |
| 60 | let parents = active.editable_parents(self.text)?; |
| 61 | let [paragraph] = parents |
| 62 | .get(&self.text) |
| 63 | .map(Vec::as_slice) |
| 64 | .unwrap_or_default() |
| 65 | else { |
| 66 | return Err(invalid("Select text belonging to one paragraph")); |
| 67 | }; |
| 68 | let [parent] = parents |
| 69 | .get(paragraph) |
| 70 | .map(Vec::as_slice) |
| 71 | .unwrap_or_default() |
| 72 | else { |
| 73 | return Err(invalid("Select a paragraph with one parent")); |
| 74 | }; |
| 75 | let left = &view.nodes[paragraph]; |
| 76 | let Kind::Paragraph { lists, .. } = &left.kind else { |
| 77 | return Err(invalid("Select ordinary paragraph text")); |
| 78 | }; |
| 79 | if left.content != [self.text] |
| 80 | || !matches!( |
| 81 | view.nodes[parent].kind, |
| 82 | Kind::Outline { .. } |
| 83 | | Kind::OutlineGroup |
| 84 | | Kind::Paragraph { .. } |
| 85 | | Kind::Cell { .. } |
| 86 | ) |
| 87 | { |
| 88 | return Err(invalid( |
| 89 | "Select ordinary paragraph text inside an outline or table cell", |
| 90 | )); |
| 91 | } |
| 92 | let mut ancestors = BTreeSet::new(); |
| 93 | let mut pending = vec![*paragraph]; |
| 94 | while let Some(id) = pending.pop() { |
| 95 | if !ancestors.insert(id) { |
| 96 | continue; |
| 97 | } |
| 98 | if matches!(view.nodes[&id].kind, Kind::Title) { |
| 99 | return Err(invalid( |
| 100 | "Title containers cannot be split into ordinary paragraphs", |
| 101 | )); |
| 102 | } |
| 103 | pending.extend(parents.get(&id).into_iter().flatten().copied()); |
| 104 | } |
| 105 | let raw = &active.live.revision; |
| 106 | let (text, runs) = ordinary_text(view, raw, self.text)?; |
| 107 | let resolved = view.text_runs(self.text)?; |
| 108 | let link = |at: usize| { |
| 109 | resolved |
| 110 | .get(at) |
| 111 | .is_some_and(|run| run.format.hyperlink == Some(true)) |
| 112 | }; |
| 113 | if let Some(after) = runs |
| 114 | .iter() |
| 115 | .position(|run| run.start < self.offset && self.offset < run.end) |
| 116 | { |
| 117 | if link(after) { |
| 118 | return Err(invalid("A split cannot divide a hyperlink")); |
| 119 | } |
| 120 | } else if let Some(after) = runs |
| 121 | .iter() |
| 122 | .position(|run| run.start == self.offset && run.end > run.start && self.offset > 0) |
| 123 | && link(after - 1) |
| 124 | && link(after) |
| 125 | && !resolved[after].text.starts_with('\u{fddf}') |
| 126 | { |
| 127 | return Err(invalid("A split cannot divide a hyperlink")); |
| 128 | } |
| 129 | let node = &view.nodes[&self.text]; |
| 130 | let length = u32::try_from(text.encode_utf16().count()) |
| 131 | .map_err(|_| invalid("Paragraph exceeds the UTF-16 offset range"))?; |
| 132 | if self.offset > length || lists.len() > 251 || lists.len() > list_ids.len() { |
| 133 | return Err(invalid( |
| 134 | "Choose a position within the paragraph and at most 251 list levels", |
| 135 | )); |
| 136 | } |
| 137 | let author_id = ExGuid { |
| 138 | guid: self.guid, |
| 139 | n: 3, |
| 140 | }; |
| 141 | let normal_id = ExGuid { |
| 142 | guid: self.guid, |
| 143 | n: 4, |
| 144 | }; |
| 145 | // The text's last characters' style, past a final run an edit emptied. |
| 146 | let typing = runs |
| 147 | .iter() |
| 148 | .rev() |
| 149 | .find(|run| run.end > run.start) |
| 150 | .or(runs.last()) |
| 151 | .and_then(|run| run.format) |
| 152 | .unwrap_or(normal_id); |
| 153 | // Enter at the start leaves the emptied upper paragraph the first run's style, as |
| 154 | // OneNote does (`evidence/structural-edits/xml/c2s-*`). |
| 155 | let leading = runs.first().and_then(|run| run.format); |
| 156 | let modified = current_timestamps()?.0.to_le_bytes(); |
| 157 | let mut changed = BTreeMap::new(); |
| 158 | if (self.offset == 0 && !text.is_empty() && leading.is_none()) |
| 159 | || runs.iter().any(|run| run.format.is_none()) |
| 160 | { |
| 161 | changed.insert( |
| 162 | normal_id, |
| 163 | PropertyObject { |
| 164 | jcid: 0x12004d, |
| 165 | bytes: properties(&[])?, |
| 166 | global_ids: Arc::new(BTreeMap::from([(0, self.guid)])), |
| 167 | }, |
| 168 | ); |
| 169 | } |
| 170 | let mut prefix = fragment( |
| 171 | &raw.objects[&self.text], |
| 172 | node, |
| 173 | 0..self.offset, |
| 174 | if text.is_empty() { |
| 175 | typing |
| 176 | } else if self.offset == 0 { |
| 177 | leading.unwrap_or(normal_id) |
| 178 | } else { |
| 179 | normal_id |
| 180 | }, |
| 181 | )?; |
| 182 | let mut suffix = fragment(&raw.objects[&self.text], node, self.offset..length, typing)?; |
| 183 | prefix.set(&[(0x14001d7a, &modified)])?; |
| 184 | // Note tags and the recording link stay with the original text. |
| 185 | suffix.remove(&[0x40003489, 0x1c001c98, 0x14001c99])?; |
| 186 | suffix.set(&[(0x14001d7a, &modified)])?; |
| 187 | suffix.reference(right_text)?; |
| 188 | changed.insert(self.text, prefix); |
| 189 | changed.insert(right_text, suffix); |
| 190 | changed.insert( |
| 191 | author_id, |
| 192 | PropertyObject { |
| 193 | jcid: 0x120001, |
| 194 | bytes: properties(&crate::create::author_properties(&self.author))?, |
| 195 | global_ids: Arc::new(BTreeMap::from([(0, self.guid)])), |
| 196 | }, |
| 197 | ); |
| 198 | let mut right = PropertyObject::from_object(&raw.objects[paragraph])?; |
| 199 | right.reference(paragraph_id)?; |
| 200 | let content = right.reference(right_text)?; |
| 201 | let author = right.reference(author_id)?; |
| 202 | right.set(&[ |
| 203 | (0x24001c1f, &content), |
| 204 | (0x14001d09, &self.created.to_le_bytes()), |
| 205 | (0x20001d78, &author), |
| 206 | (0x20001d79, &author), |
| 207 | (0x14001d7a, &modified), |
| 208 | ])?; |
| 209 | if !lists.is_empty() { |
| 210 | let mut references = Vec::new(); |
| 211 | for (i, old) in lists.iter().enumerate() { |
| 212 | if !matches!(view.nodes[old].kind, Kind::List { .. }) { |
| 213 | return Err(invalid("The paragraph list is unavailable")); |
| 214 | } |
| 215 | let id = list_ids[i]; |
| 216 | let mut list = PropertyObject::from_object(&raw.objects[old])?; |
| 217 | list.remove(&[0x14001cb7])?; |
| 218 | list.reference(id)?; |
| 219 | references.extend_from_slice(&right.reference(id)?); |
| 220 | changed.insert(id, list); |
| 221 | } |
| 222 | right.set(&[(0x24001c26, &references)])?; |
| 223 | } |
| 224 | changed.insert(paragraph_id, right); |
| 225 | let mut original = PropertyObject::from_object(&raw.objects[paragraph])?; |
| 226 | original.remove(&[0x24001c20])?; |
| 227 | let author = original.reference(author_id)?; |
| 228 | original.set(&[(0x20001d79, &author), (0x14001d7a, &modified)])?; |
| 229 | changed.insert(*paragraph, original); |
| 230 | let mut parent_object = PropertyObject::from_object(&raw.objects[parent])?; |
| 231 | let mut children = Vec::new(); |
| 232 | for child in &view.nodes[parent].children { |
| 233 | children.extend_from_slice(&parent_object.reference(*child)?); |
| 234 | if child == paragraph { |
| 235 | children.extend_from_slice(&parent_object.reference(paragraph_id)?); |
| 236 | } |
| 237 | } |
| 238 | parent_object.set(&[(0x24001c20, &children)])?; |
| 239 | changed.insert(*parent, parent_object); |
| 240 | for id in ancestors { |
| 241 | let object = match changed.entry(id) { |
| 242 | std::collections::btree_map::Entry::Occupied(entry) => entry.into_mut(), |
| 243 | std::collections::btree_map::Entry::Vacant(entry) => { |
| 244 | entry.insert(PropertyObject::from_object(&raw.objects[&id])?) |
| 245 | } |
| 246 | }; |
| 247 | object.set(&[(0x14001d7a, &modified)])?; |
| 248 | } |
| 249 | update_title(active, view.clone(), &mut changed)?; |
| 250 | Ok(changed) |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | fn fragment( |
| 255 | raw: &Object<'_>, |
| 256 | node: &Element<'_>, |
| 257 | range: Range<u32>, |
| 258 | empty: ExGuid, |
| 259 | ) -> Result<PropertyObject, Error> { |
| 260 | let Kind::RichText { text, runs, .. } = &node.kind else { |
| 261 | return Err(invalid("Select ordinary paragraph text")); |
| 262 | }; |
| 263 | let units: Vec<_> = text.encode_utf16().collect(); |
| 264 | let start = usize::try_from(range.start) |
| 265 | .map_err(|_| invalid("Choose a position within the paragraph"))?; |
| 266 | let end = usize::try_from(range.end) |
| 267 | .map_err(|_| invalid("Choose a position within the paragraph"))?; |
| 268 | let slice = units |
| 269 | .get(start..end) |
| 270 | .ok_or_else(|| invalid("Choose a position within the paragraph"))?; |
| 271 | let text = String::from_utf16(slice) |
| 272 | .map_err(|_| invalid("Choose a position between Unicode scalars"))?; |
| 273 | let mut segments = Vec::new(); |
| 274 | for run in runs { |
| 275 | if run.start < range.end && range.start < run.end { |
| 276 | segments.push(( |
| 277 | run.end.min(range.end) - range.start, |
| 278 | run.format.unwrap_or(empty), |
| 279 | )); |
| 280 | } |
| 281 | } |
| 282 | if segments.is_empty() { |
| 283 | segments.push((0, empty)); |
| 284 | } |
| 285 | if !range.is_empty() && end == units.len() { |
| 286 | let last = runs.last().unwrap(); |
| 287 | if last.start == last.end { |
| 288 | segments.push((range.end - range.start, last.format.unwrap_or(empty))); |
| 289 | } |
| 290 | } |
| 291 | if segments[..segments.len() - 1] |
| 292 | .windows(2) |
| 293 | .any(|pair| pair[0].0 >= pair[1].0) |
| 294 | { |
| 295 | return Err(invalid("Text-run boundaries must be strictly increasing")); |
| 296 | } |
| 297 | let mut object = PropertyObject::from_object(raw)?; |
| 298 | let mut ends = Vec::new(); |
| 299 | let mut references = Vec::new(); |
| 300 | for (end, id) in segments { |
| 301 | ends.extend_from_slice(&end.to_le_bytes()); |
| 302 | references.extend_from_slice(&object.reference(id)?); |
| 303 | } |
| 304 | ends.truncate(ends.len() - 4); |
| 305 | object.set(&[ |
| 306 | (0x1c001c22, &string(&text)), |
| 307 | (0x1c001e12, &ends), |
| 308 | (0x24001e13, &references), |
| 309 | ])?; |
| 310 | Ok(object) |
| 311 | } |
| 312 | |
| 313 | /// A text object's text and runs, unless it holds what a split or join cannot divide or |
| 314 | /// merge: generated title text, equations, embedded objects or per-run data. Hyperlink |
| 315 | /// fields are ordinary runs; a recording link belongs to the text object. |
| 316 | fn ordinary_text<'a>( |
| 317 | view: &'a crate::document::Revision<'_>, |
| 318 | raw: &crate::ResolvedRevision<'_>, |
| 319 | id: ExGuid, |
| 320 | ) -> Result<(&'a str, &'a [crate::document::TextRun]), Error> { |
| 321 | let node = &view.nodes[&id]; |
| 322 | let Kind::RichText { |
| 323 | text, |
| 324 | runs, |
| 325 | boilerplate, |
| 326 | .. |
| 327 | } = &node.kind |
| 328 | else { |
| 329 | return Err(invalid("Select ordinary paragraph text")); |
| 330 | }; |
| 331 | if *boilerplate { |
| 332 | return Err(invalid("Generated text cannot be split or joined")); |
| 333 | } |
| 334 | for run in view.text_runs(id)? { |
| 335 | if [run.format.math, run.format.embedded_object].contains(&Some(true)) |
| 336 | || run.text.contains('\u{fffc}') |
| 337 | { |
| 338 | return Err(invalid( |
| 339 | "This paragraph contains an equation or embedded object that cannot be split or joined", |
| 340 | )); |
| 341 | } |
| 342 | } |
| 343 | let ObjectData::Properties(blob) = raw.objects[&id].data else { |
| 344 | unreachable!() |
| 345 | }; |
| 346 | if PropertySets::parse(blob)?.sets[0] |
| 347 | .iter() |
| 348 | .any(|p| matches!(p.id, 0x40003499 | 0x24003458)) |
| 349 | { |
| 350 | return Err(invalid( |
| 351 | "This paragraph contains run metadata that cannot be split or joined", |
| 352 | )); |
| 353 | } |
| 354 | Ok((text, runs)) |
| 355 | } |
| 356 | |
| 357 | /// Joins adjacent ordinary text in one outline or table cell. |
| 358 | /// The left paragraph survives. Empty left text adopts the right text identity; |
| 359 | /// otherwise the left text survives. The left paragraph's tags are retained. |
| 360 | /// Right-side tags are removed from active text, including when the left text is empty. |
| 361 | #[derive(Debug, Clone, PartialEq)] |
| 362 | pub(crate) struct ParagraphJoin { |
| 363 | left: ExGuid, |
| 364 | right: ExGuid, |
| 365 | author: String, |
| 366 | } |
| 367 | |
| 368 | impl ParagraphJoin { |
| 369 | /// Select the preceding leaf paragraph's text and the following paragraph's text. |
| 370 | pub(crate) fn new(left: ExGuid, right: ExGuid, author: &str) -> Result<Self, Error> { |
| 371 | if left == right || left.guid == [0; 16] || right.guid == [0; 16] || author.contains('\0') { |
| 372 | return Err(invalid( |
| 373 | "Choose two distinct text objects and an author name without NUL", |
| 374 | )); |
| 375 | } |
| 376 | Ok(Self { |
| 377 | left, |
| 378 | right, |
| 379 | author: author.to_owned(), |
| 380 | }) |
| 381 | } |
| 382 | |
| 383 | pub(crate) fn changes(&self, active: &ActivePage<'_>) -> Result<Changes, Error> { |
| 384 | let pages = &active.pages; |
| 385 | if pages.len() != 1 { |
| 386 | return Err(invalid("Joining requires a single active page")); |
| 387 | } |
| 388 | let view = &active.view; |
| 389 | let parents = active.editable_parents(self.left)?; |
| 390 | active.editable_parents(self.right)?; |
| 391 | let parent = |id| -> Result<ExGuid, Error> { |
| 392 | let [parent] = parents.get(&id).map(Vec::as_slice).unwrap_or_default() else { |
| 393 | return Err(invalid("Select text with a unique editable parent")); |
| 394 | }; |
| 395 | Ok(*parent) |
| 396 | }; |
| 397 | let left = parent(self.left)?; |
| 398 | let right = parent(self.right)?; |
| 399 | for (paragraph, text) in [(left, self.left), (right, self.right)] { |
| 400 | if !matches!(view.nodes[&paragraph].kind, Kind::Paragraph { .. }) |
| 401 | || view.nodes[&paragraph].content != [text] |
| 402 | { |
| 403 | return Err(invalid("Select ordinary paragraph text")); |
| 404 | } |
| 405 | let mut at = paragraph; |
| 406 | while at != pages[0] { |
| 407 | if matches!(view.nodes[&at].kind, Kind::Title) { |
| 408 | return Err(invalid( |
| 409 | "Title containers cannot be joined with ordinary paragraphs", |
| 410 | )); |
| 411 | } |
| 412 | at = parent(at)?; |
| 413 | } |
| 414 | } |
| 415 | if !view.nodes[&left].children.is_empty() { |
| 416 | return Err(invalid("Select the preceding paragraph's last descendant")); |
| 417 | } |
| 418 | let right_parent = parent(right)?; |
| 419 | if !matches!( |
| 420 | view.nodes[&right_parent].kind, |
| 421 | Kind::Outline { .. } | Kind::OutlineGroup | Kind::Paragraph { .. } | Kind::Cell { .. } |
| 422 | ) { |
| 423 | return Err(invalid("Select paragraphs in one outline or table cell")); |
| 424 | } |
| 425 | let mut stem = left; |
| 426 | while parent(stem)? != right_parent { |
| 427 | let previous = stem; |
| 428 | stem = parent(stem)?; |
| 429 | if !matches!( |
| 430 | view.nodes[&stem].kind, |
| 431 | Kind::Paragraph { .. } | Kind::OutlineGroup |
| 432 | ) || view.nodes[&stem].children.last() != Some(&previous) |
| 433 | { |
| 434 | return Err(invalid( |
| 435 | "Select adjacent text within one outline or table cell", |
| 436 | )); |
| 437 | } |
| 438 | } |
| 439 | let siblings = &view.nodes[&right_parent].children; |
| 440 | if !siblings.windows(2).any(|pair| pair == [stem, right]) { |
| 441 | return Err(invalid("Select adjacent paragraph text in document order")); |
| 442 | } |
| 443 | let children = &view.nodes[&right].children; |
| 444 | if !children.is_empty() |
| 445 | && !view.nodes[&stem].children.is_empty() |
| 446 | && view.nodes[&stem].child_level != view.nodes[&right].child_level |
| 447 | { |
| 448 | return Err(invalid( |
| 449 | "Joining these child indentation levels requires a hierarchy edit", |
| 450 | )); |
| 451 | } |
| 452 | let raw = &active.live.revision; |
| 453 | let (a, a_runs) = ordinary_text(view, raw, self.left)?; |
| 454 | let (b, b_runs) = ordinary_text(view, raw, self.right)?; |
| 455 | let length = u32::try_from(a.encode_utf16().count()) |
| 456 | .map_err(|_| invalid("Paragraph exceeds the UTF-16 offset range"))?; |
| 457 | let right_length = u32::try_from(b.encode_utf16().count()) |
| 458 | .map_err(|_| invalid("Paragraph exceeds the UTF-16 offset range"))?; |
| 459 | length |
| 460 | .checked_add(right_length) |
| 461 | .ok_or_else(|| invalid("Joined text exceeds the UTF-16 offset range"))?; |
| 462 | let modified = current_timestamps()?.0.to_le_bytes(); |
| 463 | let mut changed = BTreeMap::new(); |
| 464 | let (survivor, mut text) = if a.is_empty() { |
| 465 | let mut target = PropertyObject::from_object(&raw.objects[&self.right])?; |
| 466 | target.copy_property( |
| 467 | &PropertyObject::from_object(&raw.objects[&self.left])?, |
| 468 | 0x40003489, |
| 469 | )?; |
| 470 | (self.right, target) |
| 471 | } else { |
| 472 | let mut target = PropertyObject::from_object(&raw.objects[&self.left])?; |
| 473 | let mut styles = BTreeMap::new(); |
| 474 | let mut ends = Vec::new(); |
| 475 | let mut references = Vec::new(); |
| 476 | let mut empty_style = None; |
| 477 | for run in a_runs.iter().filter(|run| run.start < run.end) { |
| 478 | let id = if let Some(id) = run.format { |
| 479 | id |
| 480 | } else if let Some(id) = empty_style { |
| 481 | id |
| 482 | } else { |
| 483 | let id = ExGuid { |
| 484 | guid: fresh_guid()?, |
| 485 | n: 1, |
| 486 | }; |
| 487 | changed.insert( |
| 488 | id, |
| 489 | PropertyObject { |
| 490 | jcid: 0x12004d, |
| 491 | bytes: properties(&[])?, |
| 492 | global_ids: Arc::new(BTreeMap::from([(0, id.guid)])), |
| 493 | }, |
| 494 | ); |
| 495 | empty_style = Some(id); |
| 496 | id |
| 497 | }; |
| 498 | references.extend_from_slice(&target.reference(id)?); |
| 499 | ends.extend_from_slice(&run.end.to_le_bytes()); |
| 500 | } |
| 501 | let left_base = character_properties(view, raw, self.left)?; |
| 502 | let right_base = character_properties(view, raw, self.right)?; |
| 503 | for (i, run) in b_runs.iter().enumerate() { |
| 504 | if run.start == run.end && i != b_runs.len() - 1 { |
| 505 | continue; |
| 506 | } |
| 507 | let id = if let Some(id) = styles.get(&run.format) { |
| 508 | *id |
| 509 | } else { |
| 510 | let mut style = if let Some(id) = run.format { |
| 511 | PropertyObject::from_object(&raw.objects[&id])? |
| 512 | } else { |
| 513 | PropertyObject { |
| 514 | jcid: 0x12004d, |
| 515 | bytes: properties(&[])?, |
| 516 | global_ids: Arc::new(BTreeMap::new()), |
| 517 | } |
| 518 | }; |
| 519 | if left_base != right_base { |
| 520 | let mut values = right_base.clone(); |
| 521 | for property in &PropertySets::parse(&style.bytes)?.sets[0] { |
| 522 | let key = property.id & 0x7fffffff; |
| 523 | if is_character_property(key) { |
| 524 | let value = match property.value { |
| 525 | crate::Value::NoData => &[][..], |
| 526 | crate::Value::Bytes(bytes) => bytes, |
| 527 | _ => unreachable!(), |
| 528 | }; |
| 529 | values.insert(key, (property.id, value.to_vec())); |
| 530 | } |
| 531 | } |
| 532 | for key in left_base.keys() { |
| 533 | if values.contains_key(key) { |
| 534 | continue; |
| 535 | } |
| 536 | let value = match key >> 26 & 31 { |
| 537 | 2 => Vec::new(), |
| 538 | _ if matches!(*key, 0x14001c0c | 0x14001c0d) => { |
| 539 | 0xff000000_u32.to_le_bytes().to_vec() |
| 540 | } |
| 541 | _ => { |
| 542 | return Err(invalid( |
| 543 | "The right paragraph's implicit font or language cannot be preserved under the left style", |
| 544 | )); |
| 545 | } |
| 546 | }; |
| 547 | values.insert(*key, (*key, value)); |
| 548 | } |
| 549 | style.set( |
| 550 | &values |
| 551 | .values() |
| 552 | .map(|(id, value)| (*id, value.as_slice())) |
| 553 | .collect::<Vec<_>>(), |
| 554 | )?; |
| 555 | } |
| 556 | let id = if let Some(id) = run |
| 557 | .format |
| 558 | .filter(|id| raw.objects[id].data == ObjectData::Properties(&style.bytes)) |
| 559 | { |
| 560 | id |
| 561 | } else { |
| 562 | let id = ExGuid { |
| 563 | guid: fresh_guid()?, |
| 564 | n: 1, |
| 565 | }; |
| 566 | style.reference(id)?; |
| 567 | changed.insert(id, style); |
| 568 | id |
| 569 | }; |
| 570 | styles.insert(run.format, id); |
| 571 | id |
| 572 | }; |
| 573 | references.extend_from_slice(&target.reference(id)?); |
| 574 | ends.extend_from_slice(&(length + run.end).to_le_bytes()); |
| 575 | } |
| 576 | ends.truncate(ends.len() - 4); |
| 577 | if ends |
| 578 | .chunks_exact(4) |
| 579 | .map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap())) |
| 580 | .collect::<Vec<_>>() |
| 581 | .windows(2) |
| 582 | .any(|pair| pair[0] >= pair[1]) |
| 583 | { |
| 584 | return Err(invalid("Text-run boundaries must be strictly increasing")); |
| 585 | } |
| 586 | target.set(&[ |
| 587 | (0x1c001c22, &string(&format!("{a}{b}"))), |
| 588 | (0x1c001e12, &ends), |
| 589 | (0x24001e13, &references), |
| 590 | ])?; |
| 591 | (self.left, target) |
| 592 | }; |
| 593 | text.set(&[(0x14001d7a, &modified)])?; |
| 594 | changed.insert(survivor, text); |
| 595 | let author = ExGuid { |
| 596 | guid: fresh_guid()?, |
| 597 | n: 1, |
| 598 | }; |
| 599 | changed.insert( |
| 600 | author, |
| 601 | PropertyObject { |
| 602 | jcid: 0x120001, |
| 603 | bytes: properties(&crate::create::author_properties(&self.author))?, |
| 604 | global_ids: Arc::new(BTreeMap::from([(0, author.guid)])), |
| 605 | }, |
| 606 | ); |
| 607 | let mut left_object = PropertyObject::from_object(&raw.objects[&left])?; |
| 608 | let content = left_object.reference(survivor)?; |
| 609 | let author = left_object.reference(author)?; |
| 610 | left_object.set(&[(0x24001c1f, &content), (0x20001d79, &author)])?; |
| 611 | changed.insert(left, left_object); |
| 612 | if !children.is_empty() { |
| 613 | let target = match changed.entry(stem) { |
| 614 | std::collections::btree_map::Entry::Occupied(entry) => entry.into_mut(), |
| 615 | std::collections::btree_map::Entry::Vacant(entry) => { |
| 616 | entry.insert(PropertyObject::from_object(&raw.objects[&stem])?) |
| 617 | } |
| 618 | }; |
| 619 | let mut references = Vec::new(); |
| 620 | for id in view.nodes[&stem].children.iter().chain(children) { |
| 621 | references.extend_from_slice(&target.reference(*id)?); |
| 622 | } |
| 623 | target.set(&[(0x24001c20, &references)])?; |
| 624 | target.copy_property( |
| 625 | &PropertyObject::from_object(&raw.objects[&right])?, |
| 626 | 0x0c001c03, |
| 627 | )?; |
| 628 | } |
| 629 | let mut container = PropertyObject::from_object(&raw.objects[&right_parent])?; |
| 630 | let mut references = Vec::new(); |
| 631 | for id in siblings.iter().filter(|id| **id != right) { |
| 632 | references.extend_from_slice(&container.reference(*id)?); |
| 633 | } |
| 634 | container.set(&[(0x24001c20, &references)])?; |
| 635 | changed.insert(right_parent, container); |
| 636 | let mut pending = vec![left, right_parent]; |
| 637 | let mut ancestors = BTreeSet::new(); |
| 638 | while let Some(id) = pending.pop() { |
| 639 | if !ancestors.insert(id) { |
| 640 | continue; |
| 641 | } |
| 642 | let object = match changed.entry(id) { |
| 643 | std::collections::btree_map::Entry::Occupied(entry) => entry.into_mut(), |
| 644 | std::collections::btree_map::Entry::Vacant(entry) => { |
| 645 | entry.insert(PropertyObject::from_object(&raw.objects[&id])?) |
| 646 | } |
| 647 | }; |
| 648 | object.set(&[(0x14001d7a, &modified)])?; |
| 649 | pending.extend(parents.get(&id).into_iter().flatten().copied()); |
| 650 | } |
| 651 | update_title(active, view.clone(), &mut changed)?; |
| 652 | Ok(changed) |
| 653 | } |
| 654 | } |
| 655 | |
| 656 | fn is_character_property(id: u32) -> bool { |
| 657 | matches!( |
| 658 | id, |
| 659 | 0x08001c04 |
| 660 | ..=0x08001c09 |
| 661 | | 0x08001e16 |
| 662 | | 0x08001e14 |
| 663 | | 0x08001e19 |
| 664 | | 0x08003401 |
| 665 | | 0x08001e22 |
| 666 | | 0x08003476 |
| 667 | | 0x1c001c0a |
| 668 | | 0x10001c0b |
| 669 | | 0x14001c0c |
| 670 | | 0x14001c0d |
| 671 | | 0x14001c3b |
| 672 | ) |
| 673 | } |
| 674 | |
| 675 | fn character_properties( |
| 676 | view: &crate::document::Revision<'_>, |
| 677 | raw: &crate::ResolvedRevision<'_>, |
| 678 | text: ExGuid, |
| 679 | ) -> Result<BTreeMap<u32, (u32, Vec<u8>)>, Error> { |
| 680 | let Kind::RichText { |
| 681 | paragraph_style, .. |
| 682 | } = view.nodes[&text].kind |
| 683 | else { |
| 684 | unreachable!() |
| 685 | }; |
| 686 | let mut values = BTreeMap::new(); |
| 687 | for id in paragraph_style.into_iter().chain([text]) { |
| 688 | let ObjectData::Properties(bytes) = raw.objects[&id].data else { |
| 689 | unreachable!() |
| 690 | }; |
| 691 | for property in &PropertySets::parse(bytes)?.sets[0] { |
| 692 | let key = property.id & 0x7fffffff; |
| 693 | if is_character_property(key) { |
| 694 | let value = match property.value { |
| 695 | crate::Value::NoData => &[][..], |
| 696 | crate::Value::Bytes(bytes) => bytes, |
| 697 | _ => unreachable!(), |
| 698 | }; |
| 699 | values.insert(key, (property.id, value.to_vec())); |
| 700 | } |
| 701 | } |
| 702 | } |
| 703 | Ok(values) |
| 704 | } |