1//! Ops interpreted on a page model: what reading the page back after `Section::apply`
2//! yields, for the lowering's prediction and as the tests' oracle.
3
4use super::{PageOp, TableEdit, TextProperty, content::NATIVE_INDENTS, lower::format_in};
5use crate::{
6 Error, ExGuid, OutlineEdit, TextAttribute,
7 document::{Format, Kind},
8 page::{
9 Outline, Page, PageObject, PageParagraph, Paragraph, ParagraphContent, Table, TextObject,
10 },
11};
12use std::collections::BTreeSet;
13
14fn invalid(message: &'static str) -> Error {
15 Error { offset: 0, message }
16}
17
18/// A length in points as the store keeps it, in half inches.
19fn stored(points: f32) -> f32 {
20 points / 36.0 * 36.0
21}
22
23/// Where a flat paragraph list lies: a page object, a title outline, then the table cells
24/// on the way down, each as (paragraph index, row, cell).
25#[derive(Clone, Debug)]
26pub(crate) struct Path {
27 object: usize,
28 title_outline: Option<usize>,
29 cells: Vec<(usize, usize, usize)>,
30}
31
32/// The lists of a page, each with its path and owner (outline or cell identity).
33pub(crate) fn lists(page: &Page) -> Vec<(Path, ExGuid, &Vec<PageParagraph>)> {
34 fn cells<'p>(
35 path: &Path,
36 list: &'p [PageParagraph],
37 out: &mut Vec<(Path, ExGuid, &'p Vec<PageParagraph>)>,
38 ) {
39 for (index, paragraph) in list.iter().enumerate() {
40 if let ParagraphContent::Table(table) = &paragraph.content {
41 for (r, row) in table.rows.iter().enumerate() {
42 for (c, cell) in row.cells.iter().enumerate() {
43 let mut path = path.clone();
44 path.cells.push((index, r, c));
45 out.push((path.clone(), cell.id, &cell.paragraphs));
46 cells(&path, &cell.paragraphs, out);
47 }
48 }
49 }
50 }
51 }
52 let mut out = Vec::new();
53 for (object, value) in page.objects.iter().enumerate() {
54 let outlines: Vec<(Option<usize>, &Outline)> = match value {
55 PageObject::Outline(outline) => vec![(None, outline)],
56 PageObject::Title(title) => title
57 .outlines
58 .iter()
59 .enumerate()
60 .map(|(i, o)| (Some(i), o))
61 .collect(),
62 _ => Vec::new(),
63 };
64 for (title_outline, outline) in outlines {
65 let path = Path {
66 object,
67 title_outline,
68 cells: Vec::new(),
69 };
70 out.push((path.clone(), outline.id, &outline.paragraphs));
71 cells(&path, &outline.paragraphs, &mut out);
72 }
73 }
74 out
75}
76
77pub(crate) fn list_at<'p>(page: &'p mut Page, path: &Path) -> &'p mut Vec<PageParagraph> {
78 let outline = match (&mut page.objects[path.object], path.title_outline) {
79 (PageObject::Outline(outline), None) => outline,
80 (PageObject::Title(title), Some(i)) => &mut title.outlines[i],
81 _ => unreachable!(),
82 };
83 let mut list = &mut outline.paragraphs;
84 for (index, r, c) in &path.cells {
85 let ParagraphContent::Table(table) = &mut list[*index].content else {
86 unreachable!()
87 };
88 list = &mut table.rows[*r].cells[*c].paragraphs;
89 }
90 list
91}
92
93/// The list holding paragraph `id`, and its index there.
94fn find(page: &Page, id: ExGuid) -> Option<(Path, usize)> {
95 lists(page).into_iter().find_map(|(path, _, list)| {
96 list.iter()
97 .position(|p| p.id == id)
98 .map(|index| (path, index))
99 })
100}
101
102/// The list a container's children lie in: its own for an outline or cell, the one
103/// holding it for a paragraph.
104fn container(page: &Page, id: ExGuid) -> Option<(Path, Option<usize>)> {
105 lists(page).into_iter().find_map(|(path, owner, list)| {
106 if owner == id {
107 Some((path, None))
108 } else {
109 list.iter()
110 .position(|p| p.id == id)
111 .map(|index| (path, Some(index)))
112 }
113 })
114}
115
116/// The entries of `list` in the subtree of the paragraph at `index`.
117fn subtree(list: &[PageParagraph], index: usize) -> std::ops::Range<usize> {
118 let mut members = BTreeSet::from([list[index].id]);
119 let mut end = index + 1;
120 while end < list.len() && list[end].parent.is_some_and(|p| members.contains(&p)) {
121 members.insert(list[end].id);
122 end += 1;
123 }
124 index..end
125}
126
127pub(crate) fn paragraph(page: &Page, id: ExGuid) -> Option<&PageParagraph> {
128 lists(page)
129 .into_iter()
130 .find_map(|(_, _, list)| list.iter().find(|p| p.id == id))
131}
132
133pub(crate) fn outline(page: &Page, id: ExGuid) -> Option<&Outline> {
134 page.objects.iter().find_map(|object| match object {
135 PageObject::Outline(outline) if outline.id == id => Some(outline),
136 _ => None,
137 })
138}
139
140pub(crate) fn table(page: &Page, id: ExGuid) -> Option<&Table> {
141 lists(page).into_iter().find_map(|(_, _, list)| {
142 list.iter().find_map(|p| match &p.content {
143 ParagraphContent::Table(table) if table.id == id => Some(table),
144 _ => None,
145 })
146 })
147}
148
149/// Whether a paragraph of the page lists `list` among its list nodes.
150pub(crate) fn owns_list(page: &Page, list: ExGuid) -> bool {
151 lists(page)
152 .into_iter()
153 .any(|(_, _, paragraphs)| paragraphs.iter().any(|p| p.lists.contains(&list)))
154}
155
156/// Identities of paragraphs and what they hold, tables included.
157pub(crate) fn identities(paragraphs: &[PageParagraph]) -> Vec<ExGuid> {
158 let mut out = Vec::new();
159 for paragraph in paragraphs {
160 out.push(paragraph.id);
161 match &paragraph.content {
162 ParagraphContent::Text(text) => out.push(text.id),
163 ParagraphContent::Table(table) => {
164 out.push(table.id);
165 for row in &table.rows {
166 out.push(row.id);
167 for cell in &row.cells {
168 out.push(cell.id);
169 out.extend(identities(&cell.paragraphs));
170 }
171 }
172 }
173 ParagraphContent::Image(image) => out.push(image.id),
174 ParagraphContent::Attachment(attachment) => out.push(attachment.id),
175 ParagraphContent::Ink(ink) => {
176 out.push(ink.id);
177 out.extend(ink.strokes.iter().map(|s| s.id));
178 }
179 ParagraphContent::Unsupported(unsupported) => out.push(unsupported.id),
180 }
181 out.extend(paragraph.lists.iter().copied());
182 }
183 out
184}
185
186/// Whether the page holds `id` as an object of the model.
187pub(crate) fn holds(page: &Page, id: ExGuid) -> bool {
188 page.objects.iter().any(|object| {
189 object.id() == id
190 || match object {
191 PageObject::Outline(outline) => identities(&outline.paragraphs).contains(&id),
192 PageObject::Title(title) => title
193 .outlines
194 .iter()
195 .any(|o| o.id == id || identities(&o.paragraphs).contains(&id)),
196 PageObject::Ink(ink) => ink.strokes.iter().any(|s| s.id == id),
197 PageObject::Image(_) | PageObject::Attachment(_) | PageObject::Unsupported(_) => {
198 false
199 }
200 }
201 })
202}
203
204/// The paragraph holding text object `text`: its list and index.
205fn text_holder(page: &Page, text: ExGuid) -> Option<(Path, usize)> {
206 lists(page).into_iter().find_map(|(path, _, list)| {
207 list.iter()
208 .position(|p| p.text().is_some_and(|t| t.id == text))
209 .map(|index| (path, index))
210 })
211}
212
213/// The text of the title's date and time fields, in order.
214pub(crate) fn date_fields(page: &Page) -> Vec<&TextObject> {
215 page.objects
216 .iter()
217 .filter_map(|object| match object {
218 PageObject::Title(title) => {
219 let date = title.date?;
220 title.outlines.iter().find(|outline| outline.id == date)
221 }
222 _ => None,
223 })
224 .flat_map(|outline| outline.paragraphs.iter().filter_map(PageParagraph::text))
225 .collect()
226}
227
228/// Text object `text` wherever the page holds it.
229pub(crate) fn paragraph_text(page: &Page, text: ExGuid) -> Option<&TextObject> {
230 lists(page).into_iter().find_map(|(_, _, list)| {
231 list.iter()
232 .find_map(|paragraph| paragraph.text().filter(|object| object.id == text))
233 })
234}
235
236fn text_mut(page: &mut Page, text: ExGuid) -> Result<&mut TextObject, Error> {
237 let (path, index) =
238 text_holder(page, text).ok_or_else(|| invalid("Text is not on the page"))?;
239 Ok(list_at(page, &path)[index].text_mut().unwrap())
240}
241
242fn paragraph_mut(page: &mut Page, id: ExGuid) -> Result<&mut PageParagraph, Error> {
243 let (path, index) = find(page, id).ok_or_else(|| invalid("Paragraph is not on the page"))?;
244 Ok(&mut list_at(page, &path)[index])
245}
246
247fn table_mut(page: &mut Page, id: ExGuid) -> Result<&mut Table, Error> {
248 let (path, index) = lists(page)
249 .into_iter()
250 .find_map(|(path, _, list)| {
251 list.iter()
252 .position(|p| matches!(&p.content, ParagraphContent::Table(t) if t.id == id))
253 .map(|index| (path, index))
254 })
255 .ok_or_else(|| invalid("Table is not on the page"))?;
256 let ParagraphContent::Table(table) = &mut list_at(page, &path)[index].content else {
257 unreachable!()
258 };
259 Ok(table)
260}
261
262/// `text` with a range replaced as the text writer does: inserted text takes the format of
263/// the run it lands in, and emptied text keeps its last run's.
264pub(crate) fn replace_text(
265 text: &Paragraph,
266 range: std::ops::Range<u32>,
267 with: &str,
268) -> Result<Paragraph, Error> {
269 let length = text.utf16_offset(text.text().len())?;
270 if range.start > range.end || range.end > length {
271 return Err(invalid("The edit range exceeds the text"));
272 }
273 // Replacing text with itself leaves its formatting as it is.
274 if text.text()[text.byte_offset(range.start)?..text.byte_offset(range.end)?] == *with {
275 return Ok(text.clone());
276 }
277 let format = format_in(text, range.start)?.clone();
278 let removed = range.end - range.start;
279 if length - removed == 0 && with.is_empty() {
280 return Ok(Paragraph::new(
281 String::new(),
282 text.spans().last().unwrap().format.clone(),
283 ));
284 }
285 let mut result = text.clone();
286 result.apply(crate::page::text::Edit {
287 range,
288 replacement: Paragraph::new(with.to_owned(), format),
289 })?;
290 Ok(result)
291}
292
293/// `text` with every span taking `from`'s paragraph formatting, which the text object
294/// stores for all its runs.
295fn paragraph_formatted(text: &Paragraph, from: &Format) -> Paragraph {
296 let mut previous = 0;
297 Paragraph::from_runs(text.spans().iter().map(|span| {
298 let mut format = span.format.clone();
299 format.alignment = from.alignment;
300 format.rtl = from.rtl;
301 format.space_before = from.space_before;
302 format.space_after = from.space_after;
303 format.line_spacing = from.line_spacing;
304 let run = (text.text()[previous..span.end].to_owned(), format);
305 previous = span.end;
306 run
307 }))
308}
309
310fn set(format: &mut Format, attribute: &TextAttribute) {
311 let color = |value: &Option<[u8; 3]>| {
312 Some(value.map_or(0xff000000, |[r, g, b]| u32::from_le_bytes([r, g, b, 0])))
313 };
314 match attribute {
315 TextAttribute::Bold(v) => format.bold = Some(*v),
316 TextAttribute::Italic(v) => format.italic = Some(*v),
317 TextAttribute::Underline(v) => format.underline = Some(*v),
318 TextAttribute::Strike(v) => format.strike = Some(*v),
319 TextAttribute::Superscript(v) => {
320 format.superscript = Some(*v);
321 if *v {
322 format.subscript = Some(false);
323 }
324 }
325 TextAttribute::Subscript(v) => {
326 format.subscript = Some(*v);
327 if *v {
328 format.superscript = Some(false);
329 }
330 }
331 TextAttribute::Font(font) => format.font = Some(font.clone()),
332 TextAttribute::FontSize(size) => format.font_size = Some(*size),
333 TextAttribute::Color(value) => format.color = color(value),
334 TextAttribute::Highlight(value) => format.highlight = color(value),
335 TextAttribute::Language(language) => format.language = Some(*language),
336 TextAttribute::Hidden(v) => format.hidden = Some(*v),
337 TextAttribute::Hyperlink(v) => format.hyperlink = Some(*v),
338 TextAttribute::HyperlinkLabel(v) => format.hyperlink_label = Some(*v),
339 TextAttribute::Math(v) => format.math = Some(*v),
340 }
341}
342
343fn clear(format: &mut Format, property: TextProperty) {
344 match property {
345 TextProperty::Bold => format.bold = None,
346 TextProperty::Italic => format.italic = None,
347 TextProperty::Underline => format.underline = None,
348 TextProperty::Strike => format.strike = None,
349 TextProperty::Superscript => format.superscript = None,
350 TextProperty::Subscript => format.subscript = None,
351 TextProperty::Hidden => format.hidden = None,
352 TextProperty::Hyperlink => format.hyperlink = None,
353 TextProperty::HyperlinkLabel => format.hyperlink_label = None,
354 TextProperty::Math => format.math = None,
355 TextProperty::Font => format.font = None,
356 TextProperty::FontSize => format.font_size = None,
357 TextProperty::Color => format.color = None,
358 TextProperty::Highlight => format.highlight = None,
359 }
360}
361
362/// `text` with a range reformatted as the format writer does; a reformatted run states its
363/// language.
364pub(crate) fn format_text(
365 text: &Paragraph,
366 range: std::ops::Range<u32>,
367 attributes: &[TextAttribute],
368 cleared: &[TextProperty],
369 style: &Format,
370) -> Result<Paragraph, Error> {
371 let apply = |format: &Format| {
372 let mut format = format.clone();
373 for attribute in attributes {
374 set(&mut format, attribute);
375 }
376 for property in cleared {
377 clear(&mut format, *property);
378 }
379 // A cleared property shows the paragraph style's value.
380 format = format.inherit(style);
381 if format.language.is_none() {
382 format.language = Some(0x409);
383 }
384 format
385 };
386 if text.text().is_empty() {
387 return Ok(Paragraph::new(
388 String::new(),
389 apply(&text.spans()[0].format),
390 ));
391 }
392 let (start, end) = (text.byte_offset(range.start)?, text.byte_offset(range.end)?);
393 let mut runs = Vec::new();
394 let mut previous = 0;
395 for span in text.spans() {
396 for (from, to, selected) in [
397 (previous, start.clamp(previous, span.end), false),
398 (
399 start.clamp(previous, span.end),
400 end.clamp(previous, span.end),
401 true,
402 ),
403 (end.clamp(previous, span.end), span.end, false),
404 ] {
405 if from < to {
406 let format = if selected {
407 apply(&span.format)
408 } else {
409 span.format.clone()
410 };
411 runs.push((text.text()[from..to].to_owned(), format));
412 }
413 }
414 previous = span.end;
415 }
416 Ok(Paragraph::from_runs(runs))
417}
418
419/// A text as `Insert` stores it: an unset language is the writer's insertion language, and
420/// paragraph formatting is stored only where every span shares a value other than the default.
421fn inserted(text: &Paragraph) -> Paragraph {
422 let spans = text.spans();
423 macro_rules! shared {
424 ($field:ident, $type:ty, $store:expr) => {{
425 let first = spans[0].format.$field.unwrap_or_default();
426 (spans
427 .iter()
428 .all(|s| s.format.$field.unwrap_or_default() == first)
429 && first != <$type>::default())
430 .then(|| $store(first))
431 }};
432 }
433 let alignment = shared!(alignment, u8, |v| v);
434 let rtl = shared!(rtl, bool, |v| v);
435 let space_before = shared!(space_before, f32, stored);
436 let space_after = shared!(space_after, f32, stored);
437 let line_spacing = shared!(line_spacing, f32, stored);
438 let mut previous = 0;
439 Paragraph::from_runs(spans.iter().map(|span| {
440 let mut format = span.format.clone();
441 format.language.get_or_insert(0x409);
442 format.alignment = alignment;
443 format.rtl = rtl;
444 format.space_before = space_before;
445 format.space_after = space_after;
446 format.line_spacing = line_spacing;
447 let run = (text.text()[previous..span.end].to_owned(), format);
448 previous = span.end;
449 run
450 }))
451}
452
453/// A stroke as the store keeps it: points on the HIMETRIC grid, pen size in HIMETRIC,
454/// pressure in levels.
455fn stroked(stroke: &crate::page::InkStroke) -> crate::page::InkStroke {
456 let himetric = |points: f32| points * 2540.0 / 72.0 * 72.0 / 2540.0;
457 crate::page::InkStroke {
458 points: stroke
459 .points
460 .iter()
461 .map(|[x, y]| [crate::page::ink::snap(*x), crate::page::ink::snap(*y)])
462 .collect(),
463 width: himetric(stroke.width),
464 height: himetric(stroke.height),
465 pressure: stroke
466 .pressure
467 .iter()
468 .copied()
469 .map(crate::page::ink::level)
470 .collect(),
471 ..stroke.clone()
472 }
473}
474
475fn inked(ink: &crate::page::Ink) -> crate::page::Ink {
476 crate::page::Ink {
477 strokes: ink.strokes.iter().map(stroked).collect(),
478 shape: ink.shape.as_ref().map(crate::page::ink::InkShape::snapped),
479 ..ink.clone()
480 }
481}
482
483/// Paragraphs as `Insert` stores them.
484fn insertion(paragraphs: &[PageParagraph]) -> Vec<PageParagraph> {
485 paragraphs
486 .iter()
487 .map(|paragraph| {
488 let mut paragraph = paragraph.clone();
489 match &mut paragraph.content {
490 ParagraphContent::Text(text) if !crate::page::Math::is_equation(&text.text) => {
491 text.text = inserted(&text.text);
492 }
493 // An equation is written whole, without paragraph formatting.
494 ParagraphContent::Text(text) => {
495 text.text = paragraph_formatted(&text.text, &Format::default());
496 }
497 ParagraphContent::Table(table) => {
498 for cell in table.rows.iter_mut().flat_map(|row| &mut row.cells) {
499 if cell.indents.is_empty() {
500 cell.indents = NATIVE_INDENTS.to_vec();
501 }
502 cell.paragraphs = insertion(&cell.paragraphs);
503 }
504 }
505 ParagraphContent::Ink(ink) => *ink = inked(ink),
506 _ => {}
507 }
508 paragraph
509 })
510 .collect()
511}
512
513/// Inserts `paragraphs` (roots then descendants) into `container` before `before`.
514fn insert(
515 page: &mut Page,
516 container: ExGuid,
517 before: Option<ExGuid>,
518 paragraphs: Vec<PageParagraph>,
519) -> Result<(), Error> {
520 let (path, owner) = self::container(page, container)
521 .ok_or_else(|| invalid("The container is not on the page"))?;
522 let list = list_at(page, &path);
523 let at = match before {
524 Some(before) => list
525 .iter()
526 .position(|p| p.id == before)
527 .ok_or_else(|| invalid("The anchor is not on the page"))?,
528 None => match owner {
529 Some(index) => subtree(list, index).end,
530 None => list.len(),
531 },
532 };
533 list.splice(at..at, paragraphs);
534 Ok(())
535}
536
537/// The absolute level of the children of `container`.
538fn child_level(page: &Page, container: ExGuid) -> u32 {
539 paragraph(page, container).map_or(0, |p| p.level)
540}
541
542/// Shifts the levels of the subtree at `index` so its root lies at `level`.
543fn relevel(list: &mut [PageParagraph], index: usize, level: u32) {
544 let range = subtree(list, index);
545 let delta = i64::from(level) - i64::from(list[index].level);
546 for paragraph in &mut list[range] {
547 paragraph.level = (i64::from(paragraph.level) + delta) as u32;
548 }
549}
550
551/// The title a page's writers store after changing it: the title text's first line when
552/// it has one, which only an edit of that text changes, else the first line of body text.
553fn retitle(page: &mut Page, edited: Option<ExGuid>) {
554 let line = |text: &str| {
555 crate::edit::without_fields(text.trim_start().split('\r').next().unwrap_or_default())
556 };
557 match find_title_text(page) {
558 Some(title) => {
559 let text = text_of(page, title);
560 if line(&text).is_empty() {
561 automatic(page);
562 } else if edited == Some(title) {
563 page.title = line(&text);
564 }
565 }
566 None => automatic(page),
567 }
568}
569
570fn automatic(page: &mut Page) {
571 let mut roots: Vec<&PageObject> = page
572 .objects
573 .iter()
574 .filter(|object| !matches!(object, PageObject::Title(_)))
575 .collect();
576 roots.sort_by(|a, b| {
577 let (a, b) = (a.layout(), b.layout());
578 a.y.unwrap_or(0.0)
579 .total_cmp(&b.y.unwrap_or(0.0))
580 .then_with(|| a.x.unwrap_or(0.0).total_cmp(&b.x.unwrap_or(0.0)))
581 });
582 fn first(list: &[PageParagraph]) -> Option<String> {
583 list.iter().find_map(|paragraph| match &paragraph.content {
584 ParagraphContent::Text(text) => {
585 Some(crate::edit::automatic_title(text.text.text())).filter(|t| !t.is_empty())
586 }
587 ParagraphContent::Table(table) => table
588 .rows
589 .iter()
590 .flat_map(|row| &row.cells)
591 .find_map(|cell| first(&cell.paragraphs)),
592 _ => None,
593 })
594 }
595 let title = roots
596 .into_iter()
597 .find_map(|object| match object {
598 PageObject::Outline(outline) => first(&outline.paragraphs),
599 _ => None,
600 })
601 .unwrap_or_default();
602 page.title = title;
603}
604
605/// Applies `op` to `page` as reading the page back after `Section::apply` would show it.
606pub fn apply(page: &mut Page, op: &PageOp) -> Result<(), Error> {
607 interpret(page, op)?;
608 // A page lists the definitions its paragraphs reference.
609 let mut referenced = BTreeSet::new();
610 for (_, _, list) in lists(page) {
611 for paragraph in list {
612 referenced.extend(paragraph.lists.iter().copied());
613 referenced.extend(paragraph.style);
614 let tags = paragraph.tags.iter().chain(match &paragraph.content {
615 ParagraphContent::Text(text) => text.tags.as_slice(),
616 ParagraphContent::Table(table) => table.tags.as_slice(),
617 ParagraphContent::Image(image) => image.tags.as_slice(),
618 ParagraphContent::Attachment(file) => file.tags.as_slice(),
619 _ => &[],
620 });
621 referenced.extend(tags.filter_map(|tag| tag.definition));
622 }
623 }
624 for object in &page.objects {
625 let tags = match object {
626 PageObject::Image(image) => image.tags.as_slice(),
627 PageObject::Attachment(file) => file.tags.as_slice(),
628 _ => &[],
629 };
630 referenced.extend(tags.iter().filter_map(|tag| tag.definition));
631 }
632 page.definitions.retain(|id, _| referenced.contains(id));
633 Ok(())
634}
635
636fn interpret(page: &mut Page, op: &PageOp) -> Result<(), Error> {
637 match op {
638 PageOp::Date { created, fields } => {
639 for (text, shown) in fields {
640 if !date_fields(page).iter().any(|field| field.id == *text) {
641 return Err(invalid("Select the title's date or time"));
642 }
643 let object = text_mut(page, *text)?;
644 let format = object.text.spans()[0].format.clone();
645 object.text = Paragraph::new(shown.clone(), format);
646 }
647 page.created = Some(*created);
648 }
649 PageOp::Color(color) => page.color = *color,
650 PageOp::RuleLines(lines) => page.rule_lines = *lines,
651 PageOp::Text { text, range, with } => {
652 let object = text_mut(page, *text)?;
653 object.text = replace_text(&object.text, range.clone(), with)?;
654 retitle(page, Some(*text));
655 }
656 PageOp::Format {
657 text,
658 range,
659 set,
660 clear,
661 } => {
662 let (path, index) =
663 text_holder(page, *text).ok_or_else(|| invalid("Text is not on the page"))?;
664 let style = list_at(page, &path)[index]
665 .style
666 .and_then(|style| page.definitions.get(&style))
667 .map(|definition| definition.format.clone())
668 .unwrap_or_default();
669 let object = text_mut(page, *text)?;
670 object.text = format_text(&object.text, range.clone(), set, clear, &style)?;
671 }
672 PageOp::Link {
673 text,
674 range,
675 target,
676 } => {
677 let current = text_mut(page, *text)?.text.clone();
678 for op in super::apply::link_ops(&current, *text, range.clone(), target.as_deref())? {
679 interpret(page, &op)?;
680 }
681 }
682 PageOp::Equation { text, math } => {
683 let (path, index) =
684 text_holder(page, *text).ok_or_else(|| invalid("Text is not on the page"))?;
685 let style = list_at(page, &path)[index]
686 .style
687 .and_then(|style| page.definitions.get(&style))
688 .map(|definition| definition.format.clone())
689 .unwrap_or_default();
690 let paragraph = &mut list_at(page, &path)[index];
691 // Runs read back through the paragraph style and format, which fill what they
692 // leave unset.
693 let inherited = style.inherit(&paragraph.format);
694 let object = paragraph.text_mut().unwrap();
695 let previous = object.text.spans()[0].format.clone();
696 let math = Paragraph::from_runs(math.spans().iter().scan(0, |start, span| {
697 let mut format = span.format.clone();
698 format.language.get_or_insert(0x409);
699 let run = (
700 math.text()[*start..span.end].to_owned(),
701 format.inherit(&inherited),
702 );
703 *start = span.end;
704 Some(run)
705 }));
706 object.text = paragraph_formatted(&math, &previous);
707 }
708 PageOp::Insert {
709 container,
710 before,
711 paragraphs,
712 } => {
713 insert(page, *container, *before, insertion(paragraphs))?;
714 retitle(page, None);
715 }
716 PageOp::Split {
717 text,
718 at,
719 paragraph,
720 right,
721 lists,
722 } => {
723 let (path, index) =
724 text_holder(page, *text).ok_or_else(|| invalid("Text is not on the page"))?;
725 let definitions: Vec<_> = {
726 let list = list_at(page, &path);
727 list[index].lists.clone()
728 };
729 let copies: Vec<_> = definitions
730 .iter()
731 .zip(lists)
732 .map(|(old, new)| {
733 let mut definition = page.definitions.get(old).cloned();
734 if let Some(crate::page::Definition {
735 kind: Kind::List { restart, .. },
736 ..
737 }) = &mut definition
738 {
739 *restart = None;
740 }
741 (*new, definition)
742 })
743 .collect();
744 for (id, definition) in copies {
745 if let Some(definition) = definition {
746 page.definitions.insert(id, definition);
747 }
748 }
749 let list = list_at(page, &path);
750 let left = &mut list[index];
751 let source = left.text().unwrap().text.clone();
752 let length = source.utf16_offset(source.text().len())?;
753 let (head, tail) = (source.slice(0..*at)?, source.slice(*at..length)?);
754 let mut new = left.clone();
755 left.text_mut().unwrap().text = head;
756 new.id = *paragraph;
757 new.lists = lists.clone();
758 new.media = Default::default();
759 new.content = ParagraphContent::Text(TextObject {
760 id: *right,
761 date_field: None,
762 text: tail,
763 tags: Vec::new(),
764 });
765 let left_id = list[index].id;
766 for child in &mut list[index + 1..] {
767 if child.parent == Some(left_id) {
768 child.parent = Some(*paragraph);
769 }
770 }
771 list.insert(index + 1, new);
772 retitle(page, None);
773 }
774 PageOp::Join { left, right } => {
775 let (path, index) =
776 text_holder(page, *right).ok_or_else(|| invalid("Text is not on the page"))?;
777 let (left_path, left_index) =
778 text_holder(page, *left).ok_or_else(|| invalid("Text is not on the page"))?;
779 let list = list_at(page, &path);
780 let removed = list[index].clone();
781 let right_text = removed.text().unwrap().clone();
782 let definitions = page.definitions.clone();
783 let left_list = list_at(page, &left_path);
784 let left_style = left_list[left_index].style;
785 let adopts = left_list[left_index].text().unwrap().text.text().is_empty();
786 if adopts {
787 // The text object brings its paragraph style and recording link.
788 left_list[left_index].style = removed.style;
789 left_list[left_index].media = removed.media.clone();
790 }
791 let upper = left_list[left_index].text_mut().unwrap();
792 if adopts {
793 upper.id = right_text.id;
794 upper.text = right_text.text;
795 } else {
796 // The lower text keeps its look under the upper paragraph's style: a flag or
797 // colour only that style states becomes false or automatic.
798 let differs = left_style != removed.style;
799 let left = left_style
800 .and_then(|style| definitions.get(&style))
801 .map(|definition| definition.format.clone())
802 .unwrap_or_default();
803 let mut previous = 0;
804 let lower = Paragraph::from_runs(right_text.text.spans().iter().map(|span| {
805 let mut format = span.format.clone();
806 if differs {
807 for (field, base) in [
808 (&mut format.bold, left.bold),
809 (&mut format.italic, left.italic),
810 (&mut format.underline, left.underline),
811 (&mut format.strike, left.strike),
812 (&mut format.superscript, left.superscript),
813 (&mut format.subscript, left.subscript),
814 (&mut format.hidden, left.hidden),
815 (&mut format.hyperlink, left.hyperlink),
816 (&mut format.hyperlink_label, left.hyperlink_label),
817 (&mut format.math, left.math),
818 ] {
819 if field.is_none() && base.is_some() {
820 *field = Some(false);
821 }
822 }
823 for (field, base) in [
824 (&mut format.color, left.color),
825 (&mut format.highlight, left.highlight),
826 ] {
827 if field.is_none() && base.is_some() {
828 *field = Some(0xff000000);
829 }
830 }
831 }
832 let run = (
833 right_text.text.text()[previous..span.end].to_owned(),
834 format,
835 );
836 previous = span.end;
837 run
838 }));
839 let paragraph = upper.text.spans()[0].format.clone();
840 upper.text.append(lower)?;
841 upper.text = paragraph_formatted(&upper.text, &paragraph);
842 }
843 let left_id = left_list[left_index].id;
844 let list = list_at(page, &path);
845 // The right paragraph's children join the left paragraph's ancestor beside it,
846 // which is the outline group holding it where it lies deeper.
847 let mut stem = left_id;
848 while let Some(parent) = list.iter().find(|p| p.id == stem).and_then(|p| p.parent) {
849 if Some(parent) == removed.parent {
850 break;
851 }
852 stem = parent;
853 }
854 let range = subtree(list, index);
855 let stem_at = list.iter().position(|p| p.id == stem).unwrap();
856 let stem_level = list[stem_at].level;
857 if range.len() > 1 && stem_level > removed.level {
858 // The group takes the right paragraph's child level for all its members.
859 let level = list[range.start + 1].level;
860 let mut member = stem_at;
861 loop {
862 relevel(list, member, level);
863 let Some(previous) = list[..member]
864 .iter()
865 .rposition(|p| p.parent == removed.parent)
866 .filter(|at| list[*at].level == stem_level)
867 else {
868 break;
869 };
870 member = previous;
871 }
872 for child in &mut list[range.start + 1..range.end] {
873 if child.parent == Some(removed.id) {
874 child.parent = removed.parent;
875 }
876 }
877 } else {
878 let delta = i64::from(stem_level) - i64::from(removed.level);
879 for child in &mut list[range.start + 1..range.end] {
880 child.level = (i64::from(child.level) + delta) as u32;
881 if child.parent == Some(removed.id) {
882 child.parent = Some(stem);
883 }
884 }
885 }
886 list.remove(index);
887 retitle(page, None);
888 }
889 PageOp::Move {
890 object,
891 parent,
892 before,
893 } => {
894 let Some(parent) = parent else {
895 let from = page
896 .objects
897 .iter()
898 .position(|o| o.id() == *object)
899 .ok_or_else(|| invalid("The object is not on the page"))?;
900 let moved = page.objects.remove(from);
901 let at = page_position(page, *before)?;
902 page.objects.insert(at, moved);
903 retitle(page, None);
904 return Ok(());
905 };
906 let (path, index) =
907 find(page, *object).ok_or_else(|| invalid("The paragraph is not on the page"))?;
908 let list = list_at(page, &path);
909 let mut moved: Vec<PageParagraph> = list.drain(subtree(list, index)).collect();
910 let into_cell = table_cells(page).contains(parent);
911 let base = child_level(page, *parent);
912 let level = if into_cell {
913 let (path, _) = self::container(page, *parent).unwrap();
914 list_at(page, &path)
915 .iter()
916 .find(|p| p.parent.is_none())
917 .map_or(1, |p| p.level)
918 } else if moved[0].level > base {
919 moved[0].level
920 } else {
921 base + 1
922 };
923 moved[0].parent = paragraph(page, *parent).map(|_| *parent);
924 relevel(&mut moved, 0, level);
925 insert(page, *parent, *before, moved)?;
926 drop_emptied(page);
927 retitle(page, None);
928 }
929 PageOp::Delete { object } => {
930 if let Some(at) = page.objects.iter().position(|o| o.id() == *object) {
931 page.objects.remove(at);
932 } else {
933 let (path, index) =
934 find(page, *object).ok_or_else(|| invalid("The object is not on the page"))?;
935 let list = list_at(page, &path);
936 list.drain(subtree(list, index));
937 }
938 drop_emptied(page);
939 retitle(page, None);
940 }
941 PageOp::Level { paragraph, level } => {
942 let (path, index) = find(page, *paragraph)
943 .ok_or_else(|| invalid("The paragraph is not on the page"))?;
944 relevel(list_at(page, &path), index, *level);
945 }
946 PageOp::Outline { object, edit } => match edit {
947 OutlineEdit::Collapsed(value) => paragraph_mut(page, *object)?.collapsed = *value,
948 OutlineEdit::Position { x, y } => {
949 let layout = match page.objects.iter_mut().find(|o| o.id() == *object) {
950 Some(PageObject::Ink(ink)) => &mut ink.layout,
951 Some(PageObject::Attachment(file)) => &mut file.layout,
952 _ => &mut outline_mut(page, *object)?.layout,
953 };
954 layout.x = Some(stored(*x));
955 layout.y = Some(stored(*y));
956 retitle(page, None);
957 }
958 OutlineEdit::Width { points, user_set } => {
959 let outline = outline_mut(page, *object)?;
960 outline.layout.max_width = Some(stored(*points));
961 outline.layout.width_set_by_user = Some(*user_set);
962 outline.layout.reserved_width = None;
963 }
964 },
965 PageOp::Paragraph {
966 paragraph,
967 alignment,
968 rtl,
969 space_before,
970 space_after,
971 line_spacing,
972 language,
973 } => {
974 let text = &mut paragraph_mut(page, *paragraph)?
975 .text_mut()
976 .ok_or_else(|| invalid("Paragraph formatting belongs to text"))?
977 .text;
978 let mut previous = 0;
979 let runs: Vec<(String, Format)> = text
980 .spans()
981 .iter()
982 .map(|span| {
983 let mut format = span.format.clone();
984 format.alignment = alignment.or(format.alignment);
985 format.rtl = rtl.or(format.rtl);
986 format.space_before = space_before.map(stored).or(format.space_before);
987 format.space_after = space_after.map(stored).or(format.space_after);
988 format.line_spacing = line_spacing.map(stored).or(format.line_spacing);
989 // A run's own language outranks the text's.
990 format.language = format.language.or(*language);
991 let run = (text.text()[previous..span.end].to_owned(), format);
992 previous = span.end;
993 run
994 })
995 .collect();
996 *text = Paragraph::from_runs(runs);
997 }
998 PageOp::Style {
999 paragraph,
1000 style,
1001 definition,
1002 } => {
1003 let old = paragraph_mut(page, *paragraph)?
1004 .style
1005 .and_then(|style| page.definitions.get(&style))
1006 .map(|definition| definition.format.clone())
1007 .unwrap_or_default();
1008 let style = define(page, *style, definition);
1009 let new = page.definitions[&style].format.clone();
1010 let paragraph = paragraph_mut(page, *paragraph)?;
1011 paragraph.style = Some(style);
1012 if let Some(text) = paragraph.text_mut() {
1013 text.text = restyled(&text.text, &old, &new);
1014 }
1015 }
1016 PageOp::Unstyle { paragraph: id } => {
1017 let Some(style) = paragraph_mut(page, *id)?.style.take() else {
1018 return Ok(());
1019 };
1020 let old = page
1021 .definitions
1022 .get(&style)
1023 .ok_or_else(|| invalid("The page has no such paragraph style"))?
1024 .format
1025 .clone();
1026 if let Some(text) = paragraph_mut(page, *id)?.text_mut() {
1027 text.text = restyled(&text.text, &old, &Format::default());
1028 }
1029 }
1030 PageOp::Restyle {
1031 style,
1032 into,
1033 definition,
1034 } => {
1035 let old = page
1036 .definitions
1037 .get(style)
1038 .ok_or_else(|| invalid("The page has no such paragraph style"))?
1039 .format
1040 .clone();
1041 let mut definition = definition.clone();
1042 let format = &mut definition.format;
1043 for points in [
1044 &mut format.space_before,
1045 &mut format.space_after,
1046 &mut format.line_spacing,
1047 ]
1048 .into_iter()
1049 .flatten()
1050 {
1051 *points = stored(*points);
1052 }
1053 page.definitions
1054 .entry(*into)
1055 .or_insert_with(|| definition.clone());
1056 let paths: Vec<Path> = lists(page).into_iter().map(|(path, ..)| path).collect();
1057 for path in paths {
1058 for paragraph in list_at(page, &path) {
1059 if paragraph.style == Some(*style) {
1060 paragraph.style = Some(*into);
1061 if let Some(text) = paragraph.text_mut() {
1062 text.text = restyled(&text.text, &old, &definition.format);
1063 }
1064 }
1065 }
1066 }
1067 }
1068 PageOp::Media { paragraph, media } => {
1069 let paragraph = paragraph_mut(page, *paragraph)?;
1070 if !matches!(
1071 paragraph.content,
1072 ParagraphContent::Text(_) | ParagraphContent::Attachment(_)
1073 ) {
1074 return Err(invalid("Link a paragraph of text or a file to a recording"));
1075 }
1076 if media.recordings.is_empty() != media.time_ms.is_none() {
1077 return Err(invalid(
1078 "A recording link names recordings and a moment in them",
1079 ));
1080 }
1081 paragraph.media = media.clone();
1082 }
1083 PageOp::List { paragraph, lists } => {
1084 paragraph_mut(page, *paragraph)?.lists = lists.iter().map(|(id, _)| *id).collect();
1085 for (id, definition) in lists {
1086 page.definitions.insert(*id, definition.clone());
1087 }
1088 }
1089 PageOp::Tags {
1090 target,
1091 tags,
1092 definitions,
1093 } => {
1094 let mut tags = tags.clone();
1095 for tag in &mut tags {
1096 if let Some(id) = &mut tag.definition
1097 && let Some((_, definition)) = definitions.iter().find(|(d, _)| d == id)
1098 {
1099 *id = define(page, *id, definition);
1100 }
1101 }
1102 if let Ok(paragraph) = paragraph_mut(page, *target) {
1103 paragraph.tags = tags;
1104 } else if let Ok(text) = text_mut(page, *target) {
1105 text.tags = tags;
1106 } else if let Ok(table) = table_mut(page, *target) {
1107 table.tags = tags;
1108 } else if let Ok(image) = image_mut(page, *target) {
1109 image.tags = tags;
1110 } else {
1111 attachment_mut(page, *target)?.tags = tags;
1112 }
1113 }
1114 PageOp::Add { object, before } => {
1115 let object = match object {
1116 PageObject::Outline(outline) => {
1117 let mut outline = outline.clone();
1118 outline.paragraphs = insertion(&outline.paragraphs);
1119 if outline.indents.is_empty() {
1120 outline.indents = NATIVE_INDENTS.to_vec();
1121 }
1122 let layout = &mut outline.layout;
1123 layout.x = layout.x.map(stored);
1124 layout.y = layout.y.map(stored);
1125 match layout.max_width {
1126 Some(points) if (points, layout.width_set_by_user) != (468.0, None) => {
1127 layout.max_width = Some(stored(points));
1128 layout.width_set_by_user = Some(layout.width_set_by_user == Some(true));
1129 }
1130 _ => {
1131 layout.max_width = Some(468.0);
1132 layout.width_set_by_user = None;
1133 }
1134 }
1135 layout.reserved_width = None;
1136 layout.max_height = Some(0.6_f32 * 36.0);
1137 PageObject::Outline(outline)
1138 }
1139 PageObject::Ink(ink) => PageObject::Ink(inked(ink)),
1140 PageObject::Attachment(file) => {
1141 let mut file = file.clone();
1142 let layout = &mut file.layout;
1143 for value in [
1144 &mut layout.x,
1145 &mut layout.y,
1146 &mut layout.max_width,
1147 &mut layout.max_height,
1148 ] {
1149 *value = value.map(stored);
1150 }
1151 layout.width_set_by_user = layout
1152 .max_width
1153 .map(|_| layout.width_set_by_user == Some(true));
1154 PageObject::Attachment(file)
1155 }
1156 object => object.clone(),
1157 };
1158 let outline = matches!(object, PageObject::Outline(_));
1159 let at = page_position(page, *before)?;
1160 page.objects.insert(at, object);
1161 // Placing an object before another moves it there, which retitles as a move does.
1162 if outline || before.is_some() {
1163 retitle(page, None);
1164 }
1165 }
1166 PageOp::Picture {
1167 picture,
1168 layout,
1169 alt,
1170 } => {
1171 let image = image_mut(page, *picture)?;
1172 image.layout = crate::document::Layout {
1173 x: layout.x.map(stored),
1174 y: layout.y.map(stored),
1175 max_width: layout.max_width.map(stored),
1176 max_height: layout.max_height.map(stored),
1177 width_set_by_user: layout
1178 .max_width
1179 .map(|_| layout.width_set_by_user == Some(true)),
1180 reserved_width: image.layout.reserved_width,
1181 };
1182 image.alt = alt.clone();
1183 }
1184 PageOp::Attachment {
1185 attachment,
1186 filename,
1187 source_path,
1188 size,
1189 } => {
1190 let stored_attachment = attachment_mut(page, *attachment)?;
1191 stored_attachment.filename = filename.clone();
1192 stored_attachment.source_path = source_path.clone();
1193 stored_attachment.size = size.map(|[w, h]| [stored(w), stored(h)]);
1194 }
1195 PageOp::Strokes { ink, add, remove } => {
1196 let ink = ink_mut(page, *ink)?;
1197 ink.strokes.retain(|stroke| !remove.contains(&stroke.id));
1198 ink.strokes.extend(add.iter().map(stroked));
1199 }
1200 PageOp::Table { table, edit } => {
1201 let table = table_mut(page, *table)?;
1202 match edit {
1203 TableEdit::Rows { before, rows } => {
1204 let at = match before {
1205 Some(before) => table
1206 .rows
1207 .iter()
1208 .position(|r| r.id == *before)
1209 .ok_or_else(|| invalid("The anchor row is not in the table"))?,
1210 None => table.rows.len(),
1211 };
1212 let mut rows = rows.clone();
1213 for cell in rows.iter_mut().flat_map(|row| &mut row.cells) {
1214 if cell.indents.is_empty() {
1215 cell.indents = table
1216 .rows
1217 .first()
1218 .and_then(|r| r.cells.first())
1219 .map_or(NATIVE_INDENTS.to_vec(), |c| c.indents.clone());
1220 }
1221 cell.paragraphs = insertion(&cell.paragraphs);
1222 }
1223 table.rows.splice(at..at, rows);
1224 }
1225 TableEdit::Column { at, width, cells } => {
1226 let template = table
1227 .rows
1228 .first()
1229 .and_then(|r| r.cells.first())
1230 .map(|c| c.indents.clone());
1231 for (row, cell) in table.rows.iter_mut().zip(cells) {
1232 let mut cell = cell.clone();
1233 if cell.indents.is_empty() {
1234 cell.indents = template.clone().unwrap_or(NATIVE_INDENTS.to_vec());
1235 }
1236 cell.paragraphs = insertion(&cell.paragraphs);
1237 row.cells.insert(*at as usize, cell);
1238 }
1239 table.columns.insert(
1240 *at as usize,
1241 crate::page::TableColumn {
1242 width: stored(*width),
1243 locked: false,
1244 },
1245 );
1246 }
1247 TableEdit::DeleteRow(row) => table.rows.retain(|r| r.id != *row),
1248 TableEdit::DeleteColumn(at) => {
1249 table.columns.remove(*at as usize);
1250 for row in &mut table.rows {
1251 row.cells.remove(*at as usize);
1252 }
1253 }
1254 TableEdit::Columns(columns) => {
1255 table.columns = columns
1256 .iter()
1257 .map(|c| crate::page::TableColumn {
1258 width: stored(c.width),
1259 locked: c.locked,
1260 })
1261 .collect();
1262 }
1263 TableEdit::Borders(borders) => table.borders = Some(*borders),
1264 TableEdit::Cell {
1265 cell,
1266 shading,
1267 indents,
1268 } => {
1269 let cell = table
1270 .rows
1271 .iter_mut()
1272 .flat_map(|row| &mut row.cells)
1273 .find(|c| c.id == *cell)
1274 .ok_or_else(|| invalid("The cell is not in the table"))?;
1275 cell.shading = *shading;
1276 cell.indents = indents.iter().map(|v| stored(*v)).collect();
1277 }
1278 }
1279 }
1280 }
1281 Ok(())
1282}
1283
1284/// Removes outlines a tree edit left without paragraphs, as the tree writer does.
1285fn drop_emptied(page: &mut Page) {
1286 page.objects.retain(
1287 |object| !matches!(object, PageObject::Outline(outline) if outline.paragraphs.is_empty()),
1288 );
1289}
1290
1291/// The identity a read-only definition is stored under: an equal one the page holds, as
1292/// the writers share identical read-only objects, else `id`.
1293/// `text` moved from paragraph style `old` to `new`: what the old style gave, the new one
1294/// gives instead.
1295fn restyled(text: &Paragraph, old: &Format, new: &Format) -> Paragraph {
1296 let mut previous = 0;
1297 Paragraph::from_runs(text.spans().iter().map(|span| {
1298 let mut format = span.format.clone();
1299 macro_rules! restyle {
1300 ($($field:ident),*) => {$(
1301 if old.$field.is_some() && format.$field == old.$field {
1302 format.$field = new.$field.clone();
1303 } else if format.$field.is_none() {
1304 format.$field = new.$field.clone();
1305 }
1306 )*};
1307 }
1308 restyle!(
1309 bold,
1310 italic,
1311 underline,
1312 strike,
1313 superscript,
1314 subscript,
1315 hidden,
1316 hyperlink,
1317 hyperlink_label,
1318 math,
1319 embedded_object,
1320 font,
1321 font_size,
1322 color,
1323 highlight,
1324 language,
1325 alignment,
1326 rtl,
1327 space_before,
1328 space_after,
1329 line_spacing,
1330 list_spacing,
1331 math_object
1332 );
1333 let run = (text.text()[previous..span.end].to_owned(), format);
1334 previous = span.end;
1335 run
1336 }))
1337}
1338
1339fn define(page: &mut Page, id: ExGuid, definition: &crate::page::Definition) -> ExGuid {
1340 if page.definitions.contains_key(&id) {
1341 return id;
1342 }
1343 if let Some((existing, _)) = page.definitions.iter().find(|(_, d)| *d == definition) {
1344 return *existing;
1345 }
1346 page.definitions.insert(id, definition.clone());
1347 id
1348}
1349
1350/// Where a page object goes before `before`, or after the other children; titles, which
1351/// the page lists apart, read after them.
1352fn page_position(page: &Page, before: Option<ExGuid>) -> Result<usize, Error> {
1353 match before {
1354 Some(before) => page
1355 .objects
1356 .iter()
1357 .position(|o| o.id() == before)
1358 .ok_or_else(|| invalid("The anchor is not on the page")),
1359 None => Ok(page
1360 .objects
1361 .iter()
1362 .position(|o| matches!(o, PageObject::Title(_)))
1363 .unwrap_or(page.objects.len())),
1364 }
1365}
1366
1367fn table_cells(page: &Page) -> BTreeSet<ExGuid> {
1368 lists(page)
1369 .into_iter()
1370 .filter(|(path, _, _)| !path.cells.is_empty())
1371 .map(|(_, owner, _)| owner)
1372 .collect()
1373}
1374
1375fn outline_mut(page: &mut Page, id: ExGuid) -> Result<&mut Outline, Error> {
1376 page.objects
1377 .iter_mut()
1378 .find_map(|object| match object {
1379 PageObject::Outline(outline) if outline.id == id => Some(outline),
1380 _ => None,
1381 })
1382 .ok_or_else(|| invalid("The outline is not on the page"))
1383}
1384
1385fn image_mut(page: &mut Page, id: ExGuid) -> Result<&mut crate::page::Image, Error> {
1386 if let Some(at) = page.objects.iter().position(|o| o.id() == id) {
1387 let PageObject::Image(image) = &mut page.objects[at] else {
1388 return Err(invalid("Select a picture"));
1389 };
1390 return Ok(image);
1391 }
1392 let (path, index) = lists(page)
1393 .into_iter()
1394 .find_map(|(path, _, list)| {
1395 list.iter()
1396 .position(|p| matches!(&p.content, ParagraphContent::Image(i) if i.id == id))
1397 .map(|index| (path, index))
1398 })
1399 .ok_or_else(|| invalid("The picture is not on the page"))?;
1400 let ParagraphContent::Image(image) = &mut list_at(page, &path)[index].content else {
1401 unreachable!()
1402 };
1403 Ok(image)
1404}
1405
1406fn attachment_mut(page: &mut Page, id: ExGuid) -> Result<&mut crate::page::Attachment, Error> {
1407 if let Some(at) = page.objects.iter().position(|o| o.id() == id) {
1408 let PageObject::Attachment(file) = &mut page.objects[at] else {
1409 return Err(invalid("Select an attachment"));
1410 };
1411 return Ok(file);
1412 }
1413 let (path, index) = lists(page)
1414 .into_iter()
1415 .find_map(|(path, _, list)| {
1416 list.iter()
1417 .position(|p| matches!(&p.content, ParagraphContent::Attachment(a) if a.id == id))
1418 .map(|index| (path, index))
1419 })
1420 .ok_or_else(|| invalid("The attachment is not on the page"))?;
1421 let ParagraphContent::Attachment(file) = &mut list_at(page, &path)[index].content else {
1422 unreachable!()
1423 };
1424 Ok(file)
1425}
1426
1427fn ink_mut(page: &mut Page, id: ExGuid) -> Result<&mut crate::page::Ink, Error> {
1428 if let Some(at) = page.objects.iter().position(|o| o.id() == id) {
1429 let PageObject::Ink(ink) = &mut page.objects[at] else {
1430 return Err(invalid("Select ink"));
1431 };
1432 return Ok(ink);
1433 }
1434 let (path, index) = lists(page)
1435 .into_iter()
1436 .find_map(|(path, _, list)| {
1437 list.iter()
1438 .position(|p| matches!(&p.content, ParagraphContent::Ink(i) if i.id == id))
1439 .map(|index| (path, index))
1440 })
1441 .ok_or_else(|| invalid("The ink is not on the page"))?;
1442 let ParagraphContent::Ink(ink) = &mut list_at(page, &path)[index].content else {
1443 unreachable!()
1444 };
1445 Ok(ink)
1446}
1447
1448/// The text object of the page title, when it has one.
1449fn find_title_text(page: &Page) -> Option<ExGuid> {
1450 page.objects
1451 .iter()
1452 .filter_map(|object| match object {
1453 PageObject::Title(title) => Some(title),
1454 _ => None,
1455 })
1456 .flat_map(|title| title.outlines.iter().filter(|o| Some(o.id) != title.date))
1457 .flat_map(|outline| &outline.paragraphs)
1458 .filter_map(|p| p.text().filter(|t| t.date_field.is_none()))
1459 .map(|t| t.id)
1460 .next_back()
1461}
1462
1463fn text_of(page: &Page, text: ExGuid) -> String {
1464 lists(page)
1465 .into_iter()
1466 .find_map(|(_, _, list)| {
1467 list.iter().find_map(|p| {
1468 p.text()
1469 .filter(|t| t.id == text)
1470 .map(|t| t.text.text().to_owned())
1471 })
1472 })
1473 .unwrap_or_default()
1474}