1use crate::{ExGuid, document::Format};
2use std::{fmt, ops::Range};
3
4#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
5pub struct Span {
6 /// Exclusive UTF-8 boundary; the start is the preceding span's end.
7 pub end: usize,
8 pub format: Format,
9}
10
11#[derive(Clone, Debug, PartialEq, serde::Serialize)]
12/// Editable text styles are coalesced independently of serialized run boundaries.
13pub struct Paragraph {
14 text: String,
15 spans: Vec<Span>,
16}
17
18impl<'de> serde::Deserialize<'de> for Paragraph {
19 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
20 #[derive(serde::Deserialize)]
21 struct Stored {
22 text: String,
23 spans: Vec<Span>,
24 }
25 let Stored { text, spans } = Stored::deserialize(deserializer)?;
26 let mut previous = 0;
27 if spans.is_empty()
28 || spans.iter().any(|span| {
29 let broken = span.end < previous || !text.is_char_boundary(span.end);
30 previous = span.end;
31 broken
32 })
33 || previous != text.len()
34 {
35 return Err(serde::de::Error::custom(
36 "Text formatting splits a character or extends outside the paragraph",
37 ));
38 }
39 Ok(Self { text, spans })
40 }
41}
42
43/// Which side of a hidden field a visible boundary maps to.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum Affinity {
46 Upstream,
47 Downstream,
48}
49
50#[derive(Clone)]
51pub struct TextProjection {
52 text: Paragraph,
53 spans: Vec<ProjectedSpan>,
54 source_len: u32,
55}
56
57#[derive(Clone)]
58struct ProjectedSpan {
59 visible: Range<u32>,
60 source_start: u32,
61}
62
63impl TextProjection {
64 pub fn text(&self) -> &Paragraph {
65 &self.text
66 }
67
68 /// Maps visible UTF-8 boundaries to downstream source UTF-16 positions in one pass.
69 pub fn source_boundaries(&self) -> impl Iterator<Item = (usize, u32)> + '_ {
70 let mut spans = self.spans.iter().peekable();
71 let mut visible = 0;
72 self.text
73 .text
74 .char_indices()
75 .map(move |(byte, character)| {
76 while spans.peek().is_some_and(|span| span.visible.end <= visible) {
77 spans.next();
78 }
79 let span = spans.peek().unwrap();
80 let source = span.source_start + (visible - span.visible.start);
81 visible += character.len_utf16() as u32;
82 (byte, source)
83 })
84 .chain(std::iter::once((self.text.text.len(), self.source_len)))
85 }
86
87 pub fn source_offset(&self, visible: u32, affinity: Affinity) -> Result<u32, EditError> {
88 self.text.byte_offset(visible)?;
89 Ok(match affinity {
90 Affinity::Downstream => self
91 .spans
92 .iter()
93 .find(|span| span.visible.end > visible)
94 .map(|span| span.source_start + (visible - span.visible.start))
95 .unwrap_or(self.source_len),
96 Affinity::Upstream => self
97 .spans
98 .iter()
99 .rev()
100 .find(|span| span.visible.start < visible)
101 .map(|span| span.source_start + (visible - span.visible.start))
102 .unwrap_or(0),
103 })
104 }
105
106 /// Hidden source positions collapse to their visible boundary.
107 pub fn visible_offset(&self, source: u32) -> Result<u32, EditError> {
108 if source > self.source_len {
109 return Err(EditError::InvalidRange);
110 }
111 for span in &self.spans {
112 if source < span.source_start {
113 return Ok(span.visible.start);
114 }
115 let length = span.visible.end - span.visible.start;
116 if source < span.source_start + length {
117 let visible = span.visible.start + source - span.source_start;
118 self.text.byte_offset(visible)?;
119 return Ok(visible);
120 }
121 }
122 self.text.utf16_offset(self.text.text.len())
123 }
124}
125
126#[derive(Clone, Debug, PartialEq)]
127pub struct Edit {
128 pub range: Range<u32>,
129 pub replacement: Paragraph,
130}
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub enum EditError {
134 InvalidRange,
135 TextTooLong,
136 InvalidStructure,
137 UnsupportedContent,
138 /// The system random source failed while allocating a new identity.
139 Identity,
140}
141
142impl fmt::Display for EditError {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 f.write_str(match self {
145 Self::Identity => "System random source failed",
146 Self::UnsupportedContent => {
147 "This paragraph contains content the text editor cannot edit"
148 }
149 Self::InvalidRange => "Text range is outside the paragraph or splits a surrogate pair",
150 Self::TextTooLong => "Text exceeds the UTF-16 offset range",
151 Self::InvalidStructure => {
152 "This outline has duplicate objects or broken paragraph links"
153 }
154 })
155 }
156}
157
158impl std::error::Error for EditError {}
159
160impl From<EditError> for crate::Error {
161 fn from(error: EditError) -> Self {
162 Self {
163 offset: 0,
164 message: match error {
165 EditError::InvalidRange => {
166 "Text range is outside the paragraph or splits a surrogate pair"
167 }
168 EditError::TextTooLong => "Text exceeds the UTF-16 offset range",
169 EditError::InvalidStructure => {
170 "This outline has duplicate objects or broken paragraph links"
171 }
172 EditError::UnsupportedContent => {
173 "This paragraph contains content the text editor cannot edit"
174 }
175 EditError::Identity => "System random source failed",
176 },
177 }
178 }
179}
180
181/// A fresh identity for a new paragraph or text object. Identities share one random GUID
182/// per 255 allocations, as OneNote's do within a session: an object's stored id table has
183/// an entry per distinct GUID it references, in every revision that rewrites it. `n` starts
184/// at 1: OneNote never stores an object as `{guid},0`, and OneNote 2010 loses outline
185/// elements stored that way (corpus/math-edit/native-drop).
186pub fn new_id() -> Result<ExGuid, EditError> {
187 static NEXT: std::sync::Mutex<Option<ExGuid>> = std::sync::Mutex::new(None);
188 let mut next = NEXT.lock().map_err(|_| EditError::Identity)?;
189 let id = match *next {
190 Some(id) => id,
191 None => ExGuid {
192 guid: crate::write::fresh_guid().map_err(|_| EditError::Identity)?,
193 n: 1,
194 },
195 };
196 *next = (id.n < 255).then_some(ExGuid { n: id.n + 1, ..id });
197 Ok(id)
198}
199
200/// A GUID of its own, as a recording's identity takes (AudioRecordingGuid).
201pub fn new_guid() -> Result<[u8; 16], EditError> {
202 crate::write::fresh_guid().map_err(|_| EditError::Identity)
203}
204
205impl Paragraph {
206 pub fn project(&self) -> Result<TextProjection, EditError> {
207 let mut runs = Vec::new();
208 let mut spans: Vec<ProjectedSpan> = Vec::new();
209 let mut byte = 0;
210 let mut source = 0_u32;
211 let mut visible = 0_u32;
212 for span in &self.spans {
213 let fragment = &self.text[byte..span.end];
214 let length: u32 = fragment
215 .encode_utf16()
216 .count()
217 .try_into()
218 .map_err(|_| EditError::TextTooLong)?;
219 let source_end = source.checked_add(length).ok_or(EditError::TextTooLong)?;
220 if span.format.hidden != Some(true) {
221 runs.push((fragment.replace('\u{000b}', "\n"), span.format.clone()));
222 if length > 0 {
223 if let Some(last) = spans.last_mut()
224 && last.source_start + (last.visible.end - last.visible.start) == source
225 {
226 last.visible.end += length;
227 } else {
228 spans.push(ProjectedSpan {
229 visible: visible..visible + length,
230 source_start: source,
231 });
232 }
233 visible += length;
234 }
235 }
236 byte = span.end;
237 source = source_end;
238 }
239 if runs.is_empty() {
240 let mut format = self.spans[0].format.clone();
241 format.hidden = Some(false);
242 runs.push((String::new(), format));
243 }
244 Ok(TextProjection {
245 text: Self::from_runs(runs),
246 spans,
247 source_len: source,
248 })
249 }
250
251 pub fn new(text: String, format: Format) -> Self {
252 let end = text.len();
253 Self {
254 text,
255 spans: vec![Span { end, format }],
256 }
257 }
258
259 pub fn from_runs(runs: impl IntoIterator<Item = (String, Format)>) -> Self {
260 let mut text = String::new();
261 let mut spans: Vec<Span> = Vec::new();
262 for (fragment, format) in runs {
263 text.push_str(&fragment);
264 if let Some(last) = spans.last_mut() {
265 if last.format == format {
266 last.end = text.len();
267 continue;
268 }
269 if fragment.is_empty() && !text.is_empty() {
270 continue;
271 }
272 if last.end == 0 {
273 spans.clear();
274 }
275 }
276 spans.push(Span {
277 end: text.len(),
278 format,
279 });
280 }
281 if spans.is_empty() {
282 spans.push(Span {
283 end: 0,
284 format: Format::default(),
285 });
286 }
287 Self { text, spans }
288 }
289
290 pub fn text(&self) -> &str {
291 &self.text
292 }
293
294 pub fn spans(&self) -> &[Span] {
295 &self.spans
296 }
297
298 pub fn byte_offset(&self, utf16: u32) -> Result<usize, EditError> {
299 let mut units = 0_u64;
300 for (byte, character) in self.text.char_indices() {
301 if units == u64::from(utf16) {
302 return Ok(byte);
303 }
304 units += character.len_utf16() as u64;
305 if units > u64::from(utf16) {
306 return Err(EditError::InvalidRange);
307 }
308 }
309 if units == u64::from(utf16) {
310 Ok(self.text.len())
311 } else {
312 Err(EditError::InvalidRange)
313 }
314 }
315
316 pub fn utf16_offset(&self, byte: usize) -> Result<u32, EditError> {
317 self.text
318 .get(..byte)
319 .ok_or(EditError::InvalidRange)?
320 .encode_utf16()
321 .count()
322 .try_into()
323 .map_err(|_| EditError::TextTooLong)
324 }
325
326 pub fn format_at(&self, utf16: u32) -> Result<&Format, EditError> {
327 let byte = self.byte_offset(utf16)?;
328 let index = self.spans.partition_point(|span| span.end < byte);
329 Ok(&self.spans[index].format)
330 }
331
332 pub fn slice(&self, range: Range<u32>) -> Result<Self, EditError> {
333 if range.start > range.end {
334 return Err(EditError::InvalidRange);
335 }
336 let start = self.byte_offset(range.start)?;
337 let end = self.byte_offset(range.end)?;
338 if start == end {
339 return Ok(Self::new(
340 String::new(),
341 self.format_at(range.start)?.clone(),
342 ));
343 }
344 let mut previous = 0;
345 let mut spans = Vec::new();
346 for span in &self.spans {
347 if previous < end && span.end > start {
348 spans.push(Span {
349 end: span.end.min(end) - start,
350 format: span.format.clone(),
351 });
352 }
353 previous = span.end;
354 }
355 Ok(Self {
356 text: self.text[start..end].to_owned(),
357 spans,
358 })
359 }
360
361 /// Returns the inverse edit; applying that inverse returns a redo operation.
362 pub fn apply(&mut self, edit: Edit) -> Result<Edit, EditError> {
363 let end = self.utf16_offset(self.text.len())?;
364 let removed = self.slice(edit.range.clone())?;
365 let inserted = edit.replacement.utf16_offset(edit.replacement.text.len())?;
366 let new_end = edit
367 .range
368 .start
369 .checked_add(inserted)
370 .ok_or(EditError::TextTooLong)?;
371 let resulting_len = end
372 .checked_sub(edit.range.end - edit.range.start)
373 .and_then(|n| n.checked_add(inserted))
374 .ok_or(EditError::TextTooLong)?;
375 if resulting_len == 0 {
376 *self = edit.replacement;
377 return Ok(Edit {
378 range: edit.range.start..new_end,
379 replacement: removed,
380 });
381 }
382 let prefix = self.slice(0..edit.range.start)?;
383 let suffix = self.slice(edit.range.end..end)?;
384 let mut runs = Vec::new();
385 for part in [prefix, edit.replacement, suffix] {
386 let mut start = 0;
387 for span in part.spans {
388 if span.end > start {
389 runs.push((part.text[start..span.end].to_owned(), span.format));
390 }
391 start = span.end;
392 }
393 }
394 *self = Self::from_runs(runs);
395 Ok(Edit {
396 range: edit.range.start..new_end,
397 replacement: removed,
398 })
399 }
400
401 pub fn append(&mut self, other: Self) -> Result<Edit, EditError> {
402 let end = self.utf16_offset(self.text.len())?;
403 self.apply(Edit {
404 range: end..end,
405 replacement: other,
406 })
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 #[test]
415 fn serialized_text_rejects_broken_span_boundaries() {
416 for (text, ends) in [
417 ("", vec![]),
418 ("a", vec![0]),
419 ("a", vec![2]),
420 ("é", vec![1, 2]),
421 ("abc", vec![2, 1, 3]),
422 ("a", vec![usize::MAX]),
423 ] {
424 let json = serde_json::json!({
425 "text": text,
426 "spans": ends.into_iter().map(|end| Span {
427 end,
428 format: Format::default(),
429 }).collect::<Vec<_>>(),
430 });
431 assert!(serde_json::from_value::<Paragraph>(json).is_err());
432 }
433 for text in ["", "é🌳", "a\u{000b}b"] {
434 let original = Paragraph::new(text.into(), Format::default());
435 let restored: Paragraph =
436 serde_json::from_str(&serde_json::to_string(&original).unwrap()).unwrap();
437 assert_eq!(restored, original);
438 restored.project().unwrap();
439 }
440 }
441
442 fn regular() -> Format {
443 Format {
444 font: Some("Arial".into()),
445 font_size: Some(11.0),
446 ..Format::default()
447 }
448 }
449
450 fn bold() -> Format {
451 Format {
452 bold: Some(true),
453 ..regular()
454 }
455 }
456 #[test]
457 fn soft_breaks_keep_source_offsets_and_formatting() {
458 let source = Paragraph::from_runs([("A\u{000b}".into(), regular()), ("🌳".into(), bold())]);
459 let projection = source.project().unwrap();
460 assert_eq!(source.text(), "A\u{000b}🌳");
461 assert_eq!(projection.text().text(), "A\n🌳");
462 assert_eq!(projection.text().spans(), source.spans());
463 assert_eq!(
464 projection.source_boundaries().collect::<Vec<_>>(),
465 [(0, 0), (1, 1), (2, 2), (6, 4)]
466 );
467 for offset in [0, 1, 2, 4] {
468 assert_eq!(projection.visible_offset(offset), Ok(offset));
469 assert_eq!(
470 projection.source_offset(offset, Affinity::Downstream),
471 Ok(offset)
472 );
473 }
474 assert!(projection.visible_offset(3).is_err());
475 }
476
477 #[test]
478 fn hidden_fields_keep_source_offsets_at_both_sides_of_a_gap() {
479 let hidden = Format {
480 hidden: Some(true),
481 ..regular()
482 };
483 let source = Paragraph::from_runs([
484 ("A".into(), regular()),
485 ("🌳".into(), hidden.clone()),
486 ("e\u{301}".into(), bold()),
487 ("X".into(), hidden),
488 ]);
489 let original = source.clone();
490 let projection = source.project().unwrap();
491 assert_eq!(projection.text().text(), "Ae\u{301}");
492 assert_eq!(
493 projection.source_boundaries().collect::<Vec<_>>(),
494 [(0, 0), (1, 3), (2, 4), (4, 6)]
495 );
496 for (visible, upstream, downstream) in [(0, 0, 0), (1, 1, 3), (2, 4, 4), (3, 5, 6)] {
497 assert_eq!(
498 projection.source_offset(visible, Affinity::Upstream),
499 Ok(upstream)
500 );
501 assert_eq!(
502 projection.source_offset(visible, Affinity::Downstream),
503 Ok(downstream)
504 );
505 }
506 for (source, visible) in [(0, 0), (1, 1), (2, 1), (3, 1), (4, 2), (5, 3), (6, 3)] {
507 assert_eq!(projection.visible_offset(source), Ok(visible));
508 }
509 assert_eq!(source, original);
510 }
511
512 #[test]
513 fn projection_validates_visible_surrogates_and_preserves_empty_field_boundaries() {
514 let visible = Paragraph::new("a🌳z".into(), regular()).project().unwrap();
515 assert_eq!(
516 visible.source_offset(2, Affinity::Downstream),
517 Err(EditError::InvalidRange)
518 );
519 assert_eq!(visible.visible_offset(2), Err(EditError::InvalidRange));
520 let hidden = Paragraph::new(
521 "🌳".into(),
522 Format {
523 hidden: Some(true),
524 ..regular()
525 },
526 )
527 .project()
528 .unwrap();
529 assert!(hidden.text().text().is_empty());
530 assert_eq!(hidden.source_boundaries().collect::<Vec<_>>(), [(0, 2)]);
531 assert_eq!(
532 visible.source_boundaries().collect::<Vec<_>>(),
533 [(0, 0), (1, 1), (5, 3), (6, 4)]
534 );
535 assert_eq!(hidden.source_offset(0, Affinity::Upstream), Ok(0));
536 assert_eq!(hidden.source_offset(0, Affinity::Downstream), Ok(2));
537 assert_eq!(hidden.visible_offset(1), Ok(0));
538 assert_eq!(hidden.visible_offset(3), Err(EditError::InvalidRange));
539 }
540
541 #[test]
542 fn offsets_distinguish_bytes_utf16_and_scalars() {
543 let text = Paragraph::new("a🌳e\u{301}".into(), regular());
544 for (utf16, byte) in [(0, 0), (1, 1), (3, 5), (4, 6), (5, 8)] {
545 assert_eq!(text.byte_offset(utf16), Ok(byte));
546 assert_eq!(text.utf16_offset(byte), Ok(utf16));
547 }
548 assert_eq!(text.byte_offset(2), Err(EditError::InvalidRange));
549 assert_eq!(text.utf16_offset(2), Err(EditError::InvalidRange));
550 assert_eq!(text.byte_offset(6), Err(EditError::InvalidRange));
551 assert_eq!(text.utf16_offset(9), Err(EditError::InvalidRange));
552 }
553
554 #[test]
555 fn replacement_preserves_styles_outside_selection() {
556 let mut text = Paragraph::from_runs([
557 ("plain ".into(), regular()),
558 ("bold".into(), bold()),
559 (" end".into(), regular()),
560 ]);
561 let original = text.clone();
562 let edit = Edit {
563 range: 4..8,
564 replacement: Paragraph::new("🌳".into(), bold()),
565 };
566 let undo = text.apply(edit).unwrap();
567 assert_eq!(text.text(), "plai🌳ld end");
568 assert_eq!(
569 text.spans()
570 .iter()
571 .map(|s| (s.end, s.format.bold))
572 .collect::<Vec<_>>(),
573 [(4, None), (10, Some(true)), (14, None)]
574 );
575 let after = text.clone();
576 let redo = text.apply(undo).unwrap();
577 assert_eq!(text, original);
578 text.apply(redo).unwrap();
579 assert_eq!(text, after);
580 }
581
582 #[test]
583 fn invalid_edits_leave_text_and_styles_unchanged() {
584 let original = Paragraph::new("a🌳b".into(), regular());
585 for (start, end) in [(2, 3), (0, 2), (3, 2), (0, 5)] {
586 let mut text = original.clone();
587 assert_eq!(
588 text.apply(Edit {
589 range: start..end,
590 replacement: Paragraph::new("x".into(), bold())
591 }),
592 Err(EditError::InvalidRange)
593 );
594 assert_eq!(text, original);
595 }
596 }
597
598 #[test]
599 fn undo_restores_empty_paragraph_format() {
600 let mut text = Paragraph::new(String::new(), regular());
601 let original = text.clone();
602 let undo = text
603 .apply(Edit {
604 range: 0..0,
605 replacement: Paragraph::new("bold".into(), bold()),
606 })
607 .unwrap();
608 text.apply(undo).unwrap();
609 assert_eq!(text, original);
610 }
611
612 #[test]
613 fn every_scalar_range_round_trips_styled_unicode_edits() {
614 let original = Paragraph::from_runs([
615 ("a🌳".into(), regular()),
616 ("e\u{301}Χ©ΧœΧ•Χ".into(), bold()),
617 ("Z".into(), regular()),
618 ]);
619 let boundaries: Vec<_> = original
620 .text()
621 .char_indices()
622 .map(|(n, _)| n)
623 .chain([original.text().len()])
624 .collect();
625 for &start in &boundaries {
626 for &end in boundaries.iter().filter(|&&end| end >= start) {
627 let range =
628 original.utf16_offset(start).unwrap()..original.utf16_offset(end).unwrap();
629 for replacement in ["", "πŸ‘©‍πŸ‘©‍πŸ‘§‍πŸ‘¦", "xyz", "\u{301}"] {
630 let mut edited = original.clone();
631 let undo = edited
632 .apply(Edit {
633 range: range.clone(),
634 replacement: Paragraph::new(replacement.into(), bold()),
635 })
636 .unwrap();
637 assert_eq!(
638 edited.text(),
639 format!(
640 "{}{replacement}{}",
641 &original.text()[..start],
642 &original.text()[end..]
643 )
644 );
645 let after = edited.clone();
646 let redo = edited.apply(undo).unwrap();
647 assert_eq!(edited, original);
648 edited.apply(redo).unwrap();
649 assert_eq!(edited, after);
650 }
651 }
652 let at = original.utf16_offset(start).unwrap();
653 let mut left = original.slice(0..at).unwrap();
654 let right = original
655 .slice(at..original.utf16_offset(original.text.len()).unwrap())
656 .unwrap();
657 left.append(right).unwrap();
658 assert_eq!(left, original);
659 }
660 }
661}