yrby 0.6.0 → 0.6.1

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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/Cargo.lock +29 -0
  4. data/README.md +64 -14
  5. data/ext/yrby/Cargo.toml +3 -0
  6. data/ext/yrby/crates/html-core/Cargo.toml +15 -0
  7. data/ext/yrby/{src/render_rules.rs → crates/html-core/src/lib.rs} +6 -0
  8. data/ext/yrby/crates/lexical-html/Cargo.toml +16 -0
  9. data/ext/yrby/{src/lexical_html.rs → crates/lexical-html/src/lib.rs} +14 -10
  10. data/ext/yrby/crates/prosemirror-html/Cargo.toml +16 -0
  11. data/ext/yrby/{src/prosemirror_html.rs → crates/prosemirror-html/src/lib.rs} +14 -10
  12. data/ext/yrby/src/lib.rs +3 -3
  13. data/ext/yrby/src/read.rs +3 -3
  14. data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/common.rs +355 -0
  15. data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/dynamic.rs +276 -0
  16. data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/macros.rs +49 -0
  17. data/ext/yrby/target/debug/build/rb-sys-4407948463231c4f/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +8934 -0
  18. data/ext/yrby/target/debug/build/rb-sys-dbeea42737529c2d/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +8934 -0
  19. data/ext/yrby/target/debug/build/serde-58ea0ee887cc2602/out/private.rs +6 -0
  20. data/ext/yrby/target/debug/build/serde_core-41f407c21c1f205e/out/private.rs +5 -0
  21. data/ext/yrby/target/debug/build/thiserror-0f1416a82ff26f22/out/private.rs +5 -0
  22. data/lib/generators/yrby/install/install_generator.rb +44 -0
  23. data/lib/generators/yrby/install/templates/document_channel.rb +36 -0
  24. data/lib/generators/yrby/tables/tables_generator.rb +41 -0
  25. data/lib/generators/yrby/tables/templates/create_y_tables.rb +24 -0
  26. data/lib/y/lexxy.rb +25 -4
  27. data/lib/y/version.rb +1 -1
  28. metadata +20 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93704595e7dd617d4eccd0645a3dbc99bc798e11ec2591c86f47f0da08381d2c
4
- data.tar.gz: 3f4fb67f5fc0368fe31a74cdae2104b9cea8bc44e349b398efc20f84fca7061a
3
+ metadata.gz: ba28bef094ec97908a5ce4b01a61aedbbbcd49585c88a47acfbcfe0dab02c5be
4
+ data.tar.gz: dae39a894e92836681c0aa73dc54201aecf1d3fa68eaac38496e420579d760b3
5
5
  SHA512:
6
- metadata.gz: fbe8957a8298efdb2991fa2de86a184e3395cb4315f7e467c2ee1dc1c37e750404468e9a2e71bf55967a44ca281424e4310fc4d985503171ca50a10bb3341e90
7
- data.tar.gz: a29fd67645ef0b5312e34213eaf13c42e0eb8628a4e1da1d6af08ff6a68b77673f0f6e32c74f52da5797a3b802573ca78559e4a94a0c36da5b22331102a0ade6
6
+ metadata.gz: de4dedd7b4eb4e14e343b83b20007587cd129cbc0f42ff03f21118043ebced3853b9a2e2c51e77688ba1b8665e058422e24d1cc18d7acfd85fd6c99e435b43b9
7
+ data.tar.gz: 93291be948f34edc50c98266e554bd41a2beb7f8e74b1aeab87b19dc41efed425603f0634cefd117f24077b7d4fecf967b3ff4e40614b2276f2ccb40e85b5dc6
data/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project aims
5
5
  to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.6.1] - 2026-08-04
8
+
9
+ ### Fixed
10
+
11
+ - `Y::Lexxy` emits the attachment tag the node was created with instead of
12
+ a hardcoded `<action-text-attachment>`. Lexxy makes the tag configurable
13
+ (`Lexxy.configure`'s `attachmentTagName`, paired with
14
+ `ActionText::Attachment.tag_name` in Rails), and each attachment node
15
+ stores its tag, so a custom-tag app's rendered HTML now matches its
16
+ editor. A stored value that doesn't look like a tag name falls back to
17
+ the default; documents from before the tag was stored render unchanged.
18
+
7
19
  ## [0.6.0] - 2026-07-11
8
20
 
9
21
  ### Added
data/Cargo.lock CHANGED
@@ -617,6 +617,9 @@ dependencies = [
617
617
  "rb-sys-env 0.1.2",
618
618
  "serde_json",
619
619
  "yrs",
620
+ "yrs-html-core",
621
+ "yrs-lexical-html",
622
+ "yrs-prosemirror-html",
620
623
  ]
621
624
 
622
625
  [[package]]
@@ -637,6 +640,32 @@ dependencies = [
637
640
  "thiserror",
638
641
  ]
639
642
 
643
+ [[package]]
644
+ name = "yrs-html-core"
645
+ version = "0.1.0"
646
+ dependencies = [
647
+ "serde_json",
648
+ "yrs",
649
+ ]
650
+
651
+ [[package]]
652
+ name = "yrs-lexical-html"
653
+ version = "0.1.0"
654
+ dependencies = [
655
+ "serde_json",
656
+ "yrs",
657
+ "yrs-html-core",
658
+ ]
659
+
660
+ [[package]]
661
+ name = "yrs-prosemirror-html"
662
+ version = "0.1.0"
663
+ dependencies = [
664
+ "serde_json",
665
+ "yrs",
666
+ "yrs-html-core",
667
+ ]
668
+
640
669
  [[package]]
641
670
  name = "zmij"
642
671
  version = "1.0.14"
data/README.md CHANGED
@@ -5,14 +5,16 @@
5
5
  Collaborative editing for Rails, backed by [y-crdt](https://github.com/y-crdt/y-crdt)
6
6
  (the Rust library behind Y.js). Your Rails server speaks the y-websocket sync
7
7
  protocol directly, so there's no separate Node process hosting the Y.js
8
- documents.
8
+ documents. Pronounced "yer-bee".
9
+
10
+ ![Two people typing on separate lines of the same document, each keystroke synced through a Rails server, seen from a third browser with labeled carets](docs/images/collab.gif)
9
11
 
10
12
  ```ruby
11
13
  class DocumentChannel < ApplicationCable::Channel
12
- include Y::ActionCable::Sync
14
+ include Y::ActionCable
13
15
 
14
- on_load { |key| MyStore.load(key) }
15
- on_change { |key, update| MyStore.append(key, update) }
16
+ on_load { |key| Y::Document.load_state(key) }
17
+ on_change { |key, update| Y::Document.append(key, update) }
16
18
 
17
19
  def subscribed = sync_subscribed(params[:id])
18
20
  def receive(data) = sync_receive(data, params[:id])
@@ -59,7 +61,7 @@ and `Doc#read_map` reconstruct it server-side, in Ruby.
59
61
  The surface is intentionally small, but the focus is durability, resiliency, delivery
60
62
  guarantees, correctness, and thread safety.
61
63
 
62
- Towards that goal, `yrby` adds capabilities that stand out even in the Yjs ecosystem:
64
+ Towards that goal, `yrby` adds opinionated defaults on top of normal Yjs syncing:
63
65
 
64
66
  - Built-in update acknowledgement: the `ActionCableProvider` in `yrby-client` will continue to
65
67
  send updates until an ack is received from the server. [`yrby-actioncable`](https://rubygems.org/gems/yrby-actioncable)
@@ -101,8 +103,9 @@ Issues and PRs are welcome.
101
103
  # Core CRDT + protocol primitives:
102
104
  gem "yrby"
103
105
 
104
- # For the Rails/ActionCable server concern (Y::ActionCable::Sync):
105
- gem "yrby-actioncable"
106
+ # For the Rails side (the sync channel, document models, the generator).
107
+ # Formerly yrby-actioncable; that name stops at 0.3.1.
108
+ gem "yrby-rails"
106
109
  ```
107
110
 
108
111
  Requires Ruby 3.4 or newer. The release workflow builds precompiled gems for
@@ -457,25 +460,72 @@ Y.wrap_update(update_bytes) # => wrap a raw doc update as a sync Update frame
457
460
 
458
461
  ### ActionCable Integration
459
462
 
460
- `Y::ActionCable::Sync` (from the `yrby-actioncable` gem) is a channel
461
- concern that implements the full y-websocket protocol (document sync +
462
- awareness/presence) over ActionCable:
463
+ In a Rails app, one generator creates the channel and the migration:
464
+
465
+ ```bash
466
+ bin/rails generate yrby:install
467
+ bin/rails db:migrate
468
+ ```
469
+
470
+ The models ship in the gem, the way Action Text owns
471
+ `ActionText::RichText`:
472
+
473
+ - **`Y::Document`** — one row per document, addressed two ways: by `key`
474
+ (what a channel addresses — one opaque, unique string, sometimes
475
+ app-supplied, never parsed) and, optionally, by polymorphic `record` +
476
+ `name` (which model attribute it backs; `name` is the attribute name,
477
+ `"body"` — one document per attribute per record, the
478
+ ActionText::RichText scheme). Key-only documents leave the binding nil.
479
+ Either side can arrive first: `Y::Document.for(record, name)` finds or
480
+ creates the binding, derives a readable key (`post/1/body`), and adopts
481
+ a key-only row already holding that key, so a channel writing first and
482
+ a binding created later converge on one document. The row also holds
483
+ the merged `state` snapshot — CRDT state only; derived data (rendered
484
+ HTML, search text) is the application's job, typically in the channel's
485
+ on_change. `.load_state(key)` / `.append(key, update)` are the store
486
+ calls the generated channel uses.
487
+ - **`Y::DocumentUpdate`** — the uncompacted tail: one delta per row,
488
+ compacted into `state` and deleted once the tail reaches `compact_every`
489
+ (default 64). Loading reads the snapshot plus the current tail; an
490
+ empty tail returns `state` directly. Compaction serializes on a
491
+ per-document row lock and skips causally-gapped rows — they're
492
+ quarantined until they heal rather than compacted into state or
493
+ deleted. Destroying a document deletes its updates with it.
494
+
495
+ The migration creates `y_documents` and `y_document_updates`. To rename
496
+ them, edit the generated migration and point `Y::Document.table_name` /
497
+ `Y::DocumentUpdate.table_name` at the new names in an initializer.
498
+
499
+ Storage is swappable: the channel only needs `on_load` and `on_change`
500
+ answered, and they can point at anything.
501
+
502
+ `include Y::ActionCable` (from the `yrby-rails` gem) is the channel
503
+ integration: the y-websocket protocol (document sync +
504
+ awareness/presence) over ActionCable. (`include Y::ActionCable::Sync`
505
+ keeps working and has the same effect.)
463
506
 
464
507
  ```ruby
465
508
  # app/channels/document_channel.rb
466
509
  class DocumentChannel < ApplicationCable::Channel
467
- include Y::ActionCable::Sync
510
+ include Y::ActionCable
468
511
 
469
- on_load { |key| MyStore.load(key) } # source of truth
470
- on_change { |key, update| MyStore.append(key, update) } # durable record
512
+ on_load { |key| Y::Document.load_state(key) } # rebuild from storage
513
+ on_change { |key, update| Y::Document.append(key, update) } # record, then broadcast
471
514
 
472
515
  def subscribed
516
+ return reject unless authorized?(params[:id])
517
+
473
518
  sync_subscribed params[:id]
474
519
  end
475
520
 
476
521
  def receive(data)
477
522
  sync_receive(data, params[:id])
478
523
  end
524
+
525
+ private
526
+
527
+ # Everyone is denied until you wire this to your app's auth.
528
+ def authorized?(_document_key) = false
479
529
  end
480
530
  ```
481
531
 
@@ -571,7 +621,7 @@ It is up to you to durably record it:
571
621
 
572
622
  ```ruby
573
623
  class DocumentChannel < ApplicationCable::Channel
574
- include Y::ActionCable::Sync
624
+ include Y::ActionCable
575
625
 
576
626
  # ...
577
627
 
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"
@@ -45,6 +45,12 @@ pub struct Emitter {
45
45
  frames: Vec<Vec<Segment>>,
46
46
  }
47
47
 
48
+ impl Default for Emitter {
49
+ fn default() -> Self {
50
+ Self::new()
51
+ }
52
+ }
53
+
48
54
  impl Emitter {
49
55
  pub fn new() -> Self {
50
56
  Emitter {
@@ -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 `render_rules`) and
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
- use crate::render_rules::{
48
- resolve_parts, xml_attrs_json, xml_ref_attr, Content, Emitter, NodeRule, Rules, Segment,
49
- TypeMap,
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 fixture-parity surface most
125
- /// tests pin. With no callback rules, segments always flatten.
126
- #[cfg(test)]
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
- crate::render_rules::flatten(segs)
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 = crate::render_rules::flatten(segs).into_html().unwrap();
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
- use crate::render_rules::{
40
- any_attr_string, resolve_parts, xml_attrs_json, xml_ref_attr, Content, Emitter, MarkRule,
41
- NodeRule, Rules, Segment, TypeMap,
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 fixture-parity surface most
100
- /// tests pin. With no callback rules, segments always flatten.
101
- #[cfg(test)]
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
- crate::render_rules::flatten(segs)
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
- crate::render_rules::flatten(segs).into_html().unwrap(),
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 = crate::render_rules::type_map_json(&map, |ty| {
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
  ///
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();