| 1 | #[path = "support/ops.rs"] |
| 2 | mod ops; |
| 3 | use onestore::{ |
| 4 | ExGuid, RevisionIndex, Store, |
| 5 | document::{Document, Kind}, |
| 6 | page::{Page, PageObject, Paragraph}, |
| 7 | }; |
| 8 | |
| 9 | const SOURCE: &[u8] = include_bytes!("../../../corpus/paragraph-format/before/synthetic.one"); |
| 10 | |
| 11 | fn page(bytes: &[u8]) -> (ExGuid, Page) { |
| 12 | let store = Store::parse(bytes).unwrap(); |
| 13 | assert!(store.checksum_mismatches.is_empty()); |
| 14 | let index = RevisionIndex::parse(&store).unwrap(); |
| 15 | index.validate_current().unwrap(); |
| 16 | let document = Document::parse(&index).unwrap(); |
| 17 | document |
| 18 | .pages() |
| 19 | .unwrap() |
| 20 | .into_iter() |
| 21 | .find_map(|(space, _)| { |
| 22 | let page = Page::from_space(&document, space).unwrap(); |
| 23 | (page.title == "Paragraph formatting").then_some((space, page)) |
| 24 | }) |
| 25 | .unwrap() |
| 26 | } |
| 27 | |
| 28 | #[test] |
| 29 | fn paragraph_properties_preserve_native_mixed_runs_and_shared_styles() { |
| 30 | let (space, before) = page(SOURCE); |
| 31 | let mut after = before.clone(); |
| 32 | let mut count = 0; |
| 33 | for object in &mut after.objects { |
| 34 | if let PageObject::Outline(outline) = object { |
| 35 | for paragraph in &mut outline.paragraphs { |
| 36 | let text = paragraph.text_mut().unwrap(); |
| 37 | let mut start = 0; |
| 38 | text.text = Paragraph::from_runs(text.text.spans().iter().map(|span| { |
| 39 | let value = text.text.text()[start..span.end].to_owned(); |
| 40 | start = span.end; |
| 41 | let mut format = span.format.clone(); |
| 42 | format.alignment = Some(1); |
| 43 | format.space_before = Some(0.0); |
| 44 | format.space_after = Some(2.0); |
| 45 | format.line_spacing = Some(16.0); |
| 46 | (value, format) |
| 47 | })); |
| 48 | count += 1; |
| 49 | } |
| 50 | } |
| 51 | } |
| 52 | assert_eq!(count, 6); |
| 53 | let prepared = ops::saved(SOURCE, space, &after).unwrap(); |
| 54 | assert_eq!(page(prepared.as_slice()).1, after); |
| 55 | let before_store = Store::parse(SOURCE).unwrap(); |
| 56 | let before_index = RevisionIndex::parse(&before_store).unwrap(); |
| 57 | let before_doc = Document::parse(&before_index).unwrap(); |
| 58 | let after_store = Store::parse(prepared.as_slice()).unwrap(); |
| 59 | let after_index = RevisionIndex::parse(&after_store).unwrap(); |
| 60 | let old = before_index.resolve_active(space).unwrap(); |
| 61 | let new = after_index.resolve_active(space).unwrap(); |
| 62 | for (id, node) in &before_doc.active(space).unwrap().nodes { |
| 63 | if matches!(node.kind, Kind::Style { .. }) { |
| 64 | assert_eq!(old.objects[id].data, new.objects[id].data); |
| 65 | } |
| 66 | } |
| 67 | if let Some(directory) = std::env::var_os("ONESTORE_PARAGRAPH_FORMAT_EXPORT") { |
| 68 | let directory = std::path::PathBuf::from(directory); |
| 69 | std::fs::create_dir(&directory).unwrap(); |
| 70 | std::fs::write(directory.join("synthetic.one"), prepared.as_slice()).unwrap(); |
| 71 | std::fs::write( |
| 72 | directory.join("Open Notebook.onetoc2"), |
| 73 | onestore::create_table_of_contents( |
| 74 | "Open Notebook.onetoc2", |
| 75 | &[("synthetic.one", after_store.header.file_id)], |
| 76 | ) |
| 77 | .unwrap(), |
| 78 | ) |
| 79 | .unwrap(); |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | #[test] |
| 84 | fn paragraph_formatting_rejects_invalid_values_and_conflicting_run_settings() { |
| 85 | let (space, before) = page(SOURCE); |
| 86 | let invalid: &[fn(&mut onestore::document::Format)] = &[ |
| 87 | |format| format.alignment = Some(3), |
| 88 | |format| format.space_before = Some(-1.0), |
| 89 | |format| format.space_after = Some(f32::NAN), |
| 90 | |format| format.line_spacing = Some(f32::INFINITY), |
| 91 | |format| format.space_after = Some(1_000_100.0), |
| 92 | ]; |
| 93 | for change in invalid { |
| 94 | let mut after = before.clone(); |
| 95 | let text = after |
| 96 | .objects |
| 97 | .iter_mut() |
| 98 | .find_map(|object| match object { |
| 99 | PageObject::Outline(outline) => { |
| 100 | outline.paragraphs.iter_mut().find_map(|p| p.text_mut()) |
| 101 | } |
| 102 | _ => None, |
| 103 | }) |
| 104 | .unwrap(); |
| 105 | let mut format = text.text.format_at(0).unwrap().clone(); |
| 106 | change(&mut format); |
| 107 | text.text = Paragraph::new(text.text.text().to_owned(), format); |
| 108 | assert!(ops::saved(SOURCE, space, &after).is_err()); |
| 109 | } |
| 110 | let mut after = before.clone(); |
| 111 | let text = after |
| 112 | .objects |
| 113 | .iter_mut() |
| 114 | .find_map(|object| match object { |
| 115 | PageObject::Outline(outline) => { |
| 116 | outline.paragraphs.iter_mut().find_map(|p| p.text_mut()) |
| 117 | } |
| 118 | _ => None, |
| 119 | }) |
| 120 | .unwrap(); |
| 121 | let mut left = text.text.format_at(0).unwrap().clone(); |
| 122 | let mut right = left.clone(); |
| 123 | left.alignment = Some(1); |
| 124 | right.alignment = Some(2); |
| 125 | text.text = Paragraph::from_runs([("Left".into(), left), ("Right".into(), right)]); |
| 126 | assert!(ops::saved(SOURCE, space, &after).is_err()); |
| 127 | } |