yrby 0.7.1-aarch64-linux → 0.8.1-aarch64-linux

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.
@@ -5,15 +5,16 @@ require "generators/yrby/tables/tables_generator"
5
5
 
6
6
  module Yrby
7
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.
8
+ # `bin/rails generate yrby:install` runs yrby:tables to create the storage
9
+ # migration. The models and Y::DocumentChannel ship in the gem, so that is
10
+ # the only file it generates by default. Pass --channel to also generate
11
+ # an application channel for custom authorization or room-keyed documents.
12
12
  class InstallGenerator < ::Rails::Generators::Base
13
13
  source_root File.expand_path("templates", __dir__)
14
+ class_option :channel, type: :boolean, default: false, desc: "Generate a custom DocumentChannel"
14
15
 
15
16
  def create_channel
16
- template "document_channel.rb", "app/channels/document_channel.rb"
17
+ template "document_channel.rb", "app/channels/document_channel.rb" if options[:channel]
17
18
  end
18
19
 
19
20
  def create_tables
@@ -25,15 +26,25 @@ module Yrby
25
26
 
26
27
  Next steps:
27
28
 
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:
29
+ 1. bin/rails db:migrate
30
+ 2. Render a collaborative document on a page where the user may
31
+ edit the record:
32
32
 
33
- import { ActionCableProvider } from "yrby-client"
34
- const provider = new ActionCableProvider(doc, consumer,
35
- "DocumentChannel", { id: documentId })
36
- provider.connect()
33
+ <%= collaborative_document_tag @post, :body %>
34
+
35
+ 3. Install the yrby-client npm package. The tag renders an
36
+ element that connects automatically. Once it syncs, your code
37
+ gets the document and can bind it to any Yjs editor:
38
+
39
+ import "yrby-client/element"
40
+
41
+ document.addEventListener("yrby:synced", ({ target, detail }) => {
42
+ const editor = bindYourEditor(target, detail.doc, detail.provider)
43
+ detail.signal.addEventListener("abort", () => editor.destroy(), { once: true })
44
+ })
45
+
46
+ Run with --channel to also generate app/channels/document_channel.rb,
47
+ and implement its authorized? method before you use it.
37
48
 
38
49
  The README's Editors section links working integrations for
39
50
  Tiptap, Lexxy, Rhino Editor, and CodeMirror.
@@ -16,11 +16,7 @@ class DocumentChannel < ApplicationCable::Channel
16
16
  # and yrby-client retries it.
17
17
  on_change { |key, update| Y::Document.append(key, update) }
18
18
 
19
- def subscribed
20
- return reject unless authorized?(params[:id])
21
-
22
- sync_subscribed(params[:id])
23
- end
19
+ def subscribed = sync_subscribed(params[:id])
24
20
 
25
21
  def receive(data) = sync_receive(data, params[:id])
26
22
 
@@ -28,8 +24,9 @@ class DocumentChannel < ApplicationCable::Channel
28
24
 
29
25
  # Everyone is denied until you fill this in. Wire it to your app's auth:
30
26
  # 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.
27
+ # and write this document. This runs once when the client subscribes, and
28
+ # the subscription stays authorized until it ends. Raising in on_change is
29
+ # for store failures, so don't use it for access control.
33
30
  def authorized?(_document_key)
34
31
  false
35
32
  end
@@ -10,10 +10,13 @@ module Yrby
10
10
  # yrby:install, and by other gems building on the same storage.
11
11
  #
12
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).
13
+ # 1.gigabyte - 1: the largest limit every adapter accepts. MySQL maps
14
+ # anything over 16 MB to longblob (a compacted snapshot is the whole
15
+ # document; a 16 MB cap would break compaction); Postgres raises above
16
+ # 1 GB - 1 and ignores the limit on bytea otherwise; SQLite ignores
17
+ # limits. Payload is 16.megabytes - 1 (one update can carry a big paste
18
+ # or a client's accumulated offline edits; the 64 KB default blob is
19
+ # too small).
17
20
  # The partial unique index's WHERE only keeps
18
21
  # key-only rows out of the index: uniqueness holds without it, since
19
22
  # unique indexes treat NULLs as distinct on every supported database,
@@ -6,7 +6,7 @@ class CreateYTables < ActiveRecord::Migration<%= migration_version %>
6
6
  t.string :key, null: false, index: { unique: true }
7
7
  t.references :record, polymorphic: true, null: true, index: false
8
8
  t.string :name
9
- t.binary :state, limit: 4.gigabytes - 1
9
+ t.binary :state, limit: 1.gigabyte - 1
10
10
  t.timestamps
11
11
  t.index %i[record_type record_id name], unique: true,
12
12
  where: "record_type IS NOT NULL",
data/lib/y/3.4/yrby.so CHANGED
Binary file
data/lib/y/4.0/yrby.so CHANGED
Binary file
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Y
4
+ module Collaborative
5
+ # The collaborative document for one attribute of one record, such as a
6
+ # post's body. Ruby code and the shipped channel both use it, so they read
7
+ # and write the same rows.
8
+ class Attribute
9
+ attr_reader :record, :name
10
+
11
+ def initialize(record, name)
12
+ raise ArgumentError, "collaboration requires a persisted record" unless record.persisted?
13
+
14
+ @record = record
15
+ @name = name.to_s.dup.freeze
16
+ end
17
+
18
+ # The document as update bytes, or nil before the first write.
19
+ def load_state = existing_row&.load_state
20
+
21
+ # Records one change and returns once it's saved. The channel waits for
22
+ # this to return before it acknowledges or broadcasts the change.
23
+ def append(update) = document_row.append(update)
24
+
25
+ # Builds a new Y::Doc from storage on every call.
26
+ def y_doc
27
+ Y::Doc.new.tap do |doc|
28
+ state = load_state
29
+ doc.apply_update(state) if state
30
+ end
31
+ end
32
+
33
+ # The key clients sync the document under, such as "post/1/body". If a
34
+ # key-only channel created the document before it was linked to a
35
+ # record, this returns that original key.
36
+ def key = existing_row&.key || Y::Document.key_for(record, name)
37
+
38
+ # Finds or creates the Y::Document or Y::EncryptedDocument row. Writes
39
+ # and maintenance work such as compaction go through it.
40
+ def document_row = document_class.for(record, name)
41
+
42
+ private
43
+
44
+ # Returns the row if it exists without creating one, so reading a
45
+ # document never adds a row. It selects only id and key because the
46
+ # row's load_state reads the snapshot itself, and loading the snapshot
47
+ # here too would fetch it twice.
48
+ def existing_row = document_class.select(:id, :key).find_by(record:, name:)
49
+
50
+ def document_class = record.class.collaborative_document_class(name)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Y
4
+ module Collaborative
5
+ # The view side of a collaborative document. The engine includes it into
6
+ # Action View.
7
+ #
8
+ # <%= collaborative_document_tag @post, :body %>
9
+ #
10
+ # renders the auto-connecting element:
11
+ #
12
+ # <yrby-document grant="<signed sgid>" name="body"></yrby-document>
13
+ #
14
+ # Importing "yrby-client/element" registers the element. It subscribes to
15
+ # Y::DocumentChannel by itself and exposes the synced Y.Doc through its
16
+ # `doc` property and `yrby:synced` event, much like the element behind
17
+ # turbo_stream_from.
18
+ #
19
+ # The grant is a signed GlobalID scoped to this record and attribute
20
+ # (record.collaborative_sgid(name)). Only render the tag on pages where
21
+ # the user may edit the record, because by default anyone holding the
22
+ # grant can open the document. To also check the user's current
23
+ # permissions when they subscribe, use Y::DocumentChannel.authorize_document.
24
+ #
25
+ # Extra options pass through to the element and a block becomes its
26
+ # content, so the element can wrap the mount point an editor binds to:
27
+ #
28
+ # <%= collaborative_document_tag @post, :body, id: "editor" %>
29
+ #
30
+ # expires_in: limits how long the grant lasts. refresh: is a URL the
31
+ # element fetches a new grant from when a subscription is rejected, so it
32
+ # can resubscribe without a page load. The app's action there re-runs its
33
+ # own authorization and renders { grant: record.collaborative_sgid(name) }:
34
+ #
35
+ # <%= collaborative_document_tag @post, :body, expires_in: 10.minutes,
36
+ # refresh: grant_post_path(@post) %>
37
+ module Helper
38
+ def collaborative_document_tag(record, name, expires_in: nil, refresh: nil, **, &)
39
+ grant = record.collaborative_sgid(name, **{ expires_in: expires_in }.compact)
40
+ attributes = { grant: grant, name: name }
41
+ attributes[:refresh] = refresh if refresh
42
+ # The helper's attributes go last because a later key wins in a keyword
43
+ # splat, so a template can't override the grant or the name.
44
+ tag.yrby_document(**, **attributes, &)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "global_id"
5
+
6
+ module Y
7
+ # The signed token that connects a page to a channel for record-backed
8
+ # collaborative documents.
9
+ #
10
+ # The client never picks its own document. The server signs a token for the
11
+ # record and attribute, the page renders it, and the channel looks the
12
+ # record up from it. The token is a signed GlobalID scoped to one attribute.
13
+ #
14
+ # # the view
15
+ # tag.div data: { grant: post.collaborative_sgid(:body) }
16
+ #
17
+ # # the channel
18
+ # def authorized?(_key)
19
+ # record.present? && record.editable_by?(current_user)
20
+ # end
21
+ #
22
+ # # Not memoized, because AnyCable gives each command a fresh channel
23
+ # # instance and a cached record would go stale. Y::DocumentChannel
24
+ # # does the same.
25
+ # def record
26
+ # Y::Collaborative.locate(params[:grant], :body)
27
+ # end
28
+ #
29
+ # The engine includes this into ActiveRecord::Base so any channel's
30
+ # authorized? can use it. lexxy-realtime uses the same token flow.
31
+ module Collaborative
32
+ extend ActiveSupport::Concern
33
+
34
+ class << self
35
+ # The signed-GlobalID purpose for one collaborative attribute. A token
36
+ # signed for one attribute verifies only against that attribute's
37
+ # purpose, so it can't locate the record through any other attribute.
38
+ # The purpose leaves out the channel name, so a custom channel and the
39
+ # shipped one can both resolve the same tokens for an attribute.
40
+ def sgid_purpose(name) = "yrby/#{name}"
41
+
42
+ # Looks up the record for a token from `collaborative_sgid(name)`.
43
+ # Returns nil for an invalid, tampered, expired, or wrong-attribute
44
+ # token, and for a record that no longer exists.
45
+ def locate(sgid, name)
46
+ GlobalID::Locator.locate_signed(sgid, for: sgid_purpose(name))
47
+ rescue ActiveRecord::RecordNotFound
48
+ nil
49
+ end
50
+ end
51
+
52
+ included do
53
+ class_attribute :collaborative_document_options,
54
+ instance_accessor: false, default: {}.freeze
55
+ end
56
+
57
+ class_methods do
58
+ # Declares how one attribute's document is stored. The shipped channel
59
+ # and Ruby reads both follow it, and undeclared attributes use plain
60
+ # Y::Document.
61
+ def has_collaborative_document(name, encrypted: false) # rubocop:disable Naming/PredicatePrefix
62
+ self.collaborative_document_options = collaborative_document_options.merge(
63
+ name.to_s => { encrypted: encrypted }.freeze
64
+ ).freeze
65
+ end
66
+
67
+ # Returns the model that stores this attribute's document,
68
+ # Y::EncryptedDocument for an attribute declared encrypted and
69
+ # Y::Document for the rest. It resolves the constant on every call so
70
+ # that declaring an attribute doesn't load the engine's models while the
71
+ # app's models are still loading.
72
+ def collaborative_document_class(name)
73
+ encrypted = collaborative_document_options.dig(name.to_s, :encrypted)
74
+ encrypted ? Y::EncryptedDocument : Y::Document
75
+ end
76
+ end
77
+
78
+ # Returns the document for one attribute, stored the way the model
79
+ # declared. It responds to load_state, append, y_doc, and key.
80
+ def collaborative_document(name)
81
+ Attribute.new(self, name)
82
+ end
83
+
84
+ # A signed token that a channel can pass to Y::Collaborative.locate to get
85
+ # this record back, for this attribute only. Pass expires_in: to limit how
86
+ # long the grant lasts. Without it GlobalID's own default applies, which is
87
+ # one month under Rails. We pass the option through only when it's given,
88
+ # because an explicit nil would mean "never expire".
89
+ def collaborative_sgid(name, expires_in: nil)
90
+ options = { for: Y::Collaborative.sgid_purpose(name) }
91
+ options[:expires_in] = expires_in if expires_in
92
+ to_sgid(**options).to_s
93
+ end
94
+ end
95
+ end
96
+
97
+ require "y/collaborative/attribute"
98
+ require "y/collaborative/helper"
data/lib/y/lexxy.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Y
4
4
  # The Lexxy renderer: Y::Lexical (core Lexical) plus the Lexxy-specific
5
- # schema, applied beneath the app's rules — an app rule for one of these
5
+ # schema, applied beneath the app's rules: an app rule for one of these
6
6
  # types simply replaces it. This is the byte-parity class: the fixture
7
7
  # tests hold `Y::Lexxy.new(doc).to_html` identical to a live editor's own
8
8
  # serialized value.
@@ -36,14 +36,14 @@ module Y
36
36
  %(<th class="lexxy-content__table-cell--header" #{style}>#{node.content}</th>)
37
37
  end
38
38
 
39
- # Attribute order follows Lexxy's export — checked items put aria-checked
39
+ # Attribute order follows Lexxy's export: checked items put aria-checked
40
40
  # before value; items holding a nested list append the
41
41
  # lexxy-nested-listitem class after value.
42
42
  def self.list_item(node)
43
43
  out = +"<li"
44
44
  checked = node.attrs["__checked"]
45
- out << %( aria-checked="#{checked}") unless checked.nil?
46
- out << %( value="#{node.attrs["__value"] || 1}")
45
+ out << %( aria-checked="#{RenderRules.escape_attr(checked)}") unless checked.nil?
46
+ out << %( value="#{RenderRules.escape_attr(node.attrs["__value"] || 1)}")
47
47
  out << %( class="lexxy-nested-listitem") if node.child_types.include?("list")
48
48
  "#{out}>#{node.content}</li>"
49
49
  end
data/lib/y/rendering.rb CHANGED
@@ -10,7 +10,7 @@ module Y
10
10
  # `marks:`. A rule is consulted before the built-in schema, so it can add a
11
11
  # custom node or override a built-in one.
12
12
  #
13
- # The usual way in is the block form — one `rules.node` call per type,
13
+ # The usual way in is the block form, one `rules.node` call per type,
14
14
  # keyword options for markup-as-data, a Ruby block for logic (see Builder
15
15
  # below). `contains:` says what's inside the node: :inline (formatted
16
16
  # text, the default), :blocks (child block nodes), or :none (a leaf). The
@@ -26,7 +26,7 @@ module Y
26
26
  # attrs: { "class" => ["callout callout--", :kind] },
27
27
  # contains: :blocks
28
28
  # end
29
- # `tag` is the element; `attrs` values are templates — a String literal,
29
+ # `tag` is the element; `attrs` values are templates: a String literal,
30
30
  # a Symbol referencing one of the node's stored attributes, or an Array
31
31
  # mixing both (an attribute that resolves empty is omitted); `text` is a
32
32
  # template for literal text content; `void: true` emits no closing tag;
@@ -45,20 +45,20 @@ module Y
45
45
  # document is locked) and receives a RenderRules::Node with the node's
46
46
  # type, stored attributes, children already rendered to HTML, and
47
47
  # child_types (its element/block children by type). Its return value is
48
- # spliced in verbatim — it is trusted HTML, so escape any attribute
48
+ # spliced in verbatim: it is trusted HTML, so escape any attribute
49
49
  # values you interpolate.
50
50
  #
51
51
  # Mark rules (ProseMirror only) are declarative: `tag` plus `attrs`
52
52
  # templates whose Symbol refs resolve against the mark's own attributes. A
53
53
  # custom mark wraps outside every built-in mark; several custom marks nest
54
54
  # alphabetically. A rule for a built-in mark's stored name replaces its
55
- # wrap (the markup changes, the semantics don't — an overridden code mark
55
+ # wrap (the markup changes, the semantics don't: an overridden code mark
56
56
  # still excludes the other formatting).
57
57
  module RenderRules
58
58
  # What a callback receives. `attrs` keys are as stored (Lexical's own
59
59
  # props keep their "__" prefix); `content` is the node's children,
60
60
  # already rendered to an HTML string; `child_types` lists the node's
61
- # element/block children by type, in document order — the structural
61
+ # element/block children by type, in document order: the structural
62
62
  # facts attrs and content can't answer (a gallery's image count, whether
63
63
  # a list item holds a nested list).
64
64
  Node = Data.define(:type, :attrs, :content, :child_types)
@@ -137,7 +137,7 @@ module Y
137
137
  # from stored values. Text content escapes `&`, `<`, `>` (quotes stay
138
138
  # literal, matching the browser serializer); attribute values also
139
139
  # escape `"`. Prefer these over ERB::Util.html_escape when byte parity
140
- # with editor output matters — html_escape also rewrites apostrophes.
140
+ # with editor output matters: html_escape also rewrites apostrophes.
141
141
  def escape_text(value)
142
142
  value.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
143
143
  end
@@ -212,10 +212,10 @@ module Y
212
212
  # they compile the rules config, hold the callbacks, and splice deferred
213
213
  # segments after a render. The native handle does everything else.
214
214
  class Lexical
215
- # `Y::Lexical.new(doc, nodes: { "type" => rule })` — see Y::RenderRules
215
+ # `Y::Lexical.new(doc, nodes: { "type" => rule })`; see Y::RenderRules
216
216
  # for the rule forms. This is core Lexical only: paragraphs, headings,
217
217
  # quotes, code, lists, tables, links, text formatting. Editor-specific
218
- # nodes arrive as rules — Y::Lexxy subclasses this with the Lexxy schema;
218
+ # nodes arrive as rules. Y::Lexxy subclasses this with the Lexxy schema;
219
219
  # a different Lexical editor brings its own rule set the same way.
220
220
  def initialize(doc, nodes: {})
221
221
  builder = RenderRules::Builder.new(marks_allowed: false)
@@ -232,23 +232,35 @@ module Y
232
232
  RenderRules.splice(result, @render_callbacks)
233
233
  end
234
234
 
235
- # What node types this document actually contains — the discovery aid
235
+ # What node types this document actually contains: the discovery aid
236
236
  # for writing rules. Facts per type: "count", "attrs" (names as stored),
237
237
  # "children" (child node types), "text" (whether it holds text runs),
238
- # and "handled" ("builtin", "rule", or nil — nil marks the types you
238
+ # and "handled" ("builtin", "rule", or nil; nil marks the types you
239
239
  # still need a rule for). Children plus text is how you pick contains:.
240
240
  def node_types(root = nil)
241
241
  json = root.nil? ? @native.node_types : @native.node_types(root)
242
242
  json && JSON.parse(json)
243
243
  end
244
+
245
+ # The node types to_html can't fully render: types in the document that
246
+ # neither the built-in schema nor a registered rule handles. An unknown
247
+ # container or inline wrapper renders its children without its own
248
+ # markup, so its text stays. An unknown decorator renders nothing, so
249
+ # content kept only in its attributes is missing from the HTML. Returns
250
+ # [] when every type is handled, and when the root is missing or isn't
251
+ # Lexical-shaped, because to_html returns nil there.
252
+ def unknown_types(root = nil)
253
+ types = node_types(root)
254
+ types ? types.filter_map { |type, info| type if info["handled"].nil? } : []
255
+ end
244
256
  end
245
257
 
246
258
  class ProseMirror
247
- # `Y::ProseMirror.new(doc, nodes: {...}, marks: {...})` — see
259
+ # `Y::ProseMirror.new(doc, nodes: {...}, marks: {...})`; see
248
260
  # Y::RenderRules for the rule forms. This is core ProseMirror only:
249
261
  # prosemirror-schema-basic plus the prosemirror-tables family, and the
250
- # full mark set (marks are native — see `rules.mark` for overrides).
251
- # Editor-specific nodes arrive as rules — Y::Tiptap subclasses this with
262
+ # full mark set (marks are native; see `rules.mark` for overrides).
263
+ # Editor-specific nodes arrive as rules. Y::Tiptap subclasses this with
252
264
  # Tiptap's extension nodes; a different ProseMirror editor brings its
253
265
  # own rule set the same way.
254
266
  def initialize(doc, nodes: {}, marks: {})
@@ -267,15 +279,27 @@ module Y
267
279
  RenderRules.splice(result, @render_callbacks)
268
280
  end
269
281
 
270
- # What node types this document actually contains — the discovery aid
282
+ # What node types this document actually contains: the discovery aid
271
283
  # for writing rules. Facts per type: "count", "attrs" (names as stored),
272
284
  # "children" (child node types), "text" (whether it holds text runs),
273
- # and "handled" ("builtin", "rule", or nil — nil marks the types you
285
+ # and "handled" ("builtin", "rule", or nil; nil marks the types you
274
286
  # still need a rule for). Children plus text is how you pick contains:.
275
287
  def node_types(root = nil)
276
288
  json = root.nil? ? @native.node_types : @native.node_types(root)
277
289
  json && JSON.parse(json)
278
290
  end
291
+
292
+ # The node types to_html can't fully render: types in the document that
293
+ # neither the built-in schema nor a registered rule handles. An unknown
294
+ # node renders its text and children without its own markup. A node that
295
+ # keeps its content only in its attributes renders nothing. Marks aren't
296
+ # listed, and an unknown mark renders its text unformatted. Returns []
297
+ # when every type is handled, and when the root is missing or isn't
298
+ # ProseMirror-shaped, because to_html returns nil there.
299
+ def unknown_types(root = nil)
300
+ types = node_types(root)
301
+ types ? types.filter_map { |type, info| type if info["handled"].nil? } : []
302
+ end
279
303
  end
280
304
 
281
305
  private_constant :NativeLexical, :NativeProseMirror
data/lib/y/tiptap.rb CHANGED
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Y
4
- # The Tiptap renderer: Y::ProseMirror (core ProseMirror — schema-basic plus
5
- # tables) plus Tiptap's extension nodes, applied beneath the app's rules —
4
+ # The Tiptap renderer: Y::ProseMirror (core ProseMirror, schema-basic plus
5
+ # tables) plus Tiptap's extension nodes, applied beneath the app's rules,
6
6
  # an app rule for one of these types simply replaces it. This is the
7
7
  # byte-parity class: the fixture tests hold `Y::Tiptap.new(doc).to_html`
8
8
  # identical to a live editor's own `getHTML()`.
9
9
  #
10
10
  # Tiptap's marks (underline, highlight, sub/superscript, textStyle) render
11
- # natively in the base class — mark serialization is text-run machinery
11
+ # natively in the base class: mark serialization is text-run machinery
12
12
  # the rule system can't express.
13
13
  class Tiptap < ProseMirror
14
14
  # Tiptap's TaskItem markup: the data-checked flag (false when unset), a
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.7.1"
4
+ VERSION = "0.8.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yrby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.1
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - JP Camara
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-20 00:00:00.000000000 Z
11
+ date: 2026-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -72,6 +72,9 @@ files:
72
72
  - lib/y.rb
73
73
  - lib/y/3.4/yrby.so
74
74
  - lib/y/4.0/yrby.so
75
+ - lib/y/collaborative.rb
76
+ - lib/y/collaborative/attribute.rb
77
+ - lib/y/collaborative/helper.rb
75
78
  - lib/y/decoder.rb
76
79
  - lib/y/lexxy.rb
77
80
  - lib/y/rendering.rb