1//! `Section::apply`: each op checked against the page it names, then written through the
2//! typed writers onto the section's copy of that page.
3
4use super::{
5 Edit, Op, OpError, PageOp, SectionOp, TableEdit, TextProperty,
6 content::{self, AttachmentIds},
7 levels,
8 lower::{ParagraphFields, format_edits, paragraph_fields},
9 properties,
10 table::{self, Structure},
11};
12use crate::{
13 Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, Section,
14 TextAttribute, TreeEdit,
15 active::{ActivePage, Changes},
16 document::Kind,
17 page::{
18 Attachment, Image, Ink, PageObject, PageParagraph, Paragraph, ParagraphContent, TableCell,
19 TableColumn,
20 },
21};
22use std::{
23 collections::{BTreeMap, BTreeSet},
24 ops::Range,
25};
26
27/// `HasConflictPages` and `HasVersionPages` (MS-ONE 2.3.71, 2.3.84), true when present.
28const HAS_CONFLICT_PAGES: u32 = 0x88001d97;
29const HAS_VERSION_PAGES: u32 = 0x88003462;
30
31fn invalid(message: &'static str) -> Error {
32 Error { offset: 0, message }
33}
34
35impl<'a> Section<'a> {
36 /// Applies an edit whole or not at all, `author` naming who made it. Its modification
37 /// times are `edit.at`.
38 pub fn apply(&mut self, author: &str, edit: &Edit) -> Result<(), OpError> {
39 if author.contains('\0') {
40 return Err(OpError::Unsupported("Choose an author name without NUL"));
41 }
42 let undo = edit.ops.len() > 1 || edit.ops.iter().any(stores_more_than_once);
43 crate::create::at(edit.at, || {
44 self.atomically(undo, |section| {
45 for op in &edit.ops {
46 match op {
47 Op::Page { space, op } => Writer {
48 section: &mut *section,
49 space: *space,
50 author,
51 }
52 .apply(op)?,
53 Op::Section(op) => section.apply_section(author, op)?,
54 }
55 }
56 let spaces: BTreeSet<_> = edit
57 .ops
58 .iter()
59 .filter_map(|op| match op {
60 Op::Page { space, .. } => Some(*space),
61 Op::Section(_) => None,
62 })
63 .collect();
64 for space in spaces {
65 // A page the edit went on to delete has no cells left to check.
66 if section.active(space).is_err() {
67 continue;
68 }
69 Writer {
70 section: &mut *section,
71 space,
72 author,
73 }
74 .playlist()?;
75 let page = section.active(space)?;
76 if page.view.nodes.values().any(|node| {
77 matches!(node.kind, Kind::Cell { .. }) && node.children.is_empty()
78 }) {
79 return Err(OpError::Unsupported(
80 "A table cell keeps a paragraph; insert its replacement first",
81 )
82 .into());
83 }
84 }
85 Ok(())
86 })
87 })
88 .map_err(|error| self.classify(error))
89 }
90
91 /// Lowers `after` against the page `space` holds and applies the result as one edit;
92 /// returns the ops, which reach `after` where the writers can store it.
93 #[cfg(test)]
94 pub(crate) fn apply_page(
95 &mut self,
96 author: &str,
97 at: u64,
98 space: ExGuid,
99 after: &crate::page::Page,
100 ) -> Result<Vec<PageOp>, OpError> {
101 let before = self
102 .page(space)
103 .map_err(|error| self.classify(Failure::Rejected(error)))?;
104 let ops = super::lower_page(&before, after)
105 .map_err(|error| OpError::Unsupported(error.message))?;
106 let edit = Edit {
107 at,
108 ops: ops
109 .iter()
110 .map(|op| Op::Page {
111 space,
112 op: op.clone(),
113 })
114 .collect(),
115 };
116 self.apply(author, &edit)?;
117 Ok(ops)
118 }
119
120 fn classify(&self, failure: Failure) -> OpError {
121 match failure {
122 Failure::Refused(error) => error,
123 Failure::Rejected(error) if self.broken() => OpError::Failed(error),
124 Failure::Rejected(error) => OpError::Unsupported(error.message),
125 }
126 }
127
128 fn apply_section(&mut self, author: &str, op: &SectionOp) -> Result<(), Failure> {
129 match op {
130 SectionOp::Create(creation) => {
131 let (root, roots, objects) = creation.changes(self)?;
132 self.apply_changes(self.root(), &[], root)?;
133 self.create_space(creation.space(), roots, objects)?;
134 }
135 SectionOp::Import { creation, page } => {
136 self.apply_section(author, &SectionOp::Create(creation.clone()))?;
137 self.fill(author, creation.space(), page)?;
138 }
139 SectionOp::Conflict {
140 of,
141 creation,
142 page,
143 objects,
144 } => {
145 let level = self
146 .pages()?
147 .into_iter()
148 .find(|(space, ..)| space == of)
149 .map(|(.., level)| level)
150 .ok_or(OpError::TargetUnavailable(*of))?;
151 let space = creation.space();
152 let (roots, created, metadata) = creation.conflict(self, &page.title, level)?;
153 self.create_space(space, roots, created)?;
154 self.fill(author, space, page)?;
155 self.mark_conflict(space, creation, page, objects)?;
156 self.link_conflict(*of, space, metadata)?;
157 }
158 SectionOp::Pages(edits) => {
159 for (space, changes) in crate::pages::section_changes(self, edits, &[])? {
160 self.apply_changes(space, &[], changes)?;
161 }
162 }
163 SectionOp::Delete(pages) => {
164 let mut listed = Vec::new();
165 let conflicts = self.conflicts()?;
166 for space in pages {
167 let of = conflicts.iter().find_map(|(of, conflicts)| {
168 conflicts
169 .iter()
170 .any(|page| page.space == *space)
171 .then_some(*of)
172 });
173 match of {
174 Some(of) => self.unlink_conflict(of, *space)?,
175 None => listed.push(*space),
176 }
177 }
178 if !listed.is_empty() {
179 for (space, changes) in crate::pages::section_changes(self, &[], &listed)? {
180 self.apply_changes(space, &[], changes)?;
181 }
182 }
183 }
184 SectionOp::RestoreVersion {
185 page,
186 version,
187 guid,
188 } => {
189 self.restore(author, *page, *version, *guid)?;
190 self.flag_metadata(*page, HAS_VERSION_PAGES, true)?;
191 }
192 SectionOp::DeleteVersions { page, versions } => {
193 if self.unlist(*page, versions)? {
194 self.flag_metadata(*page, HAS_VERSION_PAGES, false)?;
195 }
196 }
197 SectionOp::Color(color) => {
198 let root = self.root();
199 let page = self.active(root)?;
200 let metadata = *page
201 .view
202 .roots
203 .get(&2)
204 .filter(|id| page.live.revision.objects.contains_key(id))
205 .ok_or_else(|| invalid("The section has no metadata to color"))?;
206 let mut object = crate::write::PropertyObject::from_object(
207 &page.live.revision.objects[&metadata],
208 )?;
209 object.set(&[(0x14001cbe, &color.unwrap_or(0xffff_ffff).to_le_bytes())])?;
210 self.apply_changes(root, &[], BTreeMap::from([(metadata, object)]))?;
211 }
212 }
213 Ok(())
214 }
215
216 /// Writes `page`'s content onto the page just created in `space`, whose title stays.
217 fn fill(
218 &mut self,
219 author: &str,
220 space: ExGuid,
221 page: &crate::page::Page,
222 ) -> Result<(), Failure> {
223 let before = self.page(space)?;
224 let mut after = page.clone();
225 after
226 .objects
227 .retain(|object| !matches!(object, PageObject::Title(_)));
228 for object in &mut after.objects {
229 // Stroke points are page coordinates; new ink takes its position from them.
230 if let PageObject::Ink(ink) = object {
231 ink.layout = Default::default();
232 }
233 }
234 for object in before.objects.iter().rev() {
235 if let PageObject::Title(_) = object {
236 after.objects.insert(0, object.clone());
237 }
238 }
239 after.title = before.title.clone();
240 after.identity = before.identity;
241 after.created = before.created;
242 after.margin_origin = before.margin_origin;
243 for op in super::lower_page(&before, &after)? {
244 Writer {
245 section: self,
246 space,
247 author,
248 }
249 .apply(&op)?;
250 }
251 Ok(())
252 }
253
254 /// Marks a filled conflict page as OneNote 2010 does: the page read-only, deletable and
255 /// a conflict page (MS-ONE 2.1.2), and each of `objects` a conflict object (2.1.1). The
256 /// created title stands for `page`'s, so objects of that title mark its counterparts.
257 fn mark_conflict(
258 &mut self,
259 space: ExGuid,
260 creation: &crate::PageCreation,
261 page: &crate::page::Page,
262 objects: &[ExGuid],
263 ) -> Result<(), Failure> {
264 let created = |n| ExGuid {
265 guid: creation.space().guid,
266 n,
267 };
268 let mut titles = BTreeMap::new();
269 for object in &page.objects {
270 let PageObject::Title(title) = object else {
271 continue;
272 };
273 titles.insert(title.id, created(13));
274 if let Some(outline) = title.outlines.first() {
275 titles.insert(outline.id, created(14));
276 if let Some(paragraph) = outline.paragraphs.first() {
277 titles.insert(paragraph.id, created(15));
278 if let Some(text) = paragraph.text() {
279 titles.insert(text.id, created(16));
280 }
281 }
282 }
283 }
284 let active = self.active(space)?;
285 let mut changes = BTreeMap::new();
286 let mut page_node = crate::write::PropertyObject::from_object(
287 &active.live.revision.objects[&creation.object()],
288 )?;
289 page_node.set(&[(0x88001cde, &[]), (0x88001d0c, &[]), (0x88001d7c, &[])])?;
290 changes.insert(creation.object(), page_node);
291 for id in objects {
292 let id = titles.get(id).copied().unwrap_or(*id);
293 if !active.live.is_reachable(id) {
294 return Err(OpError::TargetUnavailable(id).into());
295 }
296 let mut object =
297 crate::write::PropertyObject::from_object(&active.live.revision.objects[&id])?;
298 // A table is rendered as conflicting but is not selectable as such.
299 if object.jcid == 0x60022 {
300 object.set(&[(0x88001d96, &[])])?;
301 } else {
302 object.set(&[(0x88001ddb, &[]), (0x88001d96, &[])])?;
303 }
304 changes.insert(id, object);
305 }
306 self.apply_changes(space, &[], changes)?;
307 Ok(())
308 }
309
310 /// Lists conflict page `space` under page `of` as OneNote 2010 does: the page's manifest
311 /// references it and keeps a copy of its metadata, and the page's metadata and the
312 /// section's copy of it say the page has conflict pages (MS-ONE 2.2.34, 2.3.71).
313 fn link_conflict(
314 &mut self,
315 of: ExGuid,
316 space: ExGuid,
317 mut metadata: crate::write::PropertyObject,
318 ) -> Result<(), Failure> {
319 use crate::{pages::set_references, write::PropertyObject};
320 let page = self.active(of)?;
321 let manifest_id = page.view.roots[&1];
322 let manifest_node = &page.view.nodes[&manifest_id];
323 let mut spaces = manifest_node.spaces.clone();
324 spaces.push(space);
325 let mut copies = objects(manifest_node, 0x24003442);
326 let copy = crate::pages::metadata_id(space);
327 copies.push(copy);
328 metadata.reference(copy)?;
329 let mut manifest = PropertyObject::from_object(&page.live.revision.objects[&manifest_id])?;
330 set_references(&mut manifest, 0x2c001d63, &spaces)?;
331 set_references(&mut manifest, 0x24003442, &copies)?;
332 self.apply_changes(
333 of,
334 &[],
335 BTreeMap::from([(manifest_id, manifest), (copy, metadata)]),
336 )?;
337 self.flag_metadata(of, HAS_CONFLICT_PAGES, true)
338 }
339
340 /// Deletes conflict page `space` of page `of` as OneNote 2010 does: the manifest stops
341 /// referencing it (its metadata copy stays) and the last one's removal clears
342 /// `HasConflictPages`; the conflict page's own space is left as it is.
343 fn unlink_conflict(&mut self, of: ExGuid, space: ExGuid) -> Result<(), Failure> {
344 use crate::{pages::set_references, write::PropertyObject};
345 let page = self.active(of)?;
346 let manifest_id = page.view.roots[&1];
347 let spaces: Vec<ExGuid> = page.view.nodes[&manifest_id]
348 .spaces
349 .iter()
350 .copied()
351 .filter(|listed| *listed != space)
352 .collect();
353 let mut manifest = PropertyObject::from_object(&page.live.revision.objects[&manifest_id])?;
354 if spaces.is_empty() {
355 manifest.remove(&[0x2c001d63])?;
356 } else {
357 set_references(&mut manifest, 0x2c001d63, &spaces)?;
358 }
359 self.apply_changes(of, &[], BTreeMap::from([(manifest_id, manifest)]))?;
360 if spaces.is_empty() {
361 self.flag_metadata(of, HAS_CONFLICT_PAGES, false)?;
362 }
363 Ok(())
364 }
365
366 /// Sets or clears a Boolean `property` on page `of`'s metadata and on the section's copy
367 /// of it, where its series keeps one.
368 fn flag_metadata(&mut self, of: ExGuid, property: u32, value: bool) -> Result<(), Failure> {
369 use crate::write::PropertyObject;
370 let flag = |object: &crate::Object<'_>| -> Result<PropertyObject, Error> {
371 let mut object = PropertyObject::from_object(object)?;
372 if value {
373 object.set(&[(property, &[])])?;
374 } else {
375 object.remove(&[property])?;
376 }
377 Ok(object)
378 };
379 let page = self.active(of)?;
380 let stored_id = page.view.roots[&2];
381 let identity = crate::pages::metadata_guid(&page.view.nodes[&stored_id])?;
382 let stored = flag(&page.live.revision.objects[&stored_id])?;
383 self.apply_changes(of, &[], BTreeMap::from([(stored_id, stored)]))?;
384 let root = self.root();
385 let section = self.active(root)?;
386 let copy = section
387 .view
388 .nodes
389 .values()
390 .find(|node| node.spaces.contains(&of))
391 .map(|series| objects(series, 0x24003442))
392 .unwrap_or_default()
393 .into_iter()
394 .find(|id| {
395 section.view.nodes.get(id).is_some_and(|node| {
396 crate::pages::metadata_guid(node).is_ok_and(|guid| guid == identity)
397 })
398 });
399 if let Some(id) = copy {
400 let object = flag(&section.live.revision.objects[&id])?;
401 self.apply_changes(root, &[], BTreeMap::from([(id, object)]))?;
402 }
403 Ok(())
404 }
405}
406
407/// The objects an element's reference property names.
408fn objects(node: &crate::document::Element<'_>, property: u32) -> Vec<ExGuid> {
409 node.extra[0]
410 .iter()
411 .find_map(|field| match &field.value {
412 crate::document::FieldValue::Objects(ids) if field.id == property => Some(ids.clone()),
413 _ => None,
414 })
415 .unwrap_or_default()
416}
417
418/// Whether an op may store more than one revision of a page, so its failure part way needs
419/// the page as it was.
420fn stores_more_than_once(op: &Op) -> bool {
421 match op {
422 Op::Section(_) => true,
423 Op::Page { op, .. } => matches!(
424 op,
425 PageOp::Link { .. }
426 | PageOp::Insert { .. }
427 | PageOp::Move { .. }
428 | PageOp::Add { .. }
429 | PageOp::Table {
430 edit: TableEdit::Rows { .. } | TableEdit::Column { .. },
431 ..
432 }
433 ),
434 }
435}
436
437/// Why an op was not applied: a precondition it names, or what a writer refused.
438pub(crate) enum Failure {
439 Refused(OpError),
440 Rejected(Error),
441}
442
443impl From<Error> for Failure {
444 fn from(error: Error) -> Self {
445 Self::Rejected(error)
446 }
447}
448
449impl From<OpError> for Failure {
450 fn from(error: OpError) -> Self {
451 Self::Refused(error)
452 }
453}
454
455/// Writes ops onto one page of a section.
456struct Writer<'s, 'a> {
457 section: &'s mut Section<'a>,
458 space: ExGuid,
459 author: &'s str,
460}
461
462impl<'a> Writer<'_, 'a> {
463 fn page(&mut self) -> Result<&ActivePage<'a>, Error> {
464 self.section.active(self.space)
465 }
466
467 fn write(
468 &mut self,
469 changes: impl FnOnce(&ActivePage<'a>) -> Result<Changes, Error>,
470 ) -> Result<(), Failure> {
471 self.write_with(&[], changes)
472 }
473
474 fn write_with(
475 &mut self,
476 payloads: &[([u8; 16], &[u8])],
477 changes: impl FnOnce(&ActivePage<'a>) -> Result<Changes, Error>,
478 ) -> Result<(), Failure> {
479 let changes = changes(self.page()?)?;
480 self.section.apply_changes(self.space, payloads, changes)?;
481 Ok(())
482 }
483
484 /// Lists the page's recordings in its AudioRecordingGuids as OneNote keeps them: in
485 /// the order they arrived, without those since removed, absent when there are none.
486 fn playlist(&mut self) -> Result<(), Failure> {
487 let page = self.page()?;
488 let [id] = page.pages[..] else {
489 return Ok(());
490 };
491 let Kind::Page { recordings, .. } = &page.view.nodes[&id].kind else {
492 return Ok(());
493 };
494 let present: Vec<[u8; 16]> = page
495 .view
496 .nodes
497 .values()
498 .filter_map(|node| match node.kind {
499 Kind::Attachment { recording_id, .. } => recording_id,
500 _ => None,
501 })
502 .collect();
503 let mut listed: Vec<[u8; 16]> = recordings
504 .iter()
505 .filter(|recording| present.contains(recording))
506 .copied()
507 .collect();
508 for recording in present {
509 if !listed.contains(&recording) {
510 listed.push(recording);
511 }
512 }
513 if listed == *recordings {
514 return Ok(());
515 }
516 self.write(|page| content::playlist_changes(page, id, &listed))
517 }
518
519 /// Requires `id` reachable on the page.
520 /// Clears run properties over run ranges, fields and equations included.
521 fn clear_runs(&mut self, clears: &properties::Clears) -> Result<(), Failure> {
522 for (text, range, clear) in clears {
523 self.write(|page| {
524 crate::formatting::cleared_changes(page, *text, range.clone(), clear)
525 })?;
526 }
527 Ok(())
528 }
529
530 fn target(&mut self, id: ExGuid) -> Result<(), Failure> {
531 let page = self.page()?;
532 if page.live.is_reachable(id) && page.view.nodes.contains_key(&id) {
533 Ok(())
534 } else {
535 Err(OpError::TargetUnavailable(id).into())
536 }
537 }
538
539 /// Requires `id` free for a new object: not reachable on the page.
540 fn free(&mut self, id: ExGuid) -> Result<(), Failure> {
541 if id.guid == [0; 16] || self.page()?.live.is_reachable(id) {
542 Err(OpError::DuplicateIdentity(id).into())
543 } else {
544 Ok(())
545 }
546 }
547
548 /// The only parent of `id` on the page.
549 fn parent(&mut self, id: ExGuid) -> Result<ExGuid, Failure> {
550 match self.page()?.parents.get(&id).map(Vec::as_slice) {
551 Some([parent]) => Ok(*parent),
552 _ => Err(OpError::StructureChanged("Page content must have one parent").into()),
553 }
554 }
555
556 /// The container `id` lies in, outline groups passed over.
557 fn container(&mut self, id: ExGuid) -> Result<ExGuid, Failure> {
558 let mut at = self.parent(id)?;
559 while matches!(self.page()?.view.nodes[&at].kind, Kind::OutlineGroup) {
560 at = self.parent(at)?;
561 }
562 Ok(at)
563 }
564
565 /// The text object a paragraph holds.
566 fn text_of(&mut self, paragraph: ExGuid) -> Result<ExGuid, Failure> {
567 let page = self.page()?;
568 let node = &page.view.nodes[&paragraph];
569 match node.content.as_slice() {
570 [text] if matches!(page.view.nodes[text].kind, Kind::RichText { .. }) => Ok(*text),
571 _ => Err(OpError::Unsupported("Select a paragraph holding text").into()),
572 }
573 }
574
575 /// The absolute outline level of paragraph `id`; zero for an outline or cell.
576 fn level(&mut self, id: ExGuid) -> Result<u32, Failure> {
577 if !matches!(self.page()?.view.nodes[&id].kind, Kind::Paragraph { .. }) {
578 return Ok(0);
579 }
580 let container = self.container(id)?;
581 let depth = self.depth(container, id)?;
582 Ok(self.level(container)? + depth)
583 }
584
585 fn depth(&mut self, container: ExGuid, id: ExGuid) -> Result<u32, Failure> {
586 levels::stored_depths(&self.page()?.view, container)
587 .into_iter()
588 .find(|(child, _)| *child == id)
589 .map(|(_, depth)| depth)
590 .ok_or_else(|| OpError::StructureChanged("A paragraph left its container").into())
591 }
592
593 /// Stores `container`'s children at the depths they have, `targets` overriding some.
594 fn regroup(&mut self, container: ExGuid, targets: &[(ExGuid, u32)]) -> Result<(), Failure> {
595 let page = self.page()?;
596 let current = levels::stored_depths(&page.view, container);
597 let is_cell = matches!(page.view.nodes[&container].kind, Kind::Cell { .. });
598 let (children, depths): (Vec<ExGuid>, Vec<u32>) = current
599 .iter()
600 .map(|(child, depth)| {
601 let target = targets
602 .iter()
603 .find(|(id, _)| id == child)
604 .map_or(*depth, |(_, depth)| *depth);
605 (*child, target)
606 })
607 .unzip();
608 if current
609 .iter()
610 .map(|(_, depth)| *depth)
611 .eq(depths.iter().copied())
612 {
613 return Ok(());
614 }
615 let (level, runs) = levels::runs(&children, &depths, is_cell)?;
616 self.write(|page| levels::regroup_changes(page, container, level, &runs))
617 }
618
619 fn apply(&mut self, op: &PageOp) -> Result<(), Failure> {
620 match op {
621 PageOp::Text { text, range, with } => {
622 self.target(*text)?;
623 if with.contains(['\0', '\n', '\u{fffc}']) {
624 return Err(OpError::Unsupported("Use ordinary paragraph text").into());
625 }
626 self.write(|page| crate::edit::text_changes(page, *text, range.clone(), with))
627 }
628 PageOp::Format {
629 text,
630 range,
631 set,
632 clear,
633 } => {
634 self.target(*text)?;
635 self.write(|page| {
636 crate::formatting::format_changes(page, *text, range.clone(), set, clear)
637 })
638 }
639 PageOp::Link {
640 text,
641 range,
642 target,
643 } => {
644 self.target(*text)?;
645 let page = self.page()?;
646 let current = crate::page::text_of(&page.view, &page.parents, *text)?;
647 for op in link_ops(&current, *text, range.clone(), target.as_deref())? {
648 self.apply(&op)?;
649 }
650 Ok(())
651 }
652 PageOp::Equation { text, math } => {
653 self.target(*text)?;
654 self.write(|page| content::equation_changes(page, *text, math))
655 }
656 PageOp::Date { created, fields } => {
657 for (text, shown) in fields {
658 self.target(*text)?;
659 if shown.contains(['\0', '\n', '\r', '\u{fffc}']) {
660 return Err(OpError::Unsupported("Use a date as one line of text").into());
661 }
662 self.write(|page| crate::edit::field_changes(page, *text, shown))?;
663 }
664 self.write(|page| {
665 let metadata = page
666 .view
667 .roots
668 .get(&2)
669 .filter(|id| matches!(page.view.nodes[id].kind, Kind::Metadata { .. }))
670 .ok_or_else(|| invalid("The page has no metadata to date"))?;
671 let mut object = crate::write::PropertyObject::from_object(
672 &page.live.revision.objects[metadata],
673 )?;
674 object.set(&[(0x18001c65, &created.to_le_bytes())])?;
675 Ok(BTreeMap::from([(*metadata, object)]))
676 })
677 }
678 PageOp::Color(color) => self.write(|page| {
679 let [node] = crate::active::manifest_pages(&page.view)[..] else {
680 return Err(invalid("The page has no page node to color"));
681 };
682 let mut object =
683 crate::write::PropertyObject::from_object(&page.live.revision.objects[&node])?;
684 // OneNote's "No color" removes the property.
685 match color {
686 Some(color) => {
687 object.set(&[(crate::page::PAGE_COLOR, &color.to_le_bytes())])?
688 }
689 None => object.remove(&[crate::page::PAGE_COLOR])?,
690 }
691 Ok(BTreeMap::from([(node, object)]))
692 }),
693 PageOp::RuleLines(lines) => self.write(|page| {
694 let [node] = crate::active::manifest_pages(&page.view)[..] else {
695 return Err(invalid("The page has no page node to rule"));
696 };
697 let mut object =
698 crate::write::PropertyObject::from_object(&page.live.revision.objects[&node])?;
699 // OneNote's "None" removes the properties.
700 match lines {
701 Some(lines) => {
702 let properties = lines.properties();
703 let values: Vec<_> = properties
704 .iter()
705 .map(|(id, value)| (*id, &value[..]))
706 .collect();
707 object.set(&values)?
708 }
709 None => object.remove(&crate::page::RULE_LINES)?,
710 }
711 Ok(BTreeMap::from([(node, object)]))
712 }),
713 PageOp::Insert {
714 container,
715 before,
716 paragraphs,
717 } => {
718 self.target(*container)?;
719 self.insert(*container, *before, paragraphs)
720 }
721 PageOp::Split {
722 text,
723 at,
724 paragraph,
725 right,
726 lists,
727 } => {
728 self.target(*text)?;
729 for id in [*paragraph, *right].iter().chain(lists) {
730 self.free(*id)?;
731 }
732 let split = ParagraphSplit::new(*text, *at, self.author)?;
733 self.write(|page| split.changes_as(page, *paragraph, *right, lists))
734 }
735 PageOp::Join { left, right } => {
736 self.target(*left)?;
737 self.target(*right)?;
738 let join = ParagraphJoin::new(*left, *right, self.author)?;
739 self.write(|page| join.changes(page))
740 }
741 PageOp::Move {
742 object,
743 parent,
744 before,
745 } => {
746 self.target(*object)?;
747 self.move_to(*object, *parent, *before)
748 }
749 PageOp::Delete { object } => {
750 self.target(*object)?;
751 let edit = TreeEdit::delete(*object, self.author)?;
752 self.write(|page| edit.changes(page))
753 }
754 PageOp::Level { paragraph, level } => {
755 self.target(*paragraph)?;
756 let container = self.container(*paragraph)?;
757 let base = self.level(container)?;
758 let depth = level.checked_sub(base).filter(|depth| *depth > 0).ok_or(
759 OpError::Unsupported("A paragraph lies deeper than its parent"),
760 )?;
761 self.regroup(container, &[(*paragraph, depth)])
762 }
763 PageOp::Outline { object, edit } => {
764 self.target(*object)?;
765 self.write(|page| edit.changes(page, *object))
766 }
767 PageOp::Paragraph {
768 paragraph,
769 alignment,
770 rtl,
771 space_before,
772 space_after,
773 line_spacing,
774 language,
775 } => {
776 self.target(*paragraph)?;
777 let text = self.text_of(*paragraph)?;
778 let values = ParagraphFields {
779 alignment: *alignment,
780 rtl: *rtl,
781 space_before: *space_before,
782 space_after: *space_after,
783 line_spacing: *line_spacing,
784 language: *language,
785 }
786 .values()?;
787 if values.is_empty() {
788 return Ok(());
789 }
790 self.write(|page| properties::paragraph_format_changes(page, text, &values))
791 }
792 PageOp::Style {
793 paragraph,
794 style,
795 definition,
796 } => {
797 self.target(*paragraph)?;
798 let text = self.text_of(*paragraph)?;
799 // What the old style gave, the new one gives instead.
800 let page = self.page()?;
801 let spacing = match page.view.nodes[&text].kind {
802 Kind::RichText {
803 paragraph_style: Some(old),
804 ..
805 } if old != *style => {
806 let (clears, spacing) = properties::restyle_plan(page, old, Some(text))?;
807 self.clear_runs(&clears)?;
808 spacing
809 }
810 _ => Vec::new(),
811 };
812 self.write(|page| {
813 let mut changes =
814 properties::style_changes(page, text, *style, Some(definition))?;
815 if let Some((_, ids)) = spacing.first() {
816 changes
817 .get_mut(&text)
818 .expect("the text is restyled")
819 .remove(ids)?;
820 }
821 Ok(changes)
822 })
823 }
824 PageOp::Unstyle { paragraph } => {
825 self.target(*paragraph)?;
826 let text = self.text_of(*paragraph)?;
827 let page = self.page()?;
828 let Kind::RichText {
829 paragraph_style: Some(old),
830 ..
831 } = page.view.nodes[&text].kind
832 else {
833 return Ok(());
834 };
835 let (clears, spacing) = properties::restyle_plan(page, old, Some(text))?;
836 self.clear_runs(&clears)?;
837 self.write(|page| {
838 let mut target = crate::write::PropertyObject::from_object(
839 &page.live.revision.objects[&text],
840 )?;
841 let mut removed = vec![0x2000342c];
842 removed.extend(spacing.into_iter().flat_map(|(_, ids)| ids));
843 target.remove(&removed)?;
844 Ok(std::collections::BTreeMap::from([(text, target)]))
845 })
846 }
847 PageOp::Restyle {
848 style,
849 into,
850 definition,
851 } => {
852 self.target(*style)?;
853 let page = self.page()?;
854 let (Kind::Style { name, .. }, Kind::Style { name: renamed, .. }) =
855 (&page.view.nodes[style].kind, &definition.kind)
856 else {
857 return Err(OpError::Unsupported("Restyle a paragraph style").into());
858 };
859 if name.is_none() || name != renamed {
860 return Err(
861 OpError::Unsupported("A restyled paragraph style keeps its name").into(),
862 );
863 }
864 if page.live.is_reachable(*into) {
865 let ObjectData::Properties(stored) = page.live.revision.objects[into].data
866 else {
867 return Err(OpError::DuplicateIdentity(*into).into());
868 };
869 let given =
870 crate::create::properties(&properties::style_object_values(definition)?)?;
871 if stored != given.as_slice() {
872 return Err(OpError::DuplicateIdentity(*into).into());
873 }
874 } else if into.guid == [0; 16] {
875 return Err(OpError::DuplicateIdentity(*into).into());
876 }
877 let (clears, spacing) = properties::restyle_plan(page, *style, None)?;
878 self.clear_runs(&clears)?;
879 self.write(|page| {
880 properties::restyle_changes(page, *style, *into, definition, &spacing)
881 })
882 }
883 PageOp::Media { paragraph, media } => {
884 self.target(*paragraph)?;
885 let page = self.page()?;
886 let content = match page.view.nodes[paragraph].content.as_slice() {
887 [content]
888 if matches!(
889 page.view.nodes[content].kind,
890 Kind::RichText { .. } | Kind::Attachment { .. }
891 ) =>
892 {
893 *content
894 }
895 _ => {
896 return Err(OpError::Unsupported(
897 "Link a paragraph of text or a file to a recording",
898 )
899 .into());
900 }
901 };
902 if media.recordings.is_empty() != media.time_ms.is_none() {
903 return Err(OpError::Unsupported(
904 "A recording link names recordings and a moment in them",
905 )
906 .into());
907 }
908 self.write(|page| properties::media_changes(page, content, media))
909 }
910 PageOp::List { paragraph, lists } => {
911 self.target(*paragraph)?;
912 let page = self.page()?;
913 for (id, _) in lists {
914 if page
915 .parents
916 .get(id)
917 .is_some_and(|owners| owners.iter().any(|owner| owner != paragraph))
918 {
919 return Err(
920 OpError::Unsupported("A list node belongs to one paragraph").into()
921 );
922 }
923 }
924 let nodes = lists
925 .iter()
926 .map(|(id, definition)| Ok((*id, properties::list_values(definition)?)))
927 .collect::<Result<Vec<_>, Error>>()?;
928 self.write(|page| properties::list_changes(page, *paragraph, &nodes))
929 }
930 PageOp::Tags {
931 target,
932 tags,
933 definitions,
934 } => {
935 self.target(*target)?;
936 let entries: Vec<_> = tags
937 .iter()
938 .map(|tag| {
939 let model = definitions
940 .iter()
941 .find(|(id, _)| Some(*id) == tag.definition)
942 .map(|(_, definition)| definition);
943 (tag, model)
944 })
945 .collect();
946 self.write(|page| properties::tag_changes(page, *target, &entries))
947 }
948 PageOp::Add { object, before } => self.add(object, *before),
949 PageOp::Picture {
950 picture,
951 layout,
952 alt,
953 } => {
954 self.target(*picture)?;
955 let page = self.page()?;
956 let node = &page.view.nodes[picture];
957 if !matches!(node.kind, Kind::Image { .. }) {
958 return Err(OpError::Unsupported("Select a picture").into());
959 }
960 let stored = Image::read(&page.view, *picture, node)?;
961 self.write(|page| {
962 content::picture_edit_changes(
963 page,
964 *picture,
965 (&stored.layout, &stored.alt),
966 layout,
967 alt,
968 )
969 })
970 }
971 PageOp::Attachment {
972 attachment,
973 filename,
974 source_path,
975 size,
976 } => {
977 self.target(*attachment)?;
978 let page = self.page()?;
979 let node = &page.view.nodes[attachment];
980 if !matches!(node.kind, Kind::Attachment { .. }) {
981 return Err(OpError::Unsupported("Select an attachment").into());
982 }
983 let stored = Attachment::read(&page.view, *attachment, node)?;
984 let edited = Attachment {
985 filename: filename.clone(),
986 source_path: source_path.clone(),
987 size: *size,
988 ..stored.clone()
989 };
990 self.write(|page| content::attachment_edit_changes(page, &stored, &edited))
991 }
992 PageOp::Strokes { ink, add, remove } => {
993 self.target(*ink)?;
994 let page = self.page()?;
995 let node = &page.view.nodes[ink];
996 if !matches!(node.kind, Kind::Ink { .. }) {
997 return Err(OpError::Unsupported("Select ink").into());
998 }
999 let stored = Ink::read(&page.view, *ink, node)?;
1000 if !stored.groups.is_empty() {
1001 return Err(OpError::Unsupported("Grouped ink keeps its strokes").into());
1002 }
1003 for id in remove {
1004 if !stored.strokes.iter().any(|stroke| stroke.id == *id) {
1005 return Err(OpError::TargetUnavailable(*id).into());
1006 }
1007 }
1008 for stroke in add {
1009 self.free(stroke.id)?;
1010 }
1011 let kept: Vec<ExGuid> = stored
1012 .strokes
1013 .iter()
1014 .map(|stroke| stroke.id)
1015 .filter(|id| !remove.contains(id))
1016 .collect();
1017 let added: Vec<_> = add.iter().map(|stroke| (stroke.id, stroke)).collect();
1018 self.write(|page| content::strokes_changes(page, *ink, &kept, &added))
1019 }
1020 PageOp::Table { table, edit } => {
1021 self.target(*table)?;
1022 self.table(*table, edit)
1023 }
1024 }
1025 }
1026
1027 /// Moves `object` before `before` in `parent`, or among the page's children.
1028 fn move_to(
1029 &mut self,
1030 object: ExGuid,
1031 parent: Option<ExGuid>,
1032 before: Option<ExGuid>,
1033 ) -> Result<(), Failure> {
1034 let Some(parent) = parent else {
1035 let [page] = self.page()?.pages[..] else {
1036 return Err(OpError::Unsupported("Choose a page space holding one page").into());
1037 };
1038 if let Some(before) = before
1039 && self.parent(before)? != page
1040 {
1041 return Err(OpError::StructureChanged("The anchor is not on the page").into());
1042 }
1043 let edit = TreeEdit::move_to(object, page, before, self.author)?;
1044 return self.write(|page| edit.changes(page));
1045 };
1046 self.target(parent)?;
1047 let mut at = parent;
1048 loop {
1049 if at == object {
1050 return Err(
1051 OpError::StructureChanged("A subtree cannot move inside itself").into(),
1052 );
1053 }
1054 match self.page()?.parents.get(&at).map(Vec::as_slice) {
1055 Some([up]) => at = *up,
1056 _ => break,
1057 }
1058 }
1059 let holder = match before {
1060 Some(before) => {
1061 self.target(before)?;
1062 if self.container(before)? != parent {
1063 return Err(OpError::StructureChanged(
1064 "The anchor is no child of the container",
1065 )
1066 .into());
1067 }
1068 self.parent(before)?
1069 }
1070 None => parent,
1071 };
1072 let level = self.level(object)?;
1073 let edit = TreeEdit::move_to(object, holder, before, self.author)?;
1074 self.write(|page| edit.changes(page))?;
1075 if matches!(self.page()?.view.nodes[&parent].kind, Kind::Cell { .. }) {
1076 return Ok(());
1077 }
1078 let base = self.level(parent)?;
1079 let depth = if level > base { level - base } else { 1 };
1080 self.regroup(parent, &[(object, depth)])
1081 }
1082
1083 /// Inserts paragraphs into `container` before `before` as `PageOp::Insert` does.
1084 fn insert(
1085 &mut self,
1086 container: ExGuid,
1087 before: Option<ExGuid>,
1088 paragraphs: &[PageParagraph],
1089 ) -> Result<(), Failure> {
1090 if paragraphs.is_empty() {
1091 return Err(OpError::Unsupported("Insert at least one paragraph").into());
1092 }
1093 let holder = match before {
1094 Some(before) => {
1095 self.target(before)?;
1096 if self.container(before)? != container {
1097 return Err(OpError::StructureChanged(
1098 "The anchor is no child of the container",
1099 )
1100 .into());
1101 }
1102 self.parent(before)?
1103 }
1104 None => container,
1105 };
1106 let mut seen = BTreeSet::new();
1107 for paragraph in paragraphs {
1108 bare(paragraph)?;
1109 for id in super::model::identities(std::slice::from_ref(paragraph)) {
1110 if !seen.insert(id) {
1111 return Err(OpError::DuplicateIdentity(id).into());
1112 }
1113 self.free(id)?;
1114 }
1115 if let Some(parent) = paragraph.parent
1116 && parent != container
1117 && !paragraphs.iter().any(|p| p.id == parent)
1118 {
1119 return Err(OpError::StructureChanged(
1120 "An inserted paragraph's parent precedes it among them",
1121 )
1122 .into());
1123 }
1124 }
1125 let base = self.level(container)?;
1126 let mut targets: BTreeMap<ExGuid, Vec<(ExGuid, u32)>> = BTreeMap::new();
1127 for paragraph in paragraphs {
1128 let (parent, anchor, holder) = match paragraph.parent.filter(|p| *p != container) {
1129 Some(parent) => (parent, None, parent),
1130 None => (container, before, holder),
1131 };
1132 let parent_level = if parent == container {
1133 base
1134 } else {
1135 paragraphs.iter().find(|p| p.id == parent).unwrap().level
1136 };
1137 let depth = paragraph
1138 .level
1139 .checked_sub(parent_level)
1140 .filter(|depth| *depth > 0)
1141 .ok_or(OpError::Unsupported(
1142 "A paragraph lies deeper than its parent",
1143 ))?;
1144 targets
1145 .entry(parent)
1146 .or_default()
1147 .push((paragraph.id, depth));
1148 self.paragraph(holder, anchor, paragraph, None)?;
1149 }
1150 for (parent, targets) in targets {
1151 if !matches!(self.page()?.view.nodes[&parent].kind, Kind::Cell { .. }) {
1152 self.regroup(parent, &targets)?;
1153 }
1154 }
1155 Ok(())
1156 }
1157
1158 /// Creates one paragraph with its content in `holder` before `anchor`; with `outline`,
1159 /// as the first paragraph of that new outline at its position.
1160 fn paragraph(
1161 &mut self,
1162 holder: ExGuid,
1163 anchor: Option<ExGuid>,
1164 paragraph: &PageParagraph,
1165 outline: Option<(ExGuid, f32, f32)>,
1166 ) -> Result<(), Failure> {
1167 let (text, text_id) = match &paragraph.content {
1168 ParagraphContent::Text(text) => (text.text.text(), text.id),
1169 _ => (
1170 "",
1171 ExGuid {
1172 guid: crate::write::fresh_guid()?,
1173 n: 1,
1174 },
1175 ),
1176 };
1177 let equation = paragraph
1178 .text()
1179 .is_some_and(|text| crate::page::Math::is_equation(&text.text));
1180 let insertion = match outline {
1181 Some((_, x, y)) => {
1182 let page = self.page()?.pages.first().copied().unwrap_or_default();
1183 Insertion::outline(page, x, y, text, self.author)?
1184 }
1185 None => Insertion::paragraph(holder, anchor, text, self.author)?,
1186 };
1187 let object = outline.map_or(paragraph.id, |(id, _, _)| id);
1188 self.write(|page| insertion.changes_as(page, object, paragraph.id, text_id))?;
1189 match &paragraph.content {
1190 ParagraphContent::Text(target) => {
1191 if equation {
1192 return self
1193 .write(|page| content::equation_changes(page, text_id, &target.text));
1194 }
1195 let page = self.page()?;
1196 let stored = crate::page::text_of(&page.view, &page.parents, text_id)?;
1197 let values = paragraph_fields(&stored, &target.text)?.values()?;
1198 if !values.is_empty() {
1199 self.write(|page| {
1200 properties::paragraph_format_changes(page, text_id, &values)
1201 })?;
1202 }
1203 let page = self.page()?;
1204 let stored = crate::page::text_of(&page.view, &page.parents, text_id)?;
1205 for (range, set, clear) in format_edits(&stored, &target.text, true)? {
1206 self.write(|page| {
1207 crate::formatting::format_changes(page, text_id, range, &set, &clear)
1208 })?;
1209 }
1210 Ok(())
1211 }
1212 ParagraphContent::Table(table) => {
1213 super::table::validate_table(table)?;
1214 let structure = Structure {
1215 rows: table
1216 .rows
1217 .iter()
1218 .map(|row| (row.id, row.cells.iter().map(|cell| cell.id).collect()))
1219 .collect(),
1220 new_rows: table.rows.iter().map(|row| row.id).collect(),
1221 new_cells: table
1222 .rows
1223 .iter()
1224 .flat_map(|row| &row.cells)
1225 .map(|cell| {
1226 (
1227 cell.id,
1228 (table::cell_indents(&cell.indents, None), cell.shading),
1229 )
1230 })
1231 .collect(),
1232 columns: &table.columns,
1233 borders: table.borders,
1234 };
1235 self.write(|page| {
1236 table::table_changes(page, table.id, Some(paragraph.id), &structure)
1237 })?;
1238 for cell in table.rows.iter().flat_map(|row| &row.cells) {
1239 self.cell(cell)?;
1240 }
1241 Ok(())
1242 }
1243 ParagraphContent::Image(image) => {
1244 if image.layout.x.is_some() || image.layout.y.is_some() {
1245 return Err(OpError::Unsupported(
1246 "A paragraph picture has no position of its own",
1247 )
1248 .into());
1249 }
1250 self.picture(image, Some(paragraph.id))
1251 }
1252 ParagraphContent::Attachment(attachment) => {
1253 self.attachment(attachment, Some(paragraph.id))
1254 }
1255 ParagraphContent::Ink(ink) => self.ink(ink, Some(paragraph.id)),
1256 ParagraphContent::Unsupported(_) => {
1257 Err(OpError::Unsupported("Unsupported content cannot be inserted").into())
1258 }
1259 }
1260 }
1261
1262 /// Fills a new cell with its paragraphs.
1263 fn cell(&mut self, cell: &TableCell) -> Result<(), Failure> {
1264 if cell.paragraphs.is_empty() {
1265 return Err(OpError::Unsupported("A new table cell needs a paragraph").into());
1266 }
1267 self.insert(cell.id, None, &cell.paragraphs)
1268 }
1269
1270 fn picture(&mut self, image: &Image, holder: Option<ExGuid>) -> Result<(), Failure> {
1271 let Some(bytes) = &image.bytes else {
1272 return Err(OpError::Unsupported("A new picture needs its payload").into());
1273 };
1274 if !image.tags.is_empty() {
1275 return Err(OpError::Unsupported("Set a picture's tags with their own op").into());
1276 }
1277 let (file, payload) = (fresh()?, crate::write::fresh_guid()?);
1278 let bytes: &[u8] = bytes;
1279 let mut payloads = vec![(payload, bytes)];
1280 let shown = match &image.display {
1281 Some(raster) => {
1282 let shown = (fresh()?, crate::write::fresh_guid()?);
1283 payloads.push((shown.1, raster));
1284 Some(shown)
1285 }
1286 None => None,
1287 };
1288 self.write_with(&payloads, |page| {
1289 content::picture_changes(page, image, (image.id, file, payload), shown, holder)
1290 })
1291 }
1292
1293 fn attachment(
1294 &mut self,
1295 attachment: &Attachment,
1296 holder: Option<ExGuid>,
1297 ) -> Result<(), Failure> {
1298 let Some(bytes) = &attachment.bytes else {
1299 return Err(OpError::Unsupported("A new attachment needs its payload").into());
1300 };
1301 if !attachment.tags.is_empty() {
1302 return Err(OpError::Unsupported("Set a file's tags with their own op").into());
1303 }
1304 let ids = AttachmentIds {
1305 object: attachment.id,
1306 file: fresh()?,
1307 payload: crate::write::fresh_guid()?,
1308 preview: match &attachment.preview {
1309 Some(_) => Some((crate::write::fresh_guid()?, fresh()?)),
1310 None => None,
1311 },
1312 };
1313 let mut payloads: Vec<([u8; 16], &[u8])> = vec![(ids.payload, bytes)];
1314 if let (Some((payload, _)), Some(icon)) = (ids.preview, &attachment.preview) {
1315 payloads.push((payload, icon));
1316 }
1317 self.write_with(&payloads, |page| {
1318 content::attachment_changes(page, attachment, &ids, holder)
1319 })
1320 }
1321
1322 fn ink(&mut self, ink: &Ink, holder: Option<ExGuid>) -> Result<(), Failure> {
1323 for stroke in &ink.strokes {
1324 self.free(stroke.id)?;
1325 }
1326 let data = fresh()?;
1327 let strokes: Vec<_> = ink
1328 .strokes
1329 .iter()
1330 .map(|stroke| (stroke.id, stroke))
1331 .collect();
1332 self.write(|page| content::ink_changes(page, ink, ink.id, data, &strokes, holder))?;
1333 // A drawing moved before, as undoing its deletion brings it back, keeps its offset.
1334 match (ink.layout.x, ink.layout.y) {
1335 (None, None) => Ok(()),
1336 (Some(x), Some(y)) => {
1337 let edit = OutlineEdit::Position { x, y };
1338 self.write(|page| edit.changes(page, ink.id))
1339 }
1340 _ => Err(OpError::Unsupported("An ink position needs both coordinates").into()),
1341 }
1342 }
1343
1344 /// Adds a page object on top, then moves it before `before`.
1345 fn add(&mut self, object: &PageObject, before: Option<ExGuid>) -> Result<(), Failure> {
1346 self.free(object.id())?;
1347 match object {
1348 PageObject::Outline(outline) => {
1349 let (Some(x), Some(y)) = (outline.layout.x, outline.layout.y) else {
1350 return Err(OpError::Unsupported("A new outline needs a position").into());
1351 };
1352 let [first, rest @ ..] = outline.paragraphs.as_slice() else {
1353 return Err(OpError::Unsupported("A new outline needs a paragraph").into());
1354 };
1355 for paragraph in &outline.paragraphs {
1356 bare(paragraph)?;
1357 for id in super::model::identities(std::slice::from_ref(paragraph)) {
1358 self.free(id)?;
1359 }
1360 }
1361 if first.parent.is_some() {
1362 return Err(
1363 OpError::Unsupported("An outline's first paragraph has no parent").into(),
1364 );
1365 }
1366 self.paragraph(outline.id, None, first, Some((outline.id, x, y)))?;
1367 // Later paragraphs follow the first, their levels as the outline gives them; a
1368 // deeper first paragraph goes in a group, as OneNote stores a tabbed one.
1369 if !rest.is_empty() || first.level != 1 {
1370 let mut targets = vec![(first.id, first.level)];
1371 for paragraph in rest {
1372 let parent = paragraph.parent.unwrap_or(outline.id);
1373 let parent_level = rest
1374 .iter()
1375 .chain([first])
1376 .find(|p| p.id == parent)
1377 .map_or(0, |p| p.level);
1378 let depth = paragraph
1379 .level
1380 .checked_sub(parent_level)
1381 .filter(|depth| *depth > 0)
1382 .ok_or(OpError::Unsupported(
1383 "A paragraph lies deeper than its parent",
1384 ))?;
1385 self.paragraph(parent, None, paragraph, None)?;
1386 if parent == outline.id {
1387 targets.push((paragraph.id, depth));
1388 } else {
1389 self.regroup(parent, &[(paragraph.id, depth)])?;
1390 }
1391 }
1392 self.regroup(outline.id, &targets)?;
1393 }
1394 // A new outline is 468 points wide unless it says otherwise.
1395 if let Some(points) = outline.layout.max_width
1396 && (points, outline.layout.width_set_by_user) != (468.0, None)
1397 {
1398 let edit = OutlineEdit::Width {
1399 points,
1400 user_set: outline.layout.width_set_by_user == Some(true),
1401 };
1402 self.write(|page| edit.changes(page, outline.id))?;
1403 }
1404 if !outline.indents.is_empty() && outline.indents != content::NATIVE_INDENTS {
1405 let indents = content::measurement_bytes(&outline.indents, 4)?;
1406 let id = outline.id;
1407 self.write(|page| {
1408 let mut node = crate::write::PropertyObject::from_object(
1409 &page.live.revision.objects[&id],
1410 )?;
1411 node.set(&[(0x1c001c12, &indents)])?;
1412 Ok(BTreeMap::from([(id, node)]))
1413 })?;
1414 }
1415 }
1416 PageObject::Image(image) => {
1417 if image.layout.x.is_none() || image.layout.y.is_none() {
1418 return Err(
1419 OpError::Unsupported("A new page-level picture needs a position").into(),
1420 );
1421 }
1422 self.picture(image, None)?;
1423 }
1424 PageObject::Attachment(attachment) => self.attachment(attachment, None)?,
1425 PageObject::Ink(ink) => self.ink(ink, None)?,
1426 PageObject::Title(_) | PageObject::Unsupported(_) => {
1427 return Err(
1428 OpError::Unsupported("Titles and unsupported objects cannot be added").into(),
1429 );
1430 }
1431 }
1432 if before.is_some() {
1433 self.move_to(object.id(), None, before)?;
1434 }
1435 Ok(())
1436 }
1437
1438 fn table(&mut self, id: ExGuid, edit: &TableEdit) -> Result<(), Failure> {
1439 let page = self.page()?;
1440 let node = &page.view.nodes[&id];
1441 let Kind::Table {
1442 widths,
1443 locked,
1444 borders,
1445 ..
1446 } = &node.kind
1447 else {
1448 return Err(OpError::Unsupported("Select a table").into());
1449 };
1450 let mut columns: Vec<TableColumn> = widths
1451 .iter()
1452 .enumerate()
1453 .map(|(i, width)| TableColumn {
1454 width: *width,
1455 locked: locked.get(i).copied().unwrap_or(false),
1456 })
1457 .collect();
1458 let mut rows: Vec<(ExGuid, Vec<ExGuid>)> = node
1459 .children
1460 .iter()
1461 .map(|row| (*row, page.view.nodes[row].children.clone()))
1462 .collect();
1463 let mut borders = *borders;
1464 let template =
1465 rows.first()
1466 .and_then(|(_, cells)| cells.first())
1467 .and_then(|cell| match &page.view.nodes[cell].kind {
1468 Kind::Cell { indents, .. } => Some(indents.clone()),
1469 _ => None,
1470 });
1471 let mut new_rows = BTreeSet::new();
1472 let mut new_cells: BTreeMap<ExGuid, (Vec<f32>, Option<u32>)> = BTreeMap::new();
1473 let mut filled: Vec<&TableCell> = Vec::new();
1474 let new_cell = |cell: &TableCell| {
1475 (
1476 table::cell_indents(&cell.indents, template.as_deref()).to_vec(),
1477 cell.shading,
1478 )
1479 };
1480 match edit {
1481 TableEdit::Rows {
1482 before,
1483 rows: added,
1484 } => {
1485 let at = match before {
1486 Some(before) => rows.iter().position(|(row, _)| row == before).ok_or(
1487 OpError::StructureChanged("The anchor row is not in the table"),
1488 )?,
1489 None => rows.len(),
1490 };
1491 for row in added {
1492 if row.cells.len() != columns.len() {
1493 return Err(OpError::Unsupported(
1494 "Every table row has one cell per column",
1495 )
1496 .into());
1497 }
1498 new_rows.insert(row.id);
1499 for cell in &row.cells {
1500 new_cells.insert(cell.id, new_cell(cell));
1501 filled.push(cell);
1502 }
1503 }
1504 rows.splice(
1505 at..at,
1506 added
1507 .iter()
1508 .map(|row| (row.id, row.cells.iter().map(|cell| cell.id).collect())),
1509 );
1510 }
1511 TableEdit::Column { at, width, cells } => {
1512 let at = *at as usize;
1513 if at > columns.len() || cells.len() != rows.len() {
1514 return Err(OpError::Unsupported("A new column has one cell per row").into());
1515 }
1516 for ((_, row), cell) in rows.iter_mut().zip(cells) {
1517 row.insert(at, cell.id);
1518 new_cells.insert(cell.id, new_cell(cell));
1519 filled.push(cell);
1520 }
1521 columns.insert(
1522 at,
1523 TableColumn {
1524 width: *width,
1525 locked: false,
1526 },
1527 );
1528 }
1529 TableEdit::DeleteRow(row) => {
1530 let before = rows.len();
1531 rows.retain(|(id, _)| id != row);
1532 if rows.len() == before {
1533 return Err(OpError::TargetUnavailable(*row).into());
1534 }
1535 if rows.is_empty() {
1536 return Err(OpError::Unsupported(
1537 "A table keeps a row; delete the table instead",
1538 )
1539 .into());
1540 }
1541 }
1542 TableEdit::DeleteColumn(at) => {
1543 let at = *at as usize;
1544 if at >= columns.len() || columns.len() == 1 {
1545 return Err(
1546 OpError::Unsupported("Choose a column of a table with several").into(),
1547 );
1548 }
1549 columns.remove(at);
1550 for (_, cells) in &mut rows {
1551 cells.remove(at);
1552 }
1553 }
1554 TableEdit::Columns(widths) => {
1555 if widths.len() != columns.len() {
1556 return Err(OpError::Unsupported("Give every column its width").into());
1557 }
1558 columns = widths.clone();
1559 }
1560 TableEdit::Borders(value) => borders = Some(*value),
1561 TableEdit::Cell {
1562 cell,
1563 shading,
1564 indents,
1565 } => {
1566 if !rows.iter().any(|(_, cells)| cells.contains(cell)) {
1567 return Err(OpError::TargetUnavailable(*cell).into());
1568 }
1569 let Kind::Cell {
1570 shading: stored_shading,
1571 indents: stored_indents,
1572 } = &page.view.nodes[cell].kind
1573 else {
1574 return Err(OpError::Unsupported("Select a table cell").into());
1575 };
1576 let stored = (*stored_shading, stored_indents.clone());
1577 return self.write(|page| {
1578 table::cell_changes(page, *cell, (stored.0, &stored.1), *shading, indents)
1579 });
1580 }
1581 }
1582 for cell in &filled {
1583 for id in std::iter::once(cell.id).chain(super::model::identities(&cell.paragraphs)) {
1584 self.free(id)?;
1585 }
1586 }
1587 for row in &new_rows {
1588 self.free(*row)?;
1589 }
1590 if columns
1591 .iter()
1592 .any(|c| !c.width.is_finite() || c.width < 36.0)
1593 {
1594 return Err(OpError::Unsupported("Table columns are at least 36 points wide").into());
1595 }
1596 let structure = Structure {
1597 rows,
1598 new_rows,
1599 new_cells: new_cells
1600 .iter()
1601 .map(|(id, (indents, shading))| (*id, (indents.as_slice(), *shading)))
1602 .collect(),
1603 columns: &columns,
1604 borders,
1605 };
1606 self.write(|page| table::table_changes(page, id, None, &structure))?;
1607 for cell in filled {
1608 self.cell(cell)?;
1609 }
1610 Ok(())
1611 }
1612}
1613
1614fn fresh() -> Result<ExGuid, Error> {
1615 Ok(ExGuid {
1616 guid: crate::write::fresh_guid()?,
1617 n: 1,
1618 })
1619}
1620
1621/// Requires what `Insert` leaves to other ops unset.
1622fn bare(paragraph: &PageParagraph) -> Result<(), OpError> {
1623 let text_tags = paragraph.text().is_some_and(|text| !text.tags.is_empty());
1624 if paragraph.style.is_some()
1625 || !paragraph.lists.is_empty()
1626 || !paragraph.tags.is_empty()
1627 || text_tags
1628 || paragraph.collapsed
1629 {
1630 return Err(OpError::Unsupported(
1631 "Set styles, lists, tags and collapse state with their own ops",
1632 ));
1633 }
1634 if paragraph.media != Default::default() {
1635 return Err(OpError::Unsupported(
1636 "Link a paragraph to a recording with its own op",
1637 ));
1638 }
1639 match &paragraph.content {
1640 ParagraphContent::Text(text) if text.date_field.is_some() => Err(OpError::Unsupported(
1641 "New paragraphs contain plain text without fields",
1642 )),
1643 ParagraphContent::Table(table) => {
1644 if !table.tags.is_empty() {
1645 return Err(OpError::Unsupported("Set a table's tags with their own op"));
1646 }
1647 for paragraph in table
1648 .rows
1649 .iter()
1650 .flat_map(|row| &row.cells)
1651 .flat_map(|cell| &cell.paragraphs)
1652 {
1653 bare(paragraph)?;
1654 }
1655 Ok(())
1656 }
1657 _ => Ok(()),
1658 }
1659}
1660
1661/// The field code OneNote stores before a hyperlink's label.
1662fn field_code(target: &str) -> Result<String, Error> {
1663 if target.is_empty() || target.contains(['"', '\0', '\r', '\n', '\u{fddf}']) {
1664 return Err(invalid(
1665 "Choose a link target without quotes or line breaks",
1666 ));
1667 }
1668 Ok(format!("\u{fddf}HYPERLINK \"{target}\""))
1669}
1670
1671/// The text edits making `range` of `text` a hyperlink to `target`: the field code
1672/// inserted hidden before the label, both flagged as link runs as OneNote stores them. With
1673/// `None` or an existing link, the field code before the label is removed first.
1674pub(crate) fn link_ops(
1675 text: &Paragraph,
1676 id: ExGuid,
1677 range: Range<u32>,
1678 target: Option<&str>,
1679) -> Result<Vec<PageOp>, Error> {
1680 if range.is_empty() {
1681 return Err(invalid("A link needs a label"));
1682 }
1683 let at = |offset: u32| super::lower::format_in(text, offset);
1684 let mut ops = Vec::new();
1685 let mut label = range.clone();
1686 // A field code is a hidden link run ending where the label starts.
1687 let start = text.byte_offset(range.start)?;
1688 let spans = text.spans();
1689 let code = spans.iter().enumerate().find_map(|(i, span)| {
1690 let from = if i == 0 { 0 } else { spans[i - 1].end };
1691 (span.end == start
1692 && span.format.hidden == Some(true)
1693 && span.format.hyperlink == Some(true)
1694 && text.text()[from..span.end].starts_with('\u{fddf}'))
1695 .then_some(from..span.end)
1696 });
1697 if let Some(code) = code {
1698 let code = text.utf16_offset(code.start)?..text.utf16_offset(code.end)?;
1699 let length = code.end - code.start;
1700 ops.push(PageOp::Text {
1701 text: id,
1702 range: code.clone(),
1703 with: String::new(),
1704 });
1705 label = label.start - length..label.end - length;
1706 } else if target.is_none() {
1707 return Err(invalid("The range is no link"));
1708 }
1709 let base = at(range.start)?.clone();
1710 match target {
1711 Some(target) => {
1712 let code = field_code(target)?;
1713 let length = code.encode_utf16().count() as u32;
1714 ops.push(PageOp::Text {
1715 text: id,
1716 range: label.start..label.start,
1717 with: code,
1718 });
1719 let mut set = vec![
1720 TextAttribute::Hyperlink(true),
1721 TextAttribute::HyperlinkLabel(true),
1722 ];
1723 if base.hidden != Some(true) {
1724 ops.push(PageOp::Format {
1725 text: id,
1726 range: label.start..label.start + length,
1727 set: [set.clone(), vec![TextAttribute::Hidden(true)]].concat(),
1728 clear: Vec::new(),
1729 });
1730 }
1731 set.retain(|attribute| match attribute {
1732 TextAttribute::Hyperlink(_) => base.hyperlink != Some(true),
1733 _ => base.hyperlink_label != Some(true),
1734 });
1735 if !set.is_empty() || base.hidden == Some(true) {
1736 ops.push(PageOp::Format {
1737 text: id,
1738 range: label.start + length..label.end + length,
1739 set,
1740 clear: Vec::new(),
1741 });
1742 }
1743 }
1744 None => ops.push(PageOp::Format {
1745 text: id,
1746 range: label,
1747 set: Vec::new(),
1748 clear: vec![TextProperty::Hyperlink, TextProperty::HyperlinkLabel],
1749 }),
1750 }
1751 ops.retain(
1752 |op| !matches!(op, PageOp::Format { set, clear, .. } if set.is_empty() && clear.is_empty()),
1753 );
1754 Ok(ops)
1755}