1//! Paragraph properties: list nodes, note tags, paragraph styles and paragraph formatting.
2
3use super::Values;
4use crate::{
5 Error, ExGuid, ObjectData, PropertySets, Value,
6 active::{ActivePage, Changes},
7 document::{Kind, Tag},
8 page::{Definition, MediaIndex},
9 write::PropertyObject,
10};
11use std::collections::{BTreeMap, BTreeSet};
12
13fn invalid(message: &'static str) -> Error {
14 Error { offset: 0, message }
15}
16
17/// The properties of a list node carrying `definition`.
18pub(crate) fn list_values(definition: &Definition) -> Result<Values, Error> {
19 let Kind::List {
20 font,
21 format,
22 restart,
23 bullet,
24 } = &definition.kind
25 else {
26 return Err(invalid("A paragraph list must reference a list definition"));
27 };
28 if bullet.is_some() && (format.is_none() || font.is_none()) {
29 return Err(invalid("A bullet definition names its glyph and font"));
30 }
31 let mut values: Values = Vec::new();
32 if let Some(format) = format {
33 let units: Vec<u16> = format.encode_utf16().collect();
34 let count = u16::try_from(units.len())
35 .map_err(|_| invalid("List format exceeds the document range"))?;
36 let mut bytes = count.to_le_bytes().to_vec();
37 bytes.extend(units.iter().flat_map(|unit| unit.to_le_bytes()));
38 values.push((0x1c001c1a, bytes));
39 }
40 if let Some(font) = font {
41 values.push((0x1c001c52, crate::create::string(font)));
42 }
43 if let Some(restart) = restart {
44 values.push((0x14001cb7, restart.to_le_bytes().to_vec()));
45 }
46 if let Some(bullet) = bullet {
47 values.push((0x10001d0e, bullet.to_le_bytes().to_vec()));
48 // Every native bullet node carries this cleared flag alongside its index.
49 values.push((0x0c001cc0, vec![0]));
50 }
51 let style = &definition.format;
52 if let Some(font) = &style.font {
53 values.push((0x1c001c0a, crate::create::string(font)));
54 }
55 if let Some(size) = style.font_size {
56 let half = (size * 2.0).round();
57 if !(0.0..=f32::from(u16::MAX)).contains(&half) {
58 return Err(invalid("List font size is outside the document range"));
59 }
60 values.push((0x10001c0b, (half as u16).to_le_bytes().to_vec()));
61 }
62 if let Some(color) = style.color {
63 values.push((0x14001c0c, color.to_le_bytes().to_vec()));
64 }
65 if let Some(language) = style.language {
66 values.push((0x14001c3b, language.to_le_bytes().to_vec()));
67 }
68 for (flag, id) in [(style.bold, 0x08001c04), (style.italic, 0x08001c05)] {
69 if let Some(flag) = flag {
70 values.push((id | (u32::from(flag) << 31), Vec::new()));
71 }
72 }
73 Ok(values)
74}
75
76/// Gives `paragraph` the list nodes `nodes` names, rewriting stored ones to their values
77/// and creating the rest.
78pub(crate) fn list_changes(
79 active: &ActivePage<'_>,
80 paragraph: ExGuid,
81 nodes: &[(ExGuid, Values)],
82) -> Result<Changes, Error> {
83 let parents = active.editable_parents(paragraph)?;
84 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
85 let raw = &active.live.revision;
86 let mut changed = BTreeMap::new();
87 let mut target = PropertyObject::from_object(&raw.objects[&paragraph])?;
88 let mut references = Vec::new();
89 for (list, values) in nodes {
90 let mut node = match raw.objects.get(list) {
91 Some(existing) => {
92 if existing.jcid != 0x60012 {
93 return Err(invalid(
94 "A list definition identity belongs to another object",
95 ));
96 }
97 let mut node = PropertyObject::from_object(existing)?;
98 node.remove(&[
99 0x1c001c1a, 0x1c001c52, 0x14001cb7, 0x10001d0e, 0x0c001cc0, 0x1c001c0a,
100 0x10001c0b, 0x14001c0c, 0x14001c3b, 0x08001c04, 0x08001c05,
101 ])?;
102 node
103 }
104 None => PropertyObject {
105 jcid: 0x60012,
106 bytes: crate::create::properties(&[])?,
107 global_ids: std::sync::Arc::new(BTreeMap::from([(0, list.guid)])),
108 },
109 };
110 node.set(
111 &values
112 .iter()
113 .map(|(id, bytes)| (*id, bytes.as_slice()))
114 .collect::<Vec<_>>(),
115 )?;
116 node.set(&[(0x14001d7a, &modified)])?;
117 node.reference(*list)?;
118 references.extend_from_slice(&target.reference(*list)?);
119 changed.insert(*list, node);
120 }
121 if references.is_empty() {
122 target.remove(&[0x24001c26])?;
123 } else {
124 target.set(&[(0x24001c26, &references)])?;
125 }
126 target.set(&[(0x14001d7a, &modified)])?;
127 changed.insert(paragraph, target);
128 crate::formatting::touch_ancestors(raw, parents, paragraph, &modified, &mut changed)?;
129 Ok(changed)
130}
131
132/// The properties of a new tag definition object.
133fn tag_definition_values(definition: &Definition) -> Result<Values, Error> {
134 let Kind::TagDefinition {
135 label,
136 action_type,
137 shape,
138 color,
139 highlight,
140 } = &definition.kind
141 else {
142 return Err(invalid("A note tag must reference a tag definition"));
143 };
144 // NoteTagPropertyStatus: hasLabel, hasFontColor, hasHighlightColor and hasIcon, as
145 // OneNote's Customize Tags writes them.
146 let status = u32::from(label.is_some())
147 | u32::from(color.is_some()) << 1
148 | u32::from(highlight.is_some()) << 2
149 | u32::from(shape.is_some_and(|shape| shape != 0)) << 3;
150 let mut values: Values = vec![
151 (0x0c003473, vec![0]),
152 (0x10003463, action_type.unwrap_or(0).to_le_bytes().to_vec()),
153 (0x10003464, shape.unwrap_or(0).to_le_bytes().to_vec()),
154 (0x14003467, status.to_le_bytes().to_vec()),
155 ];
156 if let Some(label) = label {
157 values.push((0x1c003468, crate::create::string(label)));
158 }
159 if let Some(color) = color {
160 values.push((0x14003466, color.to_le_bytes().to_vec()));
161 }
162 if let Some(highlight) = highlight {
163 values.push((0x14003465, highlight.to_le_bytes().to_vec()));
164 }
165 Ok(values)
166}
167
168/// Replaces the note tags of `object`. A normal tag comes with its definition where the page
169/// lacks it; a task tag has none and stores its own icon, in the order OneNote writes it.
170pub(crate) fn tag_changes(
171 active: &ActivePage<'_>,
172 object: ExGuid,
173 tags: &[(&Tag, Option<&Definition>)],
174) -> Result<Changes, Error> {
175 let raw = &active.live.revision;
176 let mut definitions: Vec<(ExGuid, Option<Values>)> = Vec::new();
177 let mut sets: Vec<(Option<ExGuid>, Values)> = Vec::new();
178 let mut action_types = BTreeSet::new();
179 for (tag, definition) in tags {
180 let dates = [
181 (0x1400346e, tag.created),
182 (0x1400346f, tag.completed),
183 (0x1400346a, tag.start),
184 (0x1400346b, tag.due),
185 ];
186 let mut fields: Values = Vec::new();
187 if tag.status & 4 != 0 {
188 let (Some(action_type), Some(shape), Some(property_status)) =
189 (tag.action_type, tag.shape, tag.property_status)
190 else {
191 return Err(invalid(
192 "A task tag stores its action type, icon and property status",
193 ));
194 };
195 if !action_types.insert(action_type) {
196 return Err(invalid("An element holds one note tag per action type"));
197 }
198 fields.extend([
199 (0x0c003473, vec![0]),
200 (0x10003463, action_type.to_le_bytes().to_vec()),
201 (0x10003464, shape.to_le_bytes().to_vec()),
202 (0x14003467, property_status.to_le_bytes().to_vec()),
203 ]);
204 if let Some(task) = tag.task_id {
205 fields.push((0x1c003469, task.to_vec()));
206 }
207 for (id, value) in [dates[2], dates[3], dates[0], dates[1]] {
208 if let Some(value) = value {
209 fields.push((id, value.to_le_bytes().to_vec()));
210 }
211 }
212 fields.push((0x10003470, tag.status.to_le_bytes().to_vec()));
213 sets.push((None, fields));
214 continue;
215 }
216 let written = tag
217 .definition
218 .ok_or_else(|| invalid("A note tag must reference a tag definition"))?;
219 let action_type = match definition.map(|d| &d.kind) {
220 Some(Kind::TagDefinition { action_type, .. }) => *action_type,
221 _ => return Err(invalid("A note tag must reference a tag definition")),
222 };
223 if !action_types.insert(action_type.unwrap_or(0)) {
224 return Err(invalid("An element holds one note tag per action type"));
225 }
226 if !definitions.iter().any(|(id, _)| *id == written) {
227 let values = if raw.objects.contains_key(&written) {
228 None
229 } else {
230 let definition = definition
231 .ok_or_else(|| invalid("A note tag references a missing tag definition"))?;
232 Some(tag_definition_values(definition)?)
233 };
234 definitions.push((written, values));
235 }
236 if let Some(action_type) = tag.action_type {
237 fields.push((0x10003463, action_type.to_le_bytes().to_vec()));
238 }
239 for (id, value) in dates {
240 if let Some(value) = value {
241 fields.push((id, value.to_le_bytes().to_vec()));
242 }
243 }
244 fields.push((0x10003470, tag.status.to_le_bytes().to_vec()));
245 if let Some(task) = tag.task_id {
246 fields.push((0x1c003469, task.to_vec()));
247 }
248 sets.push((Some(written), fields));
249 }
250 let parents = active.editable_parents(object)?;
251 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
252 let mut changed = BTreeMap::new();
253 for (id, values) in &definitions {
254 let Some(values) = values else {
255 continue;
256 };
257 let mut node = PropertyObject {
258 jcid: 0x120043,
259 bytes: crate::create::properties(values)?,
260 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),
261 };
262 node.reference(*id)?;
263 changed.insert(*id, node);
264 }
265 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
266 let mut encoded = Vec::new();
267 for (definition, fields) in &sets {
268 let mut set = Vec::new();
269 if let Some(definition) = definition {
270 let reference = target.reference(*definition)?;
271 set.push((0x20003488, reference.to_vec()));
272 }
273 set.extend(fields.iter().cloned());
274 encoded.push(set);
275 }
276 target.set_sets(0x40003489, 0x44000811, &encoded)?;
277 target.set(&[(0x14001d7a, &modified)])?;
278 changed.insert(object, target);
279 crate::formatting::touch_ancestors(raw, parents, object, &modified, &mut changed)?;
280 Ok(changed)
281}
282
283/// Makes `text` reference paragraph style `style`, created from `definition` where the
284/// page does not store it.
285pub(crate) fn style_changes(
286 active: &ActivePage<'_>,
287 text: ExGuid,
288 style: ExGuid,
289 definition: Option<&Definition>,
290) -> Result<Changes, Error> {
291 let raw = &active.live.revision;
292 let mut changed = BTreeMap::new();
293 if !raw.objects.contains_key(&style) {
294 let definition = definition
295 .ok_or_else(|| invalid("A paragraph references a missing style definition"))?;
296 let values = style_object_values(definition)?;
297 let mut node = PropertyObject {
298 jcid: 0x12004d,
299 bytes: crate::create::properties(&values)?,
300 global_ids: std::sync::Arc::new(BTreeMap::from([(0, style.guid)])),
301 };
302 node.reference(style)?;
303 changed.insert(style, node);
304 }
305 let mut target = PropertyObject::from_object(&raw.objects[&text])?;
306 let reference = target.reference(style)?;
307 target.set(&[(0x2000342c, &reference)])?;
308 changed.insert(text, target);
309 Ok(changed)
310}
311
312/// Run properties to clear, by text and run range.
313pub(crate) type Clears = Vec<(ExGuid, std::ops::Range<u32>, Vec<super::TextProperty>)>;
314/// Paragraph properties to remove, by text.
315pub(crate) type Spacing = Vec<(ExGuid, Vec<u32>)>;
316
317/// Style-level values the texts of paragraph style `style` (or only text `only`) repeat:
318/// run properties equal to the style's, by run range, and each text's paragraph properties
319/// equal to the style's. Texts whose runs carry objects of their own stay as they are.
320pub(crate) fn restyle_plan(
321 active: &ActivePage<'_>,
322 style: ExGuid,
323 only: Option<ExGuid>,
324) -> Result<(Clears, Spacing), Error> {
325 use super::TextProperty::*;
326 const CHARACTER: [super::TextProperty; 10] = [
327 Bold,
328 Italic,
329 Underline,
330 Strike,
331 Superscript,
332 Subscript,
333 Font,
334 FontSize,
335 Color,
336 Highlight,
337 ];
338 const PARAGRAPH: [u32; 4] = [0x0c003477, 0x1400342e, 0x1400342f, 0x14003430];
339 let raw = &active.live.revision;
340 let values = |id: ExGuid| -> Result<BTreeMap<u32, (u32, Vec<u8>)>, Error> {
341 let ObjectData::Properties(bytes) = raw.objects[&id].data else {
342 return Err(invalid("A style holds no properties"));
343 };
344 Ok(PropertySets::parse(bytes)?.sets[0]
345 .iter()
346 .map(|property| {
347 let value = match property.value {
348 Value::Bytes(bytes) => bytes.to_vec(),
349 _ => Vec::new(),
350 };
351 (property.id & 0x7fffffff, (property.id, value))
352 })
353 .collect())
354 };
355 let stored = values(style)?;
356 let repeats = |own: &BTreeMap<u32, (u32, Vec<u8>)>, key: u32| {
357 own.get(&key)
358 .is_some_and(|value| stored.get(&key) == Some(value))
359 };
360 let mut clears = Vec::new();
361 let mut spacing = Vec::new();
362 for (&text, node) in &active.view.nodes {
363 let Kind::RichText {
364 text: characters,
365 runs,
366 paragraph_style: Some(paragraph_style),
367 boilerplate: false,
368 } = &node.kind
369 else {
370 continue;
371 };
372 if *paragraph_style != style || only.is_some_and(|only| only != text) {
373 continue;
374 }
375 let own = values(text)?;
376 if own.contains_key(&0x24003458) {
377 continue;
378 }
379 let repeated: Vec<u32> = PARAGRAPH
380 .into_iter()
381 .filter(|key| repeats(&own, *key))
382 .collect();
383 if !repeated.is_empty() {
384 spacing.push((text, repeated));
385 }
386 let mut pending: Option<(std::ops::Range<u32>, Vec<super::TextProperty>)> = None;
387 for run in runs {
388 let cleared: Vec<super::TextProperty> = match run.format {
389 Some(id) => {
390 let own = values(id)?;
391 CHARACTER
392 .into_iter()
393 .filter(|property| repeats(&own, property.id()))
394 .collect()
395 }
396 None => Vec::new(),
397 };
398 let range = if characters.is_empty() {
399 0..0
400 } else {
401 run.start..run.end
402 };
403 match &mut pending {
404 Some((open, kept)) if *kept == cleared && open.end == range.start => {
405 open.end = range.end;
406 }
407 _ => {
408 if let Some((range, clear)) = pending.take() {
409 clears.push((text, range, clear));
410 }
411 pending = Some((range, cleared));
412 }
413 }
414 }
415 clears.extend(pending.map(|(range, clear)| (text, range, clear)));
416 }
417 clears.retain(|(_, _, clear)| !clear.is_empty());
418 Ok((clears, spacing))
419}
420
421/// The texts of paragraph style `style` moved to style `into`, created from `definition`
422/// where the page lacks it, each without its `spacing` properties.
423pub(crate) fn restyle_changes(
424 active: &ActivePage<'_>,
425 style: ExGuid,
426 into: ExGuid,
427 definition: &Definition,
428 spacing: &[(ExGuid, Vec<u32>)],
429) -> Result<Changes, Error> {
430 let mut changed = BTreeMap::new();
431 for (&text, node) in &active.view.nodes {
432 if !matches!(node.kind, Kind::RichText { paragraph_style: Some(id), .. } if id == style) {
433 continue;
434 }
435 let mut moved = style_changes(active, text, into, Some(definition))?;
436 let mut target = moved.remove(&text).expect("the text is restyled");
437 if let Some((_, ids)) = spacing.iter().find(|(id, _)| *id == text) {
438 target.remove(ids)?;
439 }
440 changed.insert(text, target);
441 changed.extend(moved);
442 }
443 Ok(changed)
444}
445
446/// The properties of a paragraph style object holding `definition`: NextStyle first, as
447/// OneNote 2010 writes it, then the character formatting, the name and paragraph spacing.
448pub(crate) fn style_object_values(definition: &Definition) -> Result<Values, Error> {
449 let Kind::Style { name, next } = &definition.kind else {
450 return Err(invalid("A paragraph style must be a style definition"));
451 };
452 let format = &definition.format;
453 let mut values = Values::new();
454 if let Some(next) = next {
455 values.push((0x1c00348a, crate::create::string(next)));
456 }
457 values.extend(super::content::style_values(format));
458 if let Some(name) = name {
459 values.push((0x1c00345a, crate::create::string(name)));
460 }
461 if let Some(alignment) = format.alignment {
462 values.push((0x0c003477, vec![alignment]));
463 }
464 for (property, value) in [
465 (0x1400342e, format.space_before),
466 (0x1400342f, format.space_after),
467 (0x14003430, format.line_spacing),
468 ] {
469 if let Some(points) = value {
470 values.push((property, (points / 36.0).to_le_bytes().to_vec()));
471 }
472 }
473 Ok(values)
474}
475
476/// The text-object properties of paragraph formatting, in the order they are written.
477pub(crate) fn paragraph_values(
478 alignment: Option<u8>,
479 rtl: Option<bool>,
480 space_before: Option<f32>,
481 space_after: Option<f32>,
482 line_spacing: Option<f32>,
483 language: Option<u32>,
484) -> Result<Values, Error> {
485 let mut values = Values::new();
486 if let Some(alignment) = alignment {
487 if alignment > 2 {
488 return Err(invalid("Paragraph alignment must be left, center or right"));
489 }
490 values.push((0x0c003477, vec![alignment]));
491 }
492 if let Some(rtl) = rtl {
493 values.push((0x08003476 | (u32::from(rtl) << 31), Vec::new()));
494 }
495 for (property, value) in [
496 (0x1400342e, space_before),
497 (0x1400342f, space_after),
498 (0x14003430, line_spacing),
499 ] {
500 if let Some(points) = value {
501 let stored = points / 36.0;
502 if !stored.is_finite() || !(0.0..=27777.777).contains(&stored) {
503 return Err(invalid("Paragraph spacing is outside the document range"));
504 }
505 values.push((property, stored.to_le_bytes().to_vec()));
506 }
507 }
508 if let Some(language) = language {
509 values.push((0x14001c3b, language.to_le_bytes().to_vec()));
510 }
511 Ok(values)
512}
513
514/// Writes paragraph formatting `values` on text object `object`; alignment also sets the
515/// layout alignment fields OneNote keeps beside it.
516pub(crate) fn paragraph_format_changes(
517 active: &ActivePage<'_>,
518 object: ExGuid,
519 values: &Values,
520) -> Result<Changes, Error> {
521 let parents = active.editable_parents(object)?;
522 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
523 let raw = &active.live.revision;
524 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
525 target.set(
526 &values
527 .iter()
528 .map(|(id, bytes)| (*id, bytes.as_slice()))
529 .collect::<Vec<_>>(),
530 )?;
531 if let Some((_, alignment)) = values.iter().find(|(id, _)| *id == 0x0c003477) {
532 for property in [0x14001c3e, 0x14001c84] {
533 let fields = PropertySets::parse(&target.bytes)?;
534 let previous = fields.sets[0]
535 .iter()
536 .find(|field| field.id == property)
537 .map(|field| match field.value {
538 Value::Bytes(bytes) => bytes
539 .try_into()
540 .map(u32::from_le_bytes)
541 .map_err(|_| invalid("Invalid paragraph layout alignment")),
542 _ => Err(invalid("Invalid paragraph layout alignment")),
543 })
544 .transpose()?
545 .unwrap_or(0);
546 let value = (previous & !7) | (u32::from(alignment[0]) + 1);
547 target.set(&[(property, &value.to_le_bytes())])?;
548 }
549 }
550 target.set(&[(0x14001d7a, &modified)])?;
551 let mut changed = BTreeMap::from([(object, target)]);
552 crate::formatting::touch_ancestors(raw, parents, object, &modified, &mut changed)?;
553 Ok(changed)
554}
555
556/// Links content `object` to a moment in recordings (MediaIndex), or unlinks it.
557pub(crate) fn media_changes(
558 active: &ActivePage<'_>,
559 object: ExGuid,
560 media: &MediaIndex,
561) -> Result<Changes, Error> {
562 let parents = active.editable_parents(object)?;
563 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
564 let raw = &active.live.revision;
565 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
566 match media.time_ms {
567 Some(time) => {
568 let recordings = media.recordings.concat();
569 target.set(&[
570 (0x1c001c98, recordings.as_slice()),
571 (0x14001c99, &time.to_le_bytes()),
572 ])?;
573 }
574 None => target.remove(&[0x1c001c98, 0x14001c99])?,
575 }
576 target.set(&[(0x14001d7a, &modified)])?;
577 let mut changed = BTreeMap::from([(object, target)]);
578 crate::formatting::touch_ancestors(raw, parents, object, &modified, &mut changed)?;
579 Ok(changed)
580}