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
|
@@ -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
|
-
|
|
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"
|
|
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
|
-
|
|
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}
|
|
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
data/lib/y.rb
CHANGED
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.
|
|
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-
|
|
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/
|
|
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
|