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