diff --git a/crates/loro-ffi/src/container/text.rs b/crates/loro-ffi/src/container/text.rs index 56508102..5fcc9442 100644 --- a/crates/loro-ffi/src/container/text.rs +++ b/crates/loro-ffi/src/container/text.rs @@ -158,6 +158,50 @@ impl LoroText { self.text.unmark(from as usize..to as usize, key) } + /// Get the text in [Delta](https://quilljs.com/docs/delta/) format. + /// + /// # Example + /// ``` + /// use loro::{LoroDoc, ToJson, ExpandType, TextDelta}; + /// use serde_json::json; + /// use std::collections::HashMap; + /// + /// let doc = LoroDoc::new(); + /// let text = doc.get_text("text"); + /// text.insert(0, "Hello world!").unwrap(); + /// text.mark(0..5, "bold", true).unwrap(); + /// assert_eq!( + /// text.to_delta(), + /// vec![ + /// TextDelta::Insert { + /// insert: "Hello".to_string(), + /// attributes: Some(HashMap::from_iter([("bold".to_string(), true.into())])), + /// }, + /// TextDelta::Insert { + /// insert: " world!".to_string(), + /// attributes: None, + /// }, + /// ] + /// ); + /// text.unmark(3..5, "bold").unwrap(); + /// assert_eq!( + /// text.to_delta(), + /// vec![ + /// TextDelta::Insert { + /// insert: "Hel".to_string(), + /// attributes: Some(HashMap::from_iter([("bold".to_string(), true.into())])), + /// }, + /// TextDelta::Insert { + /// insert: "lo world!".to_string(), + /// attributes: None, + /// }, + /// ] + /// ); + /// ``` + pub fn to_delta(&self) -> Vec { + self.text.to_delta().into_iter().map(|d| d.into()).collect() + } + /// Get the text in [Delta](https://quilljs.com/docs/delta/) format. /// /// # Example @@ -170,7 +214,7 @@ impl LoroText { /// text.insert(0, "Hello world!").unwrap(); /// text.mark(0..5, "bold", true).unwrap(); /// assert_eq!( - /// text.to_delta().to_json_value(), + /// text.get_richtext_value().to_json_value(), /// json!([ /// { "insert": "Hello", "attributes": {"bold": true} }, /// { "insert": " world!" }, @@ -178,15 +222,15 @@ impl LoroText { /// ); /// text.unmark(3..5, "bold").unwrap(); /// assert_eq!( - /// text.to_delta().to_json_value(), + /// text.get_richtext_value().to_json_value(), /// json!([ /// { "insert": "Hel", "attributes": {"bold": true} }, /// { "insert": "lo world!" }, /// ]) /// ); /// ``` - pub fn to_delta(&self) -> LoroValue { - self.text.to_delta().into() + pub fn get_richtext_value(&self) -> LoroValue { + self.text.get_richtext_value().into() } /// Get the cursor at the given position. diff --git a/crates/loro-ffi/src/event.rs b/crates/loro-ffi/src/event.rs index fe26afeb..5609bf96 100644 --- a/crates/loro-ffi/src/event.rs +++ b/crates/loro-ffi/src/event.rs @@ -109,6 +109,32 @@ impl From for loro_internal::handler::TextDelta { } } +impl From for TextDelta { + fn from(value: loro::TextDelta) -> Self { + match value { + loro::TextDelta::Retain { retain, attributes } => TextDelta::Retain { + retain: retain as u32, + attributes: attributes.as_ref().map(|a| { + a.iter() + .map(|(k, v)| (k.to_string(), v.clone().into())) + .collect() + }), + }, + loro::TextDelta::Insert { insert, attributes } => TextDelta::Insert { + insert, + attributes: attributes.as_ref().map(|a| { + a.iter() + .map(|(k, v)| (k.to_string(), v.clone().into())) + .collect() + }), + }, + loro::TextDelta::Delete { delete } => TextDelta::Delete { + delete: delete as u32, + }, + } + } +} + pub enum ListDiffItem { /// Insert a new element into the list. Insert { diff --git a/crates/loro/README.md b/crates/loro/README.md index 0f3552f9..4db82624 100644 --- a/crates/loro/README.md +++ b/crates/loro/README.md @@ -51,7 +51,7 @@ let text = doc.get_text("text"); text.insert(0, "Hello world!").unwrap(); text.mark(0..5, "bold", true).unwrap(); assert_eq!( - text.to_delta().to_json_value(), + text.get_richtext_value().to_json_value(), json!([ { "insert": "Hello", "attributes": {"bold": true} }, { "insert": " world!" }, @@ -59,7 +59,7 @@ assert_eq!( ); text.unmark(3..5, "bold").unwrap(); assert_eq!( - text.to_delta().to_json_value(), + text.get_richtext_value().to_json_value(), json!([ { "insert": "Hel", "attributes": {"bold": true} }, { "insert": "lo world!" }, @@ -86,7 +86,7 @@ text_b .unwrap(); doc.import(&doc_b.export_from(&doc.oplog_vv())).unwrap(); assert_eq!( - text.to_delta().to_json_value(), + text.get_richtext_value().to_json_value(), json!([ { "insert": "Hello", "attributes": {"bold": true} }, { "insert": " world!" }, diff --git a/crates/loro/src/lib.rs b/crates/loro/src/lib.rs index 4a709833..6feb5b8a 100644 --- a/crates/loro/src/lib.rs +++ b/crates/loro/src/lib.rs @@ -25,6 +25,7 @@ use loro_internal::{ }; use std::cmp::Ordering; use std::ops::ControlFlow; +use std::ops::Deref; use std::ops::Range; use std::sync::Arc; use tracing::info; @@ -1632,7 +1633,62 @@ impl LoroText { /// /// # Example /// ``` - /// # use loro::{LoroDoc, ToJson, ExpandType}; + /// use loro::{LoroDoc, ToJson, ExpandType, TextDelta}; + /// use serde_json::json; + /// use fxhash::FxHashMap; + /// + /// let doc = LoroDoc::new(); + /// let text = doc.get_text("text"); + /// text.insert(0, "Hello world!").unwrap(); + /// text.mark(0..5, "bold", true).unwrap(); + /// assert_eq!( + /// text.to_delta(), + /// vec![ + /// TextDelta::Insert { + /// insert: "Hello".to_string(), + /// attributes: Some(FxHashMap::from_iter([("bold".to_string(), true.into())])), + /// }, + /// TextDelta::Insert { + /// insert: " world!".to_string(), + /// attributes: None, + /// }, + /// ] + /// ); + /// text.unmark(3..5, "bold").unwrap(); + /// assert_eq!( + /// text.to_delta(), + /// vec![ + /// TextDelta::Insert { + /// insert: "Hel".to_string(), + /// attributes: Some(FxHashMap::from_iter([("bold".to_string(), true.into())])), + /// }, + /// TextDelta::Insert { + /// insert: "lo world!".to_string(), + /// attributes: None, + /// }, + /// ] + /// ); + /// ``` + pub fn to_delta(&self) -> Vec { + let delta = self.handler.get_richtext_value().into_list().unwrap(); + delta + .iter() + .map(|x| { + let map = x.as_map().unwrap(); + let insert = map.get("insert").unwrap().as_string().unwrap().to_string(); + let attributes = map + .get("attributes") + .map(|v| v.as_map().unwrap().deref().clone()); + TextDelta::Insert { insert, attributes } + }) + .collect() + } + + /// Get the rich text value in [Delta](https://quilljs.com/docs/delta/) format. + /// + /// # Example + /// ``` + /// # use loro::{LoroDoc, ToJson, ExpandType, TextDelta}; /// # use serde_json::json; /// /// let doc = LoroDoc::new(); @@ -1640,7 +1696,7 @@ impl LoroText { /// text.insert(0, "Hello world!").unwrap(); /// text.mark(0..5, "bold", true).unwrap(); /// assert_eq!( - /// text.to_delta().to_json_value(), + /// text.get_richtext_value().to_json_value(), /// json!([ /// { "insert": "Hello", "attributes": {"bold": true} }, /// { "insert": " world!" }, @@ -1648,14 +1704,14 @@ impl LoroText { /// ); /// text.unmark(3..5, "bold").unwrap(); /// assert_eq!( - /// text.to_delta().to_json_value(), + /// text.get_richtext_value().to_json_value(), /// json!([ /// { "insert": "Hel", "attributes": {"bold": true} }, /// { "insert": "lo world!" }, /// ]) /// ); /// ``` - pub fn to_delta(&self) -> LoroValue { + pub fn get_richtext_value(&self) -> LoroValue { self.handler.get_richtext_value() } diff --git a/crates/loro/tests/integration_test/undo_test.rs b/crates/loro/tests/integration_test/undo_test.rs index a164f52b..2b928731 100644 --- a/crates/loro/tests/integration_test/undo_test.rs +++ b/crates/loro/tests/integration_test/undo_test.rs @@ -513,7 +513,7 @@ fn test_richtext_checkout() -> LoroResult<()> { })); doc.checkout(&ID::new(1, 6).into())?; assert_eq!( - text.to_delta().to_json_value(), + text.get_richtext_value().to_json_value(), json!([{"insert": "Hello", "attributes": {"bold": true}}]) ); Ok(()) @@ -530,7 +530,7 @@ fn undo_richtext_editing() -> LoroResult<()> { text.mark(0..5, "bold", true)?; undo.record_new_checkpoint(&doc)?; assert_eq!( - text.to_delta().to_json_value(), + text.get_richtext_value().to_json_value(), json!([ {"insert": "Hello", "attributes": {"bold": true}} ]) @@ -539,18 +539,18 @@ fn undo_richtext_editing() -> LoroResult<()> { debug_span!("round", i).in_scope(|| { undo.undo(&doc)?; assert_eq!( - text.to_delta().to_json_value(), + text.get_richtext_value().to_json_value(), json!([ {"insert": "Hello", } ]) ); undo.undo(&doc)?; - assert_eq!(text.to_delta().to_json_value(), json!([])); + assert_eq!(text.get_richtext_value().to_json_value(), json!([])); debug_span!("redo 1").in_scope(|| { undo.redo(&doc).unwrap(); }); assert_eq!( - text.to_delta().to_json_value(), + text.get_richtext_value().to_json_value(), json!([ {"insert": "Hello", } ]) @@ -559,7 +559,7 @@ fn undo_richtext_editing() -> LoroResult<()> { undo.redo(&doc).unwrap(); }); assert_eq!( - text.to_delta().to_json_value(), + text.get_richtext_value().to_json_value(), json!([ {"insert": "Hello", "attributes": {"bold": true}} ]) @@ -587,7 +587,7 @@ fn undo_richtext_editing_collab() -> LoroResult<()> { undo.record_new_checkpoint(&doc_a)?; sync(&doc_a, &doc_b); assert_eq!( - doc_a.get_text("text").to_delta().to_json_value(), + doc_a.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "A ", "attributes": {"bold": true}}, {"insert": "fox", "attributes": {"bold": true, "italic": true}}, @@ -597,7 +597,7 @@ fn undo_richtext_editing_collab() -> LoroResult<()> { for _ in 0..10 { undo.undo(&doc_a)?; assert_eq!( - doc_a.get_text("text").to_delta().to_json_value(), + doc_a.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "A " }, {"insert": "fox jumped", "attributes": {"italic": true}} @@ -606,7 +606,7 @@ fn undo_richtext_editing_collab() -> LoroResult<()> { // FIXME: right now redo/undo like this is wasteful undo.redo(&doc_a)?; assert_eq!( - doc_a.get_text("text").to_delta().to_json_value(), + doc_a.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "A ", "attributes": {"bold": true}}, {"insert": "fox", "attributes": {"bold": true, "italic": true}}, @@ -644,7 +644,7 @@ fn undo_richtext_conflict_set_style() -> LoroResult<()> { undo.record_new_checkpoint(&doc_a)?; sync(&doc_a, &doc_b); assert_eq!( - doc_a.get_text("text").to_delta().to_json_value(), + doc_a.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "A fox", "attributes": {"color": "green"}}, {"insert": " jumped", "attributes": {"color": "red"}} @@ -653,17 +653,20 @@ fn undo_richtext_conflict_set_style() -> LoroResult<()> { for _ in 0..10 { undo.undo(&doc_a)?; assert_eq!( - doc_a.get_text("text").to_delta().to_json_value(), + doc_a.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "A " }, {"insert": "fox jumped", "attributes": {"color": "red"}} ]) ); undo.undo(&doc_a)?; - assert_eq!(doc_a.get_text("text").to_delta().to_json_value(), json!([])); + assert_eq!( + doc_a.get_text("text").get_richtext_value().to_json_value(), + json!([]) + ); undo.redo(&doc_a)?; assert_eq!( - doc_a.get_text("text").to_delta().to_json_value(), + doc_a.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "A " }, {"insert": "fox jumped", "attributes": {"color": "red"}} @@ -671,7 +674,7 @@ fn undo_richtext_conflict_set_style() -> LoroResult<()> { ); undo.redo(&doc_a)?; assert_eq!( - doc_a.get_text("text").to_delta().to_json_value(), + doc_a.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "A fox", "attributes": {"color": "green"}}, {"insert": " jumped", "attributes": {"color": "red"}} @@ -762,7 +765,7 @@ fn collab_undo() -> anyhow::Result<()> { debug_span!("round A", j).in_scope(|| { assert!(!undo_a.can_redo(), "{:#?}", &undo_a); assert_eq!( - doc_a.get_text("text").to_delta().to_json_value(), + doc_a.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "Hello World! "}, {"insert": "A", "attributes": {"bold": true}}, @@ -772,7 +775,7 @@ fn collab_undo() -> anyhow::Result<()> { undo_a.undo(&doc_a)?; assert!(undo_a.can_redo()); assert_eq!( - doc_a.get_text("text").to_delta().to_json_value(), + doc_a.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "Hello "}, {"insert": "A", "attributes": {"bold": true}}, @@ -781,14 +784,14 @@ fn collab_undo() -> anyhow::Result<()> { ); undo_a.undo(&doc_a)?; assert_eq!( - doc_a.get_text("text").to_delta().to_json_value(), + doc_a.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "Hello A fox jumped."}, ]) ); undo_a.undo(&doc_a)?; assert_eq!( - doc_a.get_text("text").to_delta().to_json_value(), + doc_a.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "A fox jumped."}, ]) @@ -797,7 +800,7 @@ fn collab_undo() -> anyhow::Result<()> { assert!(!undo_a.can_undo()); undo_a.redo(&doc_a)?; assert_eq!( - doc_a.get_text("text").to_delta().to_json_value(), + doc_a.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "Hello A fox jumped."}, ]) @@ -805,7 +808,7 @@ fn collab_undo() -> anyhow::Result<()> { undo_a.redo(&doc_a)?; assert_eq!( - doc_a.get_text("text").to_delta().to_json_value(), + doc_a.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "Hello "}, {"insert": "A", "attributes": {"bold": true}}, @@ -821,7 +824,7 @@ fn collab_undo() -> anyhow::Result<()> { for _ in 0..3 { assert!(!undo_b.can_redo()); assert_eq!( - doc_b.get_text("text").to_delta().to_json_value(), + doc_b.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "Hello World! "}, {"insert": "A", "attributes": {"bold": true}}, @@ -831,7 +834,7 @@ fn collab_undo() -> anyhow::Result<()> { undo_b.undo(&doc_b)?; assert!(undo_b.can_redo()); assert_eq!( - doc_b.get_text("text").to_delta().to_json_value(), + doc_b.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "Hello World! "}, {"insert": "A", "attributes": {"bold": true}}, @@ -841,7 +844,7 @@ fn collab_undo() -> anyhow::Result<()> { undo_b.undo(&doc_b)?; assert_eq!( - doc_b.get_text("text").to_delta().to_json_value(), + doc_b.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "Hello World! "}, {"insert": "A", "attributes": {"bold": true}}, @@ -850,7 +853,7 @@ fn collab_undo() -> anyhow::Result<()> { ); undo_b.undo(&doc_b)?; assert_eq!( - doc_b.get_text("text").to_delta().to_json_value(), + doc_b.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "Hello World! "}, ]) @@ -859,7 +862,7 @@ fn collab_undo() -> anyhow::Result<()> { assert!(undo_b.can_redo()); undo_b.redo(&doc_b)?; assert_eq!( - doc_b.get_text("text").to_delta().to_json_value(), + doc_b.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "Hello World! "}, {"insert": "A", "attributes": {"bold": true}}, @@ -868,7 +871,7 @@ fn collab_undo() -> anyhow::Result<()> { ); undo_b.redo(&doc_b)?; assert_eq!( - doc_b.get_text("text").to_delta().to_json_value(), + doc_b.get_text("text").get_richtext_value().to_json_value(), json!([ {"insert": "Hello World! "}, {"insert": "A", "attributes": {"bold": true}}, @@ -931,7 +934,7 @@ fn undo_sub_sub_container() -> anyhow::Result<()> { text_b.insert(2, "o")?; text_b.insert(4, "x")?; assert_eq!( - text_b.to_delta().to_json_value(), + text_b.get_richtext_value().to_json_value(), json!([ {"insert": "FHoexllo World!"}, ]) @@ -939,7 +942,7 @@ fn undo_sub_sub_container() -> anyhow::Result<()> { sync(&doc_a, &doc_b); text_a.mark(0..3, "bold", true)?; assert_eq!( - text_a.to_delta().to_json_value(), + text_a.get_richtext_value().to_json_value(), json!([ {"insert": "Fox", "attributes": { "bold": true }}, {"insert": " World!"} @@ -948,7 +951,7 @@ fn undo_sub_sub_container() -> anyhow::Result<()> { undo_a.undo(&doc_a)?; // 4 -> 3 assert_eq!( - text_a.to_delta().to_json_value(), + text_a.get_richtext_value().to_json_value(), json!([ {"insert": "Fox World!"}, ]) @@ -959,7 +962,7 @@ fn undo_sub_sub_container() -> anyhow::Result<()> { // So we skip the test undo_a.undo(&doc_a)?; // 2 -> 1.5 assert_eq!( - text_a.to_delta().to_json_value(), + text_a.get_richtext_value().to_json_value(), json!([ {"insert": "Fox"}, ]) @@ -989,7 +992,7 @@ fn undo_sub_sub_container() -> anyhow::Result<()> { ); undo_a.redo(&doc_a)?; // 1 -> 1.5 assert_eq!( - text_a.to_delta().to_json_value(), + text_a.get_richtext_value().to_json_value(), json!([ {"insert": "Fox"}, ]) @@ -1018,7 +1021,7 @@ fn undo_sub_sub_container() -> anyhow::Result<()> { .unwrap(); undo_a.redo(&doc_a)?; // 3 -> 4 assert_eq!( - text_a.to_delta().to_json_value(), + text_a.get_richtext_value().to_json_value(), json!([ {"insert": "Fox", "attributes": { "bold": true }}, {"insert": " World!"} @@ -1080,7 +1083,7 @@ fn test_remote_merge_transform() -> LoroResult<()> { // Check the state after concurrent operations assert_eq!( - text_a.to_delta().to_json_value(), + text_a.get_richtext_value().to_json_value(), json!([ {"insert": "B", "attributes": {"bold": true}} ]) @@ -1088,14 +1091,14 @@ fn test_remote_merge_transform() -> LoroResult<()> { undo_a.undo(&doc_a)?; assert_eq!( - text_a.to_delta().to_json_value(), + text_a.get_richtext_value().to_json_value(), json!([ {"insert": "B"} ]) ); undo_a.undo(&doc_a)?; - assert_eq!(text_a.to_delta().to_json_value(), json!([])); + assert_eq!(text_a.get_richtext_value().to_json_value(), json!([])); Ok(()) } diff --git a/crates/loro/tests/loro_rust_test.rs b/crates/loro/tests/loro_rust_test.rs index d4c089a8..859a3646 100644 --- a/crates/loro/tests/loro_rust_test.rs +++ b/crates/loro/tests/loro_rust_test.rs @@ -321,10 +321,10 @@ fn travel_back_should_remove_styles() { }); doc.checkout(&f).unwrap(); assert_eq!( - text.to_delta().as_list().unwrap().len(), + text.get_richtext_value().as_list().unwrap().len(), 1, "should remove the bold style but got {:?}", - text.to_delta() + text.get_richtext_value() ); assert_eq!(doc.state_frontiers(), f); doc.check_state_correctness_slow(); @@ -427,7 +427,7 @@ fn richtext_test() { text.insert(0, "Hello world!").unwrap(); text.mark(0..5, "bold", true).unwrap(); assert_eq!( - text.to_delta().to_json_value(), + text.get_richtext_value().to_json_value(), json!([ { "insert": "Hello", "attributes": {"bold": true} }, { "insert": " world!" }, @@ -435,7 +435,7 @@ fn richtext_test() { ); text.unmark(3..5, "bold").unwrap(); assert_eq!( - text.to_delta().to_json_value(), + text.get_richtext_value().to_json_value(), json!([ { "insert": "Hel", "attributes": {"bold": true} }, { "insert": "lo world!" }, @@ -459,7 +459,7 @@ fn sync() { text_b.mark(0..5, "bold", true).unwrap(); doc.import(&doc_b.export_from(&doc.oplog_vv())).unwrap(); assert_eq!( - text.to_delta().to_json_value(), + text.get_richtext_value().to_json_value(), json!([ { "insert": "Hello", "attributes": {"bold": true} }, { "insert": " world!" }, @@ -1637,7 +1637,7 @@ fn richtext_map_value() { let text = doc.get_text("text"); text.insert(0, "Hello").unwrap(); text.mark(0..2, "comment", loro_value!({"b": {}})).unwrap(); - let delta = text.to_delta(); + let delta = text.get_richtext_value(); assert_eq!( delta, loro_value!([