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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -28
  3. data/Cargo.lock +29 -0
  4. data/README.md +352 -103
  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 +10 -10
  13. data/ext/yrby/src/protocol.rs +63 -0
  14. data/ext/yrby/src/read.rs +3 -3
  15. data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/common.rs +355 -0
  16. data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/dynamic.rs +276 -0
  17. data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/macros.rs +49 -0
  18. data/ext/yrby/target/debug/build/rb-sys-4407948463231c4f/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +8934 -0
  19. data/ext/yrby/target/debug/build/rb-sys-dbeea42737529c2d/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +8934 -0
  20. data/ext/yrby/target/debug/build/serde-58ea0ee887cc2602/out/private.rs +6 -0
  21. data/ext/yrby/target/debug/build/serde_core-41f407c21c1f205e/out/private.rs +5 -0
  22. data/ext/yrby/target/debug/build/thiserror-0f1416a82ff26f22/out/private.rs +5 -0
  23. data/lib/generators/yrby/install/install_generator.rb +44 -0
  24. data/lib/generators/yrby/install/templates/document_channel.rb +36 -0
  25. data/lib/generators/yrby/tables/tables_generator.rb +41 -0
  26. data/lib/generators/yrby/tables/templates/create_y_tables.rb +24 -0
  27. data/lib/y/decoder.rb +64 -0
  28. data/lib/y/lexxy.rb +25 -4
  29. data/lib/y/version.rb +1 -1
  30. data/lib/y.rb +1 -0
  31. metadata +21 -5
@@ -0,0 +1,6 @@
1
+ #[doc(hidden)]
2
+ pub mod __private228 {
3
+ #[doc(hidden)]
4
+ pub use crate::private::*;
5
+ }
6
+ use serde_core::__private228 as serde_core_private;
@@ -0,0 +1,5 @@
1
+ #[doc(hidden)]
2
+ pub mod __private228 {
3
+ #[doc(hidden)]
4
+ pub use crate::private::*;
5
+ }
@@ -0,0 +1,5 @@
1
+ #[doc(hidden)]
2
+ pub mod __private18 {
3
+ #[doc(hidden)]
4
+ pub use crate::private::*;
5
+ }
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "generators/yrby/tables/tables_generator"
5
+
6
+ module Yrby
7
+ module Generators
8
+ # `bin/rails generate yrby:install` — a DocumentChannel speaking the
9
+ # y-websocket protocol over the gem's document storage, plus the storage
10
+ # migration (via yrby:tables). The models ship in the gem; only the
11
+ # migration lands in the app.
12
+ class InstallGenerator < ::Rails::Generators::Base
13
+ source_root File.expand_path("templates", __dir__)
14
+
15
+ def create_channel
16
+ template "document_channel.rb", "app/channels/document_channel.rb"
17
+ end
18
+
19
+ def create_tables
20
+ invoke "yrby:tables"
21
+ end
22
+
23
+ def show_next_steps
24
+ say <<~NEXT
25
+
26
+ Next steps:
27
+
28
+ 1. Authorize document access: implement `authorized?` in
29
+ app/channels/document_channel.rb (it denies everyone until you do).
30
+ 2. bin/rails db:migrate
31
+ 3. Install the yrby-client npm package and connect an editor:
32
+
33
+ import { ActionCableProvider } from "yrby-client"
34
+ const provider = new ActionCableProvider(doc, consumer,
35
+ "DocumentChannel", { id: documentId })
36
+ provider.connect()
37
+
38
+ The README's Editors section links working integrations for
39
+ Tiptap, Lexxy, Rhino Editor, and CodeMirror.
40
+ NEXT
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Collaborative documents over Action Cable: one channel speaking the
4
+ # y-websocket protocol (sync plus presence). Storage is Y::Document +
5
+ # Y::DocumentUpdate; a document is created on its first change and its
6
+ # history goes with it when it is destroyed. Point on_load/on_change
7
+ # elsewhere to swap storage.
8
+ class DocumentChannel < ApplicationCable::Channel
9
+ include Y::ActionCable
10
+
11
+ # Rebuild a document from durable storage (nil means a brand-new document).
12
+ on_load { |key| Y::Document.load_state(key) }
13
+
14
+ # Record each CRDT delta durably. Runs before the change is acknowledged
15
+ # or broadcast; if this raises, the change is neither acked nor relayed,
16
+ # and yrby-client retries it.
17
+ on_change { |key, update| Y::Document.append(key, update) }
18
+
19
+ def subscribed
20
+ return reject unless authorized?(params[:id])
21
+
22
+ sync_subscribed(params[:id])
23
+ end
24
+
25
+ def receive(data) = sync_receive(data, params[:id])
26
+
27
+ private
28
+
29
+ # Everyone is denied until you fill this in. Wire it to your app's auth:
30
+ # identify current_user on the cable connection, then check they may read
31
+ # and write this document. Don't lean on on_change raising for access
32
+ # control; that path exists for store failures.
33
+ def authorized?(_document_key)
34
+ false
35
+ end
36
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Yrby
7
+ module Generators
8
+ # `bin/rails generate yrby:tables`: the migration for the gem-owned
9
+ # document models (Y::Document + Y::DocumentUpdate). Invoked by
10
+ # yrby:install, and by other gems building on the same storage.
11
+ #
12
+ # Template notes (kept here, not in the emitted migration): state is
13
+ # 4.gigabytes - 1 (longblob on MySQL; a compacted snapshot is the whole
14
+ # document; a 16 MB cap would break compaction) and payload is
15
+ # 16.megabytes - 1 (one update can carry a big paste or a client's
16
+ # accumulated offline edits; the 64 KB default blob is too small).
17
+ # The partial unique index's WHERE only keeps
18
+ # key-only rows out of the index: uniqueness holds without it, since
19
+ # unique indexes treat NULLs as distinct on every supported database,
20
+ # and MySQL drops the predicate harmlessly. y_document_updates indexes
21
+ # (document_id, pending) instead of bare document_id: the prefix
22
+ # serves the tail and foreign-key lookups, and the pair serves the
23
+ # clean-row count every append runs.
24
+ class TablesGenerator < ::Rails::Generators::Base
25
+ include ActiveRecord::Generators::Migration
26
+
27
+ source_root File.expand_path("templates", __dir__)
28
+
29
+ def create_migration_file
30
+ migration_template "create_y_tables.rb",
31
+ File.join(db_migrate_path, "create_y_tables.rb")
32
+ end
33
+
34
+ private
35
+
36
+ def migration_version
37
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateYTables < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :y_documents do |t|
6
+ t.string :key, null: false, index: { unique: true }
7
+ t.references :record, polymorphic: true, null: true, index: false
8
+ t.string :name
9
+ t.binary :state, limit: 4.gigabytes - 1
10
+ t.timestamps
11
+ t.index %i[record_type record_id name], unique: true,
12
+ where: "record_type IS NOT NULL",
13
+ name: "index_y_documents_on_record_and_name"
14
+ end
15
+
16
+ create_table :y_document_updates do |t|
17
+ t.references :document, null: false, foreign_key: { to_table: :y_documents }, index: false
18
+ t.binary :payload, null: false, limit: 16.megabytes - 1
19
+ t.boolean :pending, null: false, default: false
20
+ t.datetime :created_at, null: false
21
+ t.index %i[document_id pending]
22
+ end
23
+ end
24
+ end
data/lib/y/decoder.rb ADDED
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "y"
4
+
5
+ module Y
6
+ # Plain-text reconstruction of a stored Yjs document, for search indexing
7
+ # and previews. It reads the text out of the shared type the editor uses
8
+ # (Lexical's `Y.XmlText`, plain `Y.Text`, or ProseMirror's `Y.XmlFragment`)
9
+ # in-process, on the same native extension as `Doc`: no Node, no
10
+ # subprocess.
11
+ #
12
+ # state = doc.encode_state_as_update # opaque CRDT bytes from the store
13
+ # Y::Decoder.text(state) # => "hello world"
14
+ # Y::Decoder.preview(state, 280) # => "hello world…"
15
+ #
16
+ # For full-fidelity HTML, `Y::Lexxy` and `Y::Tiptap` render the document
17
+ # with the editor's own semantics; this module is the cheap plain-text
18
+ # path.
19
+ module Decoder
20
+ class Error < Y::Error; end
21
+
22
+ module_function
23
+
24
+ # Plain text of the document. `field` pins the root key (Lexical: the editor
25
+ # id; ProseMirror: "default"); omit it to use the document's sole root.
26
+ def text(state, field: nil)
27
+ field ||= Y::Doc.new.tap { |d| d.apply_update(state) }.root_names.first
28
+ return "" unless field
29
+
30
+ # A plain `Y.Text` root (a simple shared-text editor) reads straight out.
31
+ # (A yrs root's type is fixed by its first typed access, so each reader
32
+ # gets a fresh doc to try a different shared type against the same state.)
33
+ direct = load(state).read_text(field)
34
+ return normalize(direct) if direct && !direct.strip.empty?
35
+
36
+ # Lexical (each block a sibling `Y.XmlText`) and ProseMirror (blocks are
37
+ # `Y.XmlElement`s) both come back from read_xml as block-per-line markup;
38
+ # strip any element tags to plain text.
39
+ markup = load(state).read_xml(field)
40
+ markup ? normalize(strip_tags(markup)) : ""
41
+ end
42
+
43
+ # A compact, single-line preview for list UIs.
44
+ def preview(state, limit: 280, field: nil)
45
+ body = text(state, field: field).gsub(/\s+/, " ").strip
46
+ body.length > limit ? "#{body[0, limit].rstrip}…" : body
47
+ end
48
+
49
+ def load(state)
50
+ Y::Doc.new.tap { |doc| doc.apply_update(state) }
51
+ end
52
+
53
+ def strip_tags(markup)
54
+ markup.gsub(/<[^>]*>/, " ")
55
+ end
56
+
57
+ def normalize(text)
58
+ text.gsub(/[ \t]+/, " ") # collapse runs of spaces/tabs
59
+ .gsub(/ *\n */, "\n") # trim spaces left around block separators
60
+ .gsub(/\n{3,}/, "\n\n") # cap blank-line runs
61
+ .strip
62
+ end
63
+ end
64
+ end
data/lib/y/lexxy.rb CHANGED
@@ -52,24 +52,44 @@ module Y
52
52
  # and presence mirror Lexxy's exportDOM (nulls omitted, `previewable`
53
53
  # only when true, `presentation="gallery"` always).
54
54
  def self.upload(node)
55
- out = +"<action-text-attachment"
55
+ tag = attachment_tag(node)
56
+ out = "<#{tag}"
56
57
  out << attachment_attr(node, "sgid", "sgid")
57
58
  out << %( previewable="true") if node.attrs["previewable"] == true
58
59
  [%w[url src], %w[alt altText], %w[caption caption],
59
60
  %w[content-type contentType], %w[filename fileName],
60
61
  %w[filesize fileSize], %w[width width], %w[height height]]
61
62
  .each { |html_name, stored| out << attachment_attr(node, html_name, stored) }
62
- %(#{out} presentation="gallery"></action-text-attachment>)
63
+ %(#{out} presentation="gallery"></#{tag}>)
63
64
  end
64
65
 
65
66
  # A content attachment (mention, embed): `content` carries the escaped
66
67
  # inner HTML; `plainText` is not exported.
67
68
  def self.mention(node)
68
- out = +"<action-text-attachment"
69
+ tag = attachment_tag(node)
70
+ out = "<#{tag}"
69
71
  out << attachment_attr(node, "sgid", "sgid")
70
72
  out << attachment_attr(node, "content", "innerHtml")
71
73
  out << attachment_attr(node, "content-type", "contentType")
72
- "#{out}></action-text-attachment>"
74
+ "#{out}></#{tag}>"
75
+ end
76
+
77
+ # Lexxy's attachment tag is configurable (`Lexxy.configure`'s
78
+ # attachmentTagName, paired with ActionText::Attachment.tag_name on the
79
+ # Rails side), and each attachment node stores the tag it was created
80
+ # with. Emit the stored tag. The value is stored document data, not
81
+ # markup, so anything that doesn't look like a tag name falls back to
82
+ # ActionText's default.
83
+ def self.attachment_tag(node)
84
+ tag = node.attrs["tagName"].to_s
85
+ tag.match?(/\A[a-zA-Z][a-zA-Z0-9-]*\z/) ? tag : "action-text-attachment"
86
+ end
87
+
88
+ # An in-flight upload placeholder. Only the uploader's browser can
89
+ # complete it, so it is never finished content -- materialized output
90
+ # (Action Text bodies, search text) must not carry it.
91
+ def self.pending_upload(_node)
92
+ ""
73
93
  end
74
94
 
75
95
  # A stored nil (unset) is skipped; a stored empty string still emits.
@@ -90,6 +110,7 @@ module Y
90
110
  "tablecell" => { contains: :blocks, render: method(:table_cell) },
91
111
  "listitem" => { contains: :blocks, render: method(:list_item) },
92
112
  "action_text_attachment" => method(:upload),
113
+ "action_text_attachment_upload" => method(:pending_upload),
93
114
  "custom_action_text_attachment" => method(:mention)
94
115
  }.freeze
95
116
 
data/lib/y/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Y
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/y.rb CHANGED
@@ -15,6 +15,7 @@ end
15
15
  require_relative "y/rendering"
16
16
  require_relative "y/lexxy"
17
17
  require_relative "y/tiptap"
18
+ require_relative "y/decoder"
18
19
 
19
20
  module Y
20
21
  # Doc, Error, and the protocol module functions are defined in the Rust
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yrby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - JP Camara
@@ -68,7 +68,7 @@ dependencies:
68
68
  description: 'yrby is a thread-safe Ruby binding over the Rust y-crdt (yrs) library:
69
69
  CRDT documents, awareness/presence, and the y-websocket sync protocol primitives,
70
70
  with the GVL released during native work so documents sync in parallel. The ActionCable/Rails
71
- integration lives in the companion yrby-actioncable gem.'
71
+ integration lives in the companion yrby-rails gem.'
72
72
  email:
73
73
  - johnpcamara@gmail.com
74
74
  executables: []
@@ -82,14 +82,30 @@ files:
82
82
  - LICENSE
83
83
  - README.md
84
84
  - ext/yrby/Cargo.toml
85
+ - ext/yrby/crates/html-core/Cargo.toml
86
+ - ext/yrby/crates/html-core/src/lib.rs
87
+ - ext/yrby/crates/lexical-html/Cargo.toml
88
+ - ext/yrby/crates/lexical-html/src/lib.rs
89
+ - ext/yrby/crates/prosemirror-html/Cargo.toml
90
+ - ext/yrby/crates/prosemirror-html/src/lib.rs
85
91
  - ext/yrby/extconf.rb
86
- - ext/yrby/src/lexical_html.rs
87
92
  - ext/yrby/src/lib.rs
88
- - ext/yrby/src/prosemirror_html.rs
89
93
  - ext/yrby/src/protocol.rs
90
94
  - ext/yrby/src/read.rs
91
- - ext/yrby/src/render_rules.rs
95
+ - ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/common.rs
96
+ - ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/dynamic.rs
97
+ - ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/macros.rs
98
+ - ext/yrby/target/debug/build/rb-sys-4407948463231c4f/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs
99
+ - ext/yrby/target/debug/build/rb-sys-dbeea42737529c2d/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs
100
+ - ext/yrby/target/debug/build/serde-58ea0ee887cc2602/out/private.rs
101
+ - ext/yrby/target/debug/build/serde_core-41f407c21c1f205e/out/private.rs
102
+ - ext/yrby/target/debug/build/thiserror-0f1416a82ff26f22/out/private.rs
103
+ - lib/generators/yrby/install/install_generator.rb
104
+ - lib/generators/yrby/install/templates/document_channel.rb
105
+ - lib/generators/yrby/tables/tables_generator.rb
106
+ - lib/generators/yrby/tables/templates/create_y_tables.rb
92
107
  - lib/y.rb
108
+ - lib/y/decoder.rb
93
109
  - lib/y/lexxy.rb
94
110
  - lib/y/rendering.rb
95
111
  - lib/y/tiptap.rb