fix: to_delta return TextDelta
Some checks are pending
Release WASM / Release (push) Waiting to run
Test All / build (push) Waiting to run

This commit is contained in:
Leon Zhao 2025-01-08 16:29:40 +08:00
parent 32d5c9b19d
commit 763ab04797
6 changed files with 181 additions and 52 deletions

View file

@ -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<TextDelta> {
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.

View file

@ -109,6 +109,32 @@ impl From<TextDelta> for loro_internal::handler::TextDelta {
}
}
impl From<loro::TextDelta> 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 {

View file

@ -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!" },

View file

@ -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<TextDelta> {
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()
}

View file

@ -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(())
}

View file

@ -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!([