1//! The ops that turn one page model into another, computed from the models alone: each op
2//! is interpreted on a prediction of the page (`model::apply`) so later ones see what earlier
3//! ones leave, as the typed writers would store it.
4
5use super::{PageOp, TableEdit, TextProperty, Values, model};
6use crate::{
7 Error, ExGuid, TextAttribute,
8 document::{Format, Kind, Tag},
9 page::{
10 Definition, Outline, Page, PageObject, PageParagraph, Paragraph, ParagraphContent, Table,
11 TableCell, TextObject, text::new_id,
12 },
13};
14use std::{
15 collections::{BTreeMap, BTreeSet},
16 ops::Range,
17};
18
19fn invalid(message: &'static str) -> Error {
20 Error { offset: 0, message }
21}
22
23/// The ops that turn `before`, a page as stored, into `after`: O(page), for importing a
24/// page, converting queued whole-page edits and resolving a conflict, never per keystroke.
25pub fn lower_page(before: &Page, after: &Page) -> Result<Vec<PageOp>, Error> {
26 let old = View::new(before, false)?;
27 let new = View::new(after, false)?;
28 Lowering::new(before.clone(), None).run(before, after, &old, &new)
29}
30
31/// The ops that replace `before`, consecutive paragraphs of outline or table cell
32/// `container` with their parents listed first or outside the range, by `after`. `next`
33/// is the paragraph following the range; `definitions` holds the lists, tags and styles
34/// the paragraphs reference. Only the paragraphs given are read.
35pub fn lower(
36 container: ExGuid,
37 before: &[PageParagraph],
38 after: &[PageParagraph],
39 next: Option<&PageParagraph>,
40 definitions: &BTreeMap<ExGuid, Definition>,
41) -> Result<Vec<PageOp>, Error> {
42 let page = |paragraphs: &[PageParagraph]| Page {
43 title: String::new(),
44 identity: None,
45 created: None,
46 margin_origin: [0.0; 2],
47 rtl: false,
48 color: None,
49 rule_lines: None,
50 objects: vec![PageObject::Outline(Outline {
51 id: container,
52 title: false,
53 min_width: None,
54 layout: Default::default(),
55 indents: Vec::new(),
56 paragraphs: paragraphs.to_vec(),
57 unsupported: Vec::new(),
58 })],
59 definitions: definitions.clone(),
60 };
61 let (before, after) = (page(before), page(after));
62 let old = View::new(&before, true)?;
63 let new = View::new(&after, true)?;
64 let tail = next.map(|next| (next.parent.unwrap_or(container), next.id));
65 Lowering::new(before.clone(), tail).run(&before, &after, &old, &new)
66}
67
68/// Direct children of every container, in model order, plus lookups by identity.
69pub(crate) struct View<'a> {
70 pub page: &'a Page,
71 pub outlines: BTreeMap<ExGuid, &'a Outline>,
72 /// Outlines owned by a title object rather than the page.
73 pub title_outlines: BTreeSet<ExGuid>,
74 pub paragraphs: BTreeMap<ExGuid, &'a PageParagraph>,
75 pub children: BTreeMap<ExGuid, Vec<ExGuid>>,
76 pub container: BTreeMap<ExGuid, ExGuid>,
77 pub page_children: Vec<ExGuid>,
78 /// Containers a partial model names as parents without holding them, in order.
79 pub external: Vec<ExGuid>,
80 partial: bool,
81}
82
83impl<'a> View<'a> {
84 /// `partial` accepts parents outside the model, as a range of paragraphs has.
85 pub(crate) fn new(page: &'a Page, partial: bool) -> Result<Self, Error> {
86 let mut view = Self {
87 page,
88 outlines: BTreeMap::new(),
89 title_outlines: BTreeSet::new(),
90 paragraphs: BTreeMap::new(),
91 children: BTreeMap::new(),
92 container: BTreeMap::new(),
93 page_children: Vec::new(),
94 external: Vec::new(),
95 partial,
96 };
97 for object in &page.objects {
98 view.page_children.push(object.id());
99 match object {
100 PageObject::Outline(outline) => view.outline(outline)?,
101 PageObject::Title(title) => {
102 for outline in &title.outlines {
103 view.title_outlines.insert(outline.id);
104 view.outline(outline)?;
105 }
106 }
107 PageObject::Image(_)
108 | PageObject::Attachment(_)
109 | PageObject::Ink(_)
110 | PageObject::Unsupported(_) => {}
111 }
112 }
113 Ok(view)
114 }
115
116 fn outline(&mut self, outline: &'a Outline) -> Result<(), Error> {
117 if self.outlines.insert(outline.id, outline).is_some() {
118 return Err(invalid("The page model repeats an outline identity"));
119 }
120 self.children.entry(outline.id).or_default();
121 self.paragraphs_of(outline.id, &outline.paragraphs)
122 }
123
124 fn paragraphs_of(&mut self, root: ExGuid, list: &'a [PageParagraph]) -> Result<(), Error> {
125 let listed: BTreeSet<ExGuid> = if self.partial {
126 list.iter().map(|p| p.id).collect()
127 } else {
128 BTreeSet::new()
129 };
130 for paragraph in list {
131 let container = paragraph.parent.unwrap_or(root);
132 if let Some(parent) = paragraph.parent
133 && !self.paragraphs.contains_key(&parent)
134 {
135 if !self.partial || listed.contains(&parent) {
136 return Err(invalid(
137 "A paragraph's parent must precede it in its container",
138 ));
139 }
140 if !self.external.contains(&parent) {
141 self.external.push(parent);
142 }
143 }
144 if self.paragraphs.insert(paragraph.id, paragraph).is_some() {
145 return Err(invalid("The page model repeats a paragraph identity"));
146 }
147 self.children
148 .entry(container)
149 .or_default()
150 .push(paragraph.id);
151 self.children.entry(paragraph.id).or_default();
152 self.container.insert(paragraph.id, container);
153 if let ParagraphContent::Table(table) = &paragraph.content {
154 for row in &table.rows {
155 for cell in &row.cells {
156 if self.children.contains_key(&cell.id) {
157 return Err(invalid("The page model repeats a cell identity"));
158 }
159 self.children.entry(cell.id).or_default();
160 self.paragraphs_of(cell.id, &cell.paragraphs)?;
161 }
162 }
163 }
164 }
165 Ok(())
166 }
167
168 pub(crate) fn text(&self, paragraph: ExGuid) -> Option<&'a TextObject> {
169 self.paragraphs.get(&paragraph).and_then(|p| p.text())
170 }
171
172 /// Each outline's paragraphs in document order, a table's cells following its paragraph.
173 pub(crate) fn documents(&self) -> impl Iterator<Item = Vec<&'a PageParagraph>> + '_ {
174 fn walk<'a>(list: &'a [PageParagraph], out: &mut Vec<&'a PageParagraph>) {
175 for paragraph in list {
176 out.push(paragraph);
177 if let ParagraphContent::Table(table) = &paragraph.content {
178 for cell in table.rows.iter().flat_map(|row| &row.cells) {
179 walk(&cell.paragraphs, out);
180 }
181 }
182 }
183 }
184 self.outlines.values().map(|outline| {
185 let mut out = Vec::new();
186 walk(&outline.paragraphs, &mut out);
187 out
188 })
189 }
190
191 /// Every container in document order: outlines, external parents, paragraphs and cells.
192 fn containers(&self) -> Vec<ExGuid> {
193 let mut containers = Vec::new();
194 for object in &self.page.objects {
195 let outlines: Vec<&Outline> = match object {
196 PageObject::Outline(outline) => vec![outline],
197 PageObject::Title(title) => title.outlines.iter().collect(),
198 PageObject::Image(_)
199 | PageObject::Attachment(_)
200 | PageObject::Ink(_)
201 | PageObject::Unsupported(_) => Vec::new(),
202 };
203 for outline in outlines {
204 containers.push(outline.id);
205 containers.extend(&self.external);
206 collect_containers(&outline.paragraphs, &mut containers);
207 }
208 }
209 containers
210 }
211}
212
213pub(crate) fn collect_containers(list: &[PageParagraph], out: &mut Vec<ExGuid>) {
214 for paragraph in list {
215 out.push(paragraph.id);
216 if let ParagraphContent::Table(table) = &paragraph.content {
217 for row in &table.rows {
218 for cell in &row.cells {
219 out.push(cell.id);
220 collect_containers(&cell.paragraphs, out);
221 }
222 }
223 }
224 }
225}
226
227/// What the writers keep as stored, checked before any op: titles, unsupported objects,
228/// definitions other than lists and tags, outline roles, paragraph styles and formats,
229/// fields and content types.
230pub(crate) fn validate(
231 before: &Page,
232 after: &Page,
233 old: &View<'_>,
234 new: &View<'_>,
235) -> Result<(), Error> {
236 if after.margin_origin != before.margin_origin {
237 return Err(invalid("Page margins cannot be edited"));
238 }
239 for (id, definition) in &after.definitions {
240 if before
241 .definitions
242 .get(id)
243 .is_some_and(|stored| stored != definition)
244 && !matches!(
245 definition.kind,
246 Kind::List { .. } | Kind::TagDefinition { .. }
247 )
248 {
249 return Err(invalid("Style definitions cannot be edited"));
250 }
251 }
252 for paragraph in new.paragraphs.values() {
253 if let ParagraphContent::Table(table) = &paragraph.content {
254 super::table::validate_table(table)?;
255 }
256 }
257 let fixed = |page: &Page| -> Vec<String> {
258 let mut fixed: Vec<String> = page
259 .objects
260 .iter()
261 .filter_map(|object| match object {
262 PageObject::Unsupported(unsupported) => Some(format!("{unsupported:?}")),
263 PageObject::Title(title) => Some(format!(
264 "{:?} {:?} {:?} {:?}",
265 title.id,
266 title.date,
267 title.layout,
268 title.outlines.iter().map(|o| o.id).collect::<Vec<_>>()
269 )),
270 PageObject::Outline(_)
271 | PageObject::Image(_)
272 | PageObject::Attachment(_)
273 | PageObject::Ink(_) => None,
274 })
275 .collect();
276 fixed.sort();
277 fixed
278 };
279 if fixed(before) != fixed(after) {
280 return Err(invalid(
281 "Titles and unsupported objects cannot be edited through the page model",
282 ));
283 }
284 for (id, outline) in &new.outlines {
285 if outline.paragraphs.is_empty() {
286 return Err(invalid(
287 "An outline needs a paragraph; remove the outline instead",
288 ));
289 }
290 let Some(previous) = old.outlines.get(id) else {
291 continue;
292 };
293 let same = outline.title == previous.title
294 && outline.min_width == previous.min_width
295 && outline.indents == previous.indents
296 && outline.unsupported == previous.unsupported
297 && (!new.title_outlines.contains(id) || outline.layout == previous.layout);
298 if !same {
299 return Err(invalid(
300 "Outline roles, indentation tables and title geometry cannot be edited",
301 ));
302 }
303 }
304 let owners: BTreeMap<ExGuid, &PageParagraph> = old
305 .paragraphs
306 .values()
307 .filter_map(|owner| Some((owner.text()?.id, *owner)))
308 .collect();
309 for (id, paragraph) in &new.paragraphs {
310 let Some(previous) = old.paragraphs.get(id) else {
311 continue;
312 };
313 // A join moves a text object, with its style, into an emptied paragraph.
314 let owner = paragraph.text().map(|text| owners.get(&text.id));
315 let style = match owner {
316 Some(Some(owner)) => owner.style,
317 Some(None) => paragraph.style,
318 None => previous.style,
319 };
320 // `Style` and `Unstyle` give a paragraph of text a style and take it away.
321 let restyled = paragraph.style != style && paragraph.text().is_none();
322 if restyled || paragraph.format != previous.format {
323 return Err(invalid(
324 "Paragraph styles and paragraph formatting cannot be edited",
325 ));
326 }
327 match (&paragraph.content, &previous.content) {
328 (ParagraphContent::Text(text), ParagraphContent::Text(previous)) => {
329 if text.date_field != previous.date_field {
330 return Err(invalid("Text fields cannot be edited"));
331 }
332 }
333 (ParagraphContent::Table(table), ParagraphContent::Table(previous)) => {
334 if table.id != previous.id || table.layout != previous.layout {
335 return Err(invalid("Table identity and layout cannot be edited"));
336 }
337 let cells: BTreeMap<ExGuid, &TableCell> = previous
338 .rows
339 .iter()
340 .flat_map(|row| &row.cells)
341 .map(|cell| (cell.id, cell))
342 .collect();
343 let unchanged_cells = table.rows.iter().flat_map(|row| &row.cells).all(|cell| {
344 cells.get(&cell.id).is_none_or(|before| {
345 cell.layout == before.layout && cell.unsupported == before.unsupported
346 })
347 });
348 if !unchanged_cells {
349 return Err(invalid("Cell layout cannot be edited"));
350 }
351 }
352 (ParagraphContent::Unsupported(a), ParagraphContent::Unsupported(b)) if a == b => {}
353 (ParagraphContent::Ink(a), ParagraphContent::Ink(b)) => {
354 if a.id != b.id {
355 return Err(invalid("Ink identity cannot change"));
356 }
357 }
358 (ParagraphContent::Image(a), ParagraphContent::Image(b)) => {
359 super::content::picture_fixed_fields(a, b)?;
360 if (a.layout.x, a.layout.y) != (b.layout.x, b.layout.y) {
361 return Err(invalid("A paragraph picture has no position of its own"));
362 }
363 }
364 (ParagraphContent::Attachment(a), ParagraphContent::Attachment(b)) => {
365 if a.id != b.id {
366 return Err(invalid("Attachment identity cannot change"));
367 }
368 }
369 _ => return Err(invalid("Paragraph content type cannot change")),
370 }
371 }
372 Ok(())
373}
374
375/// Identities that keep their stored position: a longest increasing run of survivors,
376/// always including immovable ones. `after` may contain identities absent from `before`.
377pub(crate) fn kept_set(
378 before: &[ExGuid],
379 after: &[ExGuid],
380 movable: impl Fn(ExGuid) -> bool,
381) -> Result<BTreeSet<ExGuid>, Error> {
382 let position: BTreeMap<ExGuid, usize> =
383 after.iter().enumerate().map(|(i, id)| (*id, i)).collect();
384 let sequence: Vec<(ExGuid, usize)> = before
385 .iter()
386 .filter_map(|id| position.get(id).map(|at| (*id, *at)))
387 .collect();
388 let fixed: Vec<usize> = sequence
389 .iter()
390 .filter(|(id, _)| !movable(*id))
391 .map(|(_, at)| *at)
392 .collect();
393 if fixed.windows(2).any(|w| w[0] > w[1]) {
394 return Err(invalid(
395 "Images and unsupported objects cannot be reordered",
396 ));
397 }
398 if sequence.windows(2).all(|pair| pair[0].1 < pair[1].1) {
399 return Ok(sequence.into_iter().map(|(id, _)| id).collect());
400 }
401 // Longest increasing subsequence over `after` positions, forced through immovable items.
402 let n = sequence.len();
403 let mut best = vec![1usize; n];
404 let mut previous = vec![usize::MAX; n];
405 for i in 0..n {
406 let (id, at) = sequence[i];
407 let mandatory_before = sequence[..i]
408 .iter()
409 .filter(|(other, _)| !movable(*other))
410 .map(|(_, at)| *at)
411 .max();
412 if movable(id) && mandatory_before.is_some_and(|m| m > at) {
413 best[i] = 0;
414 continue;
415 }
416 let mandatory_after = sequence[i + 1..]
417 .iter()
418 .filter(|(other, _)| !movable(*other))
419 .map(|(_, at)| *at)
420 .min();
421 if movable(id) && mandatory_after.is_some_and(|m| m < at) {
422 best[i] = 0;
423 continue;
424 }
425 for j in 0..i {
426 if best[j] > 0 && sequence[j].1 < at && best[j] + 1 > best[i] {
427 best[i] = best[j] + 1;
428 previous[i] = j;
429 }
430 }
431 }
432 let mut kept = BTreeSet::new();
433 if let Some((mut i, _)) = best
434 .iter()
435 .enumerate()
436 .max_by_key(|(i, b)| (**b, usize::MAX - i))
437 && best[i] > 0
438 {
439 loop {
440 kept.insert(sequence[i].0);
441 if previous[i] == usize::MAX {
442 break;
443 }
444 i = previous[i];
445 }
446 }
447 for (id, _) in &sequence {
448 if !movable(*id) && !kept.contains(id) {
449 return Err(invalid(
450 "Images and unsupported objects cannot be reordered",
451 ));
452 }
453 }
454 Ok(kept)
455}
456
457/// The format of the span containing the UTF-16 position `at` (the following span at a
458/// boundary, the last at the end), which text inserted there takes.
459pub(crate) fn format_in(paragraph: &Paragraph, at: u32) -> Result<&Format, Error> {
460 let byte = paragraph.byte_offset(at)?;
461 let spans = paragraph.spans();
462 let index = spans
463 .partition_point(|span| span.end <= byte)
464 .min(spans.len() - 1);
465 Ok(&spans[index].format)
466}
467
468/// The smallest UTF-16 range whose replacement turns `before` into `after`.
469pub(crate) fn text_edit(before: &str, after: &str) -> Result<(Range<u32>, String), Error> {
470 let prefix = before
471 .char_indices()
472 .zip(after.chars())
473 .take_while(|((_, a), b)| a == b)
474 .map(|((i, a), _)| i + a.len_utf8())
475 .last()
476 .unwrap_or(0);
477 let suffix = before[prefix..]
478 .chars()
479 .rev()
480 .zip(after[prefix..].chars().rev())
481 .take_while(|(a, b)| a == b)
482 .map(|(a, _)| a.len_utf8())
483 .sum::<usize>();
484 let units = |s: &str| -> Result<u32, Error> {
485 u32::try_from(s.encode_utf16().count())
486 .map_err(|_| invalid("Text exceeds UTF-16 offset range"))
487 };
488 let start = units(&before[..prefix])?;
489 let end = start + units(&before[prefix..before.len() - suffix])?;
490 Ok((start..end, after[prefix..after.len() - suffix].to_owned()))
491}
492
493/// A character-format change over a range: attributes to set and properties to clear.
494pub(crate) type FormatEdit = (Range<u32>, Vec<TextAttribute>, Vec<TextProperty>);
495
496/// The format edits turning `stored`'s spans into `target`'s, which has the same text. A
497/// `fresh` text object was created by this edit, so an unset language keeps the writer's.
498pub(crate) fn format_edits(
499 stored: &Paragraph,
500 target: &Paragraph,
501 fresh: bool,
502) -> Result<Vec<FormatEdit>, Error> {
503 let mut boundaries = BTreeSet::new();
504 for paragraph in [stored, target] {
505 for span in paragraph.spans() {
506 boundaries.insert(paragraph.utf16_offset(span.end)?);
507 }
508 }
509 boundaries.insert(0);
510 let boundaries: Vec<u32> = boundaries.into_iter().collect();
511 let mut pending: Option<FormatEdit> = None;
512 let mut edits = Vec::new();
513 for window in boundaries.windows(2) {
514 let (start, end) = (window[0], window[1]);
515 if start == end {
516 continue;
517 }
518 let (set, clear) = attributes(format_in(stored, start)?, format_in(target, start)?, fresh)?;
519 match &mut pending {
520 Some((range, previous_set, previous_clear))
521 if *previous_set == set && *previous_clear == clear && range.end == start =>
522 {
523 range.end = end;
524 }
525 _ => {
526 if let Some(edit) = pending.take() {
527 edits.push(edit);
528 }
529 pending = Some((start..end, set, clear));
530 }
531 }
532 }
533 edits.extend(pending);
534 if target.text().is_empty() {
535 let (set, clear) = attributes(format_in(stored, 0)?, format_in(target, 0)?, fresh)?;
536 edits.push((0..0, set, clear));
537 }
538 edits.retain(|(_, set, clear)| !set.is_empty() || !clear.is_empty());
539 Ok(edits)
540}
541
542/// The change turning `current` into `target`; unsupported differences are errors. A value the
543/// target leaves unset is cleared from the run, as the model's formats are complete.
544fn attributes(
545 current: &Format,
546 target: &Format,
547 fresh: bool,
548) -> Result<(Vec<TextAttribute>, Vec<TextProperty>), Error> {
549 let mut out = Vec::new();
550 let mut cleared = Vec::new();
551 macro_rules! boolean {
552 ($field:ident, $variant:ident) => {
553 match target.$field {
554 Some(value) if current.$field != Some(value) => {
555 out.push(TextAttribute::$variant(value))
556 }
557 None if current.$field.is_some() => cleared.push(TextProperty::$variant),
558 _ => {}
559 }
560 };
561 }
562 boolean!(bold, Bold);
563 boolean!(italic, Italic);
564 boolean!(underline, Underline);
565 boolean!(strike, Strike);
566 boolean!(superscript, Superscript);
567 boolean!(subscript, Subscript);
568 boolean!(hidden, Hidden);
569 boolean!(hyperlink, Hyperlink);
570 boolean!(hyperlink_label, HyperlinkLabel);
571 boolean!(math, Math);
572 macro_rules! value {
573 ($field:ident, $property:ident, $attribute:expr) => {
574 if current.$field != target.$field {
575 match &target.$field {
576 Some(value) => out.push($attribute(value)),
577 None => cleared.push(TextProperty::$property),
578 }
579 }
580 };
581 }
582 let color =
583 |value: u32| (value != 0xff000000).then(|| value.to_le_bytes()[..3].try_into().unwrap());
584 value!(font, Font, |font: &String| TextAttribute::Font(
585 font.clone()
586 ));
587 value!(font_size, FontSize, |size: &f32| TextAttribute::FontSize(
588 *size
589 ));
590 value!(color, Color, |value: &u32| TextAttribute::Color(color(
591 *value
592 )));
593 value!(highlight, Highlight, |value: &u32| {
594 TextAttribute::Highlight(color(*value))
595 });
596 if current.language != target.language {
597 match target.language {
598 Some(language) => out.push(TextAttribute::Language(language)),
599 None if fresh => {}
600 None => {
601 return Err(invalid("Inherited character formatting cannot be restored"));
602 }
603 }
604 }
605 // An absent value and its stored default are the same formatting.
606 let flag = |a: Option<bool>, b: Option<bool>| a.unwrap_or(false) == b.unwrap_or(false);
607 let points = |a: Option<f32>, b: Option<f32>| a.unwrap_or(0.0) == b.unwrap_or(0.0);
608 let same_rest = flag(current.embedded_object, target.embedded_object)
609 && current.alignment.unwrap_or(0) == target.alignment.unwrap_or(0)
610 && flag(current.rtl, target.rtl)
611 && points(current.space_before, target.space_before)
612 && points(current.space_after, target.space_after)
613 && points(current.line_spacing, target.line_spacing)
614 && points(current.list_spacing, target.list_spacing);
615 if !same_rest {
616 return Err(invalid(
617 "Fields and paragraph spacing cannot be edited through the page model",
618 ));
619 }
620 Ok((out, cleared))
621}
622
623/// Paragraph formatting a text object stores: alignment, direction, spacing and language.
624#[derive(Clone, Copy, Debug, Default, PartialEq)]
625pub(crate) struct ParagraphFields {
626 pub alignment: Option<u8>,
627 pub rtl: Option<bool>,
628 pub space_before: Option<f32>,
629 pub space_after: Option<f32>,
630 pub line_spacing: Option<f32>,
631 pub language: Option<u32>,
632}
633
634impl ParagraphFields {
635 pub(crate) fn values(&self) -> Result<Values, Error> {
636 super::properties::paragraph_values(
637 self.alignment,
638 self.rtl,
639 self.space_before,
640 self.space_after,
641 self.line_spacing,
642 self.language,
643 )
644 }
645}
646
647/// The paragraph formatting `target` sets throughout that `stored` lacks somewhere.
648pub(crate) fn paragraph_fields(
649 stored: &Paragraph,
650 target: &Paragraph,
651) -> Result<ParagraphFields, Error> {
652 let first = format_in(target, 0)?;
653 macro_rules! field {
654 ($field:ident) => {{
655 let value = first.$field.unwrap_or_default();
656 (target
657 .spans()
658 .iter()
659 .all(|span| span.format.$field.unwrap_or_default() == value)
660 && stored
661 .spans()
662 .iter()
663 .any(|span| span.format.$field.unwrap_or_default() != value))
664 .then_some(value)
665 }};
666 }
667 Ok(ParagraphFields {
668 alignment: field!(alignment),
669 rtl: field!(rtl),
670 space_before: field!(space_before),
671 space_after: field!(space_after),
672 line_spacing: field!(line_spacing),
673 // An unset language is the writer's default, not LCID 0, which OneNote never stores
674 // on a text object and which runs without their own language would then read.
675 language: first.language.filter(|value| {
676 target
677 .spans()
678 .iter()
679 .all(|span| span.format.language == Some(*value))
680 && stored
681 .spans()
682 .iter()
683 .any(|span| span.format.language != Some(*value))
684 }),
685 })
686}
687
688/// Whether two tag lists store the same tags.
689pub(crate) fn same_tags(a: &[Tag], b: &[Tag]) -> bool {
690 let key = |t: &Tag| {
691 (
692 t.definition,
693 t.action_type,
694 t.shape,
695 t.property_status,
696 t.status,
697 t.created,
698 t.completed,
699 t.start,
700 t.due,
701 t.task_id,
702 )
703 };
704 a.len() == b.len() && a.iter().zip(b).all(|(x, y)| key(x) == key(y))
705}
706
707/// Whether the writers can split or join this text: ordinary text without generated fields,
708/// equations or embedded objects.
709fn ordinary(text: &TextObject) -> bool {
710 text.date_field.is_none()
711 && !text.text.text().contains('\u{fffc}')
712 && text
713 .text
714 .spans()
715 .iter()
716 .all(|span| span.format.math != Some(true) && span.format.embedded_object != Some(true))
717}
718
719/// Whether a split at `offset` would divide a hyperlink, which the writer refuses.
720fn divides_link(text: &Paragraph, offset: u32) -> Result<bool, Error> {
721 let byte = text.byte_offset(offset)?;
722 let link = |span: &crate::page::text::Span| span.format.hyperlink == Some(true);
723 let spans = text.spans();
724 let mut start = 0;
725 for (i, span) in spans.iter().enumerate() {
726 if start < byte && byte < span.end {
727 return Ok(link(span));
728 }
729 if byte == span.end && byte > 0 && i + 1 < spans.len() {
730 let next = &spans[i + 1];
731 return Ok(link(span) && link(next) && !text.text()[byte..].starts_with('\u{fddf}'));
732 }
733 start = span.end;
734 }
735 Ok(false)
736}
737
738/// A paragraph as `Insert` and `Add` create it: structure and content, without the
739/// properties their own ops set.
740fn bare(paragraph: &PageParagraph) -> PageParagraph {
741 let mut stripped = paragraph.clone();
742 stripped.style = None;
743 stripped.lists.clear();
744 stripped.tags.clear();
745 stripped.collapsed = false;
746 stripped.media = Default::default();
747 match &mut stripped.content {
748 ParagraphContent::Text(text) => text.tags.clear(),
749 ParagraphContent::Image(image) => image.tags.clear(),
750 ParagraphContent::Attachment(file) => file.tags.clear(),
751 ParagraphContent::Table(table) => {
752 table.tags.clear();
753 for cell in table.rows.iter_mut().flat_map(|row| &mut row.cells) {
754 cell.paragraphs = cell.paragraphs.iter().map(bare).collect();
755 }
756 }
757 _ => {}
758 }
759 stripped
760}
761
762fn bare_cells(cells: &[TableCell]) -> Vec<TableCell> {
763 cells
764 .iter()
765 .map(|cell| TableCell {
766 paragraphs: cell.paragraphs.iter().map(bare).collect(),
767 ..cell.clone()
768 })
769 .collect()
770}
771
772struct Lowering {
773 ops: Vec<PageOp>,
774 /// The page as the ops so far leave it.
775 current: Page,
776 /// The paragraph after a partial model's range: its container and identity.
777 tail: Option<(ExGuid, ExGuid)>,
778 /// Paragraphs and text objects the ops so far created.
779 created: BTreeSet<ExGuid>,
780}
781
782impl Lowering {
783 fn new(current: Page, tail: Option<(ExGuid, ExGuid)>) -> Self {
784 Self {
785 ops: Vec::new(),
786 current,
787 tail,
788 created: BTreeSet::new(),
789 }
790 }
791
792 fn emit(&mut self, op: PageOp) -> Result<(), Error> {
793 model::apply(&mut self.current, &op)?;
794 self.ops.push(op);
795 Ok(())
796 }
797
798 /// Whether the page, as the ops so far leave it, holds `id` as a paragraph, text,
799 /// list node or table part.
800 fn holds(&self, id: ExGuid) -> bool {
801 model::holds(&self.current, id)
802 }
803
804 fn run(
805 mut self,
806 before: &Page,
807 after: &Page,
808 old: &View<'_>,
809 new: &View<'_>,
810 ) -> Result<Vec<PageOp>, Error> {
811 validate(before, after, old, new)?;
812 let mut placed: BTreeMap<ExGuid, Vec<ExGuid>> = old.children.clone();
813 let mut consumed = BTreeSet::new();
814 self.split_and_join(old, new, &mut placed, &mut consumed)?;
815 self.place_page(old, new)?;
816 self.place(old, new, &placed)?;
817 self.delete(old, new, &consumed)?;
818 self.levels(new)?;
819 self.equations(new)?;
820 self.date(before, after)?;
821 self.text(new)?;
822 self.styles(after, new)?;
823 self.lists(after, new)?;
824 self.tags(after, new)?;
825 self.media(new)?;
826 self.paragraph_formatting(new)?;
827 self.formatting(old, new)?;
828 self.layout(old, new)?;
829 Ok(self.ops)
830 }
831
832 /// Splits and joins stored paragraphs as OneNote's Enter, Backspace and Delete do.
833 fn split_and_join(
834 &mut self,
835 old: &View<'_>,
836 new: &View<'_>,
837 placed: &mut BTreeMap<ExGuid, Vec<ExGuid>>,
838 consumed: &mut BTreeSet<ExGuid>,
839 ) -> Result<(), Error> {
840 // A new paragraph may be split from the stored paragraph nearest before it, whatever
841 // placement or pasted paragraphs the same edit adds between them.
842 let mut splits = Vec::new();
843 for document in new.documents() {
844 let mut left = None;
845 for paragraph in document {
846 if old.paragraphs.contains_key(&paragraph.id) {
847 left = paragraph.text().is_some().then_some(paragraph.id);
848 } else if let Some(left) = left
849 // A new cell arrives with its paragraphs.
850 && old.children.contains_key(&root(new, paragraph.id))
851 {
852 let container = new.container[&paragraph.id];
853 let at = new.children[&container]
854 .iter()
855 .position(|id| *id == paragraph.id);
856 splits.push(((container, at), left, paragraph.id));
857 }
858 }
859 }
860 splits.sort();
861 let mut split = BTreeSet::new();
862 for (_, left, right) in splits {
863 let (Some(previous), Some(after), Some(next)) =
864 (old.text(left), new.text(left), new.text(right))
865 else {
866 continue;
867 };
868 if split.contains(&left)
869 || after.id != previous.id
870 || new.paragraphs[&right].style != old.paragraphs[&left].style
871 || !ordinary(previous)
872 || old.title_outlines.contains(&root(old, left))
873 {
874 continue;
875 }
876 let Ok(offset) = after.text.utf16_offset(after.text.text().len()) else {
877 continue;
878 };
879 let length = previous.text.utf16_offset(previous.text.text().len())?;
880 // Splitting at the end only differs from appending a paragraph by the copied
881 // paragraph style; without one, an appended empty paragraph is an insertion.
882 if offset == length && new.paragraphs[&right].style.is_none() {
883 continue;
884 }
885 let (Ok(head), Ok(tail)) = (
886 previous.text.slice(0..offset),
887 previous.text.slice(offset..length),
888 ) else {
889 continue;
890 };
891 // An emptied side takes the writer's insertion style, so only its text must agree.
892 let same = |expected: &Paragraph, actual: &Paragraph| {
893 expected.text() == actual.text()
894 && (expected.text().is_empty() || expected == actual)
895 };
896 if !same(&head, &after.text)
897 || !same(&tail, &next.text)
898 || divides_link(&previous.text, offset)?
899 || old.paragraphs[&left].lists.len() > 251
900 {
901 continue;
902 }
903 // A text object a join moved into the paragraph keeps its identity there.
904 let right_text = if self.holds(next.id) {
905 new_id().map_err(|_| invalid("System random source failed"))?
906 } else {
907 next.id
908 };
909 // The split copies the left paragraph's list nodes in order; a node the model
910 // shares with a stored paragraph stays that paragraph's.
911 let stored_lists = old.paragraphs[&left].lists.len();
912 let model_lists = &new.paragraphs[&right].lists;
913 let mut lists = Vec::new();
914 for i in 0..stored_lists {
915 lists.push(match model_lists.get(i) {
916 Some(list) if model_lists.len() == stored_lists && !self.holds(*list) => *list,
917 _ => new_id().map_err(|_| invalid("System random source failed"))?,
918 });
919 }
920 self.emit(PageOp::Split {
921 text: previous.id,
922 at: offset,
923 paragraph: right,
924 right: right_text,
925 lists,
926 })?;
927 self.created.extend([right, right_text]);
928 split.insert(left);
929 // An earlier split may have carried the left paragraph to its tail.
930 let list = placed
931 .values_mut()
932 .find(|list| list.contains(&left))
933 .unwrap();
934 let at = list.iter().position(|id| *id == left).unwrap();
935 list.insert(at + 1, right);
936 placed.insert(right, placed[&left].clone());
937 placed.insert(left, Vec::new());
938 }
939 for (container, children) in &old.children {
940 if !new.children.contains_key(container) {
941 continue;
942 }
943 for pair in children.windows(2) {
944 let [left, right] = [pair[0], pair[1]];
945 let (Some(previous), Some(after), Some(removed)) =
946 (old.text(left), new.text(left), old.text(right))
947 else {
948 continue;
949 };
950 if new.paragraphs.contains_key(&right)
951 || !old.children[&left].is_empty()
952 || !old.children[&right].is_empty()
953 || removed.text.text().is_empty()
954 || !ordinary(previous)
955 || !ordinary(removed)
956 || old.title_outlines.contains(&root(old, left))
957 {
958 continue;
959 }
960 let mut joined = previous.text.clone();
961 if joined.append(removed.text.clone()).is_err() || joined != after.text {
962 continue;
963 }
964 let adopts_right = previous.text.text().is_empty();
965 if after.id
966 != if adopts_right {
967 removed.id
968 } else {
969 previous.id
970 }
971 {
972 continue;
973 }
974 self.emit(PageOp::Join {
975 left: previous.id,
976 right: removed.id,
977 })?;
978 consumed.insert(right);
979 placed.get_mut(container).unwrap().retain(|id| *id != right);
980 placed.remove(&right);
981 }
982 }
983 Ok(())
984 }
985
986 /// Adds, changes and orders what the page holds directly: outlines, pictures and ink.
987 fn place_page(&mut self, old: &View<'_>, new: &View<'_>) -> Result<(), Error> {
988 let pictures = |page: &Page| -> BTreeMap<ExGuid, crate::page::Image> {
989 page.objects
990 .iter()
991 .filter_map(|object| match object {
992 PageObject::Image(image) => Some((image.id, image.clone())),
993 _ => None,
994 })
995 .collect()
996 };
997 let stored = pictures(old.page);
998 for (id, image) in pictures(new.page) {
999 let Some(previous) = stored.get(&id) else {
1000 continue;
1001 };
1002 super::content::picture_fixed_fields(previous, &image)?;
1003 if (&previous.layout, &previous.alt) != (&image.layout, &image.alt) {
1004 self.emit(PageOp::Picture {
1005 picture: id,
1006 layout: image.layout.clone(),
1007 alt: image.alt.clone(),
1008 })?;
1009 }
1010 self.set_tags(new.page, id, &image.tags, &previous.tags)?;
1011 }
1012 let files = |page: &Page| -> BTreeMap<ExGuid, crate::page::Attachment> {
1013 page.objects
1014 .iter()
1015 .filter_map(|object| match object {
1016 PageObject::Attachment(file) => Some((file.id, file.clone())),
1017 _ => None,
1018 })
1019 .collect()
1020 };
1021 let stored = files(old.page);
1022 for (id, file) in files(new.page) {
1023 let Some(previous) = stored.get(&id) else {
1024 continue;
1025 };
1026 let (from, to) = (&previous.layout, &file.layout);
1027 if (
1028 from.max_width,
1029 from.max_height,
1030 from.width_set_by_user,
1031 from.reserved_width,
1032 ) != (
1033 to.max_width,
1034 to.max_height,
1035 to.width_set_by_user,
1036 to.reserved_width,
1037 ) {
1038 return Err(invalid("A file on the page keeps its extent"));
1039 }
1040 if (from.x, from.y) != (to.x, to.y) {
1041 let (Some(x), Some(y)) = (to.x, to.y) else {
1042 return Err(invalid("A file position needs both coordinates"));
1043 };
1044 self.emit(PageOp::Outline {
1045 object: id,
1046 edit: crate::OutlineEdit::Position { x, y },
1047 })?;
1048 }
1049 if (&previous.filename, &previous.source_path, previous.size)
1050 != (&file.filename, &file.source_path, file.size)
1051 {
1052 self.emit(PageOp::Attachment {
1053 attachment: id,
1054 filename: file.filename.clone(),
1055 source_path: file.source_path.clone(),
1056 size: file.size,
1057 })?;
1058 }
1059 self.set_tags(new.page, id, &file.tags, &previous.tags)?;
1060 }
1061 let inks = |page: &Page| -> BTreeMap<ExGuid, crate::page::Ink> {
1062 page.objects
1063 .iter()
1064 .filter_map(|object| match object {
1065 PageObject::Ink(ink) => Some((ink.id, ink.clone())),
1066 _ => None,
1067 })
1068 .collect()
1069 };
1070 let stored = inks(old.page);
1071 for (id, ink) in inks(new.page) {
1072 if let Some(previous) = stored.get(&id)
1073 && *previous != ink
1074 {
1075 self.strokes(previous, &ink)?;
1076 }
1077 }
1078 let title = |page: &Page, id: ExGuid| {
1079 page.objects
1080 .iter()
1081 .any(|object| matches!(object, PageObject::Title(title) if title.id == id))
1082 };
1083 let survivors: Vec<ExGuid> = old
1084 .page_children
1085 .iter()
1086 .copied()
1087 .filter(|id| {
1088 !title(old.page, *id) && new.page.objects.iter().any(|object| object.id() == *id)
1089 })
1090 .collect();
1091 let after: Vec<&PageObject> = new
1092 .page
1093 .objects
1094 .iter()
1095 .filter(|object| !title(new.page, object.id()))
1096 .collect();
1097 let order: Vec<ExGuid> = after.iter().map(|object| object.id()).collect();
1098 let kept = kept_set(&survivors, &order, |id| {
1099 new.outlines.contains_key(&id) && !new.title_outlines.contains(&id)
1100 })?;
1101 let mut next = None;
1102 for object in after.iter().rev() {
1103 let id = object.id();
1104 let stored = old.page_children.contains(&id);
1105 if !stored {
1106 if new.title_outlines.contains(&id) {
1107 return Err(invalid("Title outlines cannot be added"));
1108 }
1109 let added = match object {
1110 PageObject::Outline(outline) => {
1111 if outline
1112 .paragraphs
1113 .iter()
1114 .any(|p| old.paragraphs.contains_key(&p.id))
1115 {
1116 return Err(invalid(
1117 "A new outline holds new paragraphs; move stored ones after",
1118 ));
1119 }
1120 self.created.extend(model::identities(&outline.paragraphs));
1121 PageObject::Outline(Outline {
1122 paragraphs: outline.paragraphs.iter().map(bare).collect(),
1123 ..(*outline).clone()
1124 })
1125 }
1126 PageObject::Image(image) => PageObject::Image(crate::page::Image {
1127 tags: Vec::new(),
1128 ..image.clone()
1129 }),
1130 PageObject::Attachment(file) => {
1131 PageObject::Attachment(crate::page::Attachment {
1132 tags: Vec::new(),
1133 ..file.clone()
1134 })
1135 }
1136 PageObject::Ink(_) => (*object).clone(),
1137 PageObject::Title(_) | PageObject::Unsupported(_) => {
1138 return Err(invalid(
1139 "Titles and unsupported objects cannot be edited through the page model",
1140 ));
1141 }
1142 };
1143 self.emit(PageOp::Add {
1144 object: added,
1145 before: next,
1146 })?;
1147 let tags = match object {
1148 PageObject::Image(image) => image.tags.as_slice(),
1149 PageObject::Attachment(file) => file.tags.as_slice(),
1150 _ => &[],
1151 };
1152 self.set_tags(new.page, id, tags, &[])?;
1153 } else if !kept.contains(&id) {
1154 self.emit(PageOp::Move {
1155 object: id,
1156 parent: None,
1157 before: next,
1158 })?;
1159 }
1160 next = Some(id);
1161 }
1162 Ok(())
1163 }
1164
1165 /// Inserts new paragraphs and moves stored ones to the containers and order the model
1166 /// gives them, then edits the stored tables and paragraph content.
1167 fn place(
1168 &mut self,
1169 old: &View<'_>,
1170 new: &View<'_>,
1171 placed: &BTreeMap<ExGuid, Vec<ExGuid>>,
1172 ) -> Result<(), Error> {
1173 let mut inserted: BTreeSet<ExGuid> = BTreeSet::new();
1174 // New outlines and cells, whose paragraphs arrive with them.
1175 let arriving: BTreeSet<ExGuid> = new
1176 .children
1177 .keys()
1178 .filter(|id| {
1179 !old.children.contains_key(id)
1180 && !new.paragraphs.contains_key(id)
1181 && !new.external.contains(id)
1182 })
1183 .copied()
1184 .collect();
1185 for container in new.containers() {
1186 if arriving.contains(&root(new, container)) {
1187 continue;
1188 }
1189 let Some(children) = new.children.get(&container) else {
1190 continue;
1191 };
1192 let current: Vec<ExGuid> = placed
1193 .get(&container)
1194 .map(|list| {
1195 list.iter()
1196 .copied()
1197 .filter(|id| new.paragraphs.contains_key(id))
1198 .collect()
1199 })
1200 .unwrap_or_default();
1201 let kept = kept_set(&current, children, |_| true)?;
1202 let tail = self
1203 .tail
1204 .filter(|(parent, _)| *parent == container)
1205 .map(|(_, id)| id);
1206 let mut next = tail;
1207 let mut run: Vec<ExGuid> = Vec::new();
1208 for id in children.iter().rev() {
1209 let fresh = !old.paragraphs.contains_key(id) && !self.created.contains(id);
1210 if fresh && !inserted.contains(id) {
1211 run.push(*id);
1212 continue;
1213 }
1214 self.insert(container, next, &mut run, new, &mut inserted)?;
1215 if !kept.contains(id) && !inserted.contains(id) {
1216 self.emit(PageOp::Move {
1217 object: *id,
1218 parent: Some(container),
1219 before: next,
1220 })?;
1221 }
1222 next = Some(*id);
1223 }
1224 self.insert(container, next, &mut run, new, &mut inserted)?;
1225 }
1226 self.tables(old, new)?;
1227 self.content(old, new)?;
1228 Ok(())
1229 }
1230
1231 /// Inserts `run`, new siblings in reverse order, with their new descendants.
1232 fn insert(
1233 &mut self,
1234 container: ExGuid,
1235 before: Option<ExGuid>,
1236 run: &mut Vec<ExGuid>,
1237 new: &View<'_>,
1238 inserted: &mut BTreeSet<ExGuid>,
1239 ) -> Result<(), Error> {
1240 if run.is_empty() {
1241 return Ok(());
1242 }
1243 run.reverse();
1244 let mut paragraphs = Vec::new();
1245 let mut pending: Vec<ExGuid> = run.drain(..).rev().collect();
1246 while let Some(id) = pending.pop() {
1247 let paragraph = new.paragraphs[&id];
1248 paragraphs.push(bare(paragraph));
1249 inserted.insert(id);
1250 self.created.insert(id);
1251 if let Some(text) = paragraph.text() {
1252 self.created.insert(text.id);
1253 }
1254 if let ParagraphContent::Table(table) = &paragraph.content {
1255 for cell in table.rows.iter().flat_map(|row| &row.cells) {
1256 self.created.extend(model::identities(&cell.paragraphs));
1257 }
1258 }
1259 // New children follow their parent; stored ones move there later.
1260 pending.extend(
1261 new.children[&id]
1262 .iter()
1263 .rev()
1264 .filter(|child| !self.holds(**child) && !inserted.contains(child)),
1265 );
1266 }
1267 self.emit(PageOp::Insert {
1268 container,
1269 before,
1270 paragraphs,
1271 })
1272 }
1273
1274 /// Row, column, width, border and cell edits of stored tables.
1275 fn tables(&mut self, old: &View<'_>, new: &View<'_>) -> Result<(), Error> {
1276 let stored: BTreeMap<ExGuid, &Table> = old
1277 .paragraphs
1278 .values()
1279 .filter_map(|p| match &p.content {
1280 ParagraphContent::Table(table) => Some((table.id, table)),
1281 _ => None,
1282 })
1283 .collect();
1284 for paragraph in new.paragraphs.values() {
1285 let ParagraphContent::Table(table) = &paragraph.content else {
1286 continue;
1287 };
1288 let Some(previous) = stored.get(&table.id) else {
1289 continue;
1290 };
1291 self.table(previous, table)?;
1292 let cells: BTreeMap<ExGuid, &TableCell> = previous
1293 .rows
1294 .iter()
1295 .flat_map(|row| &row.cells)
1296 .map(|cell| (cell.id, cell))
1297 .collect();
1298 for cell in table.rows.iter().flat_map(|row| &row.cells) {
1299 let Some(before) = cells.get(&cell.id) else {
1300 continue;
1301 };
1302 if (cell.shading, &cell.indents) != (before.shading, &before.indents) {
1303 self.emit(PageOp::Table {
1304 table: table.id,
1305 edit: TableEdit::Cell {
1306 cell: cell.id,
1307 shading: cell.shading,
1308 indents: cell.indents.clone(),
1309 },
1310 })?;
1311 }
1312 }
1313 }
1314 Ok(())
1315 }
1316
1317 /// The row and column edits turning stored table `before` into `after`.
1318 fn table(&mut self, before: &Table, after: &Table) -> Result<(), Error> {
1319 let id = after.id;
1320 let unsupported = || invalid("Table rows and cells keep their order; add or remove them");
1321 let old_rows: Vec<ExGuid> = before.rows.iter().map(|row| row.id).collect();
1322 let new_rows: Vec<ExGuid> = after.rows.iter().map(|row| row.id).collect();
1323 let kept_rows: Vec<ExGuid> = old_rows
1324 .iter()
1325 .copied()
1326 .filter(|row| new_rows.contains(row))
1327 .collect();
1328 if new_rows
1329 .iter()
1330 .filter(|row| old_rows.contains(row))
1331 .ne(kept_rows.iter())
1332 {
1333 return Err(unsupported());
1334 }
1335 // The column pattern every kept row shares: which stored cells stay, which are new.
1336 let mut deleted: Option<Vec<u32>> = None;
1337 let mut added: Option<Vec<u32>> = None;
1338 for row in &kept_rows {
1339 let old_cells: Vec<ExGuid> = before
1340 .rows
1341 .iter()
1342 .find(|r| r.id == *row)
1343 .unwrap()
1344 .cells
1345 .iter()
1346 .map(|cell| cell.id)
1347 .collect();
1348 let new_cells: Vec<ExGuid> = after
1349 .rows
1350 .iter()
1351 .find(|r| r.id == *row)
1352 .unwrap()
1353 .cells
1354 .iter()
1355 .map(|cell| cell.id)
1356 .collect();
1357 let gone: Vec<u32> = (0..old_cells.len() as u32)
1358 .filter(|i| !new_cells.contains(&old_cells[*i as usize]))
1359 .collect();
1360 let fresh: Vec<u32> = (0..new_cells.len() as u32)
1361 .filter(|i| !old_cells.contains(&new_cells[*i as usize]))
1362 .collect();
1363 if old_cells
1364 .iter()
1365 .filter(|cell| new_cells.contains(cell))
1366 .ne(new_cells.iter().filter(|cell| old_cells.contains(cell)))
1367 || deleted.as_ref().is_some_and(|known| *known != gone)
1368 || added.as_ref().is_some_and(|known| *known != fresh)
1369 {
1370 return Err(unsupported());
1371 }
1372 deleted = Some(gone);
1373 added = Some(fresh);
1374 }
1375 let (deleted, added) = (deleted.unwrap_or_default(), added.unwrap_or_default());
1376 if kept_rows.is_empty() && before.columns.len() != after.columns.len() {
1377 return Err(unsupported());
1378 }
1379 for row in &old_rows {
1380 if !new_rows.contains(row) && !kept_rows.is_empty() {
1381 self.emit(PageOp::Table {
1382 table: id,
1383 edit: TableEdit::DeleteRow(*row),
1384 })?;
1385 }
1386 }
1387 for at in deleted.iter().rev() {
1388 self.emit(PageOp::Table {
1389 table: id,
1390 edit: TableEdit::DeleteColumn(*at),
1391 })?;
1392 }
1393 for at in &added {
1394 let cells = kept_rows
1395 .iter()
1396 .map(|row| {
1397 let row = after.rows.iter().find(|r| r.id == *row).unwrap();
1398 bare_cells(&row.cells[*at as usize..=*at as usize]).remove(0)
1399 })
1400 .collect::<Vec<_>>();
1401 for cell in &cells {
1402 self.created.extend(model::identities(&cell.paragraphs));
1403 }
1404 self.emit(PageOp::Table {
1405 table: id,
1406 edit: TableEdit::Column {
1407 at: *at,
1408 width: after.columns[*at as usize].width,
1409 cells,
1410 },
1411 })?;
1412 }
1413 // New rows, each run before the stored row that follows it.
1414 let mut run = Vec::new();
1415 let mut next = None;
1416 for row in after.rows.iter().rev() {
1417 if old_rows.contains(&row.id) {
1418 self.rows(id, next, &mut run)?;
1419 next = Some(row.id);
1420 } else {
1421 run.push(row.clone());
1422 }
1423 }
1424 self.rows(id, next, &mut run)?;
1425 if kept_rows.is_empty() {
1426 for row in &old_rows {
1427 self.emit(PageOp::Table {
1428 table: id,
1429 edit: TableEdit::DeleteRow(*row),
1430 })?;
1431 }
1432 }
1433 let current = model::table(&self.current, id)
1434 .ok_or_else(|| invalid("A table is missing after row edits"))?;
1435 if current.columns != after.columns {
1436 self.emit(PageOp::Table {
1437 table: id,
1438 edit: TableEdit::Columns(after.columns.clone()),
1439 })?;
1440 }
1441 let current = model::table(&self.current, id).unwrap();
1442 if current.borders != after.borders
1443 && let Some(borders) = after.borders
1444 {
1445 self.emit(PageOp::Table {
1446 table: id,
1447 edit: TableEdit::Borders(borders),
1448 })?;
1449 }
1450 Ok(())
1451 }
1452
1453 fn rows(
1454 &mut self,
1455 table: ExGuid,
1456 before: Option<ExGuid>,
1457 run: &mut Vec<crate::page::TableRow>,
1458 ) -> Result<(), Error> {
1459 if run.is_empty() {
1460 return Ok(());
1461 }
1462 run.reverse();
1463 let rows: Vec<crate::page::TableRow> = run
1464 .drain(..)
1465 .map(|row| crate::page::TableRow {
1466 id: row.id,
1467 cells: bare_cells(&row.cells),
1468 })
1469 .collect();
1470 for cell in rows.iter().flat_map(|row| &row.cells) {
1471 self.created.extend(model::identities(&cell.paragraphs));
1472 }
1473 self.emit(PageOp::Table {
1474 table,
1475 edit: TableEdit::Rows { before, rows },
1476 })
1477 }
1478
1479 /// Edits of stored pictures, ink and attachments that paragraphs hold.
1480 fn content(&mut self, old: &View<'_>, new: &View<'_>) -> Result<(), Error> {
1481 for (id, paragraph) in &new.paragraphs {
1482 let Some(previous) = old.paragraphs.get(id) else {
1483 continue;
1484 };
1485 match (&previous.content, &paragraph.content) {
1486 (ParagraphContent::Image(stored), ParagraphContent::Image(image))
1487 if stored != image =>
1488 {
1489 self.emit(PageOp::Picture {
1490 picture: image.id,
1491 layout: image.layout.clone(),
1492 alt: image.alt.clone(),
1493 })?;
1494 }
1495 (ParagraphContent::Ink(stored), ParagraphContent::Ink(ink)) if stored != ink => {
1496 self.strokes(stored, ink)?;
1497 }
1498 (
1499 ParagraphContent::Attachment(stored),
1500 ParagraphContent::Attachment(attachment),
1501 ) if stored != attachment => {
1502 self.emit(PageOp::Attachment {
1503 attachment: attachment.id,
1504 filename: attachment.filename.clone(),
1505 source_path: attachment.source_path.clone(),
1506 size: attachment.size,
1507 })?;
1508 }
1509 _ => {}
1510 }
1511 }
1512 Ok(())
1513 }
1514
1515 fn strokes(&mut self, stored: &crate::page::Ink, ink: &crate::page::Ink) -> Result<(), Error> {
1516 let placed = |layout: &crate::document::Layout| crate::document::Layout {
1517 x: None,
1518 y: None,
1519 ..layout.clone()
1520 };
1521 if placed(&stored.layout) != placed(&ink.layout) || stored.groups != ink.groups {
1522 return Err(invalid("Ink size and groups stay as stored"));
1523 }
1524 if (stored.layout.x, stored.layout.y) != (ink.layout.x, ink.layout.y) {
1525 let (Some(x), Some(y)) = (ink.layout.x, ink.layout.y) else {
1526 return Err(invalid("An ink position needs both coordinates"));
1527 };
1528 self.emit(PageOp::Outline {
1529 object: ink.id,
1530 edit: crate::OutlineEdit::Position { x, y },
1531 })?;
1532 if stored.strokes == ink.strokes {
1533 return Ok(());
1534 }
1535 }
1536 let mut add = Vec::new();
1537 for stroke in &ink.strokes {
1538 match stored.strokes.iter().find(|s| s.id == stroke.id) {
1539 Some(previous) if previous == stroke => {}
1540 Some(_) => return Err(invalid("A stored stroke keeps its path and pen")),
1541 None => add.push(stroke.clone()),
1542 }
1543 }
1544 let remove: Vec<ExGuid> = stored
1545 .strokes
1546 .iter()
1547 .filter(|stroke| !ink.strokes.iter().any(|s| s.id == stroke.id))
1548 .map(|stroke| stroke.id)
1549 .collect();
1550 if ink.strokes.iter().map(|s| s.id).ne(stored
1551 .strokes
1552 .iter()
1553 .filter(|s| !remove.contains(&s.id))
1554 .map(|s| s.id)
1555 .chain(add.iter().map(|s| s.id)))
1556 {
1557 return Err(invalid("Stored strokes keep their order; new ones follow"));
1558 }
1559 self.emit(PageOp::Strokes {
1560 ink: ink.id,
1561 add,
1562 remove,
1563 })
1564 }
1565
1566 fn delete(
1567 &mut self,
1568 old: &View<'_>,
1569 new: &View<'_>,
1570 consumed: &BTreeSet<ExGuid>,
1571 ) -> Result<(), Error> {
1572 let removed_outline =
1573 |id: ExGuid| old.outlines.contains_key(&id) && !new.outlines.contains_key(&id);
1574 for id in old.paragraphs.keys() {
1575 if new.paragraphs.contains_key(id) || consumed.contains(id) {
1576 continue;
1577 }
1578 let mut ancestor = old.container[id];
1579 let mut covered = false;
1580 // A surviving ancestor is placed where the model says, carrying this paragraph along.
1581 while !new.paragraphs.contains_key(&ancestor) {
1582 if removed_outline(ancestor)
1583 || (old.paragraphs.contains_key(&ancestor) && !consumed.contains(&ancestor))
1584 || (old.children.contains_key(&ancestor)
1585 && !old.paragraphs.contains_key(&ancestor)
1586 && !old.outlines.contains_key(&ancestor)
1587 && !new.children.contains_key(&ancestor)
1588 && !old.external.contains(&ancestor))
1589 {
1590 covered = true;
1591 break;
1592 }
1593 match old.container.get(&ancestor) {
1594 Some(parent) => ancestor = *parent,
1595 None => break,
1596 }
1597 }
1598 if !covered {
1599 self.emit(PageOp::Delete { object: *id })?;
1600 }
1601 }
1602 let kept: BTreeSet<ExGuid> = new.page.objects.iter().map(PageObject::id).collect();
1603 for object in &old.page.objects {
1604 if matches!(
1605 object,
1606 PageObject::Image(_) | PageObject::Attachment(_) | PageObject::Ink(_)
1607 ) && !kept.contains(&object.id())
1608 {
1609 self.emit(PageOp::Delete {
1610 object: object.id(),
1611 })?;
1612 }
1613 }
1614 for id in old.outlines.keys() {
1615 if removed_outline(*id) {
1616 if old.title_outlines.contains(id) {
1617 return Err(invalid("Title outlines cannot be removed"));
1618 }
1619 self.emit(PageOp::Delete { object: *id })?;
1620 }
1621 }
1622 Ok(())
1623 }
1624
1625 /// Gives paragraphs the levels the model does, parents first.
1626 fn levels(&mut self, new: &View<'_>) -> Result<(), Error> {
1627 for document in new.documents() {
1628 for paragraph in document {
1629 let current = model::paragraph(&self.current, paragraph.id)
1630 .ok_or_else(|| invalid("A paragraph is missing after structural edits"))?;
1631 if current.level != paragraph.level {
1632 self.emit(PageOp::Level {
1633 paragraph: paragraph.id,
1634 level: paragraph.level,
1635 })?;
1636 }
1637 }
1638 }
1639 Ok(())
1640 }
1641
1642 fn equations(&mut self, new: &View<'_>) -> Result<(), Error> {
1643 for (id, paragraph) in &new.paragraphs {
1644 let Some(text) = paragraph.text() else {
1645 continue;
1646 };
1647 let stored = model::paragraph(&self.current, *id).and_then(|p| p.text().cloned());
1648 // Deleting an equation leaves no math in the model, but its stored runs are still math.
1649 if !crate::page::Math::is_equation(&text.text)
1650 && !stored
1651 .as_ref()
1652 .is_some_and(|stored| crate::page::Math::is_equation(&stored.text))
1653 {
1654 continue;
1655 }
1656 let stored = stored
1657 .ok_or_else(|| invalid("An equation paragraph is missing after placement"))?;
1658 if stored.text != text.text {
1659 self.emit(PageOp::Equation {
1660 text: stored.id,
1661 math: text.text.clone(),
1662 })?;
1663 }
1664 }
1665 Ok(())
1666 }
1667
1668 /// The page's date, with every date and time field of the title showing it.
1669 fn date(&mut self, before: &Page, after: &Page) -> Result<(), Error> {
1670 let fields: Vec<(ExGuid, String)> = model::date_fields(after)
1671 .into_iter()
1672 .map(|text| (text.id, text.text.text().to_owned()))
1673 .collect();
1674 let shown = fields.iter().all(|(id, shown)| {
1675 model::paragraph_text(&self.current, *id).is_some_and(|text| text.text.text() == shown)
1676 });
1677 if before.color != after.color {
1678 self.emit(PageOp::Color(after.color))?;
1679 }
1680 if before.rule_lines != after.rule_lines {
1681 self.emit(PageOp::RuleLines(after.rule_lines))?;
1682 }
1683 if before.created == after.created && shown {
1684 return Ok(());
1685 }
1686 let created = after
1687 .created
1688 .ok_or_else(|| invalid("A page date cannot be removed"))?;
1689 self.emit(PageOp::Date { created, fields })
1690 }
1691
1692 fn text(&mut self, new: &View<'_>) -> Result<(), Error> {
1693 for (id, paragraph) in &new.paragraphs {
1694 let Some(text) = paragraph.text() else {
1695 continue;
1696 };
1697 let Some(stored) = model::paragraph(&self.current, *id).and_then(|p| p.text().cloned())
1698 else {
1699 return Err(invalid("A paragraph is missing after structural edits"));
1700 };
1701 if stored.id != text.id {
1702 return Err(invalid("Text object identities cannot change"));
1703 }
1704 if stored.text.text() == text.text.text() {
1705 continue;
1706 }
1707 let (range, with) = text_edit(stored.text.text(), text.text.text())?;
1708 self.emit(PageOp::Text {
1709 text: stored.id,
1710 range,
1711 with,
1712 })?;
1713 }
1714 Ok(())
1715 }
1716
1717 fn styles(&mut self, after: &Page, new: &View<'_>) -> Result<(), Error> {
1718 for (id, paragraph) in &new.paragraphs {
1719 let current = model::paragraph(&self.current, *id)
1720 .ok_or_else(|| invalid("A paragraph is missing after text edits"))?;
1721 let Some(style) = paragraph.style else {
1722 if current.style.is_some() {
1723 self.emit(PageOp::Unstyle { paragraph: *id })?;
1724 }
1725 continue;
1726 };
1727 if current.style == Some(style) {
1728 continue;
1729 }
1730 if paragraph.text().is_none() {
1731 return Err(invalid("Only text paragraphs take a paragraph style"));
1732 }
1733 let definition = after
1734 .definitions
1735 .get(&style)
1736 .ok_or_else(|| invalid("A paragraph references a missing style definition"))?;
1737 self.emit(PageOp::Style {
1738 paragraph: *id,
1739 style,
1740 definition: definition.clone(),
1741 })?;
1742 }
1743 Ok(())
1744 }
1745
1746 /// Gives each paragraph the list nodes its model references; one another paragraph
1747 /// owns is copied, as native list nodes belong to one paragraph.
1748 fn lists(&mut self, after: &Page, new: &View<'_>) -> Result<(), Error> {
1749 for (id, paragraph) in &new.paragraphs {
1750 let current = model::paragraph(&self.current, *id)
1751 .ok_or_else(|| invalid("A paragraph is missing after text edits"))?;
1752 let same = paragraph.lists.len() == current.lists.len()
1753 && paragraph
1754 .lists
1755 .iter()
1756 .zip(&current.lists)
1757 .all(|(model, node)| {
1758 model == node
1759 && after.definitions.get(model) == self.current.definitions.get(node)
1760 });
1761 if same {
1762 continue;
1763 }
1764 let own = current.lists.clone();
1765 let mut lists = Vec::new();
1766 for list in &paragraph.lists {
1767 let definition = after
1768 .definitions
1769 .get(list)
1770 .ok_or_else(|| invalid("A paragraph references a missing list definition"))?;
1771 let node = if own.contains(list) || !model::owns_list(&self.current, *list) {
1772 *list
1773 } else {
1774 new_id().map_err(|_| invalid("System random source failed"))?
1775 };
1776 lists.push((node, definition.clone()));
1777 }
1778 self.emit(PageOp::List {
1779 paragraph: *id,
1780 lists,
1781 })?;
1782 }
1783 Ok(())
1784 }
1785
1786 fn tags(&mut self, after: &Page, new: &View<'_>) -> Result<(), Error> {
1787 for (id, paragraph) in &new.paragraphs {
1788 let current = model::paragraph(&self.current, *id)
1789 .ok_or_else(|| invalid("A paragraph is missing after text edits"))?
1790 .clone();
1791 let mut targets = vec![(*id, &paragraph.tags, current.tags.clone())];
1792 match (&paragraph.content, &current.content) {
1793 (ParagraphContent::Text(text), ParagraphContent::Text(stored)) => {
1794 targets.push((text.id, &text.tags, stored.tags.clone()))
1795 }
1796 (ParagraphContent::Table(table), ParagraphContent::Table(stored)) => {
1797 targets.push((table.id, &table.tags, stored.tags.clone()))
1798 }
1799 (ParagraphContent::Image(image), ParagraphContent::Image(stored)) => {
1800 targets.push((image.id, &image.tags, stored.tags.clone()))
1801 }
1802 (ParagraphContent::Attachment(file), ParagraphContent::Attachment(stored)) => {
1803 targets.push((file.id, &file.tags, stored.tags.clone()))
1804 }
1805 _ => {}
1806 }
1807 for (target, tags, stored) in targets {
1808 self.set_tags(after, target, tags, &stored)?;
1809 }
1810 }
1811 Ok(())
1812 }
1813
1814 /// Emits the op giving `target` `tags` where it stores others.
1815 fn set_tags(
1816 &mut self,
1817 after: &Page,
1818 target: ExGuid,
1819 tags: &[Tag],
1820 stored: &[Tag],
1821 ) -> Result<(), Error> {
1822 if same_tags(tags, stored) {
1823 return Ok(());
1824 }
1825 let mut definitions = Vec::new();
1826 for tag in tags {
1827 let Some(definition) = tag.definition else {
1828 continue;
1829 };
1830 if let Some(model) = after.definitions.get(&definition)
1831 && !definitions.iter().any(|(id, _)| *id == definition)
1832 {
1833 definitions.push((definition, model.clone()));
1834 }
1835 }
1836 self.emit(PageOp::Tags {
1837 target,
1838 tags: tags.to_vec(),
1839 definitions,
1840 })
1841 }
1842
1843 fn media(&mut self, new: &View<'_>) -> Result<(), Error> {
1844 for (id, paragraph) in &new.paragraphs {
1845 let current = model::paragraph(&self.current, *id)
1846 .ok_or_else(|| invalid("A paragraph is missing after text edits"))?;
1847 if current.media != paragraph.media {
1848 self.emit(PageOp::Media {
1849 paragraph: *id,
1850 media: paragraph.media.clone(),
1851 })?;
1852 }
1853 }
1854 Ok(())
1855 }
1856
1857 fn paragraph_formatting(&mut self, new: &View<'_>) -> Result<(), Error> {
1858 for (id, paragraph) in &new.paragraphs {
1859 let Some(text) = paragraph.text() else {
1860 continue;
1861 };
1862 let stored = model::paragraph(&self.current, *id)
1863 .and_then(|p| p.text().cloned())
1864 .ok_or_else(|| invalid("A paragraph is missing after text edits"))?;
1865 let fields = paragraph_fields(&stored.text, &text.text)?;
1866 if fields == ParagraphFields::default() {
1867 continue;
1868 }
1869 if text.date_field.is_some() {
1870 return Err(invalid(
1871 "Generated title fields cannot be formatted as ordinary text",
1872 ));
1873 }
1874 self.emit(PageOp::Paragraph {
1875 paragraph: *id,
1876 alignment: fields.alignment,
1877 rtl: fields.rtl,
1878 space_before: fields.space_before,
1879 space_after: fields.space_after,
1880 line_spacing: fields.line_spacing,
1881 language: fields.language,
1882 })?;
1883 }
1884 Ok(())
1885 }
1886
1887 fn formatting(&mut self, old: &View<'_>, new: &View<'_>) -> Result<(), Error> {
1888 let stored_texts: BTreeSet<ExGuid> = old
1889 .paragraphs
1890 .values()
1891 .filter_map(|p| Some(p.text()?.id))
1892 .collect();
1893 for (id, paragraph) in &new.paragraphs {
1894 let Some(text) = paragraph.text() else {
1895 continue;
1896 };
1897 let stored = model::paragraph(&self.current, *id)
1898 .and_then(|p| p.text().cloned())
1899 .ok_or_else(|| invalid("A paragraph is missing after text edits"))?;
1900 if stored.text.text() != text.text.text() {
1901 return Err(invalid("Text edits did not converge on the model"));
1902 }
1903 let fresh = !stored_texts.contains(&text.id);
1904 let edits = format_edits(&stored.text, &text.text, fresh)?;
1905 // Math runs refuse formatting, so a style that restyled them is undone whole.
1906 let math = |at: u32| {
1907 stored
1908 .text
1909 .format_at(at + 1)
1910 .is_ok_and(|format| format.math == Some(true))
1911 };
1912 if edits.iter().any(|(range, ..)| range.clone().any(math)) {
1913 self.emit(PageOp::Equation {
1914 text: stored.id,
1915 math: text.text.clone(),
1916 })?;
1917 continue;
1918 }
1919 for (range, set, clear) in edits {
1920 self.emit(PageOp::Format {
1921 text: stored.id,
1922 range,
1923 set,
1924 clear,
1925 })?;
1926 }
1927 }
1928 Ok(())
1929 }
1930
1931 fn layout(&mut self, old: &View<'_>, new: &View<'_>) -> Result<(), Error> {
1932 for (id, paragraph) in &new.paragraphs {
1933 let current = model::paragraph(&self.current, *id)
1934 .ok_or_else(|| invalid("A paragraph is missing after text edits"))?;
1935 if current.collapsed != paragraph.collapsed {
1936 self.emit(PageOp::Outline {
1937 object: *id,
1938 edit: crate::OutlineEdit::Collapsed(paragraph.collapsed),
1939 })?;
1940 }
1941 }
1942 for (id, outline) in &new.outlines {
1943 if new.title_outlines.contains(id) || !old.outlines.contains_key(id) {
1944 continue;
1945 }
1946 let stored = model::outline(&self.current, *id)
1947 .ok_or_else(|| invalid("An outline is missing after text edits"))?;
1948 let (stored_position, stored_width) = (
1949 (stored.layout.x, stored.layout.y),
1950 (stored.layout.max_width, stored.layout.width_set_by_user),
1951 );
1952 if (outline.layout.x, outline.layout.y) != stored_position {
1953 let (Some(x), Some(y)) = (outline.layout.x, outline.layout.y) else {
1954 return Err(invalid("An outline position needs both coordinates"));
1955 };
1956 self.emit(PageOp::Outline {
1957 object: *id,
1958 edit: crate::OutlineEdit::Position { x, y },
1959 })?;
1960 }
1961 if (outline.layout.max_width, outline.layout.width_set_by_user) != stored_width {
1962 let Some(points) = outline.layout.max_width else {
1963 return Err(invalid("An outline width cannot be removed"));
1964 };
1965 self.emit(PageOp::Outline {
1966 object: *id,
1967 edit: crate::OutlineEdit::Width {
1968 points,
1969 user_set: outline.layout.width_set_by_user == Some(true),
1970 },
1971 })?;
1972 }
1973 }
1974 Ok(())
1975 }
1976}
1977
1978/// The outline or cell a paragraph of `view` lies in.
1979fn root(view: &View<'_>, paragraph: ExGuid) -> ExGuid {
1980 let mut at = paragraph;
1981 while let Some(parent) = view.container.get(&at) {
1982 at = *parent;
1983 }
1984 at
1985}