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 match find_title_text(page) {
555 Some(title) => {
556 let text = text_of(page, title);
557 if title_line(&text).is_empty() {
558 automatic(page);
559 } else if edited == Some(title) {
560 page.title = title_line(&text);
561 }
562 }
563 None => automatic(page),
564 }
565}
566
567/// The title stored for title text `text`: its first line, without leading space or fields.
568/// Empty leaves the page titled by its body.
569pub fn title_line(text: &str) -> String {
570 crate::edit::without_fields(text.trim_start().split('\r').next().unwrap_or_default())
571}
572
573fn automatic(page: &mut Page) {
574 let mut roots: Vec<&PageObject> = page
575 .objects
576 .iter()
577 .filter(|object| !matches!(object, PageObject::Title(_)))
578 .collect();
579 roots.sort_by(|a, b| {
580 let (a, b) = (a.layout(), b.layout());
581 a.y.unwrap_or(0.0)
582 .total_cmp(&b.y.unwrap_or(0.0))
583 .then_with(|| a.x.unwrap_or(0.0).total_cmp(&b.x.unwrap_or(0.0)))
584 });
585 fn first(list: &[PageParagraph]) -> Option<String> {
586 list.iter().find_map(|paragraph| match &paragraph.content {
587 ParagraphContent::Text(text) => {
588 Some(crate::edit::automatic_title(text.text.text())).filter(|t| !t.is_empty())
589 }
590 ParagraphContent::Table(table) => table
591 .rows
592 .iter()
593 .flat_map(|row| &row.cells)
594 .find_map(|cell| first(&cell.paragraphs)),
595 _ => None,
596 })
597 }
598 let title = roots
599 .into_iter()
600 .find_map(|object| match object {
601 PageObject::Outline(outline) => first(&outline.paragraphs),
602 _ => None,
603 })
604 .unwrap_or_default();
605 page.title = title;
606}
607
608/// Applies `op` to `page` as reading the page back after `Section::apply` would show it.
609pub fn apply(page: &mut Page, op: &PageOp) -> Result<(), Error> {
610 interpret(page, op)?;
611 // A page lists the definitions its paragraphs reference.
612 let mut referenced = BTreeSet::new();
613 for (_, _, list) in lists(page) {
614 for paragraph in list {
615 referenced.extend(paragraph.lists.iter().copied());
616 referenced.extend(paragraph.style);
617 let tags = paragraph.tags.iter().chain(match &paragraph.content {
618 ParagraphContent::Text(text) => text.tags.as_slice(),
619 ParagraphContent::Table(table) => table.tags.as_slice(),
620 ParagraphContent::Image(image) => image.tags.as_slice(),
621 ParagraphContent::Attachment(file) => file.tags.as_slice(),
622 _ => &[],
623 });
624 referenced.extend(tags.filter_map(|tag| tag.definition));
625 }
626 }
627 for object in &page.objects {
628 let tags = match object {
629 PageObject::Image(image) => image.tags.as_slice(),
630 PageObject::Attachment(file) => file.tags.as_slice(),
631 _ => &[],
632 };
633 referenced.extend(tags.iter().filter_map(|tag| tag.definition));
634 }
635 page.definitions.retain(|id, _| referenced.contains(id));
636 Ok(())
637}
638
639fn interpret(page: &mut Page, op: &PageOp) -> Result<(), Error> {
640 match op {
641 PageOp::Date { created, fields } => {
642 for (text, shown) in fields {
643 if !date_fields(page).iter().any(|field| field.id == *text) {
644 return Err(invalid("Select the title's date or time"));
645 }
646 let object = text_mut(page, *text)?;
647 let format = object.text.spans()[0].format.clone();
648 object.text = Paragraph::new(shown.clone(), format);
649 }
650 page.created = Some(*created);
651 }
652 PageOp::Color(color) => page.color = *color,
653 PageOp::RuleLines(lines) => page.rule_lines = *lines,
654 PageOp::Text { text, range, with } => {
655 let object = text_mut(page, *text)?;
656 object.text = replace_text(&object.text, range.clone(), with)?;
657 retitle(page, Some(*text));
658 }
659 PageOp::Format {
660 text,
661 range,
662 set,
663 clear,
664 } => {
665 let (path, index) =
666 text_holder(page, *text).ok_or_else(|| invalid("Text is not on the page"))?;
667 let style = list_at(page, &path)[index]
668 .style
669 .and_then(|style| page.definitions.get(&style))
670 .map(|definition| definition.format.clone())
671 .unwrap_or_default();
672 let object = text_mut(page, *text)?;
673 object.text = format_text(&object.text, range.clone(), set, clear, &style)?;
674 }
675 PageOp::Link {
676 text,
677 range,
678 target,
679 } => {
680 let current = text_mut(page, *text)?.text.clone();
681 for op in super::apply::link_ops(&current, *text, range.clone(), target.as_deref())? {
682 interpret(page, &op)?;
683 }
684 }
685 PageOp::Equation { text, math } => {
686 let (path, index) =
687 text_holder(page, *text).ok_or_else(|| invalid("Text is not on the page"))?;
688 let style = list_at(page, &path)[index]
689 .style
690 .and_then(|style| page.definitions.get(&style))
691 .map(|definition| definition.format.clone())
692 .unwrap_or_default();
693 let paragraph = &mut list_at(page, &path)[index];
694 // Runs read back through the paragraph style and format, which fill what they
695 // leave unset.
696 let inherited = style.inherit(&paragraph.format);
697 let object = paragraph.text_mut().unwrap();
698 let previous = object.text.spans()[0].format.clone();
699 let math = Paragraph::from_runs(math.spans().iter().scan(0, |start, span| {
700 let mut format = span.format.clone();
701 format.language.get_or_insert(0x409);
702 let run = (
703 math.text()[*start..span.end].to_owned(),
704 format.inherit(&inherited),
705 );
706 *start = span.end;
707 Some(run)
708 }));
709 object.text = paragraph_formatted(&math, &previous);
710 }
711 PageOp::Insert {
712 container,
713 before,
714 paragraphs,
715 } => {
716 insert(page, *container, *before, insertion(paragraphs))?;
717 retitle(page, None);
718 }
719 PageOp::Split {
720 text,
721 at,
722 paragraph,
723 right,
724 lists,
725 } => {
726 let (path, index) =
727 text_holder(page, *text).ok_or_else(|| invalid("Text is not on the page"))?;
728 let definitions: Vec<_> = {
729 let list = list_at(page, &path);
730 list[index].lists.clone()
731 };
732 let copies: Vec<_> = definitions
733 .iter()
734 .zip(lists)
735 .map(|(old, new)| {
736 let mut definition = page.definitions.get(old).cloned();
737 if let Some(crate::page::Definition {
738 kind: Kind::List { restart, .. },
739 ..
740 }) = &mut definition
741 {
742 *restart = None;
743 }
744 (*new, definition)
745 })
746 .collect();
747 for (id, definition) in copies {
748 if let Some(definition) = definition {
749 page.definitions.insert(id, definition);
750 }
751 }
752 let list = list_at(page, &path);
753 let left = &mut list[index];
754 let source = left.text().unwrap().text.clone();
755 let length = source.utf16_offset(source.text().len())?;
756 let (head, tail) = (source.slice(0..*at)?, source.slice(*at..length)?);
757 let mut new = left.clone();
758 left.text_mut().unwrap().text = head;
759 new.id = *paragraph;
760 new.lists = lists.clone();
761 new.media = Default::default();
762 new.content = ParagraphContent::Text(TextObject {
763 id: *right,
764 date_field: None,
765 text: tail,
766 tags: Vec::new(),
767 });
768 let left_id = list[index].id;
769 for child in &mut list[index + 1..] {
770 if child.parent == Some(left_id) {
771 child.parent = Some(*paragraph);
772 }
773 }
774 list.insert(index + 1, new);
775 retitle(page, None);
776 }
777 PageOp::Join { left, right } => {
778 let (path, index) =
779 text_holder(page, *right).ok_or_else(|| invalid("Text is not on the page"))?;
780 let (left_path, left_index) =
781 text_holder(page, *left).ok_or_else(|| invalid("Text is not on the page"))?;
782 let list = list_at(page, &path);
783 let removed = list[index].clone();
784 let right_text = removed.text().unwrap().clone();
785 let definitions = page.definitions.clone();
786 let left_list = list_at(page, &left_path);
787 let left_style = left_list[left_index].style;
788 let adopts = left_list[left_index].text().unwrap().text.text().is_empty();
789 if adopts {
790 // The text object brings its paragraph style and recording link.
791 left_list[left_index].style = removed.style;
792 left_list[left_index].media = removed.media.clone();
793 }
794 let upper = left_list[left_index].text_mut().unwrap();
795 if adopts {
796 upper.id = right_text.id;
797 upper.text = right_text.text;
798 } else {
799 // The lower text keeps its look under the upper paragraph's style: a flag or
800 // colour only that style states becomes false or automatic.
801 let differs = left_style != removed.style;
802 let left = left_style
803 .and_then(|style| definitions.get(&style))
804 .map(|definition| definition.format.clone())
805 .unwrap_or_default();
806 let mut previous = 0;
807 let lower = Paragraph::from_runs(right_text.text.spans().iter().map(|span| {
808 let mut format = span.format.clone();
809 if differs {
810 for (field, base) in [
811 (&mut format.bold, left.bold),
812 (&mut format.italic, left.italic),
813 (&mut format.underline, left.underline),
814 (&mut format.strike, left.strike),
815 (&mut format.superscript, left.superscript),
816 (&mut format.subscript, left.subscript),
817 (&mut format.hidden, left.hidden),
818 (&mut format.hyperlink, left.hyperlink),
819 (&mut format.hyperlink_label, left.hyperlink_label),
820 (&mut format.math, left.math),
821 ] {
822 if field.is_none() && base.is_some() {
823 *field = Some(false);
824 }
825 }
826 for (field, base) in [
827 (&mut format.color, left.color),
828 (&mut format.highlight, left.highlight),
829 ] {
830 if field.is_none() && base.is_some() {
831 *field = Some(0xff000000);
832 }
833 }
834 }
835 let run = (
836 right_text.text.text()[previous..span.end].to_owned(),
837 format,
838 );
839 previous = span.end;
840 run
841 }));
842 let paragraph = upper.text.spans()[0].format.clone();
843 upper.text.append(lower)?;
844 upper.text = paragraph_formatted(&upper.text, &paragraph);
845 }
846 let left_id = left_list[left_index].id;
847 let list = list_at(page, &path);
848 // The right paragraph's children join the left paragraph's ancestor beside it,
849 // which is the outline group holding it where it lies deeper.
850 let mut stem = left_id;
851 while let Some(parent) = list.iter().find(|p| p.id == stem).and_then(|p| p.parent) {
852 if Some(parent) == removed.parent {
853 break;
854 }
855 stem = parent;
856 }
857 let range = subtree(list, index);
858 let stem_at = list.iter().position(|p| p.id == stem).unwrap();
859 let stem_level = list[stem_at].level;
860 if range.len() > 1 && stem_level > removed.level {
861 // The group takes the right paragraph's child level for all its members.
862 let level = list[range.start + 1].level;
863 let mut member = stem_at;
864 loop {
865 relevel(list, member, level);
866 let Some(previous) = list[..member]
867 .iter()
868 .rposition(|p| p.parent == removed.parent)
869 .filter(|at| list[*at].level == stem_level)
870 else {
871 break;
872 };
873 member = previous;
874 }
875 for child in &mut list[range.start + 1..range.end] {
876 if child.parent == Some(removed.id) {
877 child.parent = removed.parent;
878 }
879 }
880 } else {
881 let delta = i64::from(stem_level) - i64::from(removed.level);
882 for child in &mut list[range.start + 1..range.end] {
883 child.level = (i64::from(child.level) + delta) as u32;
884 if child.parent == Some(removed.id) {
885 child.parent = Some(stem);
886 }
887 }
888 }
889 list.remove(index);
890 retitle(page, None);
891 }
892 PageOp::Move {
893 object,
894 parent,
895 before,
896 } => {
897 let Some(parent) = parent else {
898 let from = page
899 .objects
900 .iter()
901 .position(|o| o.id() == *object)
902 .ok_or_else(|| invalid("The object is not on the page"))?;
903 let moved = page.objects.remove(from);
904 let at = page_position(page, *before)?;
905 page.objects.insert(at, moved);
906 retitle(page, None);
907 return Ok(());
908 };
909 let (path, index) =
910 find(page, *object).ok_or_else(|| invalid("The paragraph is not on the page"))?;
911 let list = list_at(page, &path);
912 let mut moved: Vec<PageParagraph> = list.drain(subtree(list, index)).collect();
913 let into_cell = table_cells(page).contains(parent);
914 let base = child_level(page, *parent);
915 let level = if into_cell {
916 let (path, _) = self::container(page, *parent).unwrap();
917 list_at(page, &path)
918 .iter()
919 .find(|p| p.parent.is_none())
920 .map_or(1, |p| p.level)
921 } else if moved[0].level > base {
922 moved[0].level
923 } else {
924 base + 1
925 };
926 moved[0].parent = paragraph(page, *parent).map(|_| *parent);
927 relevel(&mut moved, 0, level);
928 insert(page, *parent, *before, moved)?;
929 drop_emptied(page);
930 retitle(page, None);
931 }
932 PageOp::Delete { object } => {
933 if let Some(at) = page.objects.iter().position(|o| o.id() == *object) {
934 page.objects.remove(at);
935 } else {
936 let (path, index) =
937 find(page, *object).ok_or_else(|| invalid("The object is not on the page"))?;
938 let list = list_at(page, &path);
939 list.drain(subtree(list, index));
940 }
941 drop_emptied(page);
942 retitle(page, None);
943 }
944 PageOp::Level { paragraph, level } => {
945 let (path, index) = find(page, *paragraph)
946 .ok_or_else(|| invalid("The paragraph is not on the page"))?;
947 relevel(list_at(page, &path), index, *level);
948 }
949 PageOp::Outline { object, edit } => match edit {
950 OutlineEdit::Collapsed(value) => paragraph_mut(page, *object)?.collapsed = *value,
951 OutlineEdit::Position { x, y } => {
952 let layout = match page.objects.iter_mut().find(|o| o.id() == *object) {
953 Some(PageObject::Ink(ink)) => &mut ink.layout,
954 Some(PageObject::Attachment(file)) => &mut file.layout,
955 _ => &mut outline_mut(page, *object)?.layout,
956 };
957 layout.x = Some(stored(*x));
958 layout.y = Some(stored(*y));
959 retitle(page, None);
960 }
961 OutlineEdit::Width { points, user_set } => {
962 let outline = outline_mut(page, *object)?;
963 outline.layout.max_width = Some(stored(*points));
964 outline.layout.width_set_by_user = Some(*user_set);
965 outline.layout.reserved_width = None;
966 }
967 },
968 PageOp::Paragraph {
969 paragraph,
970 alignment,
971 rtl,
972 space_before,
973 space_after,
974 line_spacing,
975 language,
976 } => {
977 let text = &mut paragraph_mut(page, *paragraph)?
978 .text_mut()
979 .ok_or_else(|| invalid("Paragraph formatting belongs to text"))?
980 .text;
981 let mut previous = 0;
982 let runs: Vec<(String, Format)> = text
983 .spans()
984 .iter()
985 .map(|span| {
986 let mut format = span.format.clone();
987 format.alignment = alignment.or(format.alignment);
988 format.rtl = rtl.or(format.rtl);
989 format.space_before = space_before.map(stored).or(format.space_before);
990 format.space_after = space_after.map(stored).or(format.space_after);
991 format.line_spacing = line_spacing.map(stored).or(format.line_spacing);
992 // A run's own language outranks the text's.
993 format.language = format.language.or(*language);
994 let run = (text.text()[previous..span.end].to_owned(), format);
995 previous = span.end;
996 run
997 })
998 .collect();
999 *text = Paragraph::from_runs(runs);
1000 }
1001 PageOp::Style {
1002 paragraph,
1003 style,
1004 definition,
1005 } => {
1006 let old = paragraph_mut(page, *paragraph)?
1007 .style
1008 .and_then(|style| page.definitions.get(&style))
1009 .map(|definition| definition.format.clone())
1010 .unwrap_or_default();
1011 let style = define(page, *style, definition);
1012 let new = page.definitions[&style].format.clone();
1013 let paragraph = paragraph_mut(page, *paragraph)?;
1014 paragraph.style = Some(style);
1015 if let Some(text) = paragraph.text_mut() {
1016 text.text = restyled(&text.text, &old, &new);
1017 }
1018 }
1019 PageOp::Unstyle { paragraph: id } => {
1020 let Some(style) = paragraph_mut(page, *id)?.style.take() else {
1021 return Ok(());
1022 };
1023 let old = page
1024 .definitions
1025 .get(&style)
1026 .ok_or_else(|| invalid("The page has no such paragraph style"))?
1027 .format
1028 .clone();
1029 if let Some(text) = paragraph_mut(page, *id)?.text_mut() {
1030 text.text = restyled(&text.text, &old, &Format::default());
1031 }
1032 }
1033 PageOp::Restyle {
1034 style,
1035 into,
1036 definition,
1037 } => {
1038 let old = page
1039 .definitions
1040 .get(style)
1041 .ok_or_else(|| invalid("The page has no such paragraph style"))?
1042 .format
1043 .clone();
1044 let mut definition = definition.clone();
1045 let format = &mut definition.format;
1046 for points in [
1047 &mut format.space_before,
1048 &mut format.space_after,
1049 &mut format.line_spacing,
1050 ]
1051 .into_iter()
1052 .flatten()
1053 {
1054 *points = stored(*points);
1055 }
1056 page.definitions
1057 .entry(*into)
1058 .or_insert_with(|| definition.clone());
1059 let paths: Vec<Path> = lists(page).into_iter().map(|(path, ..)| path).collect();
1060 for path in paths {
1061 for paragraph in list_at(page, &path) {
1062 if paragraph.style == Some(*style) {
1063 paragraph.style = Some(*into);
1064 if let Some(text) = paragraph.text_mut() {
1065 text.text = restyled(&text.text, &old, &definition.format);
1066 }
1067 }
1068 }
1069 }
1070 }
1071 PageOp::Media { paragraph, media } => {
1072 let paragraph = paragraph_mut(page, *paragraph)?;
1073 if !matches!(
1074 paragraph.content,
1075 ParagraphContent::Text(_) | ParagraphContent::Attachment(_)
1076 ) {
1077 return Err(invalid("Link a paragraph of text or a file to a recording"));
1078 }
1079 if media.recordings.is_empty() != media.time_ms.is_none() {
1080 return Err(invalid(
1081 "A recording link names recordings and a moment in them",
1082 ));
1083 }
1084 paragraph.media = media.clone();
1085 }
1086 PageOp::List { paragraph, lists } => {
1087 paragraph_mut(page, *paragraph)?.lists = lists.iter().map(|(id, _)| *id).collect();
1088 for (id, definition) in lists {
1089 page.definitions.insert(*id, definition.clone());
1090 }
1091 }
1092 PageOp::Tags {
1093 target,
1094 tags,
1095 definitions,
1096 } => {
1097 let mut tags = tags.clone();
1098 for tag in &mut tags {
1099 if let Some(id) = &mut tag.definition
1100 && let Some((_, definition)) = definitions.iter().find(|(d, _)| d == id)
1101 {
1102 *id = define(page, *id, definition);
1103 }
1104 }
1105 if let Ok(paragraph) = paragraph_mut(page, *target) {
1106 paragraph.tags = tags;
1107 } else if let Ok(text) = text_mut(page, *target) {
1108 text.tags = tags;
1109 } else if let Ok(table) = table_mut(page, *target) {
1110 table.tags = tags;
1111 } else if let Ok(image) = image_mut(page, *target) {
1112 image.tags = tags;
1113 } else {
1114 attachment_mut(page, *target)?.tags = tags;
1115 }
1116 }
1117 PageOp::Add { object, before } => {
1118 let object = match object {
1119 PageObject::Outline(outline) => {
1120 let mut outline = outline.clone();
1121 outline.paragraphs = insertion(&outline.paragraphs);
1122 if outline.indents.is_empty() {
1123 outline.indents = NATIVE_INDENTS.to_vec();
1124 }
1125 let layout = &mut outline.layout;
1126 layout.x = layout.x.map(stored);
1127 layout.y = layout.y.map(stored);
1128 match layout.max_width {
1129 Some(points) if (points, layout.width_set_by_user) != (468.0, None) => {
1130 layout.max_width = Some(stored(points));
1131 layout.width_set_by_user = Some(layout.width_set_by_user == Some(true));
1132 }
1133 _ => {
1134 layout.max_width = Some(468.0);
1135 layout.width_set_by_user = None;
1136 }
1137 }
1138 layout.reserved_width = None;
1139 layout.max_height = Some(0.6_f32 * 36.0);
1140 PageObject::Outline(outline)
1141 }
1142 PageObject::Ink(ink) => PageObject::Ink(inked(ink)),
1143 PageObject::Attachment(file) => {
1144 let mut file = file.clone();
1145 let layout = &mut file.layout;
1146 for value in [
1147 &mut layout.x,
1148 &mut layout.y,
1149 &mut layout.max_width,
1150 &mut layout.max_height,
1151 ] {
1152 *value = value.map(stored);
1153 }
1154 layout.width_set_by_user = layout
1155 .max_width
1156 .map(|_| layout.width_set_by_user == Some(true));
1157 PageObject::Attachment(file)
1158 }
1159 object => object.clone(),
1160 };
1161 let outline = matches!(object, PageObject::Outline(_));
1162 let at = page_position(page, *before)?;
1163 page.objects.insert(at, object);
1164 // Placing an object before another moves it there, which retitles as a move does.
1165 if outline || before.is_some() {
1166 retitle(page, None);
1167 }
1168 }
1169 PageOp::Picture {
1170 picture,
1171 layout,
1172 alt,
1173 } => {
1174 let image = image_mut(page, *picture)?;
1175 image.layout = crate::document::Layout {
1176 x: layout.x.map(stored),
1177 y: layout.y.map(stored),
1178 max_width: layout.max_width.map(stored),
1179 max_height: layout.max_height.map(stored),
1180 width_set_by_user: layout
1181 .max_width
1182 .map(|_| layout.width_set_by_user == Some(true)),
1183 reserved_width: image.layout.reserved_width,
1184 };
1185 image.alt = alt.clone();
1186 }
1187 PageOp::Attachment {
1188 attachment,
1189 filename,
1190 source_path,
1191 size,
1192 } => {
1193 let stored_attachment = attachment_mut(page, *attachment)?;
1194 stored_attachment.filename = filename.clone();
1195 stored_attachment.source_path = source_path.clone();
1196 stored_attachment.size = size.map(|[w, h]| [stored(w), stored(h)]);
1197 }
1198 PageOp::Strokes { ink, add, remove } => {
1199 let ink = ink_mut(page, *ink)?;
1200 ink.strokes.retain(|stroke| !remove.contains(&stroke.id));
1201 ink.strokes.extend(add.iter().map(stroked));
1202 }
1203 PageOp::Table { table, edit } => {
1204 let table = table_mut(page, *table)?;
1205 match edit {
1206 TableEdit::Rows { before, rows } => {
1207 let at = match before {
1208 Some(before) => table
1209 .rows
1210 .iter()
1211 .position(|r| r.id == *before)
1212 .ok_or_else(|| invalid("The anchor row is not in the table"))?,
1213 None => table.rows.len(),
1214 };
1215 let mut rows = rows.clone();
1216 for cell in rows.iter_mut().flat_map(|row| &mut row.cells) {
1217 if cell.indents.is_empty() {
1218 cell.indents = table
1219 .rows
1220 .first()
1221 .and_then(|r| r.cells.first())
1222 .map_or(NATIVE_INDENTS.to_vec(), |c| c.indents.clone());
1223 }
1224 cell.paragraphs = insertion(&cell.paragraphs);
1225 }
1226 table.rows.splice(at..at, rows);
1227 }
1228 TableEdit::Column { at, width, cells } => {
1229 let template = table
1230 .rows
1231 .first()
1232 .and_then(|r| r.cells.first())
1233 .map(|c| c.indents.clone());
1234 for (row, cell) in table.rows.iter_mut().zip(cells) {
1235 let mut cell = cell.clone();
1236 if cell.indents.is_empty() {
1237 cell.indents = template.clone().unwrap_or(NATIVE_INDENTS.to_vec());
1238 }
1239 cell.paragraphs = insertion(&cell.paragraphs);
1240 row.cells.insert(*at as usize, cell);
1241 }
1242 table.columns.insert(
1243 *at as usize,
1244 crate::page::TableColumn {
1245 width: stored(*width),
1246 locked: false,
1247 },
1248 );
1249 }
1250 TableEdit::DeleteRow(row) => table.rows.retain(|r| r.id != *row),
1251 TableEdit::DeleteColumn(at) => {
1252 table.columns.remove(*at as usize);
1253 for row in &mut table.rows {
1254 row.cells.remove(*at as usize);
1255 }
1256 }
1257 TableEdit::Columns(columns) => {
1258 table.columns = columns
1259 .iter()
1260 .map(|c| crate::page::TableColumn {
1261 width: stored(c.width),
1262 locked: c.locked,
1263 })
1264 .collect();
1265 }
1266 TableEdit::Borders(borders) => table.borders = Some(*borders),
1267 TableEdit::Cell {
1268 cell,
1269 shading,
1270 indents,
1271 } => {
1272 let cell = table
1273 .rows
1274 .iter_mut()
1275 .flat_map(|row| &mut row.cells)
1276 .find(|c| c.id == *cell)
1277 .ok_or_else(|| invalid("The cell is not in the table"))?;
1278 cell.shading = *shading;
1279 cell.indents = indents.iter().map(|v| stored(*v)).collect();
1280 }
1281 }
1282 }
1283 }
1284 Ok(())
1285}
1286
1287/// Removes outlines a tree edit left without paragraphs, as the tree writer does.
1288fn drop_emptied(page: &mut Page) {
1289 page.objects.retain(
1290 |object| !matches!(object, PageObject::Outline(outline) if outline.paragraphs.is_empty()),
1291 );
1292}
1293
1294/// The identity a read-only definition is stored under: an equal one the page holds, as
1295/// the writers share identical read-only objects, else `id`.
1296/// `text` moved from paragraph style `old` to `new`: what the old style gave, the new one
1297/// gives instead.
1298fn restyled(text: &Paragraph, old: &Format, new: &Format) -> Paragraph {
1299 let mut previous = 0;
1300 Paragraph::from_runs(text.spans().iter().map(|span| {
1301 let mut format = span.format.clone();
1302 macro_rules! restyle {
1303 ($($field:ident),*) => {$(
1304 if old.$field.is_some() && format.$field == old.$field {
1305 format.$field = new.$field.clone();
1306 } else if format.$field.is_none() {
1307 format.$field = new.$field.clone();
1308 }
1309 )*};
1310 }
1311 restyle!(
1312 bold,
1313 italic,
1314 underline,
1315 strike,
1316 superscript,
1317 subscript,
1318 hidden,
1319 hyperlink,
1320 hyperlink_label,
1321 math,
1322 embedded_object,
1323 font,
1324 font_size,
1325 color,
1326 highlight,
1327 language,
1328 alignment,
1329 rtl,
1330 space_before,
1331 space_after,
1332 line_spacing,
1333 list_spacing,
1334 math_object
1335 );
1336 let run = (text.text()[previous..span.end].to_owned(), format);
1337 previous = span.end;
1338 run
1339 }))
1340}
1341
1342fn define(page: &mut Page, id: ExGuid, definition: &crate::page::Definition) -> ExGuid {
1343 if page.definitions.contains_key(&id) {
1344 return id;
1345 }
1346 if let Some((existing, _)) = page.definitions.iter().find(|(_, d)| *d == definition) {
1347 return *existing;
1348 }
1349 page.definitions.insert(id, definition.clone());
1350 id
1351}
1352
1353/// Where a page object goes before `before`, or after the other children; titles, which
1354/// the page lists apart, read after them.
1355fn page_position(page: &Page, before: Option<ExGuid>) -> Result<usize, Error> {
1356 match before {
1357 Some(before) => page
1358 .objects
1359 .iter()
1360 .position(|o| o.id() == before)
1361 .ok_or_else(|| invalid("The anchor is not on the page")),
1362 None => Ok(page
1363 .objects
1364 .iter()
1365 .position(|o| matches!(o, PageObject::Title(_)))
1366 .unwrap_or(page.objects.len())),
1367 }
1368}
1369
1370fn table_cells(page: &Page) -> BTreeSet<ExGuid> {
1371 lists(page)
1372 .into_iter()
1373 .filter(|(path, _, _)| !path.cells.is_empty())
1374 .map(|(_, owner, _)| owner)
1375 .collect()
1376}
1377
1378fn outline_mut(page: &mut Page, id: ExGuid) -> Result<&mut Outline, Error> {
1379 page.objects
1380 .iter_mut()
1381 .find_map(|object| match object {
1382 PageObject::Outline(outline) if outline.id == id => Some(outline),
1383 _ => None,
1384 })
1385 .ok_or_else(|| invalid("The outline is not on the page"))
1386}
1387
1388fn image_mut(page: &mut Page, id: ExGuid) -> Result<&mut crate::page::Image, Error> {
1389 if let Some(at) = page.objects.iter().position(|o| o.id() == id) {
1390 let PageObject::Image(image) = &mut page.objects[at] else {
1391 return Err(invalid("Select a picture"));
1392 };
1393 return Ok(image);
1394 }
1395 let (path, index) = lists(page)
1396 .into_iter()
1397 .find_map(|(path, _, list)| {
1398 list.iter()
1399 .position(|p| matches!(&p.content, ParagraphContent::Image(i) if i.id == id))
1400 .map(|index| (path, index))
1401 })
1402 .ok_or_else(|| invalid("The picture is not on the page"))?;
1403 let ParagraphContent::Image(image) = &mut list_at(page, &path)[index].content else {
1404 unreachable!()
1405 };
1406 Ok(image)
1407}
1408
1409fn attachment_mut(page: &mut Page, id: ExGuid) -> Result<&mut crate::page::Attachment, Error> {
1410 if let Some(at) = page.objects.iter().position(|o| o.id() == id) {
1411 let PageObject::Attachment(file) = &mut page.objects[at] else {
1412 return Err(invalid("Select an attachment"));
1413 };
1414 return Ok(file);
1415 }
1416 let (path, index) = lists(page)
1417 .into_iter()
1418 .find_map(|(path, _, list)| {
1419 list.iter()
1420 .position(|p| matches!(&p.content, ParagraphContent::Attachment(a) if a.id == id))
1421 .map(|index| (path, index))
1422 })
1423 .ok_or_else(|| invalid("The attachment is not on the page"))?;
1424 let ParagraphContent::Attachment(file) = &mut list_at(page, &path)[index].content else {
1425 unreachable!()
1426 };
1427 Ok(file)
1428}
1429
1430fn ink_mut(page: &mut Page, id: ExGuid) -> Result<&mut crate::page::Ink, Error> {
1431 if let Some(at) = page.objects.iter().position(|o| o.id() == id) {
1432 let PageObject::Ink(ink) = &mut page.objects[at] else {
1433 return Err(invalid("Select ink"));
1434 };
1435 return Ok(ink);
1436 }
1437 let (path, index) = lists(page)
1438 .into_iter()
1439 .find_map(|(path, _, list)| {
1440 list.iter()
1441 .position(|p| matches!(&p.content, ParagraphContent::Ink(i) if i.id == id))
1442 .map(|index| (path, index))
1443 })
1444 .ok_or_else(|| invalid("The ink is not on the page"))?;
1445 let ParagraphContent::Ink(ink) = &mut list_at(page, &path)[index].content else {
1446 unreachable!()
1447 };
1448 Ok(ink)
1449}
1450
1451/// The text object of the page title, when it has one.
1452fn find_title_text(page: &Page) -> Option<ExGuid> {
1453 page.objects
1454 .iter()
1455 .filter_map(|object| match object {
1456 PageObject::Title(title) => Some(title),
1457 _ => None,
1458 })
1459 .flat_map(|title| title.outlines.iter().filter(|o| Some(o.id) != title.date))
1460 .flat_map(|outline| &outline.paragraphs)
1461 .filter_map(|p| p.text().filter(|t| t.date_field.is_none()))
1462 .map(|t| t.id)
1463 .next_back()
1464}
1465
1466fn text_of(page: &Page, text: ExGuid) -> String {
1467 lists(page)
1468 .into_iter()
1469 .find_map(|(_, _, list)| {
1470 list.iter().find_map(|p| {
1471 p.text()
1472 .filter(|t| t.id == text)
1473 .map(|t| t.text.text().to_owned())
1474 })
1475 })
1476 .unwrap_or_default()
1477}