Skip to content

Commit 53635dd

Browse files
authored
fix: toDelta should ignore entries with null value (#875)
* fix: toDelta should ignore entries with null value * fix: TextDelta convert * chore: changeset
1 parent 907639b commit 53635dd

4 files changed

Lines changed: 90 additions & 42 deletions

File tree

.changeset/dull-pans-exercise.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"loro-crdt": patch
3+
"loro-crdt-map": patch
4+
---
5+
6+
fix: toDelta should ignore null style entries #875

crates/loro-wasm/src/convert.rs

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -722,9 +722,7 @@ pub(crate) fn loro_json_schema_to_js_json_schema(
722722
Ok(value.into())
723723
}
724724

725-
pub(crate) fn text_delta_to_js_value(
726-
delta: Vec<TextDelta>,
727-
) -> Result<JsValue, JsValue> {
725+
pub(crate) fn text_delta_to_js_value(delta: Vec<TextDelta>) -> Result<JsValue, JsValue> {
728726
let arr = Array::new();
729727
let mut iter = delta.into_iter();
730728
let mut current = iter.next();
@@ -734,26 +732,35 @@ pub(crate) fn text_delta_to_js_value(
734732

735733
// Try to merge with next items
736734
while let Some(next) = next_item.take() {
737-
match (&mut item, next) {
735+
match (&mut item, next) {
738736
(
739-
TextDelta::Insert { insert: i1, attributes: a1 },
740-
TextDelta::Insert { insert: i2, attributes: a2 }
737+
TextDelta::Insert {
738+
insert: i1,
739+
attributes: a1,
740+
},
741+
TextDelta::Insert {
742+
insert: i2,
743+
attributes: a2,
744+
},
741745
) if a1 == &a2 => {
742746
i1.push_str(&i2);
743747
// next_item is consumed, try next
744748
next_item = iter.next();
745749
}
746750
(
747-
TextDelta::Retain { retain: r1, attributes: a1 },
748-
TextDelta::Retain { retain: r2, attributes: a2 }
751+
TextDelta::Retain {
752+
retain: r1,
753+
attributes: a1,
754+
},
755+
TextDelta::Retain {
756+
retain: r2,
757+
attributes: a2,
758+
},
749759
) if a1 == &a2 => {
750760
*r1 += r2;
751761
next_item = iter.next();
752762
}
753-
(
754-
TextDelta::Delete { delete: d1 },
755-
TextDelta::Delete { delete: d2 }
756-
) => {
763+
(TextDelta::Delete { delete: d1 }, TextDelta::Delete { delete: d2 }) => {
757764
*d1 += d2;
758765
next_item = iter.next();
759766
}
@@ -762,7 +769,7 @@ pub(crate) fn text_delta_to_js_value(
762769
next_item = Some(next);
763770
break;
764771
}
765-
}
772+
}
766773
}
767774

768775
// Convert merged item to JS
@@ -771,25 +778,13 @@ pub(crate) fn text_delta_to_js_value(
771778
TextDelta::Insert { insert, attributes } => {
772779
Reflect::set(&obj, &"insert".into(), &insert.into())?;
773780
if let Some(attributes) = attributes {
774-
if !attributes.is_empty() {
775-
let attrs = Object::new();
776-
for (k, v) in attributes {
777-
Reflect::set(&attrs, &k.into(), &convert(v)?)?;
778-
}
779-
Reflect::set(&obj, &"attributes".into(), &attrs)?;
780-
}
781+
set_style_attributes(&obj, attributes)?;
781782
}
782783
}
783784
TextDelta::Retain { retain, attributes } => {
784785
Reflect::set(&obj, &"retain".into(), &retain.into())?;
785786
if let Some(attributes) = attributes {
786-
if !attributes.is_empty() {
787-
let attrs = Object::new();
788-
for (k, v) in attributes {
789-
Reflect::set(&attrs, &k.into(), &convert(v)?)?;
790-
}
791-
Reflect::set(&obj, &"attributes".into(), &attrs)?;
792-
}
787+
set_style_attributes(&obj, attributes)?;
793788
}
794789
}
795790
TextDelta::Delete { delete } => {
@@ -802,3 +797,27 @@ pub(crate) fn text_delta_to_js_value(
802797
}
803798
Ok(arr.into())
804799
}
800+
801+
fn set_style_attributes(
802+
obj: &Object,
803+
attributes: FxHashMap<String, LoroValue>,
804+
) -> Result<(), JsValue> {
805+
if !attributes.is_empty() {
806+
let attrs = Object::new();
807+
let mut is_empty = true;
808+
for (k, v) in attributes {
809+
if v.is_null() {
810+
// We should ignore attribute with null value, since it should be treated as deleted
811+
continue;
812+
}
813+
814+
is_empty = false;
815+
Reflect::set(&attrs, &k.into(), &convert(v)?)?;
816+
}
817+
818+
if !is_empty {
819+
Reflect::set(obj, &"attributes".into(), &attrs)?;
820+
}
821+
}
822+
Ok(())
823+
}

crates/loro-wasm/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5946,7 +5946,7 @@ interface LoroDoc {
59465946
export type Delta<T> =
59475947
| {
59485948
insert: T;
5949-
attributes?: { [key in string]: {} };
5949+
attributes?: { [key in string]: Value };
59505950
retain?: undefined;
59515951
delete?: undefined;
59525952
}
@@ -5958,7 +5958,7 @@ export type Delta<T> =
59585958
}
59595959
| {
59605960
retain: number;
5961-
attributes?: { [key in string]: {} };
5961+
attributes?: { [key in string]: Value };
59625962
delete?: undefined;
59635963
insert?: undefined;
59645964
};

crates/loro-wasm/tests/richtext.test.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -406,10 +406,10 @@ describe("richtext", () => {
406406

407407
// Out of bounds
408408
try {
409-
text.sliceDelta(0, 100);
410-
expect.fail("Should throw error");
409+
text.sliceDelta(0, 100);
410+
expect.fail("Should throw error");
411411
} catch (e) {
412-
// Expected
412+
// Expected
413413
}
414414
});
415415

@@ -420,9 +420,9 @@ describe("richtext", () => {
420420
italic: { expand: "before" },
421421
});
422422
const text = doc.getText("text");
423-
text.insert(0, "你好 World!");
424-
text.mark({ start: 0, end: 2 }, "bold", true);
425-
text.mark({ start: 3, end: 8 }, "italic", true);
423+
text.insert(0, "你好 World!");
424+
text.mark({ start: 0, end: 2 }, "bold", true);
425+
text.mark({ start: 3, end: 8 }, "italic", true);
426426

427427
const deltaUtf8 = text.sliceDeltaUtf8(0, 13);
428428
expect(deltaUtf8).toStrictEqual([
@@ -446,10 +446,10 @@ describe("richtext", () => {
446446

447447
// Out of bounds by UTF8
448448
try {
449-
text.sliceDeltaUtf8(0, 100);
450-
expect.fail("Should throw error");
449+
text.sliceDeltaUtf8(0, 100);
450+
expect.fail("Should throw error");
451451
} catch (e) {
452-
// Expected
452+
// Expected
453453
}
454454
});
455455

@@ -466,9 +466,9 @@ describe("richtext", () => {
466466
// ✨: 7-8
467467
// World: 8-13
468468
// 🌍: 13-15
469-
text.mark({ start: 0, end: 2 }, "emoji", "rocket");
470-
text.mark({ start: 7, end: 8 }, "emoji", "sparkles");
471-
text.mark({ start: 13, end: 15 }, "emoji", "earth");
469+
text.mark({ start: 0, end: 2 }, "emoji", "rocket");
470+
text.mark({ start: 7, end: 8 }, "emoji", "sparkles");
471+
text.mark({ start: 13, end: 15 }, "emoji", "earth");
472472

473473
const delta = text.sliceDelta(0, 15);
474474
expect(delta).toStrictEqual([
@@ -486,4 +486,27 @@ describe("richtext", () => {
486486
{ insert: "World" },
487487
] as Delta<string>[]);
488488
});
489-
});
489+
490+
it("should remove the style entry when applyDelta with style that contains null value", () => {
491+
const doc = new LoroDoc();
492+
doc
493+
.getText("text")
494+
.applyDelta([{ insert: "hello", attributes: { bold: true } }]);
495+
doc
496+
.getText("text")
497+
.applyDelta([{ retain: 2 }, { retain: 2, attributes: { bold: null } }]);
498+
expect(doc.getText("text").toDelta()).toStrictEqual([
499+
{
500+
insert: "he",
501+
attributes: { bold: true },
502+
},
503+
{
504+
insert: "ll",
505+
},
506+
{
507+
insert: "o",
508+
attributes: { bold: true },
509+
},
510+
]);
511+
});
512+
});

0 commit comments

Comments
 (0)