Skip to main content

loro_internal/delta/
text.rs

1use loro_common::{InternalString, LoroValue, PeerID};
2use rustc_hash::FxHashMap;
3use serde::{Deserialize, Serialize};
4
5use crate::change::Lamport;
6use crate::container::richtext::{Style, Styles};
7use crate::event::TextMeta;
8use crate::ToJson;
9
10use super::Meta;
11
12#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct StyleMeta {
14    map: FxHashMap<InternalString, StyleMetaItem>,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct StyleMetaItem {
19    // We need lamport and peer to compose the event
20    pub lamport: Lamport,
21    pub peer: PeerID,
22    pub value: LoroValue,
23}
24
25impl StyleMetaItem {
26    pub fn try_replace(&mut self, other: &StyleMetaItem) {
27        if (self.lamport, self.peer) < (other.lamport, other.peer) {
28            self.lamport = other.lamport;
29            self.peer = other.peer;
30            self.value = other.value.clone();
31        }
32    }
33}
34
35impl From<&Styles> for StyleMeta {
36    fn from(styles: &Styles) -> Self {
37        let mut map = FxHashMap::with_capacity_and_hasher(styles.len(), Default::default());
38        for (key, value) in styles.iter() {
39            if let Some(value) = value.get() {
40                map.insert(
41                    key.key().clone(),
42                    StyleMetaItem {
43                        value: value.to_value(),
44                        lamport: value.lamport,
45                        peer: value.peer,
46                    },
47                );
48            }
49        }
50        Self { map }
51    }
52}
53
54impl From<Styles> for StyleMeta {
55    fn from(styles: Styles) -> Self {
56        let temp = &styles;
57        temp.into()
58    }
59}
60
61impl Meta for StyleMeta {
62    fn is_empty(&self) -> bool {
63        self.map.is_empty()
64    }
65
66    fn compose(&mut self, other: &Self, _type_pair: (super::DeltaType, super::DeltaType)) {
67        for (key, value) in other.map.iter() {
68            match self.map.get_mut(key) {
69                Some(old_value) => {
70                    old_value.try_replace(value);
71                }
72                None => {
73                    self.map.insert(key.clone(), value.clone());
74                }
75            }
76        }
77    }
78
79    fn is_mergeable(&self, other: &Self) -> bool {
80        self.map == other.map
81    }
82
83    fn merge(&mut self, _: &Self) {}
84}
85
86impl Meta for TextMeta {
87    fn is_empty(&self) -> bool {
88        self.0.is_empty()
89    }
90
91    fn compose(&mut self, other: &Self, _: (super::DeltaType, super::DeltaType)) {
92        for (key, value) in other.0.iter() {
93            self.0.insert(key.clone(), value.clone());
94        }
95    }
96
97    fn is_mergeable(&self, other: &Self) -> bool {
98        self.0 == other.0
99    }
100
101    fn merge(&mut self, _: &Self) {}
102}
103
104impl StyleMeta {
105    pub(crate) fn iter(&self) -> impl Iterator<Item = (InternalString, Style)> + '_ {
106        self.map.iter().map(|(key, style)| {
107            (
108                key.clone(),
109                Style {
110                    key: key.clone(),
111                    data: style.value.clone(),
112                },
113            )
114        })
115    }
116
117    pub(crate) fn insert(&mut self, key: InternalString, value: StyleMetaItem) {
118        self.map.insert(key, value);
119    }
120
121    pub(crate) fn contains_key(&self, key: &InternalString) -> bool {
122        self.map.contains_key(key)
123    }
124
125    pub(crate) fn to_value(&self) -> LoroValue {
126        LoroValue::Map(self.to_map_without_null_value().into())
127    }
128
129    fn to_map_without_null_value(&self) -> FxHashMap<String, LoroValue> {
130        self.map
131            .iter()
132            .filter_map(|(key, value)| {
133                if value.value.is_null() {
134                    None
135                } else {
136                    Some((key.to_string(), value.value.clone()))
137                }
138            })
139            .collect()
140    }
141
142    pub(crate) fn to_map(&self) -> FxHashMap<String, LoroValue> {
143        self.map
144            .iter()
145            .map(|(key, value)| (key.to_string(), value.value.clone()))
146            .collect()
147    }
148
149    pub(crate) fn to_option_map(&self) -> Option<FxHashMap<String, LoroValue>> {
150        if self.is_empty() {
151            return None;
152        }
153
154        Some(self.to_map())
155    }
156
157    pub(crate) fn to_option_map_without_null_value(&self) -> Option<FxHashMap<String, LoroValue>> {
158        let map = self.to_map_without_null_value();
159        if map.is_empty() {
160            None
161        } else {
162            Some(map)
163        }
164    }
165}
166
167impl ToJson for TextMeta {
168    fn to_json_value(&self) -> serde_json::Value {
169        let mut map = serde_json::Map::new();
170        for (key, value) in self.0.iter() {
171            let value = serde_json::to_value(value).unwrap();
172            map.insert(key.to_string(), value);
173        }
174
175        serde_json::Value::Object(map)
176    }
177
178    fn from_json(s: &str) -> Self {
179        let map: FxHashMap<String, LoroValue> = serde_json::from_str(s).unwrap();
180        TextMeta(map)
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use rustc_hash::FxHashMap;
187
188    use super::*;
189    use crate::delta::{DeltaType, Meta};
190
191    fn key(value: &str) -> InternalString {
192        value.into()
193    }
194
195    fn style_item(lamport: Lamport, peer: PeerID, value: LoroValue) -> StyleMetaItem {
196        StyleMetaItem {
197            lamport,
198            peer,
199            value,
200        }
201    }
202
203    #[test]
204    fn style_meta_item_replacement_is_ordered_by_lamport_then_peer() {
205        let mut item = style_item(1, 9, LoroValue::String("old".into()));
206        item.try_replace(&style_item(1, 8, LoroValue::String("ignored".into())));
207        assert_eq!(item.value, LoroValue::String("old".into()));
208
209        item.try_replace(&style_item(1, 10, LoroValue::String("peer-wins".into())));
210        assert_eq!(item.value, LoroValue::String("peer-wins".into()));
211
212        item.try_replace(&style_item(2, 0, LoroValue::String("lamport-wins".into())));
213        assert_eq!(item.value, LoroValue::String("lamport-wins".into()));
214    }
215
216    #[test]
217    fn style_meta_compose_keeps_latest_value_per_key_and_preserves_new_keys() {
218        let mut left = StyleMeta::default();
219        left.insert(key("bold"), style_item(2, 1, LoroValue::Bool(true)));
220        left.insert(
221            key("color"),
222            style_item(1, 1, LoroValue::String("red".into())),
223        );
224
225        let mut right = StyleMeta::default();
226        right.insert(key("bold"), style_item(1, 99, LoroValue::Bool(false)));
227        right.insert(
228            key("color"),
229            style_item(3, 0, LoroValue::String("blue".into())),
230        );
231        right.insert(
232            key("link"),
233            style_item(1, 1, LoroValue::String("docs".into())),
234        );
235
236        left.compose(&right, (DeltaType::Retain, DeltaType::Retain));
237        let values = left.to_map();
238        assert_eq!(values.get("bold"), Some(&LoroValue::Bool(true)));
239        assert_eq!(values.get("color"), Some(&LoroValue::String("blue".into())));
240        assert_eq!(values.get("link"), Some(&LoroValue::String("docs".into())));
241    }
242
243    #[test]
244    fn style_meta_value_views_distinguish_null_from_absent() {
245        let mut meta = StyleMeta::default();
246        assert!(meta.is_empty());
247        assert_eq!(meta.to_option_map(), None);
248
249        meta.insert(key("bold"), style_item(1, 1, LoroValue::Bool(true)));
250        meta.insert(key("deleted"), style_item(1, 2, LoroValue::Null));
251
252        assert!(meta.contains_key(&key("deleted")));
253        assert_eq!(
254            meta.to_map().get("deleted"),
255            Some(&LoroValue::Null),
256            "to_map keeps explicit null style values"
257        );
258        assert_eq!(
259            meta.to_map_without_null_value().get("deleted"),
260            None,
261            "serialized style values omit null entries"
262        );
263        assert_eq!(
264            meta.to_value(),
265            LoroValue::Map(
266                FxHashMap::from_iter([(String::from("bold"), LoroValue::Bool(true))]).into()
267            )
268        );
269
270        let styles: FxHashMap<_, _> = meta.iter().map(|style| (style.0, style.1.data)).collect();
271        assert_eq!(styles.get(&key("bold")), Some(&LoroValue::Bool(true)));
272        assert_eq!(styles.get(&key("deleted")), Some(&LoroValue::Null));
273    }
274
275    #[test]
276    fn text_meta_compose_and_json_roundtrip_are_map_like() {
277        let mut left = TextMeta(FxHashMap::from_iter([(
278            String::from("lang"),
279            LoroValue::String("en".into()),
280        )]));
281        let right = TextMeta(FxHashMap::from_iter([
282            (String::from("lang"), LoroValue::String("fr".into())),
283            (String::from("author"), LoroValue::String("loro".into())),
284        ]));
285
286        left.compose(&right, (DeltaType::Retain, DeltaType::Retain));
287        assert_eq!(left.0.get("lang"), Some(&LoroValue::String("fr".into())));
288        assert_eq!(
289            left.0.get("author"),
290            Some(&LoroValue::String("loro".into()))
291        );
292        assert!(left.is_mergeable(&left.clone()));
293
294        let json = left.to_json_value();
295        let decoded = TextMeta::from_json(&json.to_string());
296        assert_eq!(decoded.0, left.0);
297    }
298}