y-crdt / yrb

Ruby bindings for yrs.
https://y-crdt.github.io/yrb
MIT License
78 stars 4 forks source link

Accessing attributes of Y::Text inside ruby #155

Closed ArglosStudios closed 5 months ago

ArglosStudios commented 5 months ago

Hello,

First place, amazing project you've got!

I am using yrb with rails, alongside quill and yquill on my frontend.

I have customized some attributes of quill to generate custom html formats inside my editor via Blocks.

However yquill serializes all the data into Y::Text and sends it to the WebSocket. On my rails backend on the other hand, If I try to access the attributes of Y::Text it doesn't give me the option at all (e.g.: if I wanted to generate a PDF for download in my backend, I would have no way to access the attributes of the custom quill format I created).

Steps you can take to reproduce:

ydoc = Y::Doc.new
text = ydoc.get_text("quill")
ydoc.transact do
  text << "Hello"
  text.format(0, 4, {CustomTheme: "Format01"})
end

now the text object has no accessor method to lookup the format I just set.

From the original docs, there is a way to at least recover it by iterating over the deltas: https://docs.yjs.dev/api/shared-types/y.text

The only way I found so far with yrb is to somehow observe the mutations as they happen. However this is not a solid plan as this would grow exponentially due to the amount of documents on my system.

Is there way good way to retrieve the formats for a text object? If there isn't and it requires implementation, I'd be more than glad to help implement it (just need a little guidance).

eliias commented 5 months ago

Hey,

@ArglosStudios thanks for reporting. I think this is a simple case of nobody requested that feature so far 😅. I think the most straightforward way would be to implement text.diff, the same way yrs supports it -> https://docs.rs/yrs/0.17.4/yrs/types/text/trait.Text.html#method.diff. Do you think that would somewhat cover your use case? It might not be the most ergonomic way of dealing with formatting, but we could think about mitigating that in the Ruby API. In order to get this done, we need to implement a wrapper struct (or two structs for Diff and Change), and map the result into those structs. If you want to, please take a stab at it.

eliias commented 5 months ago

@ArglosStudios Could you check this implementation and test with the text.diff implementation and let me know if this conceptually works for you? https://github.com/y-crdt/yrb/pull/156/files#diff-6ff0cac04713b7fa7a718a23ac96767f09c0154d4518af8886aea84ce310653aR23-R32

ArglosStudios commented 5 months ago

@eliias implementation looks really good (still going though the rust code since I am not too familiar with rust).

I will test the implementation later today on a dev environment.

edit: Looking at the implementation I see it's referencing the current transaction. I wonder if I'd be to capture the whole transaction history of the document.

The biggest pain point at the moment is: let's say an user wants to export something they are writing at the editor. I'd provide a button that would kick an ActiveJob to render the document and then notify the client that it's ready to download. In order to do that, I'd have to get the latest version of the document alongside its format and assemble a PDF using the style each text block had.

In theory, there are 2 ways to tackle this:

Correct me if I am wrong but I think with your solution, I could somehow achieve the 2nd. If I diff a brand new document with the already existing, I could iter through the diffs list and assemble a final state, correct?

ArglosStudios commented 5 months ago

With the proposal, I think assembling a documents final state would be something similar to:

new_document = Y::Doc.new
diffs = new_document.diff(current_document) # current document would be the edited document that has text and formats
new_document.restore(diffs)

new_document.get_text("quill").diff do | d |
 pp d.to_s
 pp d.attributes
end

is my understand correct ?

ArglosStudios commented 5 months ago

been toying with it.

Disregard my previous question.

I can easily reconstruct a document by using the implementation you've provided.

e.g.:

ydoc = Y::Doc.new
text = ydoc.get_text("quill")
text.insert(0, "hello", { format: "bold" })
text.insert(5, "world", { format: "italic" })
text.diff.each do |d|
  pp d.insert
  pp d.attributes
end

# prints:
#"hello"
#{"format"=>"bold"}
#"world"
#{"format"=>"italic"}

text.slice!(5..10)
text.diff.each do |d|
  pp d.insert
  pp d.attributes
end

# prints
# "hello"
# {"format"=>"bold"}

this is perfect for me :D

eliias commented 5 months ago

Sweet. Give me some time to clean up, and then I will break a new release.