1#[path = "support/ops.rs"]
2mod ops;
3use onestore::{
4 ExGuid, RevisionIndex, Store,
5 document::{Document, Format, Kind},
6 page::{
7 Definition, Page, PageObject, PageParagraph, ParagraphContent, TextObject, text::new_id,
8 },
9};
10use std::collections::BTreeMap;
11
12const TREES: &[u8] =
13 include_bytes!("../../../corpus/outline-edit/tree/before/notebook/synthetic.one");
14
15fn page_by_title(bytes: &[u8], title: &str) -> (ExGuid, Page) {
16 let store = Store::parse(bytes).unwrap();
17 let index = RevisionIndex::parse(&store).unwrap();
18 let document = Document::parse(&index).unwrap();
19 document
20 .pages()
21 .unwrap()
22 .into_iter()
23 .find_map(|(space, _)| {
24 let page = Page::from_space(&document, space).unwrap();
25 (page.title == title).then_some((space, page))
26 })
27 .unwrap()
28}
29
30fn page_in(bytes: &[u8], space: ExGuid) -> Page {
31 let store = Store::parse(bytes).unwrap();
32 let index = RevisionIndex::parse(&store).unwrap();
33 Page::from_space(&Document::parse(&index).unwrap(), space).unwrap()
34}
35
36fn body_paragraphs(page: &mut Page) -> &mut Vec<PageParagraph> {
37 page.objects
38 .iter_mut()
39 .find_map(|object| match object {
40 PageObject::Outline(outline) if !outline.title => Some(&mut outline.paragraphs),
41 _ => None,
42 })
43 .unwrap()
44}
45
46fn list_ids(page: &Page) -> Vec<Vec<ExGuid>> {
47 page.objects
48 .iter()
49 .filter_map(|object| match object {
50 PageObject::Outline(outline) if !outline.title => {
51 Some(outline.paragraphs.iter().map(|p| p.lists.clone()).collect())
52 }
53 _ => None,
54 })
55 .next()
56 .unwrap()
57}
58
59fn raw_object(bytes: &[u8], space: ExGuid, id: ExGuid) -> String {
60 let store = Store::parse(bytes).unwrap();
61 let index = RevisionIndex::parse(&store).unwrap();
62 format!(
63 "{:?}",
64 index.resolve_active(space).unwrap().objects[&id].data
65 )
66}
67
68fn bullet_definition() -> Definition {
69 Definition {
70 kind: Kind::List {
71 font: Some("Courier New".into()),
72 format: Some("\u{25cb}".into()),
73 restart: None,
74 bullet: Some(4),
75 },
76 format: Format {
77 font_size: Some(11.0),
78 color: Some(0xff000000),
79 ..Default::default()
80 },
81 }
82}
83
84fn plain_paragraph(template: &PageParagraph, text: &str) -> PageParagraph {
85 let mut paragraph = template.clone();
86 paragraph.id = new_id().unwrap();
87 paragraph.parent = None;
88 paragraph.level = 1;
89 paragraph.lists.clear();
90 paragraph.tags.clear();
91 paragraph.style = None;
92 paragraph.collapsed = false;
93 paragraph.content = ParagraphContent::Text(TextObject {
94 id: new_id().unwrap(),
95 date_field: None,
96 text: onestore::page::Paragraph::new(
97 text.into(),
98 Format {
99 font: Some("Calibri".into()),
100 font_size: Some(11.0),
101 language: Some(1033),
102 ..Default::default()
103 },
104 ),
105 tags: Vec::new(),
106 });
107 paragraph
108}
109
110#[test]
111fn a_numbered_paragraph_becomes_plain_while_its_neighbours_keep_their_nodes() {
112 let (space, before) = page_by_title(TREES, "Move numbered subtree down");
113 let lists = list_ids(&before);
114 let numbered: Vec<usize> = (0..lists.len()).filter(|i| !lists[*i].is_empty()).collect();
115 assert!(numbered.len() >= 2, "{lists:?}");
116 let mut after = before.clone();
117 body_paragraphs(&mut after)[numbered[0]].lists.clear();
118 let written = ops::saved(TREES, space, &after).unwrap();
119 let stored = page_in(written.as_slice(), space);
120 let mut expected = after.clone();
121 expected.title = stored.title.clone();
122 expected.definitions.remove(&lists[numbered[0]][0]);
123 assert_eq!(stored, expected);
124 for other in &numbered[1..] {
125 for node in &lists[*other] {
126 assert_eq!(
127 raw_object(written.as_slice(), space, *node),
128 raw_object(TREES, space, *node)
129 );
130 }
131 }
132 assert_eq!(
133 ops::saved(written.as_slice(), space, &stored)
134 .unwrap()
135 .as_slice(),
136 written.as_slice()
137 );
138}
139
140#[test]
141fn bullets_and_numbering_write_from_definitions_and_read_back() {
142 let (space, before) = page_by_title(TREES, "Move numbered subtree down");
143 let lists = list_ids(&before);
144 let numbered = (0..lists.len()).find(|i| !lists[*i].is_empty()).unwrap();
145 let plain = (0..lists.len()).find(|i| lists[*i].is_empty()).unwrap();
146 let mut after = before.clone();
147 let bullet = new_id().unwrap();
148 after.definitions.insert(bullet, bullet_definition());
149 let template = body_paragraphs(&mut after)[plain].clone();
150 body_paragraphs(&mut after)[plain].lists = vec![bullet];
151 let number = new_id().unwrap();
152 let mut numbering = after.definitions[&lists[numbered][0]].clone();
153 let Kind::List { restart, .. } = &mut numbering.kind else {
154 panic!()
155 };
156 *restart = Some(3);
157 after.definitions.insert(number, numbering);
158 let mut fresh = plain_paragraph(&template, "Numbered from three");
159 fresh.lists = vec![number];
160 body_paragraphs(&mut after).push(fresh);
161 let written = ops::saved(TREES, space, &after).unwrap();
162 let stored = page_in(written.as_slice(), space);
163 let mut expected = after.clone();
164 expected.title = stored.title.clone();
165 assert_eq!(stored, expected);
166 assert_eq!(
167 ops::saved(written.as_slice(), space, &stored)
168 .unwrap()
169 .as_slice(),
170 written.as_slice()
171 );
172}
173
174#[test]
175fn a_list_definition_changes_in_place() {
176 let (space, before) = page_by_title(TREES, "Move numbered subtree down");
177 let lists = list_ids(&before);
178 let numbered = (0..lists.len()).find(|i| !lists[*i].is_empty()).unwrap();
179 let node = lists[numbered][0];
180 let mut after = before.clone();
181 let definition = after.definitions.get_mut(&node).unwrap();
182 let Kind::List { restart, .. } = &mut definition.kind else {
183 panic!()
184 };
185 *restart = Some(7);
186 definition.format.bold = Some(true);
187 let written = ops::saved(TREES, space, &after).unwrap();
188 let stored = page_in(written.as_slice(), space);
189 let mut expected = after.clone();
190 expected.title = stored.title.clone();
191 assert_eq!(stored, expected);
192 assert_eq!(list_ids(&stored)[numbered], vec![node]);
193}
194
195#[test]
196fn a_definition_shared_between_paragraphs_becomes_a_node_per_paragraph() {
197 let (space, before) = page_by_title(TREES, "Move numbered subtree down");
198 let lists = list_ids(&before);
199 let numbered = (0..lists.len()).find(|i| !lists[*i].is_empty()).unwrap();
200 let plain = (0..lists.len()).find(|i| lists[*i].is_empty()).unwrap();
201 let mut shared = before.clone();
202 body_paragraphs(&mut shared)[plain].lists = lists[numbered].clone();
203 let written = ops::saved(TREES, space, &shared).unwrap();
204 let stored = page_in(written.as_slice(), space);
205 let after = list_ids(&stored);
206 assert_eq!(after[numbered], lists[numbered]);
207 assert_eq!(after[plain].len(), 1);
208 assert_ne!(after[plain], lists[numbered]);
209 assert_eq!(
210 stored.definitions[&after[plain][0]],
211 before.definitions[&lists[numbered][0]]
212 );
213}
214
215#[test]
216fn missing_and_foreign_list_definitions_are_refused() {
217 let (space, before) = page_by_title(TREES, "Move numbered subtree down");
218 let lists = list_ids(&before);
219 let plain = (0..lists.len()).find(|i| lists[*i].is_empty()).unwrap();
220 let mut missing = before.clone();
221 body_paragraphs(&mut missing)[plain].lists = vec![new_id().unwrap()];
222 assert!(ops::saved(TREES, space, &missing).is_err());
223 let mut foreign = before.clone();
224 let paragraph = body_paragraphs(&mut foreign)[plain].id;
225 foreign.definitions.insert(paragraph, bullet_definition());
226 body_paragraphs(&mut foreign)[plain].lists = vec![paragraph];
227 assert!(ops::saved(TREES, space, &foreign).is_err());
228 let mut style: BTreeMap<ExGuid, Definition> = before.definitions.clone();
229 if let Some((id, definition)) = style
230 .iter_mut()
231 .find(|(_, d)| matches!(d.kind, Kind::Style { .. }))
232 {
233 definition.format.bold = Some(true);
234 let mut edited = before.clone();
235 edited.definitions.insert(*id, definition.clone());
236 assert!(ops::saved(TREES, space, &edited).is_err());
237 }
238}
239
240/// A fresh page gains a bullet, two numbered paragraphs with a nested numbered child, a
241/// numbering restart and a trailing plain paragraph; `ONESTORE_LIST_EXPORT` names a new
242/// directory receiving the candidate for a cold native reopen.
243#[test]
244fn bullets_numbering_nesting_and_restarts_publish_on_a_fresh_page() {
245 let source = onestore::create_section("lists.one", "Bullet item", "Author").unwrap();
246 let store = Store::parse(&source).unwrap();
247 let index = RevisionIndex::parse(&store).unwrap();
248 let document = Document::parse(&index).unwrap();
249 let (space, _) = document.pages().unwrap()[0];
250 let before = Page::from_space(&document, space).unwrap();
251 let (_, trees) = page_by_title(TREES, "Move numbered subtree down");
252 let numbering = trees
253 .definitions
254 .values()
255 .find(|d| {
256 matches!(
257 d.kind,
258 Kind::List {
259 format: Some(_),
260 ..
261 }
262 )
263 })
264 .unwrap()
265 .clone();
266 let scenario = std::env::var("ONESTORE_LIST_SCENARIO").unwrap_or_else(|_| "all".into());
267 let wants = |name: &str| scenario == "all" || scenario == name;
268 let mut after = before.clone();
269 let template = body_paragraphs(&mut after)[0].clone();
270 if wants("bullet") {
271 let bullet = new_id().unwrap();
272 after.definitions.insert(bullet, bullet_definition());
273 body_paragraphs(&mut after)[0].lists = vec![bullet];
274 }
275 for (text, level, restart, name) in [
276 ("First numbered", 1, None, "numbered"),
277 ("Second numbered", 1, None, "numbered"),
278 ("Nested numbered", 2, None, "all"),
279 ("Restarted at three", 1, Some(3), "restart"),
280 ("Nested plain", 2, None, "nested"),
281 ] {
282 if !wants(name) {
283 continue;
284 }
285 let mut paragraph = plain_paragraph(&template, text);
286 if name != "nested" {
287 let id = new_id().unwrap();
288 let mut definition = numbering.clone();
289 let Kind::List { restart: value, .. } = &mut definition.kind else {
290 panic!()
291 };
292 *value = restart;
293 after.definitions.insert(id, definition);
294 paragraph.lists = vec![id];
295 }
296 paragraph.level = level;
297 if level == 2 {
298 paragraph.parent = Some(body_paragraphs(&mut after).last().unwrap().id);
299 }
300 body_paragraphs(&mut after).push(paragraph);
301 }
302 body_paragraphs(&mut after).push(plain_paragraph(&template, "Plain again"));
303 let written = ops::saved(&source, space, &after).unwrap();
304 let stored = page_in(written.as_slice(), space);
305 let mut expected = after.clone();
306 expected.title = stored.title.clone();
307 assert_eq!(stored, expected);
308 if let Some(directory) = std::env::var_os("ONESTORE_LIST_EXPORT") {
309 let directory = std::path::PathBuf::from(directory);
310 std::fs::create_dir(&directory).unwrap();
311 std::fs::write(directory.join("lists.one"), written.as_slice()).unwrap();
312 let written_store = Store::parse(written.as_slice()).unwrap();
313 std::fs::write(
314 directory.join("Open Notebook.onetoc2"),
315 onestore::create_table_of_contents(
316 "Open Notebook.onetoc2",
317 &[("lists.one", written_store.header.file_id)],
318 )
319 .unwrap(),
320 )
321 .unwrap();
322 }
323}