1//! Make To-Do List trades bulleted and numbered paragraphs' lists for the To Do tag, and Make
2//! Bulleted List trades it back, each published as one revision (`corpus/to-do-list`).
3
4use canvas::{
5 document::TextPosition,
6 editor::{CanvasEditor, Formatting, NoteTag},
7 layout::TextEngine,
8};
9use onestore::{
10 Arena, ExGuid, Section, Store,
11 op::{Edit, Op},
12 page::{Page, PageObject},
13};
14
15const LISTS: &[u8] = include_bytes!("../../../corpus/list-edit/cold/notebook/lists.one");
16
17fn body(page: &Page) -> ExGuid {
18 page.objects
19 .iter()
20 .find_map(|object| match object {
21 PageObject::Outline(outline) => Some(outline.id),
22 _ => None,
23 })
24 .unwrap()
25}
26
27fn at(paragraph: usize) -> TextPosition {
28 TextPosition {
29 paragraph,
30 offset: 0,
31 }
32}
33
34/// Each paragraph's list, as `bullet`, `number` or none, and its tags' labels.
35fn items(page: &Page, engine: &mut TextEngine) -> Vec<(Option<&'static str>, Vec<String>)> {
36 let mut editor = CanvasEditor::from_page(page.clone(), engine).unwrap();
37 editor.focus_outline(body(page)).unwrap();
38 (0..7)
39 .map(|paragraph| {
40 editor.select([at(paragraph); 2].into()).unwrap();
41 let state = editor.format_state().unwrap();
42 let list = match (state.bullets, state.numbering) {
43 (true, _) => Some("bullet"),
44 (_, true) => Some("number"),
45 _ => None,
46 };
47 (
48 list,
49 state.tags.into_iter().map(|(tag, _)| tag.label).collect(),
50 )
51 })
52 .collect()
53}
54
55/// `SNOWBOUND_TO_DO_LIST_EXPORT` names a new directory receiving the candidate for a cold
56/// reopen.
57#[test]
58fn lists_become_to_do_lists_and_back_one_revision_each() {
59 let arena = Arena::default();
60 let mut section = Section::open(&arena, LISTS.to_vec()).unwrap();
61 let space = section.pages().unwrap()[0].0;
62 let page = section.page(space).unwrap();
63 let mut engine = TextEngine::default();
64 let mut editor = CanvasEditor::from_page(page.clone(), &mut engine).unwrap();
65 editor.focus_outline(body(&page)).unwrap();
66 let to_do = NoteTag::defaults()[0].clone();
67 let mut publish = |editor: &mut CanvasEditor, at| {
68 let ops = editor
69 .take_ops()
70 .unwrap()
71 .into_iter()
72 .map(|op| Op::Page { space, op })
73 .collect();
74 section.apply("Author", &Edit { at, ops }).unwrap();
75 };
76 // Through "Nested numbered", the last paragraph each edit takes.
77 let nested = TextPosition {
78 paragraph: 3,
79 offset: 15,
80 };
81 editor.select([at(0), nested].into()).unwrap();
82 editor
83 .format(&mut engine, Formatting::ToDoList(to_do.clone(), 0))
84 .unwrap();
85 publish(&mut editor, 134_000_000_000_000_000);
86 editor.select([at(2), nested].into()).unwrap();
87 editor
88 .format(&mut engine, Formatting::BulletedList(to_do, 0))
89 .unwrap();
90 publish(&mut editor, 134_000_000_010_000_000);
91
92 let task = || vec!["To Do".to_owned()];
93 let expected = [
94 (None, task()),
95 (None, task()),
96 (Some("bullet"), vec![]),
97 (Some("bullet"), vec![]),
98 (Some("number"), vec![]),
99 (None, vec![]),
100 (None, vec![]),
101 ];
102 assert_eq!(items(&section.page(space).unwrap(), &mut engine), expected);
103 section.seal().unwrap();
104 let written = section.image();
105 let reread = Section::open(&arena, written.clone()).unwrap();
106 assert_eq!(items(&reread.page(space).unwrap(), &mut engine), expected);
107 export("SNOWBOUND_TO_DO_LIST_EXPORT", &written);
108}
109
110/// Writes section `written` and a table of contents to the new directory `variable` names.
111fn export(variable: &str, written: &[u8]) {
112 let Some(directory) = std::env::var_os(variable) else {
113 return;
114 };
115 let directory = std::path::PathBuf::from(directory);
116 std::fs::create_dir(&directory).unwrap();
117 std::fs::write(directory.join("lists.one"), written).unwrap();
118 let file_id = Store::parse(written).unwrap().header.file_id;
119 std::fs::write(
120 directory.join("Open Notebook.onetoc2"),
121 onestore::create_table_of_contents("Open Notebook.onetoc2", &[("lists.one", file_id)])
122 .unwrap(),
123 )
124 .unwrap();
125}
126
127/// Each top-level paragraph's text and its tags' check states.
128fn checks(page: &Page) -> Vec<(String, Vec<bool>)> {
129 page.objects
130 .iter()
131 .find_map(|object| match object {
132 PageObject::Outline(outline) => Some(&outline.paragraphs),
133 _ => None,
134 })
135 .unwrap()
136 .iter()
137 .filter(|node| node.level == 1)
138 .map(|node| {
139 let text = node.text().unwrap();
140 let checked = text.tags.iter().map(|tag| tag.status & 1 == 1).collect();
141 (text.text.text().to_owned(), checked)
142 })
143 .collect()
144}
145
146/// Enter after a checked To Do opens an unchecked one, and Enter on that empty item ends the
147/// list, each Enter one undo step and one revision. `SNOWBOUND_TO_DO_ENTER_EXPORT` names a new
148/// directory receiving the candidate for a cold reopen.
149#[test]
150fn enter_continues_a_to_do_list_and_twice_ends_it() {
151 let arena = Arena::default();
152 let mut section = Section::open(&arena, LISTS.to_vec()).unwrap();
153 let space = section.pages().unwrap()[0].0;
154 let page = section.page(space).unwrap();
155 let mut engine = TextEngine::default();
156 let mut editor = CanvasEditor::from_page(page.clone(), &mut engine).unwrap();
157 editor.focus_outline(body(&page)).unwrap();
158 let mut at_time = 134_000_000_000_000_000;
159 let mut publish = |editor: &mut CanvasEditor| {
160 let ops = editor
161 .take_ops()
162 .unwrap()
163 .into_iter()
164 .map(|op| Op::Page { space, op })
165 .collect::<Vec<_>>();
166 at_time += 10_000_000;
167 section.apply("Author", &Edit { at: at_time, ops }).unwrap();
168 };
169 let end = |paragraph, offset| [TextPosition { paragraph, offset }; 2].into();
170 editor
171 .select(
172 [
173 at(0),
174 TextPosition {
175 paragraph: 1,
176 offset: 14,
177 },
178 ]
179 .into(),
180 )
181 .unwrap();
182 editor
183 .format(
184 &mut engine,
185 Formatting::ToDoList(NoteTag::defaults()[0].clone(), 0),
186 )
187 .unwrap();
188 editor.select(end(0, 0)).unwrap();
189 editor.format(&mut engine, Formatting::Check).unwrap();
190 publish(&mut editor);
191
192 editor.select(end(0, 11)).unwrap();
193 editor.enter(&mut engine, false).unwrap();
194 publish(&mut editor);
195 editor.insert(&mut engine, "Next task").unwrap();
196 publish(&mut editor);
197 editor.select(end(2, 14)).unwrap();
198 for _ in 0..2 {
199 editor.enter(&mut engine, false).unwrap();
200 publish(&mut editor);
201 }
202 editor.insert(&mut engine, "After the list").unwrap();
203 publish(&mut editor);
204
205 let expected = [
206 ("Bullet item", vec![true]),
207 ("Next task", vec![false]),
208 ("First numbered", vec![false]),
209 ("", vec![]),
210 ("After the list", vec![]),
211 ("Second numbered", vec![]),
212 ("Restarted at three", vec![]),
213 ("Plain again", vec![]),
214 ]
215 .map(|(text, checked)| (text.to_owned(), checked));
216 assert_eq!(checks(&section.page(space).unwrap()), expected);
217 section.seal().unwrap();
218 let written = section.image();
219 let reread = Section::open(&arena, written.clone()).unwrap();
220 assert_eq!(checks(&reread.page(space).unwrap()), expected);
221 for _ in 0..3 {
222 assert!(editor.undo(&mut engine).unwrap());
223 }
224 assert_eq!(
225 checks(&editor.page().unwrap())
226 .iter()
227 .map(|(text, _)| text.as_str())
228 .take(4)
229 .collect::<Vec<_>>(),
230 [
231 "Bullet item",
232 "Next task",
233 "First numbered",
234 "Second numbered"
235 ]
236 );
237 export("SNOWBOUND_TO_DO_ENTER_EXPORT", &written);
238}