| 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 | |
| 4 | use super::{PageOp, TableEdit, TextProperty, content::NATIVE_INDENTS, lower::format_in}; |
| 5 | use crate::{ |
| 6 | Error, ExGuid, OutlineEdit, TextAttribute, |
| 7 | document::{Format, Kind}, |
| 8 | page::{ |
| 9 | Outline, Page, PageObject, PageParagraph, Paragraph, ParagraphContent, Table, TextObject, |
| 10 | }, |
| 11 | }; |
| 12 | use std::collections::BTreeSet; |
| 13 | |
| 14 | fn 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. |
| 19 | fn 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)] |
| 26 | pub(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). |
| 33 | pub(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 | |
| 77 | pub(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. |
| 94 | fn 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. |
| 104 | fn 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`. |
| 117 | fn 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 | |
| 127 | pub(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 | |
| 133 | pub(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 | |
| 140 | pub(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. |
| 150 | pub(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. |
| 157 | pub(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. |
| 187 | pub(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. |
| 205 | fn 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. |
| 214 | pub(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. |
| 229 | pub(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 | |
| 236 | fn 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 | |
| 242 | fn 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 | |
| 247 | fn 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. |
| 264 | pub(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. |
| 295 | fn 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 | |
| 310 | fn 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 | |
| 343 | fn 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. |
| 364 | pub(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. |
| 421 | fn 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. |
| 455 | fn 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 | |
| 475 | fn 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. |
| 484 | fn 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`. |
| 514 | fn 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`. |
| 538 | fn 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`. |
| 543 | fn 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. |
| 553 | fn 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 | |
| 570 | fn 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. |
| 606 | pub 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 | |
| 636 | fn 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. |
| 1285 | fn 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. |
| 1295 | fn 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 | |
| 1339 | fn 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. |
| 1352 | fn 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 | |
| 1367 | fn 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 | |
| 1375 | fn 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 | |
| 1385 | fn 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 | |
| 1406 | fn 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 | |
| 1427 | fn 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. |
| 1449 | fn 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 | |
| 1463 | fn 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 | } |