1//! OneNote's Home-ribbon commands: character formatting, lists, alignment and note tags.
2
3use super::*;
4use crate::layout::{DEFAULT_FONT, DEFAULT_FONT_SIZE};
5use onestore::document::{Format, Kind, Tag};
6use onestore::page::ParagraphContent;
7use onestore::page::text::new_id;
8use serde::{Deserialize, Serialize};
9
10/// The COLORREF OneNote stores for automatic colour, which overrides a style's colour.
11pub(super) const AUTOMATIC: u32 = 0xff00_0000;
12/// To Do's check box.
13const CHECKBOX: u16 = 3;
14
15/// A toolbar command on the selection, or with only a caret, on the text typed there next.
16#[derive(Clone, Debug, PartialEq)]
17pub enum Formatting {
18 /// Removed where every selected character has it, applied to all of them otherwise.
19 Toggle(Toggle),
20 Font(String),
21 FontSize(f32),
22 /// A COLORREF, or `None` for automatic.
23 Color(Option<u32>),
24 /// A COLORREF, or `None` for no highlight.
25 Highlight(Option<u32>),
26 /// Returns text to its paragraph style.
27 Clear,
28 /// Toggles OneNote's default bullet or number, as Ctrl+. and Ctrl+/ do.
29 Bullets,
30 Numbering,
31 /// Applies a style from OneNote's bullet or numbering library; `None` removes lists.
32 List(Option<ListStyle>),
33 Indent,
34 Outdent,
35 Align(Alignment),
36 /// A tag of the user's list and its place there, which the page stores as its action
37 /// type.
38 Tag(NoteTag, u16),
39 /// Removes every note tag from the selected paragraphs.
40 RemoveTags,
41 /// Checks the selected paragraphs' check boxes, or clears them when all are checked.
42 Check,
43 /// Gives the selected bulleted and numbered paragraphs this tag in place of their list.
44 ToDoList(NoteTag, u16),
45 /// Gives the selected paragraphs with this tag OneNote's default bullet in its place.
46 BulletedList(NoteTag, u16),
47 /// Format Painter: gives the selection the character formatting and alignment of text
48 /// picked up with [`CanvasEditor::painted_format`], as OneNote's does.
49 Paint(Format),
50 /// Gives the selected paragraphs a paragraph style, as OneNote 2010's Styles gallery
51 /// does: the page's style of that definition, made where it has none, and their text
52 /// loses its character formatting but links, fields and language.
53 Style(Definition),
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum Toggle {
58 Bold,
59 Italic,
60 Underline,
61 Strikethrough,
62 /// Excludes superscript.
63 Subscript,
64 /// Excludes subscript.
65 Superscript,
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum Alignment {
70 Left,
71 Center,
72 Right,
73}
74
75/// A style of OneNote 2010's bullet or numbering library, by its place in the gallery.
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum ListStyle {
78 Bullet(usize),
79 Number(usize),
80}
81
82/// OneNote 2010's bullet library in gallery order, as it stores each: font, glyph and
83/// ListMSAAIndex. Ctrl+. applies the third.
84pub const BULLET_LIBRARY: [(&str, &str, u16); 34] = [
85 ("Wingdings", "l", 3),
86 ("Symbol", "\u{b7}", 1),
87 ("Calibri", "\u{2022}", 1),
88 ("Courier New", "\u{25cb}", 4),
89 ("Wingdings 2", "\u{9d}", 6),
90 ("Wingdings 2", "\u{9c}", 5),
91 ("Wingdings", "\u{b2}", 13),
92 ("Tahoma", "\u{25ca}", 14),
93 ("Wingdings", "w", 15),
94 ("Wingdings 2", "\u{ae}", 34),
95 ("Wingdings", "v", 16),
96 ("Arial", "\u{25aa}", 7),
97 ("Verdana", "\u{25ab}", 8),
98 ("Wingdings", "\u{a7}", 9),
99 ("Arial", "\u{25a1}", 10),
100 ("Wingdings 3", "}", 11),
101 ("Arial", "\u{25ba}", 12),
102 ("Arial", "\u{2192}", 21),
103 ("Symbol", "\u{de}", 22),
104 ("Arial", ">", 20),
105 ("Wingdings", "\u{d8}", 23),
106 ("Arial", "*", 24),
107 ("Wingdings", "\u{ad}", 17),
108 ("Wingdings", "\u{ae}", 19),
109 ("Wingdings", "\u{af}", 18),
110 ("Arial", "-", 25),
111 ("Arial", "\u{2013}", 26),
112 ("Arial", "\u{2014}", 27),
113 ("Wingdings", "J", 28),
114 ("Wingdings", "K", 29),
115 ("Wingdings", "L", 30),
116 ("Wingdings", "\u{fc}", 31),
117 ("Wingdings", "(", 32),
118 ("Wingdings", "*", 33),
119];
120
121/// OneNote 2010's numbering library in gallery order, as NumberListFormat stores each: the
122/// number's sequence follows U+FFFD. Ctrl+/ applies the first.
123pub const NUMBER_LIBRARY: [&str; 19] = [
124 "\u{fffd}\u{0}.",
125 "\u{fffd}\u{5}.",
126 "\u{fffd}\u{4}.",
127 "\u{fffd}\u{3}.",
128 "\u{fffd}\u{2}.",
129 "\u{fffd}\u{1}.",
130 "\u{fffd}\u{0})",
131 "\u{fffd}\u{4})",
132 "\u{fffd}\u{3})",
133 "\u{fffd}\u{2})",
134 "\u{fffd}\u{1})",
135 "(\u{fffd}\u{0})",
136 "(\u{fffd}\u{3})",
137 "(\u{fffd}\u{4})",
138 "(\u{fffd}\u{2})",
139 "(\u{fffd}\u{1})",
140 "\u{fffd}\u{6}.",
141 "\u{fffd}\u{7}.",
142 "\u{fffd}\u{0}-",
143];
144
145impl ListStyle {
146 pub(super) const BULLET: Self = Self::Bullet(2);
147 pub(super) const NUMBER: Self = Self::Number(0);
148
149 /// The library style a stored list has, if it is one.
150 pub(super) fn of(kind: &Kind) -> Option<Self> {
151 let Kind::List {
152 font,
153 format: Some(format),
154 bullet,
155 ..
156 } = kind
157 else {
158 return None;
159 };
160 match bullet {
161 Some(bullet) => BULLET_LIBRARY
162 .iter()
163 .position(|entry| {
164 (Some(entry.0), entry.1, entry.2) == (font.as_deref(), format.as_str(), *bullet)
165 })
166 .map(Self::Bullet),
167 None => NUMBER_LIBRARY
168 .iter()
169 .position(|known| known == format)
170 .map(Self::Number),
171 }
172 }
173}
174
175/// OneNote's Default font (Options > General): what new outlines' text is set in, as the
176/// page's `p` quick style, and the face and colour new titles take.
177#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
178#[serde(default)]
179pub struct DefaultFont {
180 pub face: String,
181 pub size: f32,
182 /// COLORREF; none is Automatic.
183 pub color: Option<u32>,
184}
185
186impl Default for DefaultFont {
187 /// OneNote 2010's: Calibri 11, Automatic.
188 fn default() -> Self {
189 Self {
190 face: "Calibri".into(),
191 size: 11.0,
192 color: None,
193 }
194 }
195}
196
197impl DefaultFont {
198 /// The `p` quick style OneNote 2010 stores for new text in this font
199 /// (`corpus/default-font`): plain, in its colour, no highlight or paragraph spacing.
200 pub(crate) fn body_style(&self) -> Definition {
201 Definition {
202 kind: Kind::Style {
203 name: Some("p".into()),
204 next: None,
205 },
206 format: Format {
207 bold: Some(false),
208 italic: Some(false),
209 underline: Some(false),
210 strike: Some(false),
211 superscript: Some(false),
212 subscript: Some(false),
213 font: Some(self.face.clone()),
214 font_size: Some(self.size),
215 color: Some(self.color.unwrap_or(AUTOMATIC)),
216 highlight: Some(AUTOMATIC),
217 space_before: Some(0.0),
218 space_after: Some(0.0),
219 line_spacing: Some(0.0),
220 ..Format::default()
221 },
222 }
223 }
224}
225
226/// A tag of the user's list as the Customize Tags dialog edits it: what its definition
227/// stores besides its place in the list, which the page stores as its action type.
228#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
229pub struct NoteTag {
230 pub label: String,
231 /// MS-ONE's NoteTagShape; 0 has no symbol.
232 pub shape: u16,
233 /// COLORREFs; none leaves the text's own.
234 pub color: Option<u32>,
235 pub highlight: Option<u32>,
236 /// Snowbound's own art for the tag, named for its content, which a notebook's
237 /// `.snowbound` folder maps the tag's name and symbol to; OneNote draws the symbol.
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub art: Option<String>,
240}
241
242/// OneNote 2010's tag list before it is customized, the first nine on Ctrl+1 to Ctrl+9
243/// (`corpus/structural-probe/tag-gallery.one`): names, symbols and highlights.
244const DEFAULT_TAGS: [(&str, u16, Option<u32>); 29] = [
245 ("To Do", CHECKBOX, None),
246 ("Important", 13, None),
247 ("Question", 15, None),
248 ("Remember for later", 0, Some(0x0000_ffff)),
249 ("Definition", 0, Some(0x0000_ff00)),
250 ("Highlight", 136, None),
251 ("Contact", 118, None),
252 ("Address", 23, None),
253 ("Phone number", 18, None),
254 ("Web site to visit", 125, None),
255 ("Idea", 21, None),
256 ("Password", 131, None),
257 ("Critical", 17, None),
258 ("Project A", 100, None),
259 ("Project B", 101, None),
260 ("Movie to see", 122, None),
261 ("Book to read", 132, None),
262 ("Music to listen to", 121, None),
263 ("Source for article", 125, None),
264 ("Remember for blog", 24, None),
265 ("Discuss with <Person A>", 94, None),
266 ("Discuss with <Person B>", 94, None),
267 ("Discuss with manager", 95, None),
268 ("Send in e-mail", 106, None),
269 ("Schedule meeting", 12, None),
270 ("Call back", 12, None),
271 ("To Do priority 1", 28, None),
272 ("To Do priority 2", 71, None),
273 ("Client request", 8, None),
274];
275
276impl NoteTag {
277 pub fn defaults() -> Vec<Self> {
278 DEFAULT_TAGS
279 .iter()
280 .map(|&(label, shape, highlight)| Self {
281 label: label.into(),
282 shape,
283 // The highlighting tags also set black text.
284 color: highlight.map(|_| 0),
285 highlight,
286 art: None,
287 })
288 .collect()
289 }
290
291 /// The tag as its definition stores it, without Snowbound's art.
292 pub fn stored(&self) -> Self {
293 Self {
294 art: None,
295 ..self.clone()
296 }
297 }
298
299 /// The definition OneNote stores for the tag at place `action_type` in the list.
300 pub(crate) fn definition(&self, action_type: u16) -> Definition {
301 Definition {
302 kind: Kind::TagDefinition {
303 label: Some(self.label.clone()),
304 action_type: Some(action_type),
305 shape: Some(self.shape),
306 color: self.color,
307 highlight: self.highlight,
308 },
309 format: Format::default(),
310 }
311 }
312
313 /// The tag a stored definition describes, and its action type.
314 pub fn of(kind: &Kind) -> Option<(Self, u16)> {
315 let Kind::TagDefinition {
316 label,
317 action_type,
318 shape,
319 color,
320 highlight,
321 } = kind
322 else {
323 return None;
324 };
325 Some((
326 Self {
327 label: label.clone().unwrap_or_default(),
328 shape: shape.unwrap_or(0),
329 color: *color,
330 highlight: *highlight,
331 art: None,
332 },
333 action_type.unwrap_or(0),
334 ))
335 }
336}
337
338/// What the selection shows on the toolbar.
339#[derive(Clone, Debug, Default, PartialEq)]
340pub struct FormatState {
341 /// Attributes every selected character has.
342 pub toggles: Vec<Toggle>,
343 /// The font and size every selected character is laid out in, where they share one.
344 pub font: Option<String>,
345 pub font_size: Option<f32>,
346 /// The alignment every selected paragraph shares.
347 pub alignment: Option<Alignment>,
348 /// Whether every selected paragraph is bulleted or numbered.
349 pub bullets: bool,
350 pub numbering: bool,
351 /// Whether any selected paragraph is bulleted or numbered.
352 pub listed: bool,
353 /// The library style every selected paragraph's list shares.
354 pub list: Option<ListStyle>,
355 /// Tags every selected paragraph has, with their action types.
356 pub tags: Vec<(NoteTag, u16)>,
357 /// The stored name of the paragraph style every selected paragraph shares.
358 pub style: Option<String>,
359 /// The font and size the selected paragraphs' style gives, or else the Default font,
360 /// where they share one: what their text is in without formatting of its own.
361 pub style_font: Option<String>,
362 pub style_size: Option<f32>,
363}
364
365impl Toggle {
366 const ALL: [Self; 6] = [
367 Self::Bold,
368 Self::Italic,
369 Self::Underline,
370 Self::Strikethrough,
371 Self::Subscript,
372 Self::Superscript,
373 ];
374
375 fn get(self, format: &Format) -> Option<bool> {
376 match self {
377 Self::Bold => format.bold,
378 Self::Italic => format.italic,
379 Self::Underline => format.underline,
380 Self::Strikethrough => format.strike,
381 Self::Subscript => format.subscript,
382 Self::Superscript => format.superscript,
383 }
384 }
385
386 fn slot(self, format: &mut Format) -> &mut Option<bool> {
387 match self {
388 Self::Bold => &mut format.bold,
389 Self::Italic => &mut format.italic,
390 Self::Underline => &mut format.underline,
391 Self::Strikethrough => &mut format.strike,
392 Self::Subscript => &mut format.subscript,
393 Self::Superscript => &mut format.superscript,
394 }
395 }
396}
397
398/// Where a selection starts and ends, as text leaf identity and byte offset.
399type Ends = [(ExGuid, usize); 2];
400
401/// The selection's ends and the container range holding both; a selection ending at a
402/// paragraph's start ends with the paragraph before.
403pub(super) fn selected(
404 document: &TextDocument,
405 selection: Selection,
406) -> Result<(Option<ExGuid>, Range<usize>, Ends), EditError> {
407 let [anchor, focus] = selection.positions;
408 let (start, mut end) = (anchor.min(focus), anchor.max(focus));
409 if end.offset == 0 && end.paragraph > start.paragraph {
410 end.paragraph -= 1;
411 let text = document
412 .paragraph(end.paragraph)
413 .ok_or(EditError::InvalidRange)?;
414 end.offset = text.utf16_offset(text.text().len())?;
415 }
416 let (container, first, head) = document
417 .leaf(start.paragraph)
418 .ok_or(EditError::InvalidRange)?;
419 let (end_container, last, tail) = document
420 .leaf(end.paragraph)
421 .ok_or(EditError::InvalidRange)?;
422 let range = if container == end_container {
423 first..last + 1
424 } else {
425 let root = |container: Option<ExGuid>, index| {
426 container.map_or(Ok(index), |cell| document.root(cell))
427 };
428 root(container, first)?..root(end_container, last)? + 1
429 };
430 let byte = |node: &PageParagraph, offset| node.text().unwrap().text.byte_offset(offset);
431 Ok((
432 if container == end_container {
433 container
434 } else {
435 None
436 },
437 range,
438 [
439 (head.id, byte(head, start.offset)?),
440 (tail.id, byte(tail, end.offset)?),
441 ],
442 ))
443}
444
445/// The text leaves of `nodes` from `start` through `end`, each with the byte range selected.
446fn covered(
447 nodes: &[PageParagraph],
448 [start, end]: Ends,
449) -> impl Iterator<Item = (&PageParagraph, Range<usize>)> {
450 let mut done = false;
451 leaves(nodes, None)
452 .map(|(_, _, node)| node)
453 .skip_while(move |node| node.id != start.0)
454 .take_while(move |node| !std::mem::replace(&mut done, node.id == end.0))
455 .map(move |node| {
456 let from = if node.id == start.0 { start.1 } else { 0 };
457 let to = if node.id == end.0 {
458 end.1
459 } else {
460 node.text().unwrap().text.text().len()
461 };
462 (node, from..to)
463 })
464}
465
466pub(super) fn leaves_mut(nodes: &mut [PageParagraph]) -> Vec<&mut PageParagraph> {
467 let mut leaves = Vec::new();
468 for node in nodes {
469 if node.text().is_some() {
470 leaves.push(node);
471 } else if let ParagraphContent::Table(table) = &mut node.content {
472 for cell in table.rows.iter_mut().flat_map(|row| &mut row.cells) {
473 leaves.extend(leaves_mut(&mut cell.paragraphs));
474 }
475 }
476 }
477 leaves
478}
479
480/// `text` with `change` applied to the formats of bytes `range`, or to its only format when empty.
481pub(super) fn restyle(
482 text: &Paragraph,
483 range: Range<usize>,
484 change: impl Fn(&mut Format),
485) -> Paragraph {
486 if text.text().is_empty() {
487 let mut format = text.spans()[0].format.clone();
488 change(&mut format);
489 return Paragraph::new(String::new(), format);
490 }
491 let mut runs = Vec::new();
492 let mut start = 0;
493 for span in text.spans() {
494 let [from, to] = [range.start, range.end].map(|at| at.clamp(start, span.end));
495 for (part, inside) in [
496 (start..from, false),
497 (from..to, true),
498 (to..span.end, false),
499 ] {
500 if !part.is_empty() {
501 let mut format = span.format.clone();
502 if inside {
503 change(&mut format);
504 }
505 runs.push((text.text()[part].to_owned(), format));
506 }
507 }
508 start = span.end;
509 }
510 Paragraph::from_runs(runs)
511}
512
513/// The formats of the visible characters in `range`, or of the paragraphs without text when the
514/// selection holds no characters.
515fn character_formats<'a>(
516 covered: impl Iterator<Item = (&'a PageParagraph, Range<usize>)>,
517) -> Vec<&'a Format> {
518 let mut characters = Vec::new();
519 let mut blank = Vec::new();
520 for (node, range) in covered {
521 let text = &node.text().unwrap().text;
522 if text.text().is_empty() {
523 blank.push(&text.spans()[0].format);
524 }
525 let mut start = 0;
526 for span in text.spans() {
527 if start < range.end && span.end > range.start && span.format.hidden != Some(true) {
528 characters.push(&span.format);
529 }
530 start = span.end;
531 }
532 }
533 if characters.is_empty() {
534 blank
535 } else {
536 characters
537 }
538}
539
540fn common<T: PartialEq>(mut values: impl Iterator<Item = Option<T>>) -> Option<T> {
541 let first = values.next()??;
542 values
543 .all(|value| value.as_ref() == Some(&first))
544 .then_some(first)
545}
546
547/// `format` moved from paragraph style `old` to `new`: what the old style gave, or nothing
548/// did, the new one gives.
549/// `text` in paragraph style `definition` from style format `old`: it loses its character
550/// formatting but links, fields and language.
551pub(super) fn styled(text: &Paragraph, old: &Format, definition: &Definition) -> Paragraph {
552 restyle(text, 0..text.text().len(), |format| {
553 // An equation keeps its own formatting; what it took from the old style it takes
554 // from the new.
555 if [format.math, format.embedded_object].contains(&Some(true)) {
556 *format = followed(format, old, &definition.format);
557 return;
558 }
559 *format = Format {
560 hidden: format.hidden,
561 hyperlink: format.hyperlink,
562 hyperlink_label: format.hyperlink_label,
563 math: format.math,
564 embedded_object: format.embedded_object,
565 language: format.language,
566 alignment: format.alignment,
567 rtl: format.rtl,
568 list_spacing: format.list_spacing,
569 math_object: format.math_object.clone(),
570 ..definition.format.clone()
571 };
572 })
573}
574
575fn followed(format: &Format, old: &Format, new: &Format) -> Format {
576 let mut format = format.clone();
577 macro_rules! follow {
578 ($($field:ident),*) => {$(
579 if format.$field.is_none() || old.$field.is_some() && format.$field == old.$field {
580 format.$field = new.$field.clone();
581 }
582 )*};
583 }
584 follow!(
585 bold,
586 italic,
587 underline,
588 strike,
589 superscript,
590 subscript,
591 font,
592 font_size,
593 color,
594 highlight,
595 space_before,
596 space_after,
597 line_spacing
598 );
599 format
600}
601
602/// Seconds since 1980, as note tags date themselves.
603pub(super) fn time32() -> Option<u32> {
604 let now = web_time::SystemTime::now()
605 .duration_since(web_time::UNIX_EPOCH)
606 .ok()?
607 .as_secs();
608 u32::try_from(now.checked_sub(315_532_800)?).ok()
609}
610
611fn tags(node: &PageParagraph) -> impl Iterator<Item = &Tag> {
612 node.tags.iter().chain(&node.text().unwrap().tags)
613}
614
615impl CanvasEditor {
616 /// The format text typed at `position` takes: the caret's pending format, else the text's
617 /// before it. Text after a link's label or field code is plain, as OneNote types it, and
618 /// math typed into an equation is a run between its objects until it is built up.
619 pub(super) fn typing_format(&self, position: TextPosition) -> Result<Format, EditError> {
620 if let Some((id, at, format)) = &self.pending
621 && *id == self.active_outline().id
622 && *at == position
623 {
624 return Ok(format.clone());
625 }
626 let text = self
627 .active_outline()
628 .document
629 .paragraph(position.paragraph)
630 .ok_or(EditError::InvalidRange)?;
631 let mut format = text.format_at(position.offset)?.clone();
632 if format.hidden == Some(true) && format.hyperlink == Some(true)
633 || super::link::links(text, position.paragraph)
634 .iter()
635 .any(|link| link.code.is_some() && link.label.end == position.offset)
636 {
637 super::link::unlinked(&mut format);
638 }
639 if format.math == Some(true) {
640 format = onestore::page::Math::format(&format);
641 }
642 Ok(format)
643 }
644
645 fn list(&self, node: &PageParagraph) -> Option<Formatting> {
646 match &self.definitions.get(node.lists.last()?)?.kind {
647 Kind::List { format, .. } => Some(
648 if format
649 .as_ref()
650 .is_some_and(|format| format.contains('\u{fffd}'))
651 {
652 Formatting::Numbering
653 } else {
654 Formatting::Bullets
655 },
656 ),
657 _ => None,
658 }
659 }
660
661 fn list_style(&self, node: &PageParagraph) -> Option<ListStyle> {
662 ListStyle::of(&self.definitions.get(node.lists.last()?)?.kind)
663 }
664
665 fn tag_kind(&self, tag: &Tag) -> Option<&Kind<'static>> {
666 Some(&self.definitions.get(tag.definition.as_ref()?)?.kind)
667 }
668
669 /// What Format Painter picks up: the first selected character's formatting, or with only
670 /// a caret, what would be typed there.
671 pub fn painted_format(&self) -> Result<Format, EditorError> {
672 let outline = self.active_outline();
673 let (container, range, ends) = selected(&outline.document, outline.selection)?;
674 let [anchor, focus] = outline.selection.positions;
675 let nodes = &outline.document.container(container)?[range];
676 match character_formats(covered(nodes, ends)).first() {
677 Some(format) if ends[0] != ends[1] => Ok((*format).clone()),
678 _ => Ok(self.typing_format(anchor.min(focus))?),
679 }
680 }
681
682 pub fn format_state(&self) -> Result<FormatState, EditorError> {
683 let outline = self.active_outline();
684 self.format_state_of(outline, outline.selection)
685 }
686
687 fn format_state_of(
688 &self,
689 outline: &TextOutline,
690 selection: Selection,
691 ) -> Result<FormatState, EditorError> {
692 let (container, range, ends) = selected(&outline.document, selection)?;
693 let nodes = &outline.document.container(container)?[range];
694 let paragraphs = covered(nodes, ends)
695 .map(|(node, _)| node)
696 .collect::<Vec<_>>();
697 let caret;
698 let formats = if ends[0] == ends[1] {
699 let [anchor, focus] = selection.positions;
700 caret = self.typing_format(anchor.min(focus))?;
701 vec![&caret]
702 } else {
703 character_formats(covered(nodes, ends))
704 };
705 let list = |kind| {
706 paragraphs
707 .iter()
708 .all(|node| self.list(node).as_ref() == Some(&kind))
709 };
710 let styled = |node: &&PageParagraph| {
711 (node.style.as_ref())
712 .and_then(|style| self.definitions.get(style))
713 .map(|definition| &definition.format)
714 };
715 Ok(FormatState {
716 toggles: Toggle::ALL
717 .into_iter()
718 .filter(|toggle| {
719 formats
720 .iter()
721 .all(|format| toggle.get(format) == Some(true))
722 })
723 .collect(),
724 font: common(
725 formats
726 .iter()
727 .map(|format| Some(format.font.as_deref().unwrap_or(DEFAULT_FONT).to_owned())),
728 ),
729 font_size: common(
730 formats
731 .iter()
732 .map(|format| Some(format.font_size.unwrap_or(DEFAULT_FONT_SIZE))),
733 ),
734 alignment: common(paragraphs.iter().map(|node| {
735 Some(
736 match node.text().unwrap().text.spans()[0].format.alignment {
737 Some(1) => Alignment::Center,
738 Some(2) => Alignment::Right,
739 _ => Alignment::Left,
740 },
741 )
742 })),
743 bullets: list(Formatting::Bullets),
744 numbering: list(Formatting::Numbering),
745 listed: paragraphs.iter().any(|node| self.list(node).is_some()),
746 list: common(paragraphs.iter().map(|node| self.list_style(node))),
747 tags: paragraphs.first().map_or_else(Vec::new, |first| {
748 let mut shared: Vec<_> = tags(first)
749 .filter_map(|tag| NoteTag::of(self.tag_kind(tag)?))
750 .filter(|(tag, action_type)| {
751 let kind = tag.definition(*action_type).kind;
752 paragraphs
753 .iter()
754 .all(|node| tags(node).any(|tag| self.tag_kind(tag) == Some(&kind)))
755 })
756 .collect();
757 shared.sort_by_key(|(_, action_type)| *action_type);
758 shared
759 }),
760 style: common(paragraphs.iter().map(|node| {
761 match &self.definitions.get(&node.style?)?.kind {
762 Kind::Style { name, .. } => name.clone(),
763 _ => None,
764 }
765 })),
766 style_font: common(paragraphs.iter().map(|node| {
767 Some(
768 styled(node)
769 .and_then(|format| format.font.clone())
770 .unwrap_or_else(|| self.default_font.face.clone()),
771 )
772 })),
773 style_size: common(paragraphs.iter().map(|node| {
774 Some(
775 styled(node)
776 .and_then(|format| format.font_size)
777 .unwrap_or(self.default_font.size),
778 )
779 })),
780 })
781 }
782
783 /// Applies a toolbar command as one undo step that keeps the selection.
784 pub fn format(
785 &mut self,
786 engine: &mut TextEngine,
787 command: Formatting,
788 ) -> Result<(), EditorError> {
789 if self.page_selected() {
790 return self.format_page(engine, command);
791 }
792 let outline = self.active_outline();
793 let (id, title, selection) = (outline.id, outline.title, outline.selection);
794 let (container, mut range, ends) = selected(&outline.document, selection)?;
795 let mut replacement = outline.document.container(container)?[range.clone()].to_vec();
796 let mut ranges = covered(&replacement, ends)
797 .map(|(node, range)| (node.id, range))
798 .collect::<BTreeMap<_, _>>();
799 let bases = |nodes: &[PageParagraph]| {
800 covered(nodes, ends)
801 .map(|(node, _)| Ok((node.id, self.style_format(node.style)?)))
802 .collect::<Result<BTreeMap<_, _>, EditError>>()
803 };
804 match &command {
805 Formatting::Align(_)
806 | Formatting::Bullets
807 | Formatting::Numbering
808 | Formatting::List(_)
809 | Formatting::Tag(..)
810 | Formatting::RemoveTags
811 | Formatting::Check
812 | Formatting::ToDoList(..)
813 | Formatting::BulletedList(..)
814 | Formatting::Style(_)
815 if title =>
816 {
817 return Ok(());
818 }
819 Formatting::Indent | Formatting::Outdent => {
820 self.indent(engine, command == Formatting::Outdent)?;
821 return Ok(());
822 }
823 Formatting::Align(alignment) => {
824 let bases = bases(&replacement)?;
825 for node in leaves_mut(&mut replacement) {
826 if ranges.remove(&node.id).is_some() {
827 let value = match alignment {
828 Alignment::Left => bases[&node.id].alignment.map(|_| 0),
829 Alignment::Center => Some(1),
830 Alignment::Right => Some(2),
831 };
832 let text = &mut node.text_mut().unwrap().text;
833 *text = restyle(text, 0..text.text().len(), |format| {
834 format.alignment = value;
835 });
836 }
837 }
838 }
839 Formatting::Bullets | Formatting::Numbering | Formatting::List(_) => {
840 let style = match command {
841 Formatting::Bullets => Some(ListStyle::BULLET),
842 Formatting::Numbering => Some(ListStyle::NUMBER),
843 Formatting::List(style) => style,
844 _ => unreachable!(),
845 };
846 // A toggle leaves paragraphs already of its kind as they are, however styled.
847 let has = |node: &PageParagraph| match command {
848 Formatting::List(_) => self.list_style(node) == style,
849 _ => self.list(node).as_ref() == Some(&command),
850 };
851 let remove = style.is_none()
852 || !matches!(command, Formatting::List(_))
853 && covered(&replacement, ends).all(|(node, _)| has(node));
854 // A list applied after a plain sibling nests under it, as Tab would
855 // (`evidence/structural-edits/xml/pb-1.xml`); removing it, or restyling a
856 // list, leaves it where it is, as OneNote 2010 does.
857 let nodes = outline.document.container(container)?;
858 if !remove
859 && nodes[range.start].lists.is_empty()
860 && crate::document::previous_sibling(nodes, range.start, &BTreeSet::new())
861 .is_some_and(|sibling| nodes[sibling].lists.is_empty())
862 && let Some(edit) =
863 crate::document::indent(nodes, container, range.clone(), false)
864 {
865 range = edit.range;
866 replacement = edit.replacement;
867 }
868 let wanted = covered(&replacement, ends)
869 .map(|(node, _)| node)
870 .filter(|node| !remove && !has(node))
871 .filter_map(|node| {
872 let format = &node.text().unwrap().text.spans()[0].format;
873 Some((node.id, list_definition(style?, format)))
874 })
875 .collect::<Vec<_>>();
876 // A list definition belongs to one paragraph, as OneNote stores it.
877 let mut lists = BTreeMap::new();
878 for (node, definition) in wanted {
879 let id = new_id()?;
880 self.definitions.insert(id, definition);
881 lists.insert(node, id);
882 }
883 for node in leaves_mut(&mut replacement) {
884 if ranges.remove(&node.id).is_some() {
885 if remove {
886 node.lists.clear();
887 } else if let Some(id) = lists.get(&node.id) {
888 node.lists = vec![*id];
889 }
890 }
891 }
892 }
893 Formatting::Tag(tag, action_type) => {
894 let definition = tag.definition(*action_type);
895 let id = self.define_tag(&definition)?;
896 let has = |node: &PageParagraph| {
897 tags(node).any(|tag| self.tag_kind(tag) == Some(&definition.kind))
898 };
899 let remove = covered(&replacement, ends).all(|(node, _)| has(node));
900 let created = time32();
901 for node in leaves_mut(&mut replacement) {
902 if ranges.remove(&node.id).is_none() || !remove && has(node) {
903 continue;
904 }
905 let ParagraphContent::Text(text) = &mut node.content else {
906 unreachable!()
907 };
908 let added = (!remove).then_some((id, tag.shape, created));
909 self.retag([&mut node.tags, &mut text.tags], &definition.kind, added);
910 }
911 }
912 Formatting::RemoveTags => {
913 for node in leaves_mut(&mut replacement) {
914 if ranges.remove(&node.id).is_some() {
915 node.tags.clear();
916 node.text_mut().unwrap().tags.clear();
917 }
918 }
919 }
920 Formatting::Check => self.check(&mut replacement, &ranges),
921 Formatting::ToDoList(tag, action_type) => {
922 let definition = tag.definition(*action_type);
923 let id = self.define_tag(&definition)?;
924 let created = time32();
925 for node in leaves_mut(&mut replacement) {
926 if ranges.remove(&node.id).is_none() || self.list(node).is_none() {
927 continue;
928 }
929 node.lists.clear();
930 if tags(node).any(|tag| self.tag_kind(tag) == Some(&definition.kind)) {
931 continue;
932 }
933 let ParagraphContent::Text(text) = &mut node.content else {
934 unreachable!()
935 };
936 let added = Some((id, tag.shape, created));
937 self.retag([&mut node.tags, &mut text.tags], &definition.kind, added);
938 }
939 }
940 Formatting::BulletedList(tag, action_type) => {
941 let kind = tag.definition(*action_type).kind;
942 for node in leaves_mut(&mut replacement) {
943 if ranges.remove(&node.id).is_none()
944 || !tags(node).any(|tag| self.tag_kind(tag) == Some(&kind))
945 {
946 continue;
947 }
948 if self.list(node) != Some(Formatting::Bullets) {
949 let format = &node.text().unwrap().text.spans()[0].format;
950 let list = list_definition(ListStyle::BULLET, format);
951 let id = new_id()?;
952 self.definitions.insert(id, list);
953 node.lists = vec![id];
954 }
955 let ParagraphContent::Text(text) = &mut node.content else {
956 unreachable!()
957 };
958 self.retag([&mut node.tags, &mut text.tags], &kind, None);
959 }
960 }
961 Formatting::Style(definition) => {
962 let olds = bases(&replacement)?;
963 let style = self.define_style(definition)?;
964 for node in leaves_mut(&mut replacement) {
965 if ranges.remove(&node.id).is_some() {
966 node.style = Some(style);
967 let old = &olds[&node.id];
968 let text = &mut node.text_mut().unwrap().text;
969 *text = styled(text, old, definition);
970 }
971 }
972 }
973 Formatting::Toggle(_)
974 | Formatting::Font(_)
975 | Formatting::FontSize(_)
976 | Formatting::Color(_)
977 | Formatting::Highlight(_)
978 | Formatting::Paint(_)
979 | Formatting::Clear => {
980 let caret = (ends[0] == ends[1])
981 .then(|| {
982 let [anchor, focus] = selection.positions;
983 let caret = anchor.min(focus);
984 Ok::<_, EditError>((caret, self.typing_format(caret)?))
985 })
986 .transpose()?;
987 let on = match command {
988 Formatting::Toggle(toggle) => !match &caret {
989 Some((_, format)) => vec![format],
990 None => character_formats(covered(&replacement, ends)),
991 }
992 .iter()
993 .all(|format| toggle.get(format) == Some(true)),
994 _ => true,
995 };
996 let change = |base: &Format, format: &mut Format| match &command {
997 Formatting::Toggle(toggle) => {
998 let exclusive = match toggle {
999 Toggle::Subscript => Some(Toggle::Superscript),
1000 Toggle::Superscript => Some(Toggle::Subscript),
1001 _ => None,
1002 };
1003 for toggle in exclusive.into_iter().chain([*toggle]) {
1004 *toggle.slot(format) = toggle.get(base).map(|_| false);
1005 }
1006 if on {
1007 *toggle.slot(format) = Some(true);
1008 }
1009 }
1010 Formatting::Font(font) => format.font = Some(font.clone()),
1011 Formatting::FontSize(size) => format.font_size = Some(*size),
1012 Formatting::Color(color) => {
1013 format.color = color.or(base.color.map(|_| AUTOMATIC));
1014 }
1015 Formatting::Highlight(color) => {
1016 format.highlight = color.or(base.highlight.map(|_| AUTOMATIC));
1017 }
1018 Formatting::Paint(painted) => {
1019 for toggle in Toggle::ALL {
1020 *toggle.slot(format) = toggle.get(painted).or(toggle.get(base));
1021 }
1022 format.font = painted.font.clone().or_else(|| base.font.clone());
1023 format.font_size = painted.font_size.or(base.font_size);
1024 format.color = painted.color.or(base.color);
1025 format.highlight = painted.highlight.or(base.highlight);
1026 }
1027 _ => {
1028 for toggle in Toggle::ALL {
1029 *toggle.slot(format) = toggle.get(base);
1030 }
1031 format.font.clone_from(&base.font);
1032 format.font_size = base.font_size;
1033 format.color = base.color;
1034 format.highlight = base.highlight;
1035 }
1036 };
1037 let bases = bases(&replacement)?;
1038 if let Some((caret, mut format)) = caret {
1039 change(&bases[&ends[0].0], &mut format);
1040 self.pending = Some((id, caret, format));
1041 return Ok(());
1042 }
1043 for node in leaves_mut(&mut replacement) {
1044 if let Some(range) = ranges.remove(&node.id) {
1045 let base = &bases[&node.id];
1046 let text = &mut node.text_mut().unwrap().text;
1047 *text = restyle(text, range, |format| change(base, format));
1048 if let Formatting::Paint(painted) = &command
1049 && !title
1050 {
1051 *text = restyle(text, 0..text.text().len(), |format| {
1052 format.alignment = painted.alignment;
1053 });
1054 }
1055 }
1056 }
1057 }
1058 }
1059 if self.active_outline().document.container(container)?[range.clone()] == replacement[..] {
1060 return Ok(());
1061 }
1062 self.commit(
1063 engine,
1064 DocumentEdit {
1065 columns: BTreeMap::new(),
1066 container,
1067 range,
1068 replacement,
1069 },
1070 selection,
1071 )
1072 }
1073}
1074
1075impl CanvasEditor {
1076 /// The page's paragraph style holding `definition`, made where it has none.
1077 pub(super) fn define_style(&mut self, definition: &Definition) -> Result<ExGuid, EditError> {
1078 if let Some((id, _)) = self
1079 .definitions
1080 .iter()
1081 .find(|(_, kept)| *kept == definition)
1082 {
1083 return Ok(*id);
1084 }
1085 let id = new_id()?;
1086 self.definitions.insert(id, definition.clone());
1087 Ok(id)
1088 }
1089
1090 /// The page's definition of the tag `definition` describes, made where it has none.
1091 pub(super) fn define_tag(&mut self, definition: &Definition) -> Result<ExGuid, EditError> {
1092 if let Some((id, _)) = self
1093 .definitions
1094 .iter()
1095 .find(|(_, other)| other.kind == definition.kind)
1096 {
1097 return Ok(*id);
1098 }
1099 let id = new_id()?;
1100 self.definitions.insert(id, definition.clone());
1101 Ok(id)
1102 }
1103
1104 /// A paragraph style named `name`: the page's in its theme's formatting, else the page's
1105 /// first of that name, else the theme's made on the page; Normal falls back to the
1106 /// Default font's. None where nothing names it.
1107 pub(super) fn style_named(&mut self, name: &str) -> Result<Option<ExGuid>, EditError> {
1108 let themed = self.styles.get(name).cloned();
1109 let named = |definition: &Definition| matches!(&definition.kind, Kind::Style { name: Some(own), .. } if own == name);
1110 let known = self
1111 .definitions
1112 .iter()
1113 .filter(|(_, kept)| named(kept))
1114 .min_by_key(|(_, kept)| Some(*kept) != themed.as_ref())
1115 .map(|(id, _)| *id);
1116 if known.is_some() {
1117 return Ok(known);
1118 }
1119 let definition = themed.or_else(|| (name == "p").then(|| self.default_font.body_style()));
1120 definition
1121 .map(|definition| self.define_style(&definition))
1122 .transpose()
1123 }
1124}
1125
1126impl CanvasEditor {
1127 /// A toolbar command on OneNote 2010's page selection: each outline takes it as if selected
1128 /// alone, but a toggle turns on everywhere unless every outline already has it.
1129 fn format_page(
1130 &mut self,
1131 engine: &mut TextEngine,
1132 command: Formatting,
1133 ) -> Result<(), EditorError> {
1134 self.whole = None;
1135 let has = |state: FormatState| match &command {
1136 Formatting::Toggle(toggle) => state.toggles.contains(toggle),
1137 Formatting::Bullets => state.bullets,
1138 Formatting::Numbering => state.numbering,
1139 Formatting::Tag(tag, action_type) => state.tags.contains(&(tag.stored(), *action_type)),
1140 _ => false,
1141 };
1142 let mut outlines = Vec::new();
1143 for outline in self.outlines.iter().filter(|outline| !outline.title) {
1144 let state = self.format_state_of(outline, outline.whole())?;
1145 outlines.push((outline.id, has(state)));
1146 }
1147 let everywhere = outlines.iter().all(|(_, has)| *has);
1148 self.grouped(|editor| {
1149 for (id, has) in outlines {
1150 if has && !everywhere {
1151 continue;
1152 }
1153 editor.focus_outline(id)?;
1154 editor.select_all()?;
1155 editor.format(engine, command.clone())?;
1156 }
1157 Ok(editor.select_page()?)
1158 })
1159 }
1160
1161 /// A click on a check box tag: checks the check boxes of paragraph `id` in outline
1162 /// `outline`, or clears them once all are checked, as one undo step that keeps the
1163 /// selection.
1164 pub fn click_check(
1165 &mut self,
1166 engine: &mut TextEngine,
1167 outline: ExGuid,
1168 id: ExGuid,
1169 ) -> Result<(), EditorError> {
1170 self.focus_outline(outline)?;
1171 let outline = self.active_outline();
1172 let document = &outline.document;
1173 let Some(paragraph) = leaves(document.nodes(), None).position(|(_, _, node)| node.id == id)
1174 else {
1175 return self.click_block_check(engine, id);
1176 };
1177 let at = TextPosition {
1178 paragraph,
1179 offset: 0,
1180 };
1181 let (container, range, ends) = selected(document, [at; 2].into())?;
1182 let mut replacement = document.container(container)?[range.clone()].to_vec();
1183 let ranges = covered(&replacement, ends)
1184 .map(|(node, range)| (node.id, range))
1185 .collect();
1186 self.check(&mut replacement, &ranges);
1187 let selection = outline.selection;
1188 self.commit(
1189 engine,
1190 DocumentEdit {
1191 columns: BTreeMap::new(),
1192 container,
1193 range,
1194 replacement,
1195 },
1196 selection,
1197 )
1198 }
1199
1200 /// A click on the check box of a table, picture or file paragraph `id`.
1201 fn click_block_check(
1202 &mut self,
1203 engine: &mut TextEngine,
1204 id: ExGuid,
1205 ) -> Result<(), EditorError> {
1206 let outline = self.active_outline();
1207 let (container, index, node) = descendants(outline.document.nodes(), None)
1208 .find(|(_, _, node)| node.id == id)
1209 .ok_or(EditError::InvalidRange)?;
1210 let mut node = node.clone();
1211 let content = match &mut node.content {
1212 ParagraphContent::Table(table) => table.tags.as_mut_slice(),
1213 ParagraphContent::Image(image) => image.tags.as_mut_slice(),
1214 ParagraphContent::Attachment(file) => file.tags.as_mut_slice(),
1215 _ => &mut [],
1216 };
1217 self.toggle_checks(node.tags.iter_mut().chain(content).collect());
1218 let selection = outline.selection;
1219 self.commit(
1220 engine,
1221 DocumentEdit {
1222 columns: BTreeMap::new(),
1223 container,
1224 range: index..index + 1,
1225 replacement: vec![node],
1226 },
1227 selection,
1228 )
1229 }
1230
1231 /// A tag command on selected picture or file `id`, which OneNote 2010 tags itself rather
1232 /// than its paragraph (`corpus/object-tags`); false for other commands.
1233 pub fn format_object(
1234 &mut self,
1235 engine: &mut TextEngine,
1236 id: ExGuid,
1237 command: &Formatting,
1238 ) -> Result<bool, EditorError> {
1239 if !matches!(
1240 command,
1241 Formatting::Tag(..) | Formatting::RemoveTags | Formatting::Check
1242 ) {
1243 return Ok(false);
1244 }
1245 let node = self
1246 .outline_picture(id)
1247 .map(|(outline, _, _, node)| (outline, node.clone()));
1248 let mut tags = match &node {
1249 Some((_, node)) => match &node.content {
1250 ParagraphContent::Image(image) => image.tags.clone(),
1251 ParagraphContent::Attachment(file) => file.tags.clone(),
1252 _ => return Ok(false),
1253 },
1254 None => self
1255 .object_tags_mut(id)
1256 .ok_or(EditError::InvalidRange)?
1257 .clone(),
1258 };
1259 match command {
1260 Formatting::Tag(tag, action_type) => {
1261 let definition = tag.definition(*action_type);
1262 let defined = self.define_tag(&definition)?;
1263 let has = tags
1264 .iter()
1265 .any(|tag| self.tag_kind(tag) == Some(&definition.kind));
1266 let added = (!has).then_some((defined, tag.shape, time32()));
1267 self.retag([&mut tags], &definition.kind, added);
1268 }
1269 Formatting::RemoveTags => tags.clear(),
1270 _ => self.toggle_checks(tags.iter_mut().collect()),
1271 }
1272 self.finish_composition();
1273 let Some((outline, mut node)) = node else {
1274 self.set_object_tags(id, tags);
1275 return Ok(true);
1276 };
1277 match &mut node.content {
1278 ParagraphContent::Image(image) => image.tags = tags,
1279 ParagraphContent::Attachment(file) => file.tags = tags,
1280 _ => unreachable!(),
1281 }
1282 self.focus_outline(outline)?;
1283 let (container, index, _) = descendants(self.active_outline().document.nodes(), None)
1284 .find(|(_, _, other)| other.id == node.id)
1285 .ok_or(EditError::InvalidRange)?;
1286 let selection = self.active_outline().selection;
1287 self.commit(
1288 engine,
1289 DocumentEdit {
1290 columns: BTreeMap::new(),
1291 container,
1292 range: index..index + 1,
1293 replacement: vec![node],
1294 },
1295 selection,
1296 )?;
1297 Ok(true)
1298 }
1299
1300 /// A click on the check box of picture or file `id` on the page.
1301 pub fn click_object_check(&mut self, id: ExGuid) -> Result<(), EditorError> {
1302 let mut tags = self
1303 .objects
1304 .iter()
1305 .find_map(|object| object.tagged().filter(|tagged| tagged.0 == id))
1306 .ok_or(EditError::InvalidRange)?
1307 .1
1308 .to_vec();
1309 self.toggle_checks(tags.iter_mut().collect());
1310 self.finish_composition();
1311 self.set_object_tags(id, tags);
1312 Ok(())
1313 }
1314
1315 /// Takes tags of `kind` from `lists`, or with `added`, the definition's identity, its
1316 /// shape and when, gives the last list one in place of any of its action type; an element
1317 /// holds one tag of each action type, stored newest first.
1318 pub(super) fn retag<const N: usize>(
1319 &self,
1320 mut lists: [&mut Vec<Tag>; N],
1321 kind: &Kind<'static>,
1322 added: Option<(ExGuid, u16, Option<u32>)>,
1323 ) {
1324 let Kind::TagDefinition { action_type, .. } = kind else {
1325 return;
1326 };
1327 for tags in lists.iter_mut() {
1328 tags.retain(|tag| match self.tag_kind(tag) {
1329 Some(other) if added.is_none() => other != kind,
1330 Some(Kind::TagDefinition {
1331 action_type: other, ..
1332 }) => other != action_type,
1333 _ => true,
1334 });
1335 }
1336 let (Some((id, shape, created)), Some(tags)) = (added, lists.last_mut()) else {
1337 return;
1338 };
1339 let checkable = crate::outline::checkable(shape);
1340 tags.insert(
1341 0,
1342 Tag {
1343 definition: Some(id),
1344 action_type: None,
1345 shape: None,
1346 property_status: None,
1347 status: u16::from(!checkable),
1348 created,
1349 completed: if checkable { Some(0) } else { created },
1350 start: None,
1351 due: None,
1352 task_id: None,
1353 extra_set: 0,
1354 },
1355 );
1356 }
1357
1358 /// Checks the check boxes among `tags`, or clears them once all are checked; OneNote keeps
1359 /// a cleared box's completion time as zero.
1360 pub(super) fn toggle_checks(&self, mut tags: Vec<&mut Tag>) {
1361 let checked = tags
1362 .iter()
1363 .filter(|tag| self.check_box(tag).is_some())
1364 .all(|tag| tag.status & 1 != 0);
1365 let completed = if checked { Some(0) } else { time32() };
1366 for tag in &mut tags {
1367 if self.check_box(tag).is_some() && (tag.status & 1 != 0) == checked {
1368 tag.status ^= 1;
1369 tag.completed = completed;
1370 }
1371 }
1372 }
1373
1374 /// The definition and shape of `tag` when it is a check box.
1375 fn check_box(&self, tag: &Tag) -> Option<(&Kind<'static>, u16)> {
1376 match self.tag_kind(tag)? {
1377 kind @ Kind::TagDefinition {
1378 shape: Some(shape), ..
1379 } if crate::outline::checkable(*shape) => Some((kind, *shape)),
1380 _ => None,
1381 }
1382 }
1383
1384 /// Enter in a to-do list: `opened` takes an unchecked copy of each check box of `from`.
1385 pub(super) fn continue_checks(&self, from: &PageParagraph, opened: &mut PageParagraph) {
1386 let created = time32();
1387 let ParagraphContent::Text(text) = &mut opened.content else {
1388 unreachable!()
1389 };
1390 for tag in tags(from).collect::<Vec<_>>().into_iter().rev() {
1391 if let (Some((kind, shape)), Some(id)) = (self.check_box(tag), tag.definition) {
1392 self.retag(
1393 [&mut opened.tags, &mut text.tags],
1394 kind,
1395 Some((id, shape, created)),
1396 );
1397 }
1398 }
1399 }
1400
1401 /// Enter on an empty to-do item ends the list: its check boxes go.
1402 pub(super) fn end_checks(&self, node: &mut PageParagraph) {
1403 let ParagraphContent::Text(text) = &mut node.content else {
1404 unreachable!()
1405 };
1406 for tags in [&mut node.tags, &mut text.tags] {
1407 tags.retain(|tag| self.check_box(tag).is_none());
1408 }
1409 }
1410
1411 /// Checks the check boxes of the `covered` paragraphs, or clears them once all are
1412 /// checked.
1413 fn check(&self, replacement: &mut [PageParagraph], covered: &BTreeMap<ExGuid, Range<usize>>) {
1414 let tags = leaves_mut(replacement)
1415 .into_iter()
1416 .filter(|node| covered.contains_key(&node.id))
1417 .flat_map(|node| {
1418 let ParagraphContent::Text(text) = &mut node.content else {
1419 unreachable!()
1420 };
1421 node.tags.iter_mut().chain(&mut text.tags)
1422 })
1423 .collect();
1424 self.toggle_checks(tags);
1425 }
1426}
1427
1428/// The list a Tab (`deeper`) or Shift+Tab gives a paragraph whose list is one OneNote 2010
1429/// steps through as Tab nests it: from Ctrl+. through nine bullets, and from Ctrl+/ through
1430/// eight number formats, each as its gallery stores it, starting over after the last
1431/// (`evidence/toolbar-17/tab-lists.txt`). Outdenting stops at the first.
1432pub(super) fn nested_list(definition: &Definition, deeper: bool) -> Option<Definition> {
1433 const BULLETS: [usize; 9] = [2, 3, 13, 14, 9, 7, 15, 26, 8];
1434 const NUMBERS: [usize; 8] = [0, 2, 4, 6, 7, 9, 16, 17];
1435 let Kind::List { restart, .. } = &definition.kind else {
1436 return None;
1437 };
1438 let current = ListStyle::of(&definition.kind)?;
1439 let (chain, style): (&[usize], fn(usize) -> ListStyle) = match current {
1440 ListStyle::Bullet(_) => (&BULLETS, ListStyle::Bullet),
1441 ListStyle::Number(_) => (&NUMBERS, ListStyle::Number),
1442 };
1443 let at = chain.iter().position(|place| style(*place) == current)?;
1444 let at = if deeper {
1445 (at + 1) % chain.len()
1446 } else {
1447 at.checked_sub(1)?
1448 };
1449 let mut kind = list_definition(style(chain[at]), &Format::default()).kind;
1450 if let Kind::List { restart: value, .. } = &mut kind {
1451 *value = *restart;
1452 }
1453 Some(Definition {
1454 kind,
1455 format: definition.format.clone(),
1456 })
1457}
1458
1459/// The list OneNote 2010 gives a paragraph with `format` in library `style`, as its gallery
1460/// and its Ctrl+. bullet store it (`corpus/paragraph-edit/reconciliation/keyboard`); a number
1461/// also takes the text's font and language (`corpus/outline-edit/tree`).
1462pub(super) fn list_definition(style: ListStyle, format: &Format) -> Definition {
1463 let font_size = Some(format.font_size.unwrap_or(DEFAULT_FONT_SIZE));
1464 match style {
1465 ListStyle::Number(index) => Definition {
1466 kind: Kind::List {
1467 font: None,
1468 format: Some(NUMBER_LIBRARY[index].into()),
1469 restart: None,
1470 bullet: None,
1471 },
1472 format: Format {
1473 bold: Some(false),
1474 italic: Some(false),
1475 font: format.font.clone(),
1476 font_size,
1477 color: Some(AUTOMATIC),
1478 language: format.language,
1479 ..Format::default()
1480 },
1481 },
1482 ListStyle::Bullet(index) => {
1483 let (font, glyph, bullet) = BULLET_LIBRARY[index];
1484 Definition {
1485 kind: Kind::List {
1486 font: Some(font.into()),
1487 format: Some(glyph.into()),
1488 restart: None,
1489 bullet: Some(bullet),
1490 },
1491 format: Format {
1492 font_size,
1493 color: Some(AUTOMATIC),
1494 ..Format::default()
1495 },
1496 }
1497 }
1498 }
1499}
1500
1501#[cfg(test)]
1502mod tests {
1503 use super::*;
1504
1505 fn at(paragraph: usize, offset: u32) -> TextPosition {
1506 TextPosition { paragraph, offset }
1507 }
1508
1509 fn calibri() -> Format {
1510 Format {
1511 font: Some("Calibri".into()),
1512 font_size: Some(11.0),
1513 ..Format::default()
1514 }
1515 }
1516
1517 fn editor(engine: &mut TextEngine, paragraphs: Vec<Paragraph>) -> CanvasEditor {
1518 CanvasEditor::new(engine, TextDocument::new(paragraphs).unwrap(), 300.0).unwrap()
1519 }
1520
1521 fn plain(engine: &mut TextEngine, texts: &[&str]) -> CanvasEditor {
1522 let paragraphs = texts
1523 .iter()
1524 .map(|text| Paragraph::new((*text).into(), calibri()))
1525 .collect();
1526 editor(engine, paragraphs)
1527 }
1528
1529 /// Each paragraph's runs as text and the value `field` reads from their format.
1530 fn runs<T>(editor: &CanvasEditor, field: impl Fn(&Format) -> T) -> Vec<Vec<(String, T)>> {
1531 editor
1532 .active_outline()
1533 .document
1534 .paragraphs()
1535 .map(|paragraph| {
1536 let mut start = 0;
1537 paragraph
1538 .spans()
1539 .iter()
1540 .map(|span| {
1541 let text = paragraph.text()[start..span.end].to_owned();
1542 start = span.end;
1543 (text, field(&span.format))
1544 })
1545 .collect()
1546 })
1547 .collect()
1548 }
1549
1550 fn run(text: &str, value: Option<bool>) -> (String, Option<bool>) {
1551 (text.into(), value)
1552 }
1553
1554 #[test]
1555 fn toggles_apply_unless_every_character_has_them_as_one_undo_step() {
1556 let mut engine = TextEngine::default();
1557 let mut editor = editor(
1558 &mut engine,
1559 vec![
1560 Paragraph::from_runs([
1561 ("plain ".into(), calibri()),
1562 (
1563 "bold".into(),
1564 Format {
1565 bold: Some(true),
1566 ..calibri()
1567 },
1568 ),
1569 ]),
1570 Paragraph::new("next".into(), calibri()),
1571 ],
1572 );
1573 let original = editor.active_outline().document.clone();
1574 let selection = Selection::from([at(1, 2), at(0, 3)]);
1575 editor.select(selection).unwrap();
1576 let bold = |editor: &CanvasEditor| runs(editor, |format| format.bold);
1577 editor
1578 .format(&mut engine, Formatting::Toggle(Toggle::Bold))
1579 .unwrap();
1580 assert_eq!(
1581 bold(&editor),
1582 [
1583 vec![run("pla", None), run("in bold", Some(true))],
1584 vec![run("ne", Some(true)), run("xt", None)]
1585 ]
1586 );
1587 assert_eq!(editor.selection(), selection);
1588 assert!(
1589 editor
1590 .format_state()
1591 .unwrap()
1592 .toggles
1593 .contains(&Toggle::Bold)
1594 );
1595 editor
1596 .format(&mut engine, Formatting::Toggle(Toggle::Bold))
1597 .unwrap();
1598 assert_eq!(
1599 bold(&editor),
1600 [vec![run("plain bold", None)], vec![run("next", None)]]
1601 );
1602 assert!(editor.undo(&mut engine).unwrap());
1603 assert!(editor.undo(&mut engine).unwrap());
1604 assert_eq!(editor.active_outline().document, original);
1605 assert_eq!(editor.selection(), selection);
1606 assert!(!editor.undo(&mut engine).unwrap());
1607 assert!(editor.redo(&mut engine).unwrap());
1608 assert_eq!(bold(&editor)[1], [run("ne", Some(true)), run("xt", None)]);
1609 }
1610
1611 #[test]
1612 fn a_second_toggle_at_a_caret_turns_the_typing_format_back_off() {
1613 for toggle in Toggle::ALL {
1614 for text in ["", "ab"] {
1615 let mut engine = TextEngine::default();
1616 let mut editor = plain(&mut engine, &[text]);
1617 let caret = at(0, text.len() as u32);
1618 editor.select([caret; 2].into()).unwrap();
1619 let press = |editor: &mut CanvasEditor, engine: &mut TextEngine| {
1620 editor.format(engine, Formatting::Toggle(toggle)).unwrap();
1621 editor.format_state().unwrap().toggles
1622 };
1623 assert_eq!(press(&mut editor, &mut engine), [toggle], "{toggle:?}");
1624 assert_eq!(press(&mut editor, &mut engine), [], "{toggle:?}");
1625 editor.insert(&mut engine, "x").unwrap();
1626 assert_eq!(
1627 runs(&editor, |format| toggle.get(format))[0],
1628 [run(&format!("{text}x"), None)]
1629 );
1630 }
1631 let mut engine = TextEngine::default();
1632 let mut on = calibri();
1633 *toggle.slot(&mut on) = Some(true);
1634 let mut editor = editor(&mut engine, vec![Paragraph::new("ab".into(), on)]);
1635 editor.select([at(0, 1); 2].into()).unwrap();
1636 editor
1637 .format(&mut engine, Formatting::Toggle(toggle))
1638 .unwrap();
1639 assert_eq!(editor.format_state().unwrap().toggles, [], "{toggle:?}");
1640 editor.insert(&mut engine, "x").unwrap();
1641 assert_eq!(
1642 runs(&editor, |format| toggle.get(format) == Some(true))[0],
1643 [("a".into(), true), ("x".into(), false), ("b".into(), true)],
1644 "{toggle:?}"
1645 );
1646 }
1647 }
1648
1649 #[test]
1650 fn a_caret_formats_the_text_typed_next_and_scripts_exclude_each_other() {
1651 let mut engine = TextEngine::default();
1652 let mut editor = plain(&mut engine, &["ab"]);
1653 editor.select([at(0, 1); 2].into()).unwrap();
1654 for toggle in [Toggle::Bold, Toggle::Superscript, Toggle::Subscript] {
1655 editor
1656 .format(&mut engine, Formatting::Toggle(toggle))
1657 .unwrap();
1658 }
1659 assert!(!editor.undo(&mut engine).unwrap());
1660 assert_eq!(
1661 editor.format_state().unwrap().toggles,
1662 [Toggle::Bold, Toggle::Subscript]
1663 );
1664 editor.insert(&mut engine, "x").unwrap();
1665 let typed = Format {
1666 bold: Some(true),
1667 subscript: Some(true),
1668 ..calibri()
1669 };
1670 assert_eq!(
1671 runs(&editor, Clone::clone),
1672 [vec![
1673 ("a".into(), calibri()),
1674 ("x".into(), typed.clone()),
1675 ("b".into(), calibri())
1676 ]]
1677 );
1678 editor
1679 .format(&mut engine, Formatting::Toggle(Toggle::Italic))
1680 .unwrap();
1681 editor.select([at(0, 0); 2].into()).unwrap();
1682 editor.insert(&mut engine, "y").unwrap();
1683 assert_eq!(runs(&editor, |format| format.italic)[0][0], run("ya", None));
1684
1685 editor
1686 .format(&mut engine, Formatting::FontSize(20.0))
1687 .unwrap();
1688 editor.compose(&mut engine, "k".into(), 1..1).unwrap();
1689 editor.compose(&mut engine, "kl".into(), 2..2).unwrap();
1690 editor.commit_text(&mut engine, "kl".into()).unwrap();
1691 assert_eq!(
1692 runs(&editor, |format| format.font_size)[0][..2],
1693 [("y".into(), Some(11.0)), ("kl".into(), Some(20.0))]
1694 );
1695 editor.undo(&mut engine).unwrap();
1696 assert_eq!(
1697 editor
1698 .active_outline()
1699 .document
1700 .paragraphs()
1701 .next()
1702 .unwrap()
1703 .text(),
1704 "yaxb"
1705 );
1706 }
1707
1708 /// A style from the gallery replaces the paragraph's character formatting but keeps its
1709 /// language, as OneNote 2010's does; Enter at its end takes its NextStyle, from the
1710 /// theme where the page has none (lab, 2026-09-30).
1711 #[test]
1712 fn a_gallery_style_clears_formatting_and_enter_takes_its_next_style() {
1713 let mut engine = TextEngine::default();
1714 let body = Format {
1715 font: Some("Calibri".into()),
1716 font_size: Some(11.0),
1717 language: Some(0x40c),
1718 ..Format::default()
1719 };
1720 let source = TextDocument::new(vec![Paragraph::from_runs([
1721 ("Plain ".to_owned(), body.clone()),
1722 (
1723 "red".to_owned(),
1724 Format {
1725 color: Some(0xff),
1726 italic: Some(true),
1727 ..body.clone()
1728 },
1729 ),
1730 ])])
1731 .unwrap();
1732 let mut editor = CanvasEditor::new(&mut engine, source, 240.0).unwrap();
1733 let style = |name: &str, size, next: Option<&str>| Definition {
1734 kind: Kind::Style {
1735 name: Some(name.into()),
1736 next: next.map(Into::into),
1737 },
1738 format: Format {
1739 bold: Some(name != "p"),
1740 italic: Some(false),
1741 font: Some("Georgia".into()),
1742 font_size: Some(size),
1743 color: Some(0x0033_2211),
1744 space_before: Some(12.0),
1745 ..Format::default()
1746 },
1747 };
1748 let heading = style("h2", 16.0, Some("p"));
1749 editor.styles = BTreeMap::from([("p".to_owned(), style("p", 12.0, None))]);
1750 editor.select([at(0, 3); 2].into()).unwrap();
1751 editor
1752 .format(&mut engine, Formatting::Style(heading.clone()))
1753 .unwrap();
1754 let node = editor.active_outline().document.nodes()[0].clone();
1755 assert_eq!(editor.definitions[&node.style.unwrap()], heading);
1756 let spans = node.text().unwrap().text.spans().to_vec();
1757 assert_eq!(spans.len(), 1);
1758 assert_eq!(
1759 spans[0].format,
1760 Format {
1761 language: Some(0x40c),
1762 ..heading.format.clone()
1763 }
1764 );
1765 assert_eq!(editor.format_state().unwrap().style.as_deref(), Some("h2"));
1766 let length = node.text().unwrap().text.text().len() as u32;
1767 editor.select([at(0, length); 2].into()).unwrap();
1768 editor.enter(&mut engine, false).unwrap();
1769 let next = editor.active_outline().document.nodes()[1].style;
1770 assert_eq!(editor.definitions[&next.unwrap()], editor.styles["p"]);
1771 // Enter after Normal keeps Normal.
1772 editor.insert(&mut engine, "body").unwrap();
1773 editor.enter(&mut engine, false).unwrap();
1774 assert_eq!(editor.active_outline().document.nodes()[2].style, next);
1775 editor.undo(&mut engine).unwrap();
1776 editor.undo(&mut engine).unwrap();
1777 editor.undo(&mut engine).unwrap();
1778 editor.undo(&mut engine).unwrap();
1779 assert_eq!(editor.active_outline().document.nodes()[0].style, None);
1780 }
1781
1782 #[test]
1783 fn clearing_and_removing_attributes_return_to_the_paragraph_style() {
1784 let mut engine = TextEngine::default();
1785 let style = new_id().unwrap();
1786 let heading = Format {
1787 bold: Some(true),
1788 color: Some(0x0012_3456),
1789 font: Some("Georgia".into()),
1790 font_size: Some(18.0),
1791 ..Format::default()
1792 };
1793 let mut nodes = TextDocument::new(vec![Paragraph::new("Heading".into(), heading.clone())])
1794 .unwrap()
1795 .nodes()
1796 .to_vec();
1797 nodes[0].style = Some(style);
1798 let outline = Outline {
1799 id: new_id().unwrap(),
1800 title: false,
1801 min_width: None,
1802 layout: onestore::document::Layout {
1803 max_width: Some(300.0),
1804 ..Default::default()
1805 },
1806 indents: vec![18.0, 0.0, 27.0, 27.0],
1807 paragraphs: nodes,
1808 unsupported: Vec::new(),
1809 };
1810 let definitions = BTreeMap::from([(
1811 style,
1812 Definition {
1813 kind: Kind::Style {
1814 name: Some("h1".into()),
1815 next: None,
1816 },
1817 format: heading.clone(),
1818 },
1819 )]);
1820 let mut editor =
1821 CanvasEditor::from_outlines(&mut engine, vec![outline], definitions).unwrap();
1822 editor.select_all().unwrap();
1823 let state = editor.format_state().unwrap();
1824 assert_eq!(state.toggles, [Toggle::Bold]);
1825 assert_eq!(
1826 (state.font.as_deref(), state.font_size, state.alignment),
1827 (Some("Georgia"), Some(18.0), Some(Alignment::Left))
1828 );
1829 for command in [
1830 Formatting::Toggle(Toggle::Bold),
1831 Formatting::Color(None),
1832 Formatting::Highlight(Some(0x0000_ffff)),
1833 Formatting::Font("Arial".into()),
1834 Formatting::Align(Alignment::Center),
1835 ] {
1836 editor.format(&mut engine, command).unwrap();
1837 }
1838 editor.select([at(0, 0), at(0, 4)].into()).unwrap();
1839 editor
1840 .format(&mut engine, Formatting::FontSize(9.0))
1841 .unwrap();
1842 editor.select_all().unwrap();
1843 assert_eq!(editor.format_state().unwrap().font_size, None);
1844 let format = editor
1845 .active_outline()
1846 .document
1847 .paragraph(0)
1848 .unwrap()
1849 .spans()[1]
1850 .format
1851 .clone();
1852 assert_eq!(
1853 format,
1854 Format {
1855 bold: Some(false),
1856 color: Some(AUTOMATIC),
1857 highlight: Some(0x0000_ffff),
1858 font: Some("Arial".into()),
1859 alignment: Some(1),
1860 ..heading.clone()
1861 }
1862 );
1863 editor.format(&mut engine, Formatting::Clear).unwrap();
1864 assert_eq!(
1865 runs(&editor, Clone::clone),
1866 [vec![(
1867 "Heading".into(),
1868 Format {
1869 alignment: Some(1),
1870 ..heading
1871 }
1872 )]]
1873 );
1874 editor
1875 .format(&mut engine, Formatting::Highlight(None))
1876 .unwrap();
1877 for _ in 0..7 {
1878 assert!(editor.undo(&mut engine).unwrap());
1879 }
1880 assert!(!editor.undo(&mut engine).unwrap());
1881 }
1882
1883 /// Each paragraph's number as laid out, checked against laying the outline out afresh.
1884 fn numbers(engine: &mut TextEngine, editor: &CanvasEditor) -> Vec<Option<u32>> {
1885 let outline = editor.active_outline();
1886 let fresh = OutlineLayout::flow(
1887 outline.document.nodes().iter(),
1888 &outline.indents,
1889 outline.wrap_width(),
1890 true,
1891 0,
1892 None,
1893 &mut |node, previous, width, indents| {
1894 ParagraphLayout::shape(engine, node, previous, width, indents, &editor.definitions)
1895 },
1896 )
1897 .unwrap();
1898 let laid = |layout: &OutlineLayout| {
1899 layout
1900 .paragraphs
1901 .iter()
1902 .map(|paragraph| {
1903 let markers = paragraph.markers.iter().map(|(marker, origin)| {
1904 (*origin, marker.lines().next().unwrap().0.metrics().advance)
1905 });
1906 (paragraph.number.clone(), markers.collect::<Vec<_>>())
1907 })
1908 .collect::<Vec<_>>()
1909 };
1910 assert_eq!(laid(&outline.shaped), laid(&fresh));
1911 outline
1912 .shaped
1913 .paragraphs
1914 .iter()
1915 .map(|paragraph| paragraph.number.as_ref().map(|(count, _)| count.number))
1916 .collect()
1917 }
1918
1919 #[test]
1920 fn lists_toggle_switch_kind_and_renumber_only_what_changes() {
1921 let mut engine = TextEngine::default();
1922 let mut editor = plain(&mut engine, &["one", "two", "three", "four"]);
1923 let original = editor.active_outline().document.clone();
1924 let all = Selection::from([at(0, 0), at(3, 4)]);
1925 editor.select(all).unwrap();
1926 editor.format(&mut engine, Formatting::Bullets).unwrap();
1927 let state = editor.format_state().unwrap();
1928 assert!(state.bullets && !state.numbering);
1929 assert_eq!(numbers(&mut engine, &editor), [None; 4]);
1930 let bullet = editor.active_outline().document.nodes()[0].lists[0];
1931 assert_eq!(
1932 editor.definitions[&bullet],
1933 list_definition(ListStyle::BULLET, &calibri())
1934 );
1935 editor.format(&mut engine, Formatting::Numbering).unwrap();
1936 let state = editor.format_state().unwrap();
1937 assert!(!state.bullets && state.numbering);
1938 assert_eq!(
1939 numbers(&mut engine, &editor),
1940 [Some(1), Some(2), Some(3), Some(4)]
1941 );
1942 let untouched = editor.active_outline().shaped.paragraphs[0]
1943 .text
1944 .shaped
1945 .styles()
1946 .as_ptr();
1947 editor.select([at(1, 1); 2].into()).unwrap();
1948 editor.format(&mut engine, Formatting::Numbering).unwrap();
1949 // An unnumbered sibling leaves the count alone (`evidence/structural-edits/xml/c8-bs-1.xml`).
1950 assert_eq!(
1951 numbers(&mut engine, &editor),
1952 [Some(1), None, Some(2), Some(3)]
1953 );
1954 assert_eq!(
1955 editor.active_outline().shaped.paragraphs[0]
1956 .text
1957 .shaped
1958 .styles()
1959 .as_ptr(),
1960 untouched
1961 );
1962 editor.undo(&mut engine).unwrap();
1963 editor.select([at(2, 0); 2].into()).unwrap();
1964 editor.format(&mut engine, Formatting::Indent).unwrap();
1965 assert_eq!(
1966 numbers(&mut engine, &editor),
1967 [Some(1), Some(2), Some(1), Some(3)]
1968 );
1969 assert_eq!(editor.selection(), Selection::from([at(2, 0); 2]));
1970 editor.format(&mut engine, Formatting::Bullets).unwrap();
1971 assert_eq!(
1972 numbers(&mut engine, &editor),
1973 [Some(1), Some(2), None, Some(3)]
1974 );
1975 assert!(editor.format_state().unwrap().bullets);
1976 editor.format(&mut engine, Formatting::Outdent).unwrap();
1977 assert_eq!(
1978 numbers(&mut engine, &editor),
1979 [Some(1), Some(2), None, Some(3)]
1980 );
1981 editor.select(all).unwrap();
1982 let state = editor.format_state().unwrap();
1983 assert!(!state.bullets && !state.numbering);
1984 for _ in 0..6 {
1985 editor.undo(&mut engine).unwrap();
1986 }
1987 assert_eq!(editor.active_outline().document, original);
1988 assert_eq!(numbers(&mut engine, &editor), [None; 4]);
1989 }
1990
1991 #[test]
1992 fn library_styles_replace_any_list_and_none_removes_them() {
1993 let mut engine = TextEngine::default();
1994 let mut editor = plain(&mut engine, &["one", "two", "three"]);
1995 let original = editor.active_outline().document.clone();
1996 let all = Selection::from([at(0, 0), at(2, 5)]);
1997 editor.select(all).unwrap();
1998 editor.format(&mut engine, Formatting::Bullets).unwrap();
1999 assert_eq!(editor.format_state().unwrap().list, Some(ListStyle::BULLET));
2000 let pick = |style| Formatting::List(Some(style));
2001 editor
2002 .format(&mut engine, pick(ListStyle::Bullet(0)))
2003 .unwrap();
2004 let state = editor.format_state().unwrap();
2005 assert!(state.bullets && !state.numbering);
2006 assert_eq!(state.list, Some(ListStyle::Bullet(0)));
2007 let first = editor.active_outline().document.nodes()[0].lists[0];
2008 assert_eq!(
2009 editor.definitions[&first],
2010 list_definition(ListStyle::Bullet(0), &calibri())
2011 );
2012 // Picking the style again leaves the lists as they are.
2013 let stored = editor.active_outline().document.clone();
2014 editor
2015 .format(&mut engine, pick(ListStyle::Bullet(0)))
2016 .unwrap();
2017 assert_eq!(editor.active_outline().document, stored);
2018 // A number format replaces the bullets; "First." lays out as any number does.
2019 editor
2020 .format(&mut engine, pick(ListStyle::Number(17)))
2021 .unwrap();
2022 let state = editor.format_state().unwrap();
2023 assert!(state.numbering && !state.bullets);
2024 assert_eq!(state.list, Some(ListStyle::Number(17)));
2025 assert_eq!(numbers(&mut engine, &editor), [Some(1), Some(2), Some(3)]);
2026 // Paragraphs of different styles share none.
2027 editor.select([at(1, 0); 2].into()).unwrap();
2028 editor
2029 .format(&mut engine, pick(ListStyle::Number(0)))
2030 .unwrap();
2031 editor.select(all).unwrap();
2032 let state = editor.format_state().unwrap();
2033 assert!(state.numbering && state.list.is_none());
2034 editor.format(&mut engine, Formatting::List(None)).unwrap();
2035 let state = editor.format_state().unwrap();
2036 assert!(!state.bullets && !state.numbering && state.list.is_none());
2037 assert_eq!(numbers(&mut engine, &editor), [None; 3]);
2038 for _ in 0..6 {
2039 editor.undo(&mut engine).unwrap();
2040 }
2041 assert_eq!(editor.active_outline().document, original);
2042 }
2043
2044 #[test]
2045 fn restyling_a_list_after_a_plain_sibling_leaves_it_where_it_is() {
2046 let mut engine = TextEngine::default();
2047 let mut editor = plain(&mut engine, &["head", "item"]);
2048 let levels = |editor: &CanvasEditor| {
2049 editor
2050 .active_outline()
2051 .document
2052 .nodes()
2053 .iter()
2054 .map(|node| node.level)
2055 .collect::<Vec<_>>()
2056 };
2057 editor.select([at(1, 0); 2].into()).unwrap();
2058 editor.format(&mut engine, Formatting::Numbering).unwrap();
2059 assert_eq!(levels(&editor), [1, 2]);
2060 editor.format(&mut engine, Formatting::Outdent).unwrap();
2061 assert_eq!(levels(&editor), [1, 1]);
2062 editor.format(&mut engine, Formatting::Bullets).unwrap();
2063 editor
2064 .format(&mut engine, Formatting::List(Some(ListStyle::Bullet(0))))
2065 .unwrap();
2066 assert_eq!(levels(&editor), [1, 1]);
2067 assert_eq!(
2068 editor.format_state().unwrap().list,
2069 Some(ListStyle::Bullet(0))
2070 );
2071 }
2072
2073 #[test]
2074 fn tab_steps_through_onenotes_nested_bullets_and_numbers() {
2075 let chain = |start: ListStyle, steps: usize| {
2076 let mut definition = list_definition(start, &calibri());
2077 let mut styles = vec![ListStyle::of(&definition.kind).unwrap()];
2078 for _ in 0..steps {
2079 definition = nested_list(&definition, true).unwrap();
2080 styles.push(ListStyle::of(&definition.kind).unwrap());
2081 }
2082 (styles, definition)
2083 };
2084 let (bullets, deepest) = chain(ListStyle::BULLET, 9);
2085 assert_eq!(
2086 bullets,
2087 [2, 3, 13, 14, 9, 7, 15, 26, 8, 2].map(ListStyle::Bullet)
2088 );
2089 // Wingdings § is ListMSAAIndex 9 here, as the gallery stores it.
2090 assert_eq!(BULLET_LIBRARY[13], ("Wingdings", "\u{a7}", 9));
2091 assert_eq!(
2092 deepest.format,
2093 list_definition(ListStyle::BULLET, &calibri()).format
2094 );
2095 let (numbers, _) = chain(ListStyle::NUMBER, 8);
2096 assert_eq!(
2097 numbers,
2098 [0, 2, 4, 6, 7, 9, 16, 17, 0].map(ListStyle::Number)
2099 );
2100 let second = nested_list(&list_definition(ListStyle::NUMBER, &calibri()), true).unwrap();
2101 assert_eq!(
2102 ListStyle::of(&nested_list(&second, false).unwrap().kind),
2103 Some(ListStyle::NUMBER)
2104 );
2105 assert!(nested_list(&list_definition(ListStyle::BULLET, &calibri()), false).is_none());
2106 // A style off both paths keeps its list.
2107 assert!(nested_list(&list_definition(ListStyle::Bullet(33), &calibri()), true).is_none());
2108 }
2109
2110 #[test]
2111 fn every_library_style_reads_back_as_itself() {
2112 let styles = (0..BULLET_LIBRARY.len())
2113 .map(ListStyle::Bullet)
2114 .chain((0..NUMBER_LIBRARY.len()).map(ListStyle::Number));
2115 for style in styles {
2116 let definition = list_definition(style, &calibri());
2117 assert_eq!(ListStyle::of(&definition.kind), Some(style));
2118 }
2119 }
2120
2121 #[test]
2122 fn numbering_follows_stored_sequences_restarts_and_nesting() {
2123 let mut engine = TextEngine::default();
2124 let mut editor = plain(&mut engine, &["a", "b", "c", "d", "e"]);
2125 let mut nodes = editor.active_outline().document.nodes().to_vec();
2126 for (node, (sequence, restart, level)) in nodes.iter_mut().zip([
2127 ('\u{1}', None, 1),
2128 ('\u{1}', None, 1),
2129 ('\u{4}', None, 2),
2130 ('\u{1}', Some(9), 1),
2131 ('\u{1}', None, 1),
2132 ]) {
2133 let id = new_id().unwrap();
2134 let mut definition = list_definition(ListStyle::NUMBER, &calibri());
2135 let Kind::List {
2136 format,
2137 restart: value,
2138 ..
2139 } = &mut definition.kind
2140 else {
2141 unreachable!()
2142 };
2143 *format = Some(format!("(\u{fffd}{sequence})"));
2144 *value = restart;
2145 editor.definitions.insert(id, definition);
2146 node.lists = vec![id];
2147 node.level = level;
2148 }
2149 let outline = Outline {
2150 paragraphs: nodes,
2151 ..editor.active_outline().snapshot()
2152 };
2153 let editor =
2154 CanvasEditor::from_outlines(&mut engine, vec![outline], editor.definitions).unwrap();
2155 assert_eq!(
2156 numbers(&mut engine, &editor),
2157 [Some(1), Some(2), Some(1), Some(9), Some(10)]
2158 );
2159 }
2160
2161 /// OneNote 2010's numbers for siblings and children in the formats and levels given, as
2162 /// its COM interface reports them (`evidence/toolbar-17/restart.txt`); `None` is a plain
2163 /// paragraph, `Some("•")` a bullet.
2164 #[test]
2165 fn numbers_restart_where_the_format_changes() {
2166 let numbered = |paragraphs: &[(Option<&str>, u32)]| {
2167 let mut engine = TextEngine::default();
2168 let texts = vec!["x"; paragraphs.len()];
2169 let mut editor = plain(&mut engine, &texts);
2170 let mut nodes = editor.active_outline().document.nodes().to_vec();
2171 for (node, (list, level)) in nodes.iter_mut().zip(paragraphs) {
2172 node.level = *level;
2173 let style = match list {
2174 None => continue,
2175 Some("•") => ListStyle::BULLET,
2176 Some(_) => ListStyle::NUMBER,
2177 };
2178 let mut definition = list_definition(style, &calibri());
2179 if let (Some(value), Kind::List { format, .. }) = (list, &mut definition.kind)
2180 && style == ListStyle::NUMBER
2181 {
2182 *format = Some((*value).into());
2183 }
2184 let id = new_id().unwrap();
2185 editor.definitions.insert(id, definition);
2186 node.lists = vec![id];
2187 }
2188 let outline = Outline {
2189 paragraphs: nodes,
2190 ..editor.active_outline().snapshot()
2191 };
2192 let editor =
2193 CanvasEditor::from_outlines(&mut engine, vec![outline], editor.definitions)
2194 .unwrap();
2195 numbers(&mut engine, &editor)
2196 };
2197 let [dot, paren, letter] = ["\u{fffd}\u{0}.", "\u{fffd}\u{0})", "\u{fffd}\u{4}."];
2198 // Another format starts again, and so does the first format after it.
2199 assert_eq!(
2200 numbered(&[
2201 (Some(dot), 1),
2202 (Some(dot), 1),
2203 (Some(paren), 1),
2204 (Some(dot), 1),
2205 (Some(paren), 1),
2206 ]),
2207 [Some(1), Some(2), Some(1), Some(1), Some(1)]
2208 );
2209 // Plain and bulleted siblings leave the count alone.
2210 assert_eq!(
2211 numbered(&[(Some(dot), 1), (None, 1), (Some("•"), 1), (Some(dot), 1)]),
2212 [Some(1), None, None, Some(2)]
2213 );
2214 // So does another sequence; children count apart from their parents, afresh under each.
2215 assert_eq!(
2216 numbered(&[(Some(dot), 1), (Some(letter), 1), (Some(dot), 1)]),
2217 [Some(1), Some(1), Some(1)]
2218 );
2219 assert_eq!(
2220 numbered(&[
2221 (Some(dot), 1),
2222 (Some(dot), 2),
2223 (Some(dot), 2),
2224 (Some(dot), 1),
2225 (Some(dot), 2),
2226 (None, 1),
2227 (Some(dot), 1),
2228 ]),
2229 [Some(1), Some(1), Some(2), Some(2), Some(1), None, Some(3)]
2230 );
2231 }
2232
2233 fn text_tags(editor: &CanvasEditor, paragraph: usize) -> Vec<(String, u16, bool)> {
2234 let (_, _, node) = editor.active_outline().document.leaf(paragraph).unwrap();
2235 tags(node)
2236 .map(|tag| {
2237 let Some(Kind::TagDefinition { label, .. }) = editor.tag_kind(tag) else {
2238 panic!("{tag:?}")
2239 };
2240 let settled = tag.completed
2241 == if tag.status & 1 == 0 {
2242 Some(0)
2243 } else {
2244 tag.created
2245 };
2246 (
2247 label.clone().unwrap(),
2248 tag.status,
2249 settled || tag.status & 1 == 1,
2250 )
2251 })
2252 .collect()
2253 }
2254
2255 /// OneNote 2010 checks a clicked To Do box (status 1, completion time now) and clears it
2256 /// (status 0, completion time kept as zero) without moving the caret.
2257 #[test]
2258 fn clicking_a_check_box_checks_and_clears_it_as_one_step() {
2259 let mut engine = TextEngine::default();
2260 let mut editor = plain(&mut engine, &["task", "other"]);
2261 editor.select([at(0, 0), at(1, 5)].into()).unwrap();
2262 editor
2263 .format(
2264 &mut engine,
2265 Formatting::Tag(NoteTag::defaults()[0].clone(), 0),
2266 )
2267 .unwrap();
2268 editor.select([at(1, 2); 2].into()).unwrap();
2269 let outline = editor.active_outline().id;
2270 let task = editor.active_outline().document.nodes()[0].id;
2271 let selection = editor.selection();
2272 editor.click_check(&mut engine, outline, task).unwrap();
2273 assert_eq!(editor.selection(), selection);
2274 assert_eq!(text_tags(&editor, 0), [("To Do".into(), 1, true)]);
2275 assert_eq!(text_tags(&editor, 1), [("To Do".into(), 0, true)]);
2276 let tag = |editor: &CanvasEditor| {
2277 editor.active_outline().document.nodes()[0]
2278 .text()
2279 .unwrap()
2280 .tags[0]
2281 .clone()
2282 };
2283 assert!(tag(&editor).completed.is_some_and(|time| time > 0));
2284 editor.click_check(&mut engine, outline, task).unwrap();
2285 assert_eq!((tag(&editor).status, tag(&editor).completed), (0, Some(0)));
2286 editor.undo(&mut engine).unwrap();
2287 assert_eq!(tag(&editor).status, 1);
2288 }
2289
2290 #[test]
2291 fn tags_toggle_check_and_replace_tags_of_their_action_type() {
2292 let mut engine = TextEngine::default();
2293 let mut editor = plain(&mut engine, &["task", "other", "music"]);
2294 let music = new_id().unwrap();
2295 editor.definitions.insert(
2296 music,
2297 Definition {
2298 kind: Kind::TagDefinition {
2299 label: Some("Music".into()),
2300 action_type: Some(2),
2301 shape: Some(121),
2302 color: None,
2303 highlight: None,
2304 },
2305 format: Format::default(),
2306 },
2307 );
2308 let mut nodes = editor.active_outline().document.nodes().to_vec();
2309 nodes[2].tags.push(Tag {
2310 definition: Some(music),
2311 action_type: None,
2312 shape: None,
2313 property_status: None,
2314 status: 1,
2315 created: Some(1),
2316 completed: Some(1),
2317 start: None,
2318 due: None,
2319 task_id: None,
2320 extra_set: 0,
2321 });
2322 let outline = Outline {
2323 paragraphs: nodes,
2324 ..editor.active_outline().snapshot()
2325 };
2326 let mut editor =
2327 CanvasEditor::from_outlines(&mut engine, vec![outline], editor.definitions).unwrap();
2328 let original = editor.active_outline().document.clone();
2329 editor.select([at(0, 1), at(1, 2)].into()).unwrap();
2330 editor
2331 .format(
2332 &mut engine,
2333 Formatting::Tag(NoteTag::defaults()[0].clone(), 0),
2334 )
2335 .unwrap();
2336 assert_eq!(text_tags(&editor, 0), [("To Do".into(), 0, true)]);
2337 assert_eq!(text_tags(&editor, 1), [("To Do".into(), 0, true)]);
2338 assert_eq!(
2339 editor.format_state().unwrap().tags,
2340 [(NoteTag::defaults()[0].clone(), 0)]
2341 );
2342 let icon =
2343 |editor: &CanvasEditor| editor.active_outline().shaped.paragraphs[1].tags[0].icon;
2344 assert_eq!(
2345 icon(&editor),
2346 crate::outline::TagIcon::Symbol {
2347 shape: 3,
2348 checked: false
2349 }
2350 );
2351 editor.format(&mut engine, Formatting::Check).unwrap();
2352 assert_eq!(text_tags(&editor, 1), [("To Do".into(), 1, true)]);
2353 assert_eq!(
2354 icon(&editor),
2355 crate::outline::TagIcon::Symbol {
2356 shape: 3,
2357 checked: true
2358 }
2359 );
2360 editor.select([at(1, 0); 2].into()).unwrap();
2361 editor.format(&mut engine, Formatting::Check).unwrap();
2362 editor.select([at(0, 1), at(1, 2)].into()).unwrap();
2363 editor.format(&mut engine, Formatting::Check).unwrap();
2364 assert_eq!(text_tags(&editor, 0), [("To Do".into(), 1, true)]);
2365 assert_eq!(text_tags(&editor, 1), [("To Do".into(), 1, true)]);
2366
2367 editor.select([at(1, 0), at(2, 5)].into()).unwrap();
2368 editor
2369 .format(
2370 &mut engine,
2371 Formatting::Tag(NoteTag::defaults()[2].clone(), 2),
2372 )
2373 .unwrap();
2374 assert_eq!(
2375 text_tags(&editor, 1),
2376 [("Question".into(), 1, true), ("To Do".into(), 1, true)]
2377 );
2378 assert_eq!(text_tags(&editor, 2), [("Question".into(), 1, true)]);
2379 assert_eq!(
2380 editor.format_state().unwrap().tags,
2381 [(NoteTag::defaults()[2].clone(), 2)]
2382 );
2383 let questions = editor
2384 .definitions
2385 .values()
2386 .filter(|definition| **definition == NoteTag::defaults()[2].definition(2))
2387 .count();
2388 assert_eq!(questions, 1);
2389 editor
2390 .format(
2391 &mut engine,
2392 Formatting::Tag(NoteTag::defaults()[2].clone(), 2),
2393 )
2394 .unwrap();
2395 assert_eq!(text_tags(&editor, 2), []);
2396 editor.select([at(0, 0); 2].into()).unwrap();
2397 editor
2398 .format(
2399 &mut engine,
2400 Formatting::Tag(NoteTag::defaults()[0].clone(), 0),
2401 )
2402 .unwrap();
2403 assert_eq!(text_tags(&editor, 0), []);
2404 for _ in 0..7 {
2405 editor.undo(&mut engine).unwrap();
2406 }
2407 assert!(!editor.undo(&mut engine).unwrap());
2408 assert_eq!(editor.active_outline().document, original);
2409 }
2410
2411 #[test]
2412 fn lists_become_to_do_lists_and_back_as_one_undo_step_each() {
2413 let mut engine = TextEngine::default();
2414 let mut editor = plain(&mut engine, &["one", "two", "three"]);
2415 editor.select([at(0, 0), at(1, 3)].into()).unwrap();
2416 editor.format(&mut engine, Formatting::Bullets).unwrap();
2417 editor.select([at(1, 0); 2].into()).unwrap();
2418 editor.format(&mut engine, Formatting::Numbering).unwrap();
2419 let listed = editor.active_outline().document.clone();
2420 let all = Selection::from([at(0, 0), at(2, 5)]);
2421 editor.select(all).unwrap();
2422 assert!(editor.format_state().unwrap().listed);
2423 let to_do = NoteTag::defaults()[0].clone();
2424 editor
2425 .format(&mut engine, Formatting::ToDoList(to_do.clone(), 0))
2426 .unwrap();
2427 let lists = |editor: &CanvasEditor| {
2428 let document = &editor.active_outline().document;
2429 document
2430 .nodes()
2431 .iter()
2432 .map(|node| editor.list(node))
2433 .collect::<Vec<_>>()
2434 };
2435 assert_eq!(lists(&editor), [None, None, None]);
2436 let task = || vec![("To Do".to_owned(), 0, true)];
2437 assert_eq!(
2438 (0..3).map(|at| text_tags(&editor, at)).collect::<Vec<_>>(),
2439 [task(), task(), vec![]]
2440 );
2441 let state = editor.format_state().unwrap();
2442 assert!(!state.listed && state.tags.is_empty());
2443 editor.select([at(0, 0), at(1, 3)].into()).unwrap();
2444 assert_eq!(editor.format_state().unwrap().tags, [(to_do.clone(), 0)]);
2445 editor
2446 .format(&mut engine, Formatting::BulletedList(to_do, 0))
2447 .unwrap();
2448 assert_eq!(
2449 lists(&editor),
2450 [Some(Formatting::Bullets), Some(Formatting::Bullets), None]
2451 );
2452 assert_eq!(text_tags(&editor, 0), []);
2453 assert_eq!(text_tags(&editor, 1), []);
2454 let state = editor.format_state().unwrap();
2455 assert_eq!((state.list, state.tags), (Some(ListStyle::BULLET), vec![]));
2456 editor.undo(&mut engine).unwrap();
2457 assert_eq!(text_tags(&editor, 1), task());
2458 editor.undo(&mut engine).unwrap();
2459 assert_eq!(editor.active_outline().document, listed);
2460 }
2461
2462 #[test]
2463 fn the_default_tags_are_onenotes_stored_definitions_and_all_draw() {
2464 use onestore::{RevisionIndex, Store, document::Document};
2465 let bytes = include_bytes!("../../../../corpus/structural-probe/tag-gallery.one");
2466 let store = Store::parse(bytes).unwrap();
2467 let index = RevisionIndex::parse(&store).unwrap();
2468 let document = Document::parse(&index).unwrap();
2469 let (space, _) = document.pages().unwrap()[0];
2470 let stored = Page::from_space(&document, space).unwrap().definitions;
2471 let stored = stored
2472 .values()
2473 .filter(|definition| matches!(definition.kind, Kind::TagDefinition { .. }))
2474 .collect::<Vec<_>>();
2475 let defaults = NoteTag::defaults();
2476 assert_eq!(stored.len(), defaults.len());
2477 for (place, tag) in defaults.iter().enumerate() {
2478 assert!(stored.contains(&&tag.definition(place as u16)), "{tag:?}");
2479 }
2480 let mut engine = TextEngine::default();
2481 let mut editor = plain(&mut engine, &["tagged"]);
2482 for (place, tag) in defaults.iter().enumerate() {
2483 editor
2484 .format(&mut engine, Formatting::Tag(tag.clone(), place as u16))
2485 .unwrap();
2486 }
2487 assert_eq!(editor.format_state().unwrap().tags.len(), defaults.len());
2488 let paragraph = &editor.active_outline().shaped.paragraphs[0];
2489 let shapes = paragraph
2490 .tags
2491 .iter()
2492 .map(|tag| match tag.icon {
2493 crate::outline::TagIcon::Symbol { shape, .. } => shape,
2494 crate::outline::TagIcon::Task { .. } => unreachable!(),
2495 })
2496 .collect::<Vec<_>>();
2497 assert_eq!(shapes.len(), defaults.len() - 2);
2498 assert_eq!(shapes[..7], [3, 13, 15, 136, 118, 23, 18]);
2499 // As OneNote stores them unchecked: To Do, Discuss with, Schedule meeting, Call back,
2500 // the priorities and Client request.
2501 let checkable = defaults
2502 .iter()
2503 .filter(|tag| crate::outline::checkable(tag.shape))
2504 .count();
2505 assert_eq!(checkable, 9);
2506 // Remember for later and Definition draw no symbol; the newer one's green marks the text.
2507 assert!(
2508 paragraph
2509 .text
2510 .backgrounds()
2511 .all(|(_, color)| color == 0x0000_ff00)
2512 );
2513 assert!(paragraph.text.backgrounds().next().is_some());
2514 }
2515
2516 /// A customized tag stores its name, symbol and colours with its place in the list as
2517 /// its action type; applied from another place it takes a definition of its own, as
2518 /// OneNote 2010's does (`corpus/custom-tags`).
2519 #[test]
2520 fn custom_tags_store_their_look_and_place() {
2521 let mut engine = TextEngine::default();
2522 let mut editor = plain(&mut engine, &["one", "two"]);
2523 let tag = NoteTag {
2524 label: "Snow check".into(),
2525 shape: 61,
2526 color: Some(0x0000_0080),
2527 highlight: Some(0x00ff_cc00),
2528 art: None,
2529 };
2530 editor
2531 .format(&mut engine, Formatting::Tag(tag.clone(), 0))
2532 .unwrap();
2533 assert_eq!(editor.format_state().unwrap().tags, [(tag.clone(), 0)]);
2534 assert_eq!(text_tags(&editor, 0), [("Snow check".into(), 1, true)]);
2535 editor.select([at(1, 0); 2].into()).unwrap();
2536 editor
2537 .format(&mut engine, Formatting::Tag(tag.clone(), 1))
2538 .unwrap();
2539 assert_eq!(editor.format_state().unwrap().tags, [(tag.clone(), 1)]);
2540 let mut stored: Vec<_> = editor
2541 .definitions
2542 .values()
2543 .filter_map(|definition| NoteTag::of(&definition.kind))
2544 .collect();
2545 stored.sort_by_key(|(_, action_type)| *action_type);
2546 assert_eq!(stored, [(tag.clone(), 0), (tag, 1)]);
2547 let paragraph = &editor.active_outline().shaped.paragraphs[0];
2548 assert!(
2549 paragraph
2550 .text
2551 .backgrounds()
2552 .all(|(_, color)| color == 0x00ff_cc00)
2553 );
2554 assert!(paragraph.text.backgrounds().next().is_some());
2555 }
2556
2557 #[test]
2558 fn commands_reach_every_cell_a_selection_crosses() {
2559 let mut engine = TextEngine::default();
2560 let mut editor = plain(&mut engine, &["abcdef"]);
2561 editor.select([at(0, 3); 2].into()).unwrap();
2562 editor.tab(&mut engine, false).unwrap();
2563 let table = editor.active_outline().document.clone();
2564 assert_eq!(table.nodes().len(), 1);
2565 let selection = Selection::from([at(0, 1), at(1, 2)]);
2566 editor.select(selection).unwrap();
2567 editor
2568 .format(&mut engine, Formatting::Toggle(Toggle::Italic))
2569 .unwrap();
2570 assert_eq!(
2571 runs(&editor, |format| format.italic),
2572 [
2573 vec![run("a", None), run("bc", Some(true))],
2574 vec![run("de", Some(true)), run("f", None)]
2575 ]
2576 );
2577 assert!(
2578 editor
2579 .format_state()
2580 .unwrap()
2581 .toggles
2582 .contains(&Toggle::Italic)
2583 );
2584 editor
2585 .format(&mut engine, Formatting::Align(Alignment::Right))
2586 .unwrap();
2587 assert_eq!(
2588 editor.format_state().unwrap().alignment,
2589 Some(Alignment::Right)
2590 );
2591 editor.select([at(1, 0); 2].into()).unwrap();
2592 editor.format(&mut engine, Formatting::Numbering).unwrap();
2593 assert_eq!(numbers(&mut engine, &editor), [None, Some(1)]);
2594 editor
2595 .format(
2596 &mut engine,
2597 Formatting::Tag(NoteTag::defaults()[0].clone(), 0),
2598 )
2599 .unwrap();
2600 assert_eq!(editor.active_outline().shaped.paragraphs[1].tags.len(), 1);
2601 assert_eq!(editor.selection(), Selection::from([at(1, 0); 2]));
2602 for _ in 0..4 {
2603 editor.undo(&mut engine).unwrap();
2604 }
2605 assert_eq!(editor.active_outline().document, table);
2606 assert_eq!(editor.selection(), selection);
2607 }
2608
2609 #[test]
2610 fn formatting_lists_and_tags_survive_the_page_writer() {
2611 use onestore::{RevisionIndex, Store, document::Document};
2612 const SECTION: &[u8] =
2613 include_bytes!("../../../../corpus/paragraph-edit/before/notebook/synthetic.one");
2614 let page = |bytes: &[u8]| {
2615 let store = Store::parse(bytes).unwrap();
2616 let index = RevisionIndex::parse(&store).unwrap();
2617 let document = Document::parse(&index).unwrap();
2618 document
2619 .pages()
2620 .unwrap()
2621 .into_iter()
2622 .map(|(space, _)| (space, Page::from_space(&document, space).unwrap()))
2623 .find(|(_, page)| page.title == "Split middle")
2624 .unwrap()
2625 };
2626 // Tags keep their place in the element's property arena only once read.
2627 let settled = |mut page: Page| {
2628 for object in &mut page.objects {
2629 if let PageObject::Outline(outline) = object {
2630 for text in outline.paragraphs.iter_mut().filter_map(|p| p.text_mut()) {
2631 for tag in &mut text.tags {
2632 tag.extra_set = 0;
2633 }
2634 }
2635 }
2636 }
2637 page
2638 };
2639 let (space, source) = page(SECTION);
2640 let mut engine = TextEngine::default();
2641 let mut editor = CanvasEditor::from_page(source, &mut engine).unwrap();
2642 let body = editor
2643 .outlines()
2644 .iter()
2645 .find(|outline| !outline.title)
2646 .unwrap()
2647 .id;
2648 editor.focus_outline(body).unwrap();
2649 let selection = Selection::from([at(0, 2), at(0, 6)]);
2650 editor.select(selection).unwrap();
2651 for command in [
2652 Formatting::Toggle(Toggle::Bold),
2653 Formatting::Toggle(Toggle::Superscript),
2654 Formatting::FontSize(14.0),
2655 Formatting::Color(Some(0x0000_00ff)),
2656 Formatting::Highlight(Some(0x0000_ffff)),
2657 Formatting::Align(Alignment::Center),
2658 Formatting::Numbering,
2659 Formatting::Tag(NoteTag::defaults()[0].clone(), 0),
2660 Formatting::Tag(NoteTag::defaults()[2].clone(), 2),
2661 Formatting::Check,
2662 ] {
2663 editor.format(&mut engine, command).unwrap();
2664 }
2665 let state = editor.format_state().unwrap();
2666 let edited = settled(editor.page().unwrap());
2667 let written = super::ops::saved(SECTION, space, &mut editor);
2668 let (_, reread) = page(&written);
2669 let reread = settled(reread);
2670 assert_eq!(reread.objects, edited.objects);
2671 let mut editor = CanvasEditor::from_page(reread, &mut engine).unwrap();
2672 editor.focus_outline(body).unwrap();
2673 editor.select(selection).unwrap();
2674 assert_eq!(editor.format_state().unwrap(), state);
2675 assert_eq!(
2676 state,
2677 FormatState {
2678 toggles: vec![Toggle::Bold, Toggle::Superscript],
2679 font: state.font.clone(),
2680 font_size: Some(14.0),
2681 alignment: Some(Alignment::Center),
2682 bullets: false,
2683 numbering: true,
2684 listed: true,
2685 list: Some(ListStyle::NUMBER),
2686 tags: vec![
2687 (NoteTag::defaults()[0].clone(), 0),
2688 (NoteTag::defaults()[2].clone(), 2)
2689 ],
2690 style: state.style.clone(),
2691 style_font: state.style_font.clone(),
2692 style_size: state.style_size,
2693 }
2694 );
2695 // Format Painter, a gallery tag and an inserted table write as well.
2696 let painted = editor.painted_format().unwrap();
2697 editor.select([at(0, 0), at(0, 2)].into()).unwrap();
2698 editor
2699 .format(&mut engine, Formatting::Paint(painted))
2700 .unwrap();
2701 let state = editor.format_state().unwrap();
2702 assert_eq!(
2703 (state.toggles, state.font_size, state.alignment),
2704 (
2705 vec![Toggle::Bold, Toggle::Superscript],
2706 Some(14.0),
2707 Some(Alignment::Center)
2708 )
2709 );
2710 editor
2711 .format(
2712 &mut engine,
2713 Formatting::Tag(NoteTag::defaults()[25].clone(), 25),
2714 )
2715 .unwrap();
2716 editor.select([at(0, 1); 2].into()).unwrap();
2717 editor.insert_table(&mut engine, 2, 3).unwrap();
2718 assert_eq!(editor.selection(), Selection::from([at(1, 0); 2]));
2719 let edited = settled(editor.page().unwrap());
2720 let written = super::ops::saved(&written, space, &mut editor);
2721 let (_, reread) = page(&written);
2722 assert_eq!(settled(reread).objects, edited.objects);
2723 }
2724}