Module: Y::Decoder

Defined in:
lib/y/decoder.rb

Overview

Plain-text reconstruction of a stored Yjs document, for search indexing and previews. It reads the text out of the shared type the editor uses (Lexical's Y.XmlText, plain Y.Text, or ProseMirror's Y.XmlFragment) in-process, on the same native extension as Doc: no Node, no subprocess.

state = doc.encode_state_as_update  # opaque CRDT bytes from the store
Y::Decoder.text(state)              # => "hello world"
Y::Decoder.preview(state, 280)      # => "hello world…"

For full-fidelity HTML, Y::Lexxy and Y::Tiptap render the document with the editor's own semantics; this module is the cheap plain-text path.

Defined Under Namespace

Classes: Error

Class Method Summary collapse

Class Method Details

.load(state) ⇒ Object



49
50
51
# File 'lib/y/decoder.rb', line 49

def load(state)
  Y::Doc.new.tap { |doc| doc.apply_update(state) }
end

.normalize(text) ⇒ Object



57
58
59
60
61
62
# File 'lib/y/decoder.rb', line 57

def normalize(text)
  text.gsub(/[ \t]+/, " ")     # collapse runs of spaces/tabs
      .gsub(/ *\n */, "\n")    # trim spaces left around block separators
      .gsub(/\n{3,}/, "\n\n")  # cap blank-line runs
      .strip
end

.preview(state, limit: 280, field: nil) ⇒ Object

A compact, single-line preview for list UIs.



44
45
46
47
# File 'lib/y/decoder.rb', line 44

def preview(state, limit: 280, field: nil)
  body = text(state, field: field).gsub(/\s+/, " ").strip
  body.length > limit ? "#{body[0, limit].rstrip}" : body
end

.strip_tags(markup) ⇒ Object



53
54
55
# File 'lib/y/decoder.rb', line 53

def strip_tags(markup)
  markup.gsub(/<[^>]*>/, " ")
end

.text(state, field: nil) ⇒ Object

Plain text of the document. field pins the root key (Lexical: the editor id; ProseMirror: "default"); omit it to use the document's sole root.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/y/decoder.rb', line 26

def text(state, field: nil)
  field ||= Y::Doc.new.tap { |d| d.apply_update(state) }.root_names.first
  return "" unless field

  # A plain `Y.Text` root (a simple shared-text editor) reads straight out.
  # (A yrs root's type is fixed by its first typed access, so each reader
  # gets a fresh doc to try a different shared type against the same state.)
  direct = load(state).read_text(field)
  return normalize(direct) if direct && !direct.strip.empty?

  # Lexical (each block a sibling `Y.XmlText`) and ProseMirror (blocks are
  # `Y.XmlElement`s) both come back from read_xml as block-per-line markup;
  # strip any element tags to plain text.
  markup = load(state).read_xml(field)
  markup ? normalize(strip_tags(markup)) : ""
end