use docs_native_crdt::Editor; use serde_json::{json, Value}; fn paragraph(text: &str) -> Value { json!({"type":"paragraph","children":[{"type":"text","runs":[{"insert":text,"marks":{"strong":{}}}]}]}) } fn plain(node: &Value) -> String { if let Some(runs) = node["runs"].as_array() { return runs.iter().map(|r| r["insert"].as_str().unwrap()).collect(); } node["children"].as_array().map(|children| children.iter().map(plain).collect()).unwrap_or_default() } fn fixture(block: Value) -> Editor { let mut editor = Editor::new("structural-commands", 710); editor.command(&json!({"op":"insert_element","at":0,"type":"paragraph"})).unwrap(); editor.command(&json!({"op":"display_paste","at":0,"length":0,"blocks":[block]})).unwrap(); editor } fn nonempty(projection: &Value) -> Vec { projection["children"].as_array().unwrap().iter().filter(|n| !plain(n).is_empty()).cloned().collect() } #[test] fn quote_toggle_lifts_only_selected_block_preserving_prefix_identity_and_marks() { let mut editor = fixture(json!({"type":"blockquote","children":[paragraph("first"),paragraph("middle"),paragraph("last")]})); let before = nonempty(&editor.projection()); let prefix_id = before[0]["children"][0]["id"].clone(); let selected = before[0]["children"][1]["id"].clone(); editor.command(&json!({"op":"block","node":selected,"type":"blockquote"})).unwrap(); let after = nonempty(&editor.projection()); assert_eq!(after.iter().map(plain).collect::>(), vec!["first", "middle", "last"]); assert_eq!(after.iter().map(|n| n["type"].as_str().unwrap()).collect::>(), vec!["blockquote", "paragraph", "blockquote"]); assert_eq!(after[0]["children"][0]["id"], prefix_id); assert_eq!(after[1]["children"][0]["runs"][0]["marks"]["strong"], json!({})); } #[test] fn quote_range_can_lift_last_to_first_without_stale_ids() { let mut editor = fixture(json!({"type":"blockquote","children":[paragraph("first"),paragraph("second"),paragraph("third")]})); let before = nonempty(&editor.projection()); let commands: Vec<_> = before[0]["children"].as_array().unwrap().iter().rev() .map(|node| json!({"op":"block","node":node["id"],"type":"blockquote"})).collect(); editor.command(&json!({"op":"batch","commands":commands})).unwrap(); let after = nonempty(&editor.projection()); assert_eq!(after.iter().map(plain).collect::>(), vec!["first", "second", "third"]); assert!(after.iter().all(|n| n["type"] == "paragraph")); } #[test] fn adjacent_list_wrapping_merges_only_after_all_targets_are_applied() { let mut editor = Editor::new("normalize-list", 711); editor.command(&json!({"op":"insert_element","at":0,"type":"paragraph"})).unwrap(); editor.command(&json!({"op":"display_edit","at":0,"length":0,"text":"first\nsecond\nthird"})).unwrap(); let projection = editor.projection(); let prefix_text_id = projection["children"][0]["children"][0]["id"].clone(); let commands: Vec<_> = projection["children"].as_array().unwrap().iter().rev() .map(|node| json!({"op":"block","node":node["id"],"type":"bullet_list"})).collect(); editor.command(&json!({"op":"batch","commands":commands})).unwrap(); let after = nonempty(&editor.projection()); assert_eq!(after.len(), 1); assert_eq!(after[0]["type"], "bullet_list"); assert_eq!(after[0]["children"].as_array().unwrap().len(), 3); assert_eq!(plain(&after[0]), "firstsecondthird"); assert_ne!(after[0]["children"][0]["children"][0]["children"][0]["id"], prefix_text_id); } #[test] fn formatting_does_not_merge_unrelated_existing_adjacent_lists() { let list = |text| json!({"type":"bullet_list","children":[{"type":"list_item","children":[paragraph(text)]}]}); let mut editor = fixture(json!({"type":"blockquote","children":[list("one"),list("two"),paragraph("outside")]})); let before = nonempty(&editor.projection()); let node = before[0]["children"][2]["id"].clone(); editor.command(&json!({"op":"batch","commands":[{"op":"attribute","node":node,"name":"align","value":"center"}]})).unwrap(); let after = nonempty(&editor.projection()); assert_eq!(after[0]["children"].as_array().unwrap().len(), 3); assert_eq!(after[0]["children"][0]["id"], before[0]["children"][0]["id"]); assert_eq!(after[0]["children"][1]["id"], before[0]["children"][1]["id"]); } #[test] fn quote_wrap_groups_selected_paragraphs_in_one_container() { let mut editor = Editor::new("wrap-quote", 713); editor.command(&json!({"op":"insert_element","at":0,"type":"paragraph"})).unwrap(); editor.command(&json!({"op":"display_edit","at":0,"length":0,"text":"one\ntwo\nthree"})).unwrap(); let before = editor.projection(); let commands: Vec<_> = before["children"].as_array().unwrap().iter().rev() .map(|node| json!({"op":"block","node":node["id"],"type":"blockquote"})).collect(); editor.command(&json!({"op":"batch","commands":commands})).unwrap(); let after = nonempty(&editor.projection()); assert_eq!(after.len(), 1); assert_eq!(after[0]["type"], "blockquote"); assert_eq!(after[0]["children"].as_array().unwrap().len(), 3); assert_eq!(plain(&after[0]), "onetwothree"); }