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
5use 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};
13use serde::{Deserialize, Serialize};
14use std::ops::Range;
15
16mod apply;
17pub(crate) use apply::Failure;
18pub(crate) mod content;
19pub(crate) mod levels;
20pub(crate) mod lower;
21pub(crate) mod model;
22pub(crate) mod properties;
23pub(crate) mod table;
24#[cfg(test)]
25pub(crate) mod tests;
26
27pub 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.
30pub use model::apply as predict;
31
32/// Property identifiers with their encoded values.
33pub(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)]
38pub 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)]
46pub 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)]
55pub 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)]
219pub 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)]
246pub 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.
289pub 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
325impl 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)]
338pub 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
355impl 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)]
378pub 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
391impl 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
402impl std::error::Error for OpError {}