yrby 0.6.0 → 0.7.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 +73 -28
- data/Cargo.lock +29 -0
- data/README.md +352 -103
- data/ext/yrby/Cargo.toml +3 -0
- data/ext/yrby/crates/html-core/Cargo.toml +15 -0
- data/ext/yrby/{src/render_rules.rs → crates/html-core/src/lib.rs} +6 -0
- data/ext/yrby/crates/lexical-html/Cargo.toml +16 -0
- data/ext/yrby/{src/lexical_html.rs → crates/lexical-html/src/lib.rs} +14 -10
- data/ext/yrby/crates/prosemirror-html/Cargo.toml +16 -0
- data/ext/yrby/{src/prosemirror_html.rs → crates/prosemirror-html/src/lib.rs} +14 -10
- data/ext/yrby/src/lib.rs +10 -10
- data/ext/yrby/src/protocol.rs +63 -0
- data/ext/yrby/src/read.rs +3 -3
- data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/common.rs +355 -0
- data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/dynamic.rs +276 -0
- data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/macros.rs +49 -0
- data/ext/yrby/target/debug/build/rb-sys-4407948463231c4f/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +8934 -0
- data/ext/yrby/target/debug/build/rb-sys-dbeea42737529c2d/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +8934 -0
- data/ext/yrby/target/debug/build/serde-58ea0ee887cc2602/out/private.rs +6 -0
- data/ext/yrby/target/debug/build/serde_core-41f407c21c1f205e/out/private.rs +5 -0
- data/ext/yrby/target/debug/build/thiserror-0f1416a82ff26f22/out/private.rs +5 -0
- data/lib/generators/yrby/install/install_generator.rb +44 -0
- data/lib/generators/yrby/install/templates/document_channel.rb +36 -0
- data/lib/generators/yrby/tables/tables_generator.rb +41 -0
- data/lib/generators/yrby/tables/templates/create_y_tables.rb +24 -0
- data/lib/y/decoder.rb +64 -0
- data/lib/y/lexxy.rb +25 -4
- data/lib/y/version.rb +1 -1
- data/lib/y.rb +1 -0
- metadata +21 -5
data/ext/yrby/Cargo.toml
CHANGED
|
@@ -14,6 +14,9 @@ magnus = "0.8"
|
|
|
14
14
|
rb-sys = "0.9"
|
|
15
15
|
yrs = { version = "0.27", features = ["sync"] }
|
|
16
16
|
serde_json = "1.0"
|
|
17
|
+
yrs-html-core = { path = "crates/html-core" }
|
|
18
|
+
yrs-lexical-html = { path = "crates/lexical-html" }
|
|
19
|
+
yrs-prosemirror-html = { path = "crates/prosemirror-html" }
|
|
17
20
|
|
|
18
21
|
[dev-dependencies]
|
|
19
22
|
rb-sys-env = "0.1"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "yrs-html-core"
|
|
3
|
+
description = "Internal core for the yrs-*-html renderer crates; depend on those instead"
|
|
4
|
+
repository = "https://github.com/jpcamara/yrby"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
version = "0.1.0"
|
|
7
|
+
edition = "2024"
|
|
8
|
+
rust-version = "1.85"
|
|
9
|
+
authors = ["JP Camara <johnpcamara@gmail.com>"]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
publish = false
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
yrs = { version = "0.27", features = ["sync"] }
|
|
15
|
+
serde_json = "1.0"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "yrs-lexical-html"
|
|
3
|
+
description = "Render Lexical-shaped yrs documents to HTML, no browser or Node required"
|
|
4
|
+
repository = "https://github.com/jpcamara/yrby"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
version = "0.1.0"
|
|
7
|
+
edition = "2024"
|
|
8
|
+
rust-version = "1.85"
|
|
9
|
+
authors = ["JP Camara <johnpcamara@gmail.com>"]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
publish = false
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
yrs = { version = "0.27", features = ["sync"] }
|
|
15
|
+
serde_json = "1.0"
|
|
16
|
+
yrs-html-core = { path = "../html-core" }
|
|
@@ -39,15 +39,19 @@
|
|
|
39
39
|
//! with the unwrapped span. Case-transform format bits are never rendered
|
|
40
40
|
//! (their text-transform style is outside the sanitize whitelist).
|
|
41
41
|
//!
|
|
42
|
-
//! Custom nodes: rules are registered by `__type` (see `
|
|
42
|
+
//! Custom nodes: rules are registered by `__type` (see `yrs-html-core`) and
|
|
43
43
|
//! consulted before the built-in arms, so they extend the schema or override
|
|
44
44
|
//! a built-in. Declarative rules render here; callback rules emit
|
|
45
45
|
//! `Segment::Deferred` for the caller to fill in after the render.
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
47
|
+
// README examples are living code: compile-checked on every cargo test.
|
|
48
|
+
#[cfg(doctest)]
|
|
49
|
+
#[doc = include_str!("../README.md")]
|
|
50
|
+
mod readme_examples {}
|
|
51
|
+
|
|
52
|
+
// The full rules surface, re-exported: depend on this crate alone;
|
|
53
|
+
// yrs-html-core is an internal implementation crate.
|
|
54
|
+
pub use yrs_html_core::*;
|
|
51
55
|
use yrs::types::text::YChange;
|
|
52
56
|
use yrs::{
|
|
53
57
|
Any, GetString, Map, Out, ReadTxn, Text, Xml, XmlElementRef, XmlFragment, XmlFragmentRef,
|
|
@@ -121,12 +125,12 @@ pub fn render_segments<T: ReadTxn>(
|
|
|
121
125
|
Some(em.into_segments())
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
/// Rule-free rendering to a plain string — the
|
|
125
|
-
///
|
|
126
|
-
|
|
128
|
+
/// Rule-free rendering to a plain string — the simplest way to use this
|
|
129
|
+
/// crate standalone, and the fixture-parity surface the tests pin. With no
|
|
130
|
+
/// callback rules, segments always flatten.
|
|
127
131
|
pub fn render<T: ReadTxn>(txn: &T, fragment: &XmlFragmentRef) -> Option<String> {
|
|
128
132
|
render_segments(txn, fragment, &Rules::empty()).map(|segs| {
|
|
129
|
-
|
|
133
|
+
yrs_html_core::flatten(segs)
|
|
130
134
|
.into_html()
|
|
131
135
|
.expect("no callback rules registered")
|
|
132
136
|
})
|
|
@@ -1297,7 +1301,7 @@ mod tests {
|
|
|
1297
1301
|
let txn = doc.transact();
|
|
1298
1302
|
let frag = txn.get_xml_fragment("root").unwrap();
|
|
1299
1303
|
let segs = render_segments(&txn, &frag, &rules).unwrap();
|
|
1300
|
-
let html =
|
|
1304
|
+
let html = yrs_html_core::flatten(segs).into_html().unwrap();
|
|
1301
1305
|
assert_eq!(
|
|
1302
1306
|
html,
|
|
1303
1307
|
"<p><a class=\"app-link\" href=\"https://x.example\">site</a>\
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "yrs-prosemirror-html"
|
|
3
|
+
description = "Render ProseMirror-shaped yrs documents to HTML, no browser or Node required"
|
|
4
|
+
repository = "https://github.com/jpcamara/yrby"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
version = "0.1.0"
|
|
7
|
+
edition = "2024"
|
|
8
|
+
rust-version = "1.85"
|
|
9
|
+
authors = ["JP Camara <johnpcamara@gmail.com>"]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
publish = false
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
yrs = { version = "0.27", features = ["sync"] }
|
|
15
|
+
serde_json = "1.0"
|
|
16
|
+
yrs-html-core = { path = "../html-core" }
|
|
@@ -36,10 +36,14 @@
|
|
|
36
36
|
//! Declarative rules render here; callback rules emit `Segment::Deferred` for
|
|
37
37
|
//! the caller to fill in after the render.
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
39
|
+
// README examples are living code: compile-checked on every cargo test.
|
|
40
|
+
#[cfg(doctest)]
|
|
41
|
+
#[doc = include_str!("../README.md")]
|
|
42
|
+
mod readme_examples {}
|
|
43
|
+
|
|
44
|
+
// The full rules surface, re-exported: depend on this crate alone;
|
|
45
|
+
// yrs-html-core is an internal implementation crate.
|
|
46
|
+
pub use yrs_html_core::*;
|
|
43
47
|
use yrs::types::text::YChange;
|
|
44
48
|
use yrs::types::Attrs;
|
|
45
49
|
use yrs::{
|
|
@@ -96,12 +100,12 @@ pub fn render_segments<T: ReadTxn>(
|
|
|
96
100
|
Some(em.into_segments())
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
/// Rule-free rendering to a plain string — the
|
|
100
|
-
///
|
|
101
|
-
|
|
103
|
+
/// Rule-free rendering to a plain string — the simplest way to use this
|
|
104
|
+
/// crate standalone, and the fixture-parity surface the tests pin. With no
|
|
105
|
+
/// callback rules, segments always flatten.
|
|
102
106
|
pub fn render<T: ReadTxn>(txn: &T, fragment: &XmlFragmentRef) -> Option<String> {
|
|
103
107
|
render_segments(txn, fragment, &Rules::empty()).map(|segs| {
|
|
104
|
-
|
|
108
|
+
yrs_html_core::flatten(segs)
|
|
105
109
|
.into_html()
|
|
106
110
|
.expect("no callback rules registered")
|
|
107
111
|
})
|
|
@@ -1232,7 +1236,7 @@ mod tests {
|
|
|
1232
1236
|
let txn = doc.transact();
|
|
1233
1237
|
let segs = render_segments(&txn, &frag, &rules).unwrap();
|
|
1234
1238
|
assert_eq!(
|
|
1235
|
-
|
|
1239
|
+
yrs_html_core::flatten(segs).into_html().unwrap(),
|
|
1236
1240
|
"<aside class=\"callout callout--warning\"><div class=\"para\">careful</div></aside>"
|
|
1237
1241
|
);
|
|
1238
1242
|
}
|
|
@@ -1290,7 +1294,7 @@ mod tests {
|
|
|
1290
1294
|
}
|
|
1291
1295
|
let txn = doc.transact();
|
|
1292
1296
|
let map = collect_node_types(&txn, &frag).unwrap();
|
|
1293
|
-
let json =
|
|
1297
|
+
let json = yrs_html_core::type_map_json(&map, |ty| {
|
|
1294
1298
|
if is_builtin(ty) {
|
|
1295
1299
|
Some("builtin")
|
|
1296
1300
|
} else {
|
data/ext/yrby/src/lib.rs
CHANGED
|
@@ -7,16 +7,16 @@ use yrs::updates::decoder::Decode;
|
|
|
7
7
|
use yrs::updates::encoder::Encode;
|
|
8
8
|
use yrs::{Doc, GetString, ReadTxn, Transact};
|
|
9
9
|
|
|
10
|
-
mod lexical_html;
|
|
11
|
-
mod prosemirror_html;
|
|
12
10
|
mod protocol;
|
|
13
11
|
mod read;
|
|
14
|
-
mod render_rules;
|
|
15
12
|
use protocol::{
|
|
16
13
|
classify_message, has_pending, integrated_update, merged_doc_update, update_advances_doc,
|
|
17
14
|
update_is_ready,
|
|
18
15
|
};
|
|
19
16
|
use render_rules::{Rules, Segment};
|
|
17
|
+
pub(crate) use yrs_html_core as render_rules;
|
|
18
|
+
use yrs_lexical_html as lexical_html;
|
|
19
|
+
use yrs_prosemirror_html as prosemirror_html;
|
|
20
20
|
|
|
21
21
|
/// Wrapper around yrs Doc.
|
|
22
22
|
///
|
|
@@ -297,13 +297,13 @@ impl RbDoc {
|
|
|
297
297
|
match msg {
|
|
298
298
|
Message::Sync(sync_msg) => match sync_msg {
|
|
299
299
|
SyncMessage::SyncStep1(sv) => {
|
|
300
|
-
// Respond with SyncStep2 carrying
|
|
301
|
-
// state
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
let update =
|
|
300
|
+
// Respond with SyncStep2 carrying the doc's full
|
|
301
|
+
// state, pending included, matching Y.js's
|
|
302
|
+
// encodeStateAsUpdate. A peer parks a pending
|
|
303
|
+
// struct exactly as this doc does and heals it
|
|
304
|
+
// when the missing dependency arrives.
|
|
305
|
+
let txn = doc.transact();
|
|
306
|
+
let update = txn.encode_state_as_update_v1(&sv);
|
|
307
307
|
let response = Message::Sync(SyncMessage::SyncStep2(update));
|
|
308
308
|
Ok((0, 0, response.encode_v1()))
|
|
309
309
|
}
|
data/ext/yrby/src/protocol.rs
CHANGED
|
@@ -135,6 +135,27 @@ pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool
|
|
|
135
135
|
let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
|
|
136
136
|
let has_deletes = !update.delete_set().is_empty();
|
|
137
137
|
|
|
138
|
+
// A merged update can hide an internal gap behind a Skip block, and
|
|
139
|
+
// blocks past the gap can still integrate (yrs plants a Skip hole in the
|
|
140
|
+
// store) when their origins don't need the missing range. That moves
|
|
141
|
+
// neither the doc's public state vector (yrs caps it at the first hole)
|
|
142
|
+
// nor pending: invisible to every comparison below, so genuinely novel
|
|
143
|
+
// content would report "doesn't advance": applied, acked, but never
|
|
144
|
+
// recorded or broadcast. The update's own state_vector() caps at its
|
|
145
|
+
// first Skip the same way, so any insertion past a non-zero cap reveals
|
|
146
|
+
// such a gap: conservatively report it as advancing (record it; a
|
|
147
|
+
// duplicate in the log is harmless, dropped content is not). Standard
|
|
148
|
+
// Yjs providers never emit these frames (a client's own updates and
|
|
149
|
+
// diffs are gap-free), so this guards crafted or pathologically merged
|
|
150
|
+
// input, not a hot path.
|
|
151
|
+
let capped = update.state_vector();
|
|
152
|
+
for (client, ranges) in update.insertions(true).iter() {
|
|
153
|
+
let cap = capped.get(client);
|
|
154
|
+
if cap > 0 && ranges.iter().any(|range| range.end > cap) {
|
|
155
|
+
return Ok(true);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
// Fast path: blocks beyond the doc's state vector are content the doc
|
|
139
160
|
// lacks — the update advances, no probe needed. The common case (a novel
|
|
140
161
|
// edit) exits here; only retries and ambiguous diffs pay for the probe.
|
|
@@ -663,6 +684,48 @@ mod tests {
|
|
|
663
684
|
);
|
|
664
685
|
}
|
|
665
686
|
|
|
687
|
+
#[test]
|
|
688
|
+
fn a_gappy_update_with_novel_post_hole_content_advances() {
|
|
689
|
+
// A merged update hiding an internal gap behind a Skip, whose
|
|
690
|
+
// post-hole blocks integrate anyway (their origins don't need the
|
|
691
|
+
// missing range). Integration plants a Skip hole, so neither the
|
|
692
|
+
// public state vector nor pending moves, before the hidden-gap
|
|
693
|
+
// check, the probe comparison reported genuinely novel content as
|
|
694
|
+
// "doesn't advance": update_ready? accepted it, the doc applied it,
|
|
695
|
+
// but it was never recorded or broadcast. Unreachable through
|
|
696
|
+
// standard Yjs providers (a client's own updates and diffs are
|
|
697
|
+
// gap-free); this is a crafted/hostile frame shape.
|
|
698
|
+
let a = Doc::new();
|
|
699
|
+
let t1 = a.get_or_insert_text("one");
|
|
700
|
+
t1.insert(&mut a.transact_mut(), 0, "first");
|
|
701
|
+
let u1 = a
|
|
702
|
+
.transact()
|
|
703
|
+
.encode_state_as_update_v1(&yrs::StateVector::default());
|
|
704
|
+
t1.insert(&mut a.transact_mut(), 5, " second"); // the future hole
|
|
705
|
+
let sv2 = a.transact().state_vector();
|
|
706
|
+
// Novel content on a DIFFERENT root: no dependency on the hole.
|
|
707
|
+
let t3 = a.get_or_insert_text("two");
|
|
708
|
+
t3.insert(&mut a.transact_mut(), 0, "third");
|
|
709
|
+
let u3 = a.transact().encode_state_as_update_v1(&sv2);
|
|
710
|
+
|
|
711
|
+
let server = Doc::new();
|
|
712
|
+
server
|
|
713
|
+
.transact_mut()
|
|
714
|
+
.apply_update(yrs::Update::decode_v1(&u1).unwrap())
|
|
715
|
+
.unwrap();
|
|
716
|
+
// The crafted frame: u1 + u3 merged, u2's range an internal Skip.
|
|
717
|
+
let gappy = yrs::merge_updates_v1([&u1, &u3]).unwrap();
|
|
718
|
+
|
|
719
|
+
assert!(
|
|
720
|
+
update_is_ready(&server, &gappy).unwrap(),
|
|
721
|
+
"sanity: the sync flow would accept and apply this frame"
|
|
722
|
+
);
|
|
723
|
+
assert!(
|
|
724
|
+
update_advances_doc(&server, &gappy).unwrap(),
|
|
725
|
+
"novel content hiding behind a Skip must be recorded, not dropped"
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
666
729
|
// Build a causal gap: `first` inserts "a", `dependent` inserts "b" after it,
|
|
667
730
|
// so `dependent` alone parks as pending on a doc that lacks `first`.
|
|
668
731
|
fn gap_pair() -> (Vec<u8>, Vec<u8>) {
|
data/ext/yrby/src/read.rs
CHANGED
|
@@ -352,7 +352,7 @@ mod tests {
|
|
|
352
352
|
// lexxy-realtime's test server and saving GET /content/:room.
|
|
353
353
|
use yrs::updates::decoder::Decode;
|
|
354
354
|
use yrs::Update;
|
|
355
|
-
let bytes = include_bytes!("fixtures/lexical_linebreak.bin");
|
|
355
|
+
let bytes = include_bytes!("../crates/lexical-html/src/fixtures/lexical_linebreak.bin");
|
|
356
356
|
let doc = Doc::new();
|
|
357
357
|
doc.transact_mut()
|
|
358
358
|
.apply_update(Update::decode_v1(bytes).unwrap())
|
|
@@ -369,7 +369,7 @@ mod tests {
|
|
|
369
369
|
// upload's caption as its own line — while the divider stays silent.
|
|
370
370
|
use yrs::updates::decoder::Decode;
|
|
371
371
|
use yrs::{Transact, Update};
|
|
372
|
-
let bytes = include_bytes!("fixtures/lexxy_full.bin");
|
|
372
|
+
let bytes = include_bytes!("../crates/lexical-html/src/fixtures/lexxy_full.bin");
|
|
373
373
|
let doc = Doc::new();
|
|
374
374
|
doc.transact_mut()
|
|
375
375
|
.apply_update(Update::decode_v1(bytes).unwrap())
|
|
@@ -408,7 +408,7 @@ mod tests {
|
|
|
408
408
|
// must come through (get_string alone dropped all the nested ones).
|
|
409
409
|
use yrs::updates::decoder::Decode;
|
|
410
410
|
use yrs::{Transact, Update};
|
|
411
|
-
let bytes = include_bytes!("fixtures/lexical_rich.bin");
|
|
411
|
+
let bytes = include_bytes!("../crates/lexical-html/src/fixtures/lexical_rich.bin");
|
|
412
412
|
let doc = Doc::new();
|
|
413
413
|
{
|
|
414
414
|
let mut txn = doc.transact_mut();
|