1use crate::{
2 Error, ExGuid, Object, ObjectData, PropertySets,
3 active::{ActivePage, Changes},
4 create::string,
5 document::{Element, Kind},
6 write::PropertyObject,
7};
8use std::{collections::BTreeMap, ops::Range, sync::Arc};
9
10fn title_line(text: &str) -> &str {
11 text.trim_start().split('\r').next().unwrap()
12}
13
14/// `text` as it shows: hyperlink field instructions, which the text holds hidden, left out.
15pub(crate) fn without_fields(text: &str) -> String {
16 let mut line = String::new();
17 let mut rest = text;
18 while let Some(start) = rest.find('\u{fddf}') {
19 line.push_str(&rest[..start]);
20 rest = &rest[start..];
21 let Some(label) = rest
22 .strip_prefix("\u{fddf}HYPERLINK \"")
23 .and_then(|target| target.split_once('"'))
24 .map(|(_, label)| label)
25 else {
26 break;
27 };
28 rest = label;
29 }
30 line.push_str(rest);
31 line
32}
33
34/// The first line of `text` as OneNote titles a page after it: without hyperlink field
35/// instructions, which the text holds hidden, and at most 255 UTF-16 units long.
36pub(crate) fn automatic_title(text: &str) -> String {
37 let line = without_fields(title_line(text));
38 let line = line.trim();
39 let mut units = 0;
40 for (byte, character) in line.char_indices() {
41 if units >= 255 {
42 return line[..byte].trim_end().to_owned();
43 }
44 units += character.len_utf16();
45 }
46 line.to_owned()
47}
48
49/// Replaces UTF-16 character positions across ordinary text runs.
50/// Inserted text inherits the run at the start; surviving text retains its formatting.
51/// Insertion at a run boundary uses the following run, except at the end of text.
52/// The final run retains its insertion style even when emptied.
53/// Replacing a range with identical text leaves its existing formatting unchanged.
54pub(crate) fn text_changes(
55 active: &ActivePage<'_>,
56 object: ExGuid,
57 range: Range<u32>,
58 replacement: &str,
59) -> Result<Changes, Error> {
60 rewrite(active, object, range, replacement, false)
61}
62
63/// The title's date or time field showing `text`, as OneNote 2010 rewrites both when a
64/// page's date changes: the field's element takes the change time as its creation time.
65pub(crate) fn field_changes(
66 active: &ActivePage<'_>,
67 object: ExGuid,
68 text: &str,
69) -> Result<Changes, Error> {
70 let length = crate::page::text_of(&active.view, &active.parents, object)?
71 .text()
72 .encode_utf16()
73 .count();
74 let mut changes = rewrite(active, object, 0..length as u32, text, true)?;
75 let [element] = active
76 .parents
77 .get(&object)
78 .map(Vec::as_slice)
79 .unwrap_or_default()
80 else {
81 return Err(Error {
82 offset: 0,
83 message: "Select a date field belonging to one element",
84 });
85 };
86 // OneNote 2010 dates the field's element and outline and leaves the title and page
87 // above them as they were.
88 let outline = active
89 .parents
90 .get(element)
91 .and_then(|parents| parents.first())
92 .copied();
93 changes.retain(|id, _| *id == object || id == element || Some(*id) == outline);
94 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
95 let element = match changes.entry(*element) {
96 std::collections::btree_map::Entry::Occupied(entry) => entry.into_mut(),
97 std::collections::btree_map::Entry::Vacant(entry) => entry.insert(
98 crate::write::PropertyObject::from_object(&active.live.revision.objects[element])?,
99 ),
100 };
101 element.set(&[(0x14001d09, &modified)])?;
102 Ok(changes)
103}
104
105fn rewrite(
106 active: &ActivePage<'_>,
107 object: ExGuid,
108 range: Range<u32>,
109 replacement: &str,
110 field: bool,
111) -> Result<Changes, Error> {
112 let invalid = |message| Error { offset: 0, message };
113 let raw = &active.live.revision;
114 if !active.live.is_reachable(object) {
115 return Err(invalid("Object is not reachable in the active revision"));
116 }
117 let revision = &active.view;
118 // Generated fields are read-only to ordinary edits; only a date change rewrites them.
119 let parents = if field {
120 &active.parents
121 } else {
122 active.editable_parents(object)?
123 };
124 let node = revision
125 .nodes
126 .get(&object)
127 .ok_or_else(|| invalid("The text object is unavailable"))?;
128 let Kind::RichText {
129 text,
130 runs,
131 boilerplate,
132 ..
133 } = &node.kind
134 else {
135 return Err(invalid("Select a rich-text object"));
136 };
137 if *boilerplate != field {
138 return Err(invalid(if field {
139 "Select the title's date or time"
140 } else {
141 "Generated title fields cannot be edited as ordinary text"
142 }));
143 }
144 let selected = runs
145 .iter()
146 .rposition(|run| run.start <= range.start && range.start <= run.end)
147 .ok_or_else(|| invalid("The edit range exceeds the text"))?;
148 let resolved = revision.text_runs(object)?;
149 for (i, run) in runs.iter().enumerate() {
150 if i == selected || (run.start < range.end && range.start < run.end) {
151 // Hyperlink field codes and their hidden runs are ordinary text with flags;
152 // embedded objects and equations are not.
153 let format = &resolved[i].format;
154 if [format.math, format.embedded_object].contains(&Some(true))
155 || resolved[i].text.contains('\u{fffc}')
156 || run.extra_set.is_some_and(|set| !node.extra[set].is_empty())
157 {
158 return Err(invalid("This text run contains a field or embedded data"));
159 }
160 }
161 }
162 let ObjectData::Properties(blob) = raw.objects[&object].data else {
163 unreachable!()
164 };
165 let properties = PropertySets::parse(blob)?;
166 if properties.sets[0]
167 .iter()
168 .any(|p| (p.id == 0x88001cde && !field) || p.id == 0x24003458)
169 {
170 return Err(invalid(
171 "This text object is read-only or contains associated run data",
172 ));
173 }
174 let mut units = 0;
175 let mut start = None;
176 let mut end = None;
177 for (byte, character) in text.char_indices() {
178 if units == range.start {
179 start = Some(byte);
180 }
181 if units == range.end {
182 end = Some(byte);
183 }
184 units += character.len_utf16() as u32;
185 }
186 if units == range.start {
187 start = Some(text.len());
188 }
189 if units == range.end {
190 end = Some(text.len());
191 }
192 let (Some(start), Some(end)) = (start, end) else {
193 return Err(invalid(
194 "The edit range splits a surrogate pair or exceeds the text",
195 ));
196 };
197 if &text[start..end] == replacement {
198 return Ok(Changes::new());
199 }
200 let added = u32::try_from(replacement.encode_utf16().count())
201 .map_err(|_| invalid("Replacement text exceeds the UTF-16 offset range"))?;
202 let removed = range.end - range.start;
203 units
204 .checked_sub(removed)
205 .and_then(|n| n.checked_add(added))
206 .ok_or_else(|| invalid("Edited text exceeds the UTF-16 offset range"))?;
207 let mut segments = Vec::new();
208 let mut position = 0;
209 for (i, run) in runs.iter().enumerate() {
210 let length = run.end.min(range.start).saturating_sub(run.start)
211 + run.end.saturating_sub(run.start.max(range.end))
212 + if i == selected { added } else { 0 };
213 let untouched = i != selected && (run.end <= range.start || range.end <= run.start);
214 if length > 0 || i == runs.len() - 1 || untouched {
215 position += length;
216 segments.push((i, position));
217 }
218 }
219 if segments.len() != runs.len() && properties.sets[0].iter().any(|p| p.id == 0x40003499) {
220 return Err(invalid("Text edits cannot remove preserved run data"));
221 }
222 if segments[..segments.len() - 1]
223 .windows(2)
224 .any(|pair| pair[0].1 >= pair[1].1)
225 {
226 return Err(invalid("Text-run boundaries must be strictly increasing"));
227 }
228 let boundaries: Vec<_> = segments[..segments.len() - 1]
229 .iter()
230 .flat_map(|(_, end)| end.to_le_bytes())
231 .collect();
232 let formats = properties.sets[0]
233 .iter()
234 .find(|p| p.id == 0x24001e13)
235 .map(|p| {
236 let crate::Value::References { compact_ids, .. } = p.value else {
237 unreachable!()
238 };
239 if compact_ids.is_empty() {
240 Vec::new()
241 } else {
242 segments
243 .iter()
244 .flat_map(|(i, _)| compact_ids[i * 4..i * 4 + 4].iter().copied())
245 .collect()
246 }
247 });
248 let mut changed = String::with_capacity(text.len() - (end - start) + replacement.len());
249 changed.push_str(&text[..start]);
250 changed.push_str(replacement);
251 changed.push_str(&text[end..]);
252 let unicode = properties.sets[0].iter().any(|p| p.id == 0x1c001c22)
253 || !properties.sets[0].iter().any(|p| p.id == 0x1c003498)
254 || changed.chars().any(|c| c as u32 > 255);
255 let encoded = if unicode {
256 changed
257 .encode_utf16()
258 .chain([0])
259 .flat_map(u16::to_le_bytes)
260 .collect::<Vec<_>>()
261 } else {
262 changed.chars().map(|c| u8::try_from(c).unwrap()).collect()
263 };
264 let text_property = if unicode { 0x1c001c22 } else { 0x1c003498 };
265 let text_update = (text_property, encoded.as_slice());
266 let mut updates = Vec::new();
267 let insert = if properties.sets[0].iter().any(|p| p.id == text_property) {
268 updates.push(text_update);
269 None
270 } else {
271 Some(text_update)
272 };
273 if properties.sets[0].iter().any(|p| p.id == 0x1c001e12) {
274 updates.push((0x1c001e12, &boundaries));
275 }
276 if let Some(formats) = &formats {
277 updates.push((0x24001e13, formats));
278 }
279 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
280 updates.push((0x14001d7a, &modified));
281 let mut edits = vec![crate::write::ObjectEdit {
282 object,
283 updates: &updates,
284 inserts: insert.as_slice(),
285 }];
286 // Native conflict merges can discard descendant edits when ancestor timestamps stay stale.
287 let modified_update = [(0x14001d7a, modified.as_slice())];
288 let mut ancestors = std::collections::BTreeSet::new();
289 let mut pending = parents.get(&object).cloned().unwrap_or_default();
290 while let Some(id) = pending.pop() {
291 if !ancestors.insert(id) {
292 continue;
293 }
294 if revision.nodes[&id].modified.is_some() {
295 edits.push(crate::write::ObjectEdit {
296 object: id,
297 updates: &modified_update,
298 inserts: &[],
299 });
300 }
301 pending.extend(parents.get(&id).into_iter().flatten().copied());
302 }
303 let Some((page, automatic, title_text)) =
304 active.title(&BTreeMap::new(), Some((object, &changed)))?
305 else {
306 return crate::write::patched(raw, &edits);
307 };
308 let Kind::Page {
309 alternate_title, ..
310 } = &revision.nodes[&page].kind
311 else {
312 unreachable!()
313 };
314 let metadata = revision
315 .roots
316 .get(&2)
317 .ok_or_else(|| invalid("Page title metadata is unavailable"))?;
318 let (Kind::Metadata { title, .. } | Kind::ConflictMetadata { title, .. }) =
319 &revision.nodes[metadata].kind
320 else {
321 return Err(invalid("Page title metadata is unavailable"));
322 };
323 let cached: Vec<_> = title_text
324 .encode_utf16()
325 .chain([0])
326 .flat_map(u16::to_le_bytes)
327 .collect();
328 let metadata_update = [(0x1c001cf3, cached.as_slice())];
329 edits.push(crate::write::ObjectEdit {
330 object: *metadata,
331 updates: if title.is_some() {
332 &metadata_update
333 } else {
334 &[]
335 },
336 inserts: if title.is_none() {
337 &metadata_update
338 } else {
339 &[]
340 },
341 });
342 let mut alternate_update = vec![(
343 0x1c001d3c,
344 if automatic {
345 cached.as_slice()
346 } else {
347 &[0u8, 0][..]
348 },
349 )];
350 if revision.nodes[&page].modified.is_some() {
351 alternate_update.push(modified_update[0]);
352 }
353 edits.retain(|edit| edit.object != page);
354 edits.push(crate::write::ObjectEdit {
355 object: page,
356 updates: if alternate_title.is_some() {
357 &alternate_update
358 } else {
359 &alternate_update[1..]
360 },
361 inserts: if alternate_title.is_none() {
362 &alternate_update[..1]
363 } else {
364 &[]
365 },
366 });
367 crate::write::patched(raw, &edits)
368}
369
370pub(crate) fn editable_parents(
371 revision: &crate::document::Revision<'_>,
372 pages: &[ExGuid],
373 object: ExGuid,
374) -> Result<std::collections::BTreeMap<ExGuid, Vec<ExGuid>>, Error> {
375 let parents = revision.parents(pages)?;
376 check_editable(revision, &parents, pages, object)?;
377 Ok(parents)
378}
379
380/// Requires `object` on an active page with neither it nor an ancestor read-only.
381pub(crate) fn check_editable(
382 revision: &crate::document::Revision<'_>,
383 parents: &BTreeMap<ExGuid, Vec<ExGuid>>,
384 pages: &[ExGuid],
385 object: ExGuid,
386) -> Result<(), Error> {
387 let invalid = |message| Error { offset: 0, message };
388 if !parents.contains_key(&object) && !pages.contains(&object) {
389 return Err(invalid("Select content on an active editable page"));
390 }
391 let mut pending = vec![object];
392 let mut seen = std::collections::BTreeSet::new();
393 while let Some(id) = pending.pop() {
394 if !seen.insert(id) {
395 continue;
396 }
397 if revision.nodes[&id].extra[0]
398 .iter()
399 .any(|field| field.id == 0x88001cde)
400 {
401 return Err(invalid("This page or its content is read-only"));
402 }
403 pending.extend(parents.get(&id).into_iter().flatten().copied());
404 }
405 Ok(())
406}
407
408pub(crate) fn page_title(
409 revision: &crate::document::Revision<'_>,
410 pages: &[ExGuid],
411 text_update: Option<(ExGuid, &str)>,
412) -> Result<Option<(ExGuid, bool, String)>, Error> {
413 title_of(
414 |id| revision.nodes.get(&id),
415 &revision.parents(pages)?,
416 revision.nodes.keys().copied(),
417 pages,
418 text_update,
419 )
420}
421
422/// `page_title` over the elements `node` finds, whose parents from `pages` are `parents`;
423/// `candidates` lists, in order, every element that may be title text.
424pub(crate) fn title_of<'n>(
425 node: impl Fn(ExGuid) -> Option<&'n Element<'n>>,
426 parents: &BTreeMap<ExGuid, Vec<ExGuid>>,
427 candidates: impl Iterator<Item = ExGuid>,
428 pages: &[ExGuid],
429 text_update: Option<(ExGuid, &str)>,
430) -> Result<Option<(ExGuid, bool, String)>, Error> {
431 let invalid = |message| Error { offset: 0, message };
432 let titles: Vec<_> = candidates
433 .filter_map(|id| {
434 if !parents.contains_key(&id) && !pages.contains(&id) {
435 return None;
436 }
437 let node = node(id)?;
438 if !node.extra[0].iter().any(|field| field.id == 0x88001cb4) {
439 return None;
440 }
441 match &node.kind {
442 Kind::RichText {
443 text,
444 boilerplate: false,
445 ..
446 } => Some((id, text.as_str())),
447 _ => None,
448 }
449 })
450 .collect();
451 let title_text = match titles.as_slice() {
452 [] => "",
453 [(id, text)] => {
454 if let Some((_, changed)) = text_update.filter(|(object, _)| object == id) {
455 changed
456 } else {
457 text
458 }
459 }
460 _ => return Err(invalid("Title editing requires a single title text object")),
461 };
462 let automatic = title_line(title_text).is_empty();
463 if !automatic && text_update.is_none_or(|(id, _)| titles[0].0 != id) {
464 return Ok(None);
465 }
466 let [page] = pages else {
467 return Err(invalid("Title editing requires a single active page"));
468 };
469 let Kind::Page { rtl, .. } = &node(*page).unwrap().kind else {
470 unreachable!()
471 };
472 let mut title_text = without_fields(title_line(title_text));
473 if automatic {
474 let mut roots = node(*page).unwrap().children.clone();
475 roots.sort_by(|a, b| {
476 let a = &node(*a).unwrap().layout;
477 let b = &node(*b).unwrap().layout;
478 a.y.unwrap_or(0.0)
479 .total_cmp(&b.y.unwrap_or(0.0))
480 .then_with(|| {
481 if *rtl == Some(true) {
482 b.x.unwrap_or(0.0).total_cmp(&a.x.unwrap_or(0.0))
483 } else {
484 a.x.unwrap_or(0.0).total_cmp(&b.x.unwrap_or(0.0))
485 }
486 })
487 });
488 let mut pending: Vec<_> = roots.into_iter().rev().collect();
489 let mut seen = std::collections::BTreeSet::new();
490 while let Some(id) = pending.pop() {
491 if !seen.insert(id) {
492 continue;
493 }
494 let element = node(id).unwrap();
495 if let Kind::RichText {
496 text,
497 boilerplate: false,
498 ..
499 } = &element.kind
500 {
501 title_text = automatic_title(
502 if let Some((_, changed)) = text_update.filter(|(object, _)| *object == id) {
503 changed
504 } else {
505 text
506 },
507 );
508 if !title_text.is_empty() {
509 break;
510 }
511 }
512 if *rtl == Some(true) && matches!(element.kind, Kind::Row) {
513 pending.extend(element.children.iter().copied());
514 } else {
515 pending.extend(element.children.iter().rev().copied());
516 }
517 pending.extend(element.content.iter().rev().copied());
518 pending.extend(element.structure.iter().rev().copied());
519 }
520 }
521 Ok(Some((*page, automatic, title_text)))
522}
523
524/// Updates the page title for `changed` objects made on `view`, a copy of the page's view.
525pub(crate) fn update_title(
526 active: &ActivePage<'_>,
527 view: crate::document::Revision<'_>,
528 changed: &mut BTreeMap<ExGuid, PropertyObject>,
529) -> Result<(), Error> {
530 let invalid = |message| Error { offset: 0, message };
531 let (raw, pages) = (&active.live.revision, &active.pages);
532 // Shorten the moved view's lifetime to the changed property buffers.
533 let mut view = view;
534 for (id, object) in changed.iter() {
535 view.nodes.insert(
536 *id,
537 active.element(&Object {
538 jcid: object.jcid,
539 reference_count: 0,
540 data: ObjectData::Properties(&object.bytes),
541 global_ids: Arc::clone(&object.global_ids),
542 })?,
543 );
544 }
545 let title = page_title(&view, pages, None)?;
546 drop(view);
547 if let Some((_, automatic, title)) = title {
548 let metadata = raw
549 .roots
550 .get(&2)
551 .ok_or_else(|| invalid("Page title metadata is unavailable"))?;
552 if ![0x20030, 0x20038].contains(&raw.objects[metadata].jcid) {
553 return Err(invalid("Page title metadata is unavailable"));
554 }
555 let title = string(&title);
556 let mut object = PropertyObject::from_object(&raw.objects[metadata])?;
557 object.set(&[(0x1c001cf3, &title)])?;
558 changed.insert(*metadata, object);
559 changed
560 .get_mut(&pages[0])
561 .unwrap()
562 .set(&[(0x1c001d3c, if automatic { &title } else { &[0, 0] })])?;
563 }
564 Ok(())
565}