yrby 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +121 -0
- data/Cargo.lock +33 -11
- data/README.md +31 -9
- data/ext/yrby/Cargo.toml +3 -1
- data/ext/yrby/crates/html-core/Cargo.toml +2 -2
- data/ext/yrby/crates/html-core/src/lib.rs +34 -3
- data/ext/yrby/crates/lexical-html/Cargo.toml +3 -3
- data/ext/yrby/crates/lexical-html/src/lib.rs +82 -20
- data/ext/yrby/crates/prosemirror-html/Cargo.toml +3 -3
- data/ext/yrby/crates/prosemirror-html/src/lib.rs +37 -8
- data/ext/yrby/src/array.rs +396 -0
- data/ext/yrby/src/lib.rs +945 -36
- data/ext/yrby/src/map.rs +374 -0
- data/ext/yrby/src/read.rs +3 -3
- data/ext/yrby/src/shared.rs +561 -0
- data/ext/yrby/src/text.rs +294 -0
- data/ext/yrby/src/xml_text.rs +749 -0
- data/lib/y/anchor.rb +34 -0
- data/lib/y/collaborative/attribute.rb +36 -0
- data/lib/y/collaborative/helper.rb +13 -10
- data/lib/y/collaborative.rb +14 -11
- data/lib/y/doc.rb +36 -0
- data/lib/y/lexical_markdown.rb +175 -0
- data/lib/y/lexical_writer.rb +142 -0
- data/lib/y/lexxy.rb +27 -2
- data/lib/y/rendering.rb +27 -3
- data/lib/y/text.rb +13 -0
- data/lib/y/version.rb +1 -1
- data/lib/y/xml_text.rb +36 -0
- data/lib/y.rb +6 -0
- metadata +12 -1
|
@@ -52,8 +52,8 @@ pub use yjs_html_core::*;
|
|
|
52
52
|
use yrs::types::Attrs;
|
|
53
53
|
use yrs::types::text::YChange;
|
|
54
54
|
use yrs::{
|
|
55
|
-
Any, GetString, Out, ReadTxn, Text, Xml, XmlElementRef, XmlFragment, XmlFragmentRef,
|
|
56
|
-
XmlTextRef,
|
|
55
|
+
Any, GetString, Number, Out, ReadTxn, Text, Xml, XmlElementRef, XmlFragment, XmlFragmentRef,
|
|
56
|
+
XmlOut, XmlTextRef,
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
// The block tree is walked on a heap stack, so depth can't overflow the
|
|
@@ -807,16 +807,25 @@ fn hex_to_rgb(value: &str) -> Option<String> {
|
|
|
807
807
|
Some(format!("rgb({r}, {g}, {b})"))
|
|
808
808
|
}
|
|
809
809
|
|
|
810
|
+
/// The schemes a link may use, as Tiptap's link mark allows them. It
|
|
811
|
+
/// renders a link with any other scheme with an empty `href`, since a
|
|
812
|
+
/// `javascript:` link would run in the page.
|
|
813
|
+
const LINK_SCHEMES: [&str; 10] = [
|
|
814
|
+
"http", "https", "ftp", "ftps", "mailto", "tel", "callto", "sms", "cid", "xmpp",
|
|
815
|
+
];
|
|
816
|
+
|
|
810
817
|
/// `<a>` with Tiptap's attribute order (target, rel, class, href, title),
|
|
811
818
|
/// skipping any that are absent or null.
|
|
812
819
|
fn link_open(link: &std::collections::HashMap<String, Any>) -> String {
|
|
813
820
|
let mut out = String::from("<a");
|
|
814
821
|
for key in ["target", "rel", "class", "href", "title"] {
|
|
815
822
|
if let Some(Any::String(v)) = link.get(key) {
|
|
823
|
+
let unsafe_href = key == "href"
|
|
824
|
+
&& url_scheme(v).is_some_and(|scheme| !LINK_SCHEMES.contains(&scheme.as_str()));
|
|
816
825
|
out.push(' ');
|
|
817
826
|
out.push_str(key);
|
|
818
827
|
out.push_str("=\"");
|
|
819
|
-
out.push_str(&escape_attr(v));
|
|
828
|
+
out.push_str(&escape_attr(if unsafe_href { "" } else { v }));
|
|
820
829
|
out.push('"');
|
|
821
830
|
}
|
|
822
831
|
}
|
|
@@ -874,27 +883,47 @@ fn str_attr<T: ReadTxn>(txn: &T, e: &XmlElementRef, name: &str) -> Option<String
|
|
|
874
883
|
|
|
875
884
|
fn num_attr<T: ReadTxn>(txn: &T, e: &XmlElementRef, name: &str) -> Option<i64> {
|
|
876
885
|
match e.get_attribute(txn, name) {
|
|
877
|
-
Some(Out::Any(Any::Number(n))) => Some(n
|
|
878
|
-
Some(Out::Any(Any::
|
|
886
|
+
Some(Out::Any(Any::Number(Number::Int(n)))) => Some(n),
|
|
887
|
+
Some(Out::Any(Any::Number(Number::Float(n)))) => Some(n as i64),
|
|
879
888
|
_ => None,
|
|
880
889
|
}
|
|
881
890
|
}
|
|
882
891
|
|
|
883
892
|
/// Text-content escaping, matching the browser serializer: `&`, `<`, `>`.
|
|
884
|
-
|
|
893
|
+
/// Public for splice callers. Values read from a deferred segment's
|
|
894
|
+
/// attributes come from collaborators, so escape anything you put into
|
|
895
|
+
/// markup.
|
|
896
|
+
pub fn escape_text(s: &str) -> String {
|
|
885
897
|
s.replace('&', "&")
|
|
886
898
|
.replace('<', "<")
|
|
887
899
|
.replace('>', ">")
|
|
888
900
|
}
|
|
889
901
|
|
|
890
|
-
/// Attribute-value escaping: text escaping plus `"`.
|
|
891
|
-
|
|
902
|
+
/// Attribute-value escaping: text escaping plus `"`. Public for splice
|
|
903
|
+
/// callers, for document values interpolated into attribute positions.
|
|
904
|
+
pub fn escape_attr(s: &str) -> String {
|
|
892
905
|
escape_text(s).replace('"', """)
|
|
893
906
|
}
|
|
894
907
|
|
|
895
908
|
#[cfg(test)]
|
|
896
909
|
mod tests {
|
|
897
910
|
use super::*;
|
|
911
|
+
|
|
912
|
+
#[test]
|
|
913
|
+
fn a_link_to_an_unsafe_scheme_has_an_empty_href() {
|
|
914
|
+
let link = |href: &str| {
|
|
915
|
+
link_open(&std::collections::HashMap::from([(
|
|
916
|
+
"href".to_string(),
|
|
917
|
+
Any::from(href),
|
|
918
|
+
)]))
|
|
919
|
+
};
|
|
920
|
+
assert_eq!(
|
|
921
|
+
link("https://example.com"),
|
|
922
|
+
r#"<a href="https://example.com">"#
|
|
923
|
+
);
|
|
924
|
+
assert_eq!(link("ftp://example.com"), r#"<a href="ftp://example.com">"#);
|
|
925
|
+
assert_eq!(link("javascript:alert(1)"), r#"<a href="">"#);
|
|
926
|
+
}
|
|
898
927
|
use std::collections::HashMap;
|
|
899
928
|
use std::sync::Arc;
|
|
900
929
|
use yrs::updates::decoder::Decode;
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
//! Live `Y::Array` handles over a yrs `Array`: read and write the elements of a
|
|
2
|
+
//! shared list from Ruby.
|
|
3
|
+
//!
|
|
4
|
+
//! Same contract as `Y::Map`. Every operation opens its own transaction inside
|
|
5
|
+
//! `nogvl`, nothing caches a branch pointer, and Ruby values are only touched
|
|
6
|
+
//! with the GVL held.
|
|
7
|
+
//!
|
|
8
|
+
//! The method that earns this type its place is `get_map`, which hands back a
|
|
9
|
+
//! live handle to a map stored at an index. A list of records is the shape most
|
|
10
|
+
//! collaborative state actually has (a plan, a board, a set of rows), and
|
|
11
|
+
//! without a handle you could read one but never edit it in place. The handle
|
|
12
|
+
//! keeps the record's identity, so it stays on that record when others are
|
|
13
|
+
//! inserted or removed around it.
|
|
14
|
+
|
|
15
|
+
use magnus::{prelude::*, typed_data::Obj, Error, Ruby, Value};
|
|
16
|
+
use yrs::branch::BranchID;
|
|
17
|
+
use yrs::{Any, Array, ArrayRef, Doc, Transact};
|
|
18
|
+
|
|
19
|
+
use crate::map::RbMap;
|
|
20
|
+
use crate::nogvl;
|
|
21
|
+
use crate::read::out_to_any;
|
|
22
|
+
use crate::shared::{
|
|
23
|
+
any_to_ruby, element_index, guarded, identity, insert_index, resolve, ruby_to_invalue, to_in,
|
|
24
|
+
Child, ORIGIN,
|
|
25
|
+
};
|
|
26
|
+
use crate::text::RbText;
|
|
27
|
+
|
|
28
|
+
#[magnus::wrap(class = "Y::Array", free_immediately, size)]
|
|
29
|
+
pub struct RbArray {
|
|
30
|
+
doc: Doc,
|
|
31
|
+
id: BranchID,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
impl RbArray {
|
|
35
|
+
pub fn new(doc: Doc, id: BranchID) -> Self {
|
|
36
|
+
RbArray { doc, id }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// The shared value at `index`, resolved now: what `get_map`, `get_array`
|
|
40
|
+
/// and `get_text` narrow to a handle.
|
|
41
|
+
fn child(&self, index: i64) -> Result<Child, Error> {
|
|
42
|
+
let (doc, id) = (&self.doc, &self.id);
|
|
43
|
+
nogvl(move || {
|
|
44
|
+
let txn = doc.transact();
|
|
45
|
+
Child::of(
|
|
46
|
+
resolve::<ArrayRef, _>(&txn, id)
|
|
47
|
+
.and_then(|a| a.get(&txn, element_index(index, a.len(&txn))?)),
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- reads ---
|
|
53
|
+
|
|
54
|
+
/// `array[i]`: a snapshot value. Nested maps and arrays come back as deep
|
|
55
|
+
/// `Hash`/`Array`; use `get_map` for a live handle instead.
|
|
56
|
+
fn get(&self, index: i64) -> Result<Value, Error> {
|
|
57
|
+
let (doc, id) = (&self.doc, &self.id);
|
|
58
|
+
let got: Option<Any> = nogvl(move || {
|
|
59
|
+
let txn = doc.transact();
|
|
60
|
+
let a = resolve::<ArrayRef, _>(&txn, id)?;
|
|
61
|
+
let i = element_index(index, a.len(&txn))?;
|
|
62
|
+
a.get(&txn, i).map(|v| out_to_any(&txn, &v))
|
|
63
|
+
})?;
|
|
64
|
+
let ruby = Ruby::get().unwrap();
|
|
65
|
+
Ok(match got {
|
|
66
|
+
Some(a) => any_to_ruby(&ruby, &a),
|
|
67
|
+
None => ruby.qnil().as_value(),
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// A live `Y::Map` for the element at `index`, or `nil` if that element is
|
|
72
|
+
/// absent or is not a map. Mutating it mutates the document.
|
|
73
|
+
fn get_map(&self, index: i64) -> Result<Option<RbMap>, Error> {
|
|
74
|
+
Ok(match self.child(index)? {
|
|
75
|
+
Child::Map(id) => Some(RbMap::new(self.doc.clone(), id)),
|
|
76
|
+
_ => None,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// A live `Y::Text` for text stored at `index`. An agent appending to one
|
|
81
|
+
/// field of one record goes through here.
|
|
82
|
+
fn get_text(&self, index: i64) -> Result<Option<RbText>, Error> {
|
|
83
|
+
Ok(match self.child(index)? {
|
|
84
|
+
Child::Text(id) => Some(RbText::new(self.doc.clone(), id)),
|
|
85
|
+
_ => None,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// A live `Y::Array` for a nested array at `index`.
|
|
90
|
+
fn get_array(&self, index: i64) -> Result<Option<Self>, Error> {
|
|
91
|
+
Ok(match self.child(index)? {
|
|
92
|
+
Child::Array(id) => Some(RbArray::new(self.doc.clone(), id)),
|
|
93
|
+
_ => None,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fn size(&self) -> Result<usize, Error> {
|
|
98
|
+
let (doc, id) = (&self.doc, &self.id);
|
|
99
|
+
nogvl(move || {
|
|
100
|
+
let txn = doc.transact();
|
|
101
|
+
resolve::<ArrayRef, _>(&txn, id)
|
|
102
|
+
.map(|a| a.len(&txn) as usize)
|
|
103
|
+
.unwrap_or(0)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn is_empty(&self) -> Result<bool, Error> {
|
|
108
|
+
Ok(self.size()? == 0)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn snapshot(&self) -> Result<Vec<Any>, Error> {
|
|
112
|
+
let (doc, id) = (&self.doc, &self.id);
|
|
113
|
+
nogvl(move || {
|
|
114
|
+
let txn = doc.transact();
|
|
115
|
+
match resolve::<ArrayRef, _>(&txn, id) {
|
|
116
|
+
Some(a) => a.iter(&txn).map(|v| out_to_any(&txn, &v)).collect(),
|
|
117
|
+
None => Vec::new(),
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn to_a(&self) -> Result<Value, Error> {
|
|
123
|
+
let ruby = Ruby::get().unwrap();
|
|
124
|
+
Ok(ruby
|
|
125
|
+
.ary_from_iter(self.snapshot()?.iter().map(|v| any_to_ruby(&ruby, v)))
|
|
126
|
+
.as_value())
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// `each { |value| }`: yields a snapshot of each element (read under
|
|
130
|
+
/// `nogvl`, yielded with the GVL held so the block can call into Ruby),
|
|
131
|
+
/// and returns the array. Without a block, an Enumerator.
|
|
132
|
+
fn each(ruby: &Ruby, rb_self: Obj<Self>) -> Result<Value, Error> {
|
|
133
|
+
if !ruby.block_given() {
|
|
134
|
+
return Ok(rb_self.enumeratorize("each", ()).as_value());
|
|
135
|
+
}
|
|
136
|
+
let block = ruby.block_proc()?;
|
|
137
|
+
for v in &rb_self.snapshot()? {
|
|
138
|
+
let _: Value = block.call((any_to_ruby(ruby, v),))?;
|
|
139
|
+
}
|
|
140
|
+
Ok(rb_self.as_value())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- writes ---
|
|
144
|
+
|
|
145
|
+
/// Append a value and return the array, as `Array#push` does. A Ruby
|
|
146
|
+
/// `Hash` becomes a real nested `Y.Map`, so the element can be handed back
|
|
147
|
+
/// later as a live handle by `get_map`.
|
|
148
|
+
fn push(rb_self: Obj<Self>, value: Value) -> Result<Obj<Self>, Error> {
|
|
149
|
+
rb_self.insert_at(-1, value)?;
|
|
150
|
+
Ok(rb_self)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Insert at `index` and return the array, as `Array#insert` does.
|
|
154
|
+
fn insert(rb_self: Obj<Self>, index: i64, value: Value) -> Result<Obj<Self>, Error> {
|
|
155
|
+
rb_self.insert_at(index, value)?;
|
|
156
|
+
Ok(rb_self)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Insert at `index`: a negative index counts from the end (-1 appends),
|
|
160
|
+
/// and an index past the end appends.
|
|
161
|
+
fn insert_at(&self, index: i64, value: Value) -> Result<(), Error> {
|
|
162
|
+
let ruby = Ruby::get().unwrap();
|
|
163
|
+
let iv = ruby_to_invalue(&ruby, value)?;
|
|
164
|
+
let (doc, id) = (&self.doc, &self.id);
|
|
165
|
+
guarded(move || {
|
|
166
|
+
let mut txn = doc.transact_mut_with(ORIGIN);
|
|
167
|
+
let a = resolve::<ArrayRef, _>(&txn, id).ok_or("array no longer exists")?;
|
|
168
|
+
let at = insert_index(index, a.len(&txn));
|
|
169
|
+
a.insert(&mut txn, at, to_in(iv));
|
|
170
|
+
Ok(())
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Remove the element at `index`, returning its previous snapshot value.
|
|
175
|
+
fn delete_at(&self, index: i64) -> Result<Value, Error> {
|
|
176
|
+
let (doc, id) = (&self.doc, &self.id);
|
|
177
|
+
let prev: Option<Any> = guarded(move || {
|
|
178
|
+
let mut txn = doc.transact_mut_with(ORIGIN);
|
|
179
|
+
let Some(a) = resolve::<ArrayRef, _>(&txn, id) else {
|
|
180
|
+
return Ok(None);
|
|
181
|
+
};
|
|
182
|
+
let Some(i) = element_index(index, a.len(&txn)) else {
|
|
183
|
+
return Ok(None);
|
|
184
|
+
};
|
|
185
|
+
// Convert before removing: a removed shared ref would dangle.
|
|
186
|
+
let prev = a.get(&txn, i).map(|v| out_to_any(&txn, &v));
|
|
187
|
+
a.remove(&mut txn, i);
|
|
188
|
+
Ok(prev)
|
|
189
|
+
})?;
|
|
190
|
+
let ruby = Ruby::get().unwrap();
|
|
191
|
+
Ok(match prev {
|
|
192
|
+
Some(a) => any_to_ruby(&ruby, &a),
|
|
193
|
+
None => ruby.qnil().as_value(),
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fn clear(&self) -> Result<(), Error> {
|
|
198
|
+
let (doc, id) = (&self.doc, &self.id);
|
|
199
|
+
guarded(move || {
|
|
200
|
+
let mut txn = doc.transact_mut_with(ORIGIN);
|
|
201
|
+
if let Some(a) = resolve::<ArrayRef, _>(&txn, id) {
|
|
202
|
+
let len = a.len(&txn);
|
|
203
|
+
if len > 0 {
|
|
204
|
+
a.remove_range(&mut txn, 0, len);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
Ok(())
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// Replace the element at `index`. Yjs arrays have no in-place update, so
|
|
212
|
+
/// this is a remove plus an insert in one transaction. The element gets a
|
|
213
|
+
/// new identity, which is why a live handle to it must be re-fetched.
|
|
214
|
+
fn set(&self, index: i64, value: Value) -> Result<Value, Error> {
|
|
215
|
+
let ruby = Ruby::get().unwrap();
|
|
216
|
+
let iv = ruby_to_invalue(&ruby, value)?;
|
|
217
|
+
let (doc, id) = (&self.doc, &self.id);
|
|
218
|
+
guarded(move || {
|
|
219
|
+
let mut txn = doc.transact_mut_with(ORIGIN);
|
|
220
|
+
let a = resolve::<ArrayRef, _>(&txn, id).ok_or("array no longer exists")?;
|
|
221
|
+
let i = element_index(index, a.len(&txn)).ok_or("index out of range")?;
|
|
222
|
+
a.remove(&mut txn, i);
|
|
223
|
+
a.insert(&mut txn, i, to_in(iv));
|
|
224
|
+
Ok(())
|
|
225
|
+
})?;
|
|
226
|
+
Ok(value)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Ensure a root array exists and return a live handle. Called by
|
|
231
|
+
/// `Y::Doc#get_array`.
|
|
232
|
+
pub fn root_array(doc: &Doc, name: String) -> Result<RbArray, Error> {
|
|
233
|
+
let d = doc.clone();
|
|
234
|
+
let id = nogvl(move || identity(&d.get_or_insert_array(name.as_str())))?;
|
|
235
|
+
Ok(RbArray::new(doc.clone(), id))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
pub fn define(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
|
|
239
|
+
let class = module.define_class("Array", ruby.class_object())?;
|
|
240
|
+
class.define_method("[]", magnus::method!(RbArray::get, 1))?;
|
|
241
|
+
class.define_method("get", magnus::method!(RbArray::get, 1))?;
|
|
242
|
+
class.define_method("[]=", magnus::method!(RbArray::set, 2))?;
|
|
243
|
+
class.define_method("get_map", magnus::method!(RbArray::get_map, 1))?;
|
|
244
|
+
class.define_method("get_array", magnus::method!(RbArray::get_array, 1))?;
|
|
245
|
+
class.define_method("get_text", magnus::method!(RbArray::get_text, 1))?;
|
|
246
|
+
class.define_method("push", magnus::method!(RbArray::push, 1))?;
|
|
247
|
+
class.define_method("<<", magnus::method!(RbArray::push, 1))?;
|
|
248
|
+
class.define_method("insert", magnus::method!(RbArray::insert, 2))?;
|
|
249
|
+
class.define_method("delete_at", magnus::method!(RbArray::delete_at, 1))?;
|
|
250
|
+
class.define_method("clear", magnus::method!(RbArray::clear, 0))?;
|
|
251
|
+
class.define_method("size", magnus::method!(RbArray::size, 0))?;
|
|
252
|
+
class.define_method("length", magnus::method!(RbArray::size, 0))?;
|
|
253
|
+
class.define_method("empty?", magnus::method!(RbArray::is_empty, 0))?;
|
|
254
|
+
class.define_method("to_a", magnus::method!(RbArray::to_a, 0))?;
|
|
255
|
+
class.define_method("each", magnus::method!(RbArray::each, 0))?;
|
|
256
|
+
Ok(())
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#[cfg(test)]
|
|
260
|
+
mod tests {
|
|
261
|
+
//! Each method on a handle, called as Ruby calls it.
|
|
262
|
+
use super::*;
|
|
263
|
+
use crate::shared::test_support::{doc, hash, int, rb};
|
|
264
|
+
use rb_sys_test_helpers::ruby_test;
|
|
265
|
+
use yrs::{ArrayPrelim, TextPrelim, Transact};
|
|
266
|
+
|
|
267
|
+
fn list_of(values: &[i64]) -> RbArray {
|
|
268
|
+
let list = root_array(&doc(), "list".into()).unwrap();
|
|
269
|
+
for v in values {
|
|
270
|
+
list.insert_at(-1, rb(*v)).unwrap();
|
|
271
|
+
}
|
|
272
|
+
list
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[ruby_test]
|
|
276
|
+
fn a_fresh_array_is_empty() {
|
|
277
|
+
let list = list_of(&[]);
|
|
278
|
+
assert_eq!(list.size().unwrap(), 0);
|
|
279
|
+
assert!(list.is_empty().unwrap());
|
|
280
|
+
assert_eq!(list.to_a().unwrap().inspect(), "[]");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#[ruby_test]
|
|
284
|
+
fn insert_at_minus_one_appends() {
|
|
285
|
+
let list = list_of(&[1]);
|
|
286
|
+
list.insert_at(-1, rb(2)).unwrap();
|
|
287
|
+
assert_eq!(list.snapshot().unwrap(), vec![int(1), int(2)]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#[ruby_test]
|
|
291
|
+
fn insert_reads_a_negative_index_from_the_end_and_clamps() {
|
|
292
|
+
let list = list_of(&[1, 3]);
|
|
293
|
+
list.insert_at(1, rb(2)).unwrap();
|
|
294
|
+
list.insert_at(-1, rb(4)).unwrap();
|
|
295
|
+
list.insert_at(99, rb(5)).unwrap();
|
|
296
|
+
list.insert_at(-99, rb(0)).unwrap();
|
|
297
|
+
assert_eq!(
|
|
298
|
+
list.snapshot().unwrap(),
|
|
299
|
+
vec![int(0), int(1), int(2), int(3), int(4), int(5)]
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#[ruby_test]
|
|
304
|
+
fn get_reads_a_negative_index_and_is_nil_outside() {
|
|
305
|
+
let list = list_of(&[1, 2, 3]);
|
|
306
|
+
assert_eq!(list.get(0).unwrap().inspect(), "1");
|
|
307
|
+
assert_eq!(list.get(-1).unwrap().inspect(), "3");
|
|
308
|
+
assert_eq!(list.get(3).unwrap().inspect(), "nil");
|
|
309
|
+
assert_eq!(list.get(-4).unwrap().inspect(), "nil");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
#[ruby_test]
|
|
313
|
+
fn set_replaces_in_place_and_raises_outside() {
|
|
314
|
+
let list = list_of(&[1, 2]);
|
|
315
|
+
list.set(-1, rb(9)).unwrap();
|
|
316
|
+
assert_eq!(list.snapshot().unwrap(), vec![int(1), int(9)]);
|
|
317
|
+
let err = list.set(2, rb(3)).unwrap_err();
|
|
318
|
+
assert!(err.to_string().contains("index out of range"), "{err}");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[ruby_test]
|
|
322
|
+
fn delete_at_returns_the_removed_value_or_nil() {
|
|
323
|
+
let list = list_of(&[1, 2, 3]);
|
|
324
|
+
assert_eq!(list.delete_at(1).unwrap().inspect(), "2");
|
|
325
|
+
assert_eq!(list.delete_at(5).unwrap().inspect(), "nil");
|
|
326
|
+
assert_eq!(list.snapshot().unwrap(), vec![int(1), int(3)]);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#[ruby_test]
|
|
330
|
+
fn clear_removes_every_element() {
|
|
331
|
+
let list = list_of(&[1, 2, 3]);
|
|
332
|
+
list.clear().unwrap();
|
|
333
|
+
assert!(list.is_empty().unwrap());
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#[ruby_test]
|
|
337
|
+
fn child_is_the_shared_value_at_an_index() {
|
|
338
|
+
let list = list_of(&[1]);
|
|
339
|
+
list.insert_at(-1, hash(&[("name", rb("Ada"))]).as_value())
|
|
340
|
+
.unwrap();
|
|
341
|
+
assert!(matches!(list.child(-1).unwrap(), Child::Map(_)));
|
|
342
|
+
assert!(matches!(list.child(0).unwrap(), Child::Other));
|
|
343
|
+
assert!(matches!(list.child(2).unwrap(), Child::Other));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#[ruby_test]
|
|
347
|
+
fn get_map_get_text_and_get_array_narrow_by_type() {
|
|
348
|
+
let list = list_of(&[]);
|
|
349
|
+
list.insert_at(-1, hash(&[("name", rb("Ada"))]).as_value())
|
|
350
|
+
.unwrap();
|
|
351
|
+
{
|
|
352
|
+
let mut txn = list.doc.transact_mut();
|
|
353
|
+
let array = resolve::<ArrayRef, _>(&txn, &list.id).unwrap();
|
|
354
|
+
array.push_back(&mut txn, TextPrelim::new("hi"));
|
|
355
|
+
array.push_back(&mut txn, ArrayPrelim::default());
|
|
356
|
+
}
|
|
357
|
+
assert!(list.get_map(0).unwrap().is_some());
|
|
358
|
+
assert_eq!(list.get(0).unwrap().inspect(), r#"{"name" => "Ada"}"#);
|
|
359
|
+
assert!(list.get_text(0).unwrap().is_none());
|
|
360
|
+
assert!(list.get_text(1).unwrap().is_some());
|
|
361
|
+
assert!(list.get_map(1).unwrap().is_none());
|
|
362
|
+
let nested = list.get_array(-1).unwrap().expect("an array at the end");
|
|
363
|
+
nested.insert_at(-1, rb(7)).unwrap();
|
|
364
|
+
assert_eq!(list.get(2).unwrap().inspect(), "[7]");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#[ruby_test]
|
|
368
|
+
fn a_handle_to_a_deleted_array_reads_empty_and_refuses_writes() {
|
|
369
|
+
let outer = list_of(&[]);
|
|
370
|
+
{
|
|
371
|
+
let mut txn = outer.doc.transact_mut();
|
|
372
|
+
let array = resolve::<ArrayRef, _>(&txn, &outer.id).unwrap();
|
|
373
|
+
array.push_back(&mut txn, ArrayPrelim::default());
|
|
374
|
+
}
|
|
375
|
+
let inner = outer.get_array(0).unwrap().unwrap();
|
|
376
|
+
inner.insert_at(-1, rb(1)).unwrap();
|
|
377
|
+
outer.delete_at(0).unwrap();
|
|
378
|
+
assert_eq!(inner.size().unwrap(), 0);
|
|
379
|
+
assert!(inner.snapshot().unwrap().is_empty());
|
|
380
|
+
let err = inner.insert_at(-1, rb(2)).unwrap_err();
|
|
381
|
+
assert!(err.to_string().contains("array no longer exists"), "{err}");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#[ruby_test]
|
|
385
|
+
fn root_array_returns_the_same_array_for_a_name() {
|
|
386
|
+
let doc = doc();
|
|
387
|
+
root_array(&doc, "list".into())
|
|
388
|
+
.unwrap()
|
|
389
|
+
.insert_at(-1, rb(1))
|
|
390
|
+
.unwrap();
|
|
391
|
+
assert_eq!(
|
|
392
|
+
root_array(&doc, "list".into()).unwrap().snapshot().unwrap(),
|
|
393
|
+
vec![int(1)]
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|