| 1 | //! Object-level edits of a section: what an editor emits, a queue stores and |
| 2 | //! `Section::apply` writes. Identities of new objects are the emitter's, so applying an edit |
| 3 | //! to another image of the section creates the same objects; UTF-16 code units measure text. |
| 4 | |
| 5 | use crate::{ |
| 6 | ExGuid, OutlineEdit, PageCreation, PageEdit, TextAttribute, |
| 7 | document::{Layout, Tag}, |
| 8 | page::{ |
| 9 | Definition, InkStroke, MediaIndex, Page, PageObject, PageParagraph, Paragraph, TableCell, |
| 10 | TableColumn, TableRow, |
| 11 | }, |
| 12 | }; |
| 13 | use serde::{Deserialize, Serialize}; |
| 14 | use std::ops::Range; |
| 15 | |
| 16 | mod apply; |
| 17 | pub(crate) use apply::Failure; |
| 18 | pub(crate) mod content; |
| 19 | pub(crate) mod levels; |
| 20 | pub(crate) mod lower; |
| 21 | pub(crate) mod model; |
| 22 | pub(crate) mod properties; |
| 23 | pub(crate) mod table; |
| 24 | #[cfg(test)] |
| 25 | pub(crate) mod tests; |
| 26 | |
| 27 | pub use lower::{lower, lower_page}; |
| 28 | /// `op` applied to `page`, the model of what the section holds: the page `Section::apply` |
| 29 | /// leaves, as reading it back shows it. |
| 30 | pub use model::apply as predict; |
| 31 | |
| 32 | /// Property identifiers with their encoded values. |
| 33 | pub(crate) type Values = Vec<(u32, Vec<u8>)>; |
| 34 | |
| 35 | /// One user action, applied whole or not at all; it leaves every table cell a paragraph. |
| 36 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] |
| 37 | #[serde(deny_unknown_fields)] |
| 38 | pub struct Edit { |
| 39 | /// FILETIME when the action happened; the modification time of what it changes. |
| 40 | pub at: u64, |
| 41 | pub ops: Vec<Op>, |
| 42 | } |
| 43 | |
| 44 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] |
| 45 | #[serde(deny_unknown_fields)] |
| 46 | pub enum Op { |
| 47 | Page { space: ExGuid, op: PageOp }, |
| 48 | Section(SectionOp), |
| 49 | } |
| 50 | |
| 51 | /// A change to the page an object space holds. Targets are stored identities; paragraph |
| 52 | /// properties name the paragraph, text edits its text object. |
| 53 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] |
| 54 | #[serde(deny_unknown_fields)] |
| 55 | pub enum PageOp { |
| 56 | /// Replaces a range; inserted text takes the format of the run it lands in, the |
| 57 | /// following run at a boundary except at the end. |
| 58 | Text { |
| 59 | text: ExGuid, |
| 60 | range: Range<u32>, |
| 61 | with: String, |
| 62 | }, |
| 63 | /// Sets and clears character formatting over a range; an empty text's `0..0` is its |
| 64 | /// insertion format. Cleared properties are inherited again. |
| 65 | Format { |
| 66 | text: ExGuid, |
| 67 | range: Range<u32>, |
| 68 | set: Vec<TextAttribute>, |
| 69 | clear: Vec<TextProperty>, |
| 70 | }, |
| 71 | /// Makes the range a hyperlink to `target`, its field code stored hidden before the |
| 72 | /// label, or with `None` removes the link around it, leaving the label as plain text. |
| 73 | Link { |
| 74 | text: ExGuid, |
| 75 | range: Range<u32>, |
| 76 | target: Option<String>, |
| 77 | }, |
| 78 | /// Rewrites an equation's linear text and spans whole. |
| 79 | Equation { |
| 80 | text: ExGuid, |
| 81 | math: Paragraph, |
| 82 | }, |
| 83 | /// The page's date: its creation time, FILETIME, and the text each of the title's date |
| 84 | /// and time fields shows for it, as OneNote 2010 writes both when the date changes. |
| 85 | Date { |
| 86 | created: u64, |
| 87 | fields: Vec<(ExGuid, String)>, |
| 88 | }, |
| 89 | /// The page's colour (View, Page Color), COLORREF; `None` is OneNote's "No color". |
| 90 | Color(Option<u32>), |
| 91 | /// The page's rule lines (View, Rule Lines); `None` is OneNote's "None". |
| 92 | RuleLines(Option<crate::page::RuleLines>), |
| 93 | /// Inserts paragraphs before a direct child of `container`, or last. Children follow |
| 94 | /// their parents in `paragraphs`; levels are absolute. Text keeps its spans' formats; |
| 95 | /// lists, tags, styles and collapse state are set by their own ops. |
| 96 | Insert { |
| 97 | container: ExGuid, |
| 98 | before: Option<ExGuid>, |
| 99 | paragraphs: Vec<PageParagraph>, |
| 100 | }, |
| 101 | /// Enter at `at`: the text left of it stays, the rest moves to a new paragraph `paragraph` |
| 102 | /// with text object `right` and a copy of each list node under `lists`, in order. |
| 103 | Split { |
| 104 | text: ExGuid, |
| 105 | at: u32, |
| 106 | paragraph: ExGuid, |
| 107 | right: ExGuid, |
| 108 | lists: Vec<ExGuid>, |
| 109 | }, |
| 110 | /// Appends the right text to the left one and removes the right paragraph; an empty |
| 111 | /// left text is replaced by the right text object. |
| 112 | Join { |
| 113 | left: ExGuid, |
| 114 | right: ExGuid, |
| 115 | }, |
| 116 | /// Moves a subtree before a direct child of `parent`, or last; `None` names the page, |
| 117 | /// whose children are outlines, pictures, files and ink. A paragraph keeps its level where it |
| 118 | /// lies deeper than its new parent. |
| 119 | Move { |
| 120 | object: ExGuid, |
| 121 | parent: Option<ExGuid>, |
| 122 | before: Option<ExGuid>, |
| 123 | }, |
| 124 | /// Removes a subtree. |
| 125 | Delete { |
| 126 | object: ExGuid, |
| 127 | }, |
| 128 | /// Sets a paragraph's absolute outline level, regrouping its container. |
| 129 | Level { |
| 130 | paragraph: ExGuid, |
| 131 | level: u32, |
| 132 | }, |
| 133 | /// Outline position and width, or a paragraph's saved expansion state. |
| 134 | Outline { |
| 135 | object: ExGuid, |
| 136 | edit: OutlineEdit, |
| 137 | }, |
| 138 | /// Paragraph formatting stored on a paragraph's text; `None` leaves a value as it is. |
| 139 | Paragraph { |
| 140 | paragraph: ExGuid, |
| 141 | alignment: Option<u8>, |
| 142 | rtl: Option<bool>, |
| 143 | space_before: Option<f32>, |
| 144 | space_after: Option<f32>, |
| 145 | line_spacing: Option<f32>, |
| 146 | language: Option<u32>, |
| 147 | }, |
| 148 | /// References a paragraph style, created from its definition where the page lacks it. |
| 149 | Style { |
| 150 | paragraph: ExGuid, |
| 151 | style: ExGuid, |
| 152 | definition: Definition, |
| 153 | }, |
| 154 | /// Takes a paragraph's style away; values its style gave are cleared from its text, as |
| 155 | /// `Restyle` clears them. |
| 156 | Unstyle { |
| 157 | paragraph: ExGuid, |
| 158 | }, |
| 159 | /// Moves every paragraph of style `style` to style `into`, created from `definition` |
| 160 | /// (the same name) where the page lacks it: style objects are read-only, so a restyled |
| 161 | /// style is a new one. Run and paragraph values equal to what `style` gave are cleared, |
| 162 | /// so they follow `into`; values set otherwise stay. |
| 163 | Restyle { |
| 164 | style: ExGuid, |
| 165 | into: ExGuid, |
| 166 | definition: Definition, |
| 167 | }, |
| 168 | /// Links a paragraph's text or file to a moment in recordings on the page, or with |
| 169 | /// empty `media` unlinks it. |
| 170 | Media { |
| 171 | paragraph: ExGuid, |
| 172 | media: MediaIndex, |
| 173 | }, |
| 174 | /// Replaces a paragraph's list nodes; a node another paragraph owns is copied. |
| 175 | List { |
| 176 | paragraph: ExGuid, |
| 177 | lists: Vec<(ExGuid, Definition)>, |
| 178 | }, |
| 179 | /// Replaces the note tags of a paragraph, its text or a table; tag definitions the page |
| 180 | /// lacks are created from `definitions`. |
| 181 | Tags { |
| 182 | target: ExGuid, |
| 183 | tags: Vec<Tag>, |
| 184 | definitions: Vec<(ExGuid, Definition)>, |
| 185 | }, |
| 186 | /// Adds an outline with its paragraphs, a picture or file with its payloads, or ink, |
| 187 | /// before a page child or on top. |
| 188 | Add { |
| 189 | object: PageObject, |
| 190 | before: Option<ExGuid>, |
| 191 | }, |
| 192 | /// A stored picture's position, displayed size and description. |
| 193 | Picture { |
| 194 | picture: ExGuid, |
| 195 | layout: Layout, |
| 196 | alt: Option<String>, |
| 197 | }, |
| 198 | /// A stored attachment's shown name, recorded source path and icon size. |
| 199 | Attachment { |
| 200 | attachment: ExGuid, |
| 201 | filename: String, |
| 202 | source_path: Option<String>, |
| 203 | size: Option<[f32; 2]>, |
| 204 | }, |
| 205 | /// Adds and erases whole strokes of stored ink. |
| 206 | Strokes { |
| 207 | ink: ExGuid, |
| 208 | add: Vec<InkStroke>, |
| 209 | remove: Vec<ExGuid>, |
| 210 | }, |
| 211 | Table { |
| 212 | table: ExGuid, |
| 213 | edit: TableEdit, |
| 214 | }, |
| 215 | } |
| 216 | |
| 217 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] |
| 218 | #[serde(deny_unknown_fields)] |
| 219 | pub enum TableEdit { |
| 220 | /// New rows before a row, or last, each with one cell per column. |
| 221 | Rows { |
| 222 | before: Option<ExGuid>, |
| 223 | rows: Vec<TableRow>, |
| 224 | }, |
| 225 | /// A new column at `at`: one cell per row, in row order. |
| 226 | Column { |
| 227 | at: u32, |
| 228 | width: f32, |
| 229 | cells: Vec<TableCell>, |
| 230 | }, |
| 231 | DeleteRow(ExGuid), |
| 232 | DeleteColumn(u32), |
| 233 | /// Every column's width and lock. |
| 234 | Columns(Vec<TableColumn>), |
| 235 | Borders(bool), |
| 236 | /// A cell's shading (`None` clears it) and indentation table. |
| 237 | Cell { |
| 238 | cell: ExGuid, |
| 239 | shading: Option<u32>, |
| 240 | indents: Vec<f32>, |
| 241 | }, |
| 242 | } |
| 243 | |
| 244 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] |
| 245 | #[serde(deny_unknown_fields)] |
| 246 | pub enum SectionOp { |
| 247 | Create(PageCreation), |
| 248 | /// A page created with `creation` holding `page`'s content under the identities it |
| 249 | /// carries (`Page::copy` gives fresh ones); the created title stays. |
| 250 | Import { |
| 251 | creation: PageCreation, |
| 252 | page: Page, |
| 253 | }, |
| 254 | /// A read-only conflict page of the listed page `of`, as OneNote 2010 keeps the version of |
| 255 | /// a page a merge could not take: created with `creation` (not placed, its author the user |
| 256 | /// whose version it is, a title only where `page` has one) and holding `page` as `Import` |
| 257 | /// does, with `objects`, identities on `page`, marked as the conflicting ones. |
| 258 | Conflict { |
| 259 | of: ExGuid, |
| 260 | creation: PageCreation, |
| 261 | page: Page, |
| 262 | objects: Vec<ExGuid>, |
| 263 | }, |
| 264 | /// Page moves and indentation, in order. |
| 265 | Pages(Vec<PageEdit>), |
| 266 | /// Removes pages permanently, listed or conflict pages; their revisions stay stored. |
| 267 | Delete(Vec<ExGuid>), |
| 268 | /// The section's colour as COLORREF, kept in its own metadata; `None` is OneNote's |
| 269 | /// "no colour". |
| 270 | Color(Option<u32>), |
| 271 | /// Makes a version of a page (`Section::versions`) its current state, as OneNote 2010's |
| 272 | /// Restore Version does; the page as it stood becomes the newest version. New objects take |
| 273 | /// identities from `guid`. |
| 274 | RestoreVersion { |
| 275 | page: ExGuid, |
| 276 | version: ExGuid, |
| 277 | guid: [u8; 16], |
| 278 | }, |
| 279 | /// Deletes versions of a page, as OneNote 2010's Delete Version does; their revisions stay |
| 280 | /// stored. |
| 281 | DeleteVersions { |
| 282 | page: ExGuid, |
| 283 | versions: Vec<ExGuid>, |
| 284 | }, |
| 285 | } |
| 286 | |
| 287 | /// The `Restyle` ops giving the paragraphs of every style of `page` named in `sheet` |
| 288 | /// that definition instead, one per style that differs; styles sharing a name become one. |
| 289 | pub fn restyle( |
| 290 | page: &Page, |
| 291 | sheet: &std::collections::BTreeMap<String, Definition>, |
| 292 | ) -> Result<Vec<PageOp>, crate::Error> { |
| 293 | let mut into = std::collections::BTreeMap::new(); |
| 294 | let mut ops = Vec::new(); |
| 295 | for (id, stored) in &page.definitions { |
| 296 | let crate::document::Kind::Style { |
| 297 | name: Some(name), .. |
| 298 | } = &stored.kind |
| 299 | else { |
| 300 | continue; |
| 301 | }; |
| 302 | let Some(definition) = sheet.get(name).filter(|definition| *definition != stored) else { |
| 303 | continue; |
| 304 | }; |
| 305 | let target = match into.get(name) { |
| 306 | Some(target) => *target, |
| 307 | None => { |
| 308 | let target = page |
| 309 | .definitions |
| 310 | .iter() |
| 311 | .find(|(_, kept)| *kept == definition) |
| 312 | .map_or_else(crate::page::text::new_id, |(id, _)| Ok(*id))?; |
| 313 | *into.entry(name.clone()).or_insert(target) |
| 314 | } |
| 315 | }; |
| 316 | ops.push(PageOp::Restyle { |
| 317 | style: *id, |
| 318 | into: target, |
| 319 | definition: definition.clone(), |
| 320 | }); |
| 321 | } |
| 322 | Ok(ops) |
| 323 | } |
| 324 | |
| 325 | impl SectionOp { |
| 326 | /// `RestoreVersion` of `page`'s `version`, its new objects under a fresh identity. |
| 327 | pub fn restore(page: ExGuid, version: ExGuid) -> Result<Self, crate::Error> { |
| 328 | Ok(Self::RestoreVersion { |
| 329 | page, |
| 330 | version, |
| 331 | guid: crate::write::fresh_guid()?, |
| 332 | }) |
| 333 | } |
| 334 | } |
| 335 | |
| 336 | /// A character property that `PageOp::Format` can clear so the text inherits it. |
| 337 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 338 | pub enum TextProperty { |
| 339 | Bold, |
| 340 | Italic, |
| 341 | Underline, |
| 342 | Strike, |
| 343 | Superscript, |
| 344 | Subscript, |
| 345 | Hidden, |
| 346 | Hyperlink, |
| 347 | HyperlinkLabel, |
| 348 | Math, |
| 349 | Font, |
| 350 | FontSize, |
| 351 | Color, |
| 352 | Highlight, |
| 353 | } |
| 354 | |
| 355 | impl TextProperty { |
| 356 | pub(crate) fn id(self) -> u32 { |
| 357 | match self { |
| 358 | Self::Bold => 0x08001c04, |
| 359 | Self::Italic => 0x08001c05, |
| 360 | Self::Underline => 0x08001c06, |
| 361 | Self::Strike => 0x08001c07, |
| 362 | Self::Superscript => 0x08001c08, |
| 363 | Self::Subscript => 0x08001c09, |
| 364 | Self::Hidden => 0x08001e16, |
| 365 | Self::Hyperlink => 0x08001e14, |
| 366 | Self::HyperlinkLabel => 0x08001e19, |
| 367 | Self::Math => 0x08003401, |
| 368 | Self::Font => 0x1c001c0a, |
| 369 | Self::FontSize => 0x10001c0b, |
| 370 | Self::Color => 0x14001c0c, |
| 371 | Self::Highlight => 0x14001c0d, |
| 372 | } |
| 373 | } |
| 374 | } |
| 375 | |
| 376 | /// Why `Section::apply` refused an edit; the section is as it was before it. |
| 377 | #[derive(Clone, Debug, PartialEq)] |
| 378 | pub enum OpError { |
| 379 | /// An object an op names is not reachable in its space's active revision. |
| 380 | TargetUnavailable(ExGuid), |
| 381 | /// A new object's identity is already reachable. |
| 382 | DuplicateIdentity(ExGuid), |
| 383 | /// An anchor is no direct child of its container, or a move would make a cycle. |
| 384 | StructureChanged(&'static str), |
| 385 | /// The writers cannot make this change to this content. |
| 386 | Unsupported(&'static str), |
| 387 | /// Writing failed after the section began changing; it must be reopened. |
| 388 | Failed(crate::Error), |
| 389 | } |
| 390 | |
| 391 | impl std::fmt::Display for OpError { |
| 392 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 393 | match self { |
| 394 | Self::TargetUnavailable(id) => write!(f, "{id} is not on the page"), |
| 395 | Self::DuplicateIdentity(id) => write!(f, "{id} already exists"), |
| 396 | Self::StructureChanged(message) | Self::Unsupported(message) => f.write_str(message), |
| 397 | Self::Failed(error) => write!(f, "{error}"), |
| 398 | } |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | impl std::error::Error for OpError {} |