oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1use docs_native_crdt::Editor;
2use serde_json::{json, Value};
3
4fn paragraph(text: &str) -> Value {
5 json!({"type":"paragraph","children":[{"type":"text","runs":[{"insert":text,"marks":{"strong":{}}}]}]})
6}
7fn plain(node: &Value) -> String {
8 if let Some(runs) = node["runs"].as_array() { return runs.iter().map(|r| r["insert"].as_str().unwrap()).collect(); }
9 node["children"].as_array().map(|children| children.iter().map(plain).collect()).unwrap_or_default()
10}
11fn fixture(block: Value) -> Editor {
12 let mut editor = Editor::new("structural-commands", 710);
13 editor.command(&json!({"op":"insert_element","at":0,"type":"paragraph"})).unwrap();
14 editor.command(&json!({"op":"display_paste","at":0,"length":0,"blocks":[block]})).unwrap();
15 editor
16}
17fn nonempty(projection: &Value) -> Vec<Value> {
18 projection["children"].as_array().unwrap().iter().filter(|n| !plain(n).is_empty()).cloned().collect()
19}
20
21#[test]
22fn quote_toggle_lifts_only_selected_block_preserving_prefix_identity_and_marks() {
23 let mut editor = fixture(json!({"type":"blockquote","children":[paragraph("first"),paragraph("middle"),paragraph("last")]}));
24 let before = nonempty(&editor.projection());
25 let prefix_id = before[0]["children"][0]["id"].clone();
26 let selected = before[0]["children"][1]["id"].clone();
27 editor.command(&json!({"op":"block","node":selected,"type":"blockquote"})).unwrap();
28 let after = nonempty(&editor.projection());
29 assert_eq!(after.iter().map(plain).collect::<Vec<_>>(), vec!["first", "middle", "last"]);
30 assert_eq!(after.iter().map(|n| n["type"].as_str().unwrap()).collect::<Vec<_>>(), vec!["blockquote", "paragraph", "blockquote"]);
31 assert_eq!(after[0]["children"][0]["id"], prefix_id);
32 assert_eq!(after[1]["children"][0]["runs"][0]["marks"]["strong"], json!({}));
33}
34
35#[test]
36fn quote_range_can_lift_last_to_first_without_stale_ids() {
37 let mut editor = fixture(json!({"type":"blockquote","children":[paragraph("first"),paragraph("second"),paragraph("third")]}));
38 let before = nonempty(&editor.projection());
39 let commands: Vec<_> = before[0]["children"].as_array().unwrap().iter().rev()
40 .map(|node| json!({"op":"block","node":node["id"],"type":"blockquote"})).collect();
41 editor.command(&json!({"op":"batch","commands":commands})).unwrap();
42 let after = nonempty(&editor.projection());
43 assert_eq!(after.iter().map(plain).collect::<Vec<_>>(), vec!["first", "second", "third"]);
44 assert!(after.iter().all(|n| n["type"] == "paragraph"));
45}
46
47#[test]
48fn adjacent_list_wrapping_merges_only_after_all_targets_are_applied() {
49 let mut editor = Editor::new("normalize-list", 711);
50 editor.command(&json!({"op":"insert_element","at":0,"type":"paragraph"})).unwrap();
51 editor.command(&json!({"op":"display_edit","at":0,"length":0,"text":"first\nsecond\nthird"})).unwrap();
52 let projection = editor.projection();
53 let prefix_text_id = projection["children"][0]["children"][0]["id"].clone();
54 let commands: Vec<_> = projection["children"].as_array().unwrap().iter().rev()
55 .map(|node| json!({"op":"block","node":node["id"],"type":"bullet_list"})).collect();
56 editor.command(&json!({"op":"batch","commands":commands})).unwrap();
57 let after = nonempty(&editor.projection());
58 assert_eq!(after.len(), 1);
59 assert_eq!(after[0]["type"], "bullet_list");
60 assert_eq!(after[0]["children"].as_array().unwrap().len(), 3);
61 assert_eq!(plain(&after[0]), "firstsecondthird");
62 assert_ne!(after[0]["children"][0]["children"][0]["children"][0]["id"], prefix_text_id);
63}
64
65#[test]
66fn formatting_does_not_merge_unrelated_existing_adjacent_lists() {
67 let list = |text| json!({"type":"bullet_list","children":[{"type":"list_item","children":[paragraph(text)]}]});
68 let mut editor = fixture(json!({"type":"blockquote","children":[list("one"),list("two"),paragraph("outside")]}));
69 let before = nonempty(&editor.projection());
70 let node = before[0]["children"][2]["id"].clone();
71 editor.command(&json!({"op":"batch","commands":[{"op":"attribute","node":node,"name":"align","value":"center"}]})).unwrap();
72 let after = nonempty(&editor.projection());
73 assert_eq!(after[0]["children"].as_array().unwrap().len(), 3);
74 assert_eq!(after[0]["children"][0]["id"], before[0]["children"][0]["id"]);
75 assert_eq!(after[0]["children"][1]["id"], before[0]["children"][1]["id"]);
76}
77
78#[test]
79fn quote_wrap_groups_selected_paragraphs_in_one_container() {
80 let mut editor = Editor::new("wrap-quote", 713);
81 editor.command(&json!({"op":"insert_element","at":0,"type":"paragraph"})).unwrap();
82 editor.command(&json!({"op":"display_edit","at":0,"length":0,"text":"one\ntwo\nthree"})).unwrap();
83 let before = editor.projection();
84 let commands: Vec<_> = before["children"].as_array().unwrap().iter().rev()
85 .map(|node| json!({"op":"block","node":node["id"],"type":"blockquote"})).collect();
86 editor.command(&json!({"op":"batch","commands":commands})).unwrap();
87 let after = nonempty(&editor.projection());
88 assert_eq!(after.len(), 1);
89 assert_eq!(after[0]["type"], "blockquote");
90 assert_eq!(after[0]["children"].as_array().unwrap().len(), 3);
91 assert_eq!(plain(&after[0]), "onetwothree");
92}
93