yrby 0.2.3 → 0.3.1
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 +67 -0
- data/Cargo.lock +644 -0
- data/README.md +38 -2
- data/ext/yrby/src/lib.rs +43 -7
- data/ext/yrby/src/protocol.rs +494 -23
- data/ext/yrby/src/read.rs +84 -8
- data/lib/y/version.rb +1 -1
- metadata +2 -4
- data/lib/y/decoder/version.rb +0 -7
- data/lib/y/decoder.rb +0 -66
- data/lib/yrby-decoder.rb +0 -4
data/ext/yrby/src/read.rs
CHANGED
|
@@ -62,6 +62,17 @@ fn lexical_type<T: ReadTxn>(txn: &T, t: &XmlTextRef) -> String {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/// The `__type` of an embedded Lexical `Y.Map`. Two kinds appear inside a
|
|
66
|
+
/// block: text-node metadata (`"text"`) and node maps like the LineBreakNode
|
|
67
|
+
/// (`"linebreak"`). Structure confirmed from live-editor bytes (see the
|
|
68
|
+
/// captured-fixture test).
|
|
69
|
+
fn lexical_map_type<T: ReadTxn>(txn: &T, m: &MapRef) -> String {
|
|
70
|
+
match m.get(txn, "__type") {
|
|
71
|
+
Some(Out::Any(Any::String(s))) => s.to_string(),
|
|
72
|
+
_ => String::new(),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
/// Gather the text of an inline Lexical element (its text runs and any nested
|
|
66
77
|
/// inline elements) without introducing block breaks.
|
|
67
78
|
fn inline_lexical_text<T: ReadTxn>(txn: &T, t: &XmlTextRef, buf: &mut String) {
|
|
@@ -69,7 +80,12 @@ fn inline_lexical_text<T: ReadTxn>(txn: &T, t: &XmlTextRef, buf: &mut String) {
|
|
|
69
80
|
match d.insert {
|
|
70
81
|
Out::Any(Any::String(s)) => buf.push_str(&s),
|
|
71
82
|
Out::YXmlText(child) => inline_lexical_text(txn, &child, buf),
|
|
72
|
-
|
|
83
|
+
Out::YMap(m) => match lexical_map_type(txn, &m).as_str() {
|
|
84
|
+
"linebreak" => buf.push('\n'),
|
|
85
|
+
"tab" => buf.push('\t'),
|
|
86
|
+
_ => {} // per-text-node metadata: no text of its own
|
|
87
|
+
},
|
|
88
|
+
_ => {} // decorator embeds: no text
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
91
|
}
|
|
@@ -81,17 +97,30 @@ fn walk_lexical_block<T: ReadTxn>(txn: &T, t: &XmlTextRef, out: &mut Vec<String>
|
|
|
81
97
|
for d in t.diff(txn, YChange::identity) {
|
|
82
98
|
match d.insert {
|
|
83
99
|
Out::Any(Any::String(s)) => line.push_str(&s),
|
|
100
|
+
// Node maps: linebreak/tab carry no text, so emit the character
|
|
101
|
+
// they represent ("foo⏎bar" must not become "foobar"). Metadata
|
|
102
|
+
// maps ("text") stay silent.
|
|
103
|
+
Out::YMap(m) => match lexical_map_type(txn, &m).as_str() {
|
|
104
|
+
"linebreak" => line.push('\n'),
|
|
105
|
+
"tab" => line.push('\t'),
|
|
106
|
+
_ => {}
|
|
107
|
+
},
|
|
84
108
|
Out::YXmlText(child) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
let ty = lexical_type(txn, &child);
|
|
110
|
+
match ty.as_str() {
|
|
111
|
+
// Defensive only: real Lexical stores these as Y.Map embeds.
|
|
112
|
+
"linebreak" => line.push('\n'),
|
|
113
|
+
"tab" => line.push('\t'),
|
|
114
|
+
_ if is_inline_lexical_type(&ty) => inline_lexical_text(txn, &child, &mut line),
|
|
115
|
+
_ => {
|
|
116
|
+
if !line.is_empty() {
|
|
117
|
+
out.push(std::mem::take(&mut line));
|
|
118
|
+
}
|
|
119
|
+
walk_lexical_block(txn, &child, out);
|
|
90
120
|
}
|
|
91
|
-
walk_lexical_block(txn, &child, out);
|
|
92
121
|
}
|
|
93
122
|
}
|
|
94
|
-
_ => {} //
|
|
123
|
+
_ => {} // decorator embeds we don't read for text
|
|
95
124
|
}
|
|
96
125
|
}
|
|
97
126
|
if !line.is_empty() {
|
|
@@ -256,6 +285,53 @@ mod tests {
|
|
|
256
285
|
assert_eq!(map_json(&txn, &map), "{}");
|
|
257
286
|
}
|
|
258
287
|
|
|
288
|
+
#[test]
|
|
289
|
+
fn lexical_soft_line_break_and_tab_emit_their_characters() {
|
|
290
|
+
// A paragraph "foo⏎bar" (shift-enter): Lexical stores the LineBreakNode
|
|
291
|
+
// as an embedded Y.Map with __type=linebreak (the same shape as the
|
|
292
|
+
// per-text-node metadata maps, which must stay silent). It must come
|
|
293
|
+
// through as '\n', not vanish and glue the words. Same for tab.
|
|
294
|
+
use yrs::{Text, XmlTextPrelim};
|
|
295
|
+
let doc = Doc::new();
|
|
296
|
+
let frag = doc.get_or_insert_xml_fragment("lex");
|
|
297
|
+
{
|
|
298
|
+
let mut txn = doc.transact_mut();
|
|
299
|
+
let block = frag.push_back(&mut txn, XmlTextPrelim::new(""));
|
|
300
|
+
let meta: MapPrelim = [("__type", yrs::In::from("text"))].into_iter().collect();
|
|
301
|
+
block.insert_embed(&mut txn, 0, meta); // metadata map: no text
|
|
302
|
+
block.push(&mut txn, "foo");
|
|
303
|
+
let br: MapPrelim = [("__type", yrs::In::from("linebreak"))]
|
|
304
|
+
.into_iter()
|
|
305
|
+
.collect();
|
|
306
|
+
block.insert_embed(&mut txn, 4, br);
|
|
307
|
+
block.push(&mut txn, "bar");
|
|
308
|
+
let tab: MapPrelim = [("__type", yrs::In::from("tab"))].into_iter().collect();
|
|
309
|
+
block.insert_embed(&mut txn, 8, tab);
|
|
310
|
+
block.push(&mut txn, "baz");
|
|
311
|
+
}
|
|
312
|
+
let txn = doc.transact();
|
|
313
|
+
assert_eq!(xml_blocks_text(&txn, &frag), "foo\nbar\tbaz");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#[test]
|
|
317
|
+
fn lexical_real_captured_linebreak_extracts_as_newline() {
|
|
318
|
+
// Ground truth: bytes captured from a LIVE Lexxy editor (agent-browser
|
|
319
|
+
// typing "foo", pressing Shift+Enter, typing "barbaz"), served by the
|
|
320
|
+
// yrby test server's durable store. The hand-built test above models
|
|
321
|
+
// this structure; this one IS the structure. Regenerate by driving
|
|
322
|
+
// lexxy-realtime's test server and saving GET /content/:room.
|
|
323
|
+
use yrs::updates::decoder::Decode;
|
|
324
|
+
use yrs::Update;
|
|
325
|
+
let bytes = include_bytes!("fixtures/lexical_linebreak.bin");
|
|
326
|
+
let doc = Doc::new();
|
|
327
|
+
doc.transact_mut()
|
|
328
|
+
.apply_update(Update::decode_v1(bytes).unwrap())
|
|
329
|
+
.unwrap();
|
|
330
|
+
let txn = doc.transact();
|
|
331
|
+
let frag = txn.get_xml_fragment("root").unwrap();
|
|
332
|
+
assert_eq!(xml_blocks_text(&txn, &frag), "foo\nbarbaz");
|
|
333
|
+
}
|
|
334
|
+
|
|
259
335
|
#[test]
|
|
260
336
|
fn lexical_complex_doc_extracts_all_nested_text() {
|
|
261
337
|
// A real Lexxy/Lexical doc with every block type: headings, formatted
|
data/lib/y/version.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.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- JP Camara
|
|
@@ -77,6 +77,7 @@ extensions:
|
|
|
77
77
|
extra_rdoc_files: []
|
|
78
78
|
files:
|
|
79
79
|
- CHANGELOG.md
|
|
80
|
+
- Cargo.lock
|
|
80
81
|
- Cargo.toml
|
|
81
82
|
- LICENSE
|
|
82
83
|
- README.md
|
|
@@ -86,10 +87,7 @@ files:
|
|
|
86
87
|
- ext/yrby/src/protocol.rs
|
|
87
88
|
- ext/yrby/src/read.rs
|
|
88
89
|
- lib/y.rb
|
|
89
|
-
- lib/y/decoder.rb
|
|
90
|
-
- lib/y/decoder/version.rb
|
|
91
90
|
- lib/y/version.rb
|
|
92
|
-
- lib/yrby-decoder.rb
|
|
93
91
|
- lib/yrby.rb
|
|
94
92
|
homepage: https://github.com/jpcamara/yrby
|
|
95
93
|
licenses:
|
data/lib/y/decoder/version.rb
DELETED
data/lib/y/decoder.rb
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "y"
|
|
4
|
-
require "y/decoder/version"
|
|
5
|
-
|
|
6
|
-
module Y
|
|
7
|
-
# Plain-text reconstruction of a stored Yjs document, in pure Ruby — for search
|
|
8
|
-
# indexing and previews. The core `yrby` gem moves and stores opaque CRDT
|
|
9
|
-
# updates without reading them; this reads the text out of the shared type the
|
|
10
|
-
# editor uses (Lexical's `Y.XmlText`, plain `Y.Text`, or ProseMirror's
|
|
11
|
-
# `Y.XmlFragment`), in-process, on the native extension core already ships — no
|
|
12
|
-
# Node, no subprocess, no binary.
|
|
13
|
-
#
|
|
14
|
-
# state = doc.encode_state_as_update # opaque CRDT bytes from the store
|
|
15
|
-
# Y::Decoder.text(state) # => "hello world"
|
|
16
|
-
# Y::Decoder.preview(state, 280) # => "hello world…"
|
|
17
|
-
#
|
|
18
|
-
# Full-fidelity reconstruction (the exact Lexical EditorState / HTML, which
|
|
19
|
-
# needs @lexical/yjs) is a separate, opt-in concern — see the `yrby-decode`
|
|
20
|
-
# package's Bun binary. This gem stays pure Ruby on purpose.
|
|
21
|
-
module Decoder
|
|
22
|
-
class Error < Y::Error; end
|
|
23
|
-
|
|
24
|
-
module_function
|
|
25
|
-
|
|
26
|
-
# Plain text of the document. `field` pins the root key (Lexical: the editor
|
|
27
|
-
# id; ProseMirror: "default"); omit it to use the document's sole root.
|
|
28
|
-
def text(state, field: nil)
|
|
29
|
-
field ||= Y::Doc.new.tap { |d| d.apply_update(state) }.root_names.first
|
|
30
|
-
return "" unless field
|
|
31
|
-
|
|
32
|
-
# A plain `Y.Text` root (a simple shared-text editor) reads straight out.
|
|
33
|
-
# (A yrs root's type is fixed by its first typed access, so each reader
|
|
34
|
-
# gets a fresh doc to try a different shared type against the same state.)
|
|
35
|
-
direct = load(state).read_text(field)
|
|
36
|
-
return normalize(direct) if direct && !direct.strip.empty?
|
|
37
|
-
|
|
38
|
-
# Lexical (each block a sibling `Y.XmlText`) and ProseMirror (blocks are
|
|
39
|
-
# `Y.XmlElement`s) both come back from read_xml as block-per-line markup;
|
|
40
|
-
# strip any element tags to plain text.
|
|
41
|
-
markup = load(state).read_xml(field)
|
|
42
|
-
markup ? normalize(strip_tags(markup)) : ""
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# A compact, single-line preview for list UIs.
|
|
46
|
-
def preview(state, limit: 280, field: nil)
|
|
47
|
-
body = text(state, field: field).gsub(/\s+/, " ").strip
|
|
48
|
-
body.length > limit ? "#{body[0, limit].rstrip}…" : body
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def load(state)
|
|
52
|
-
Y::Doc.new.tap { |doc| doc.apply_update(state) }
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def strip_tags(markup)
|
|
56
|
-
markup.gsub(/<[^>]*>/, " ")
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def normalize(text)
|
|
60
|
-
text.gsub(/[ \t]+/, " ") # collapse runs of spaces/tabs
|
|
61
|
-
.gsub(/ *\n */, "\n") # trim spaces left around block separators
|
|
62
|
-
.gsub(/\n{3,}/, "\n\n") # cap blank-line runs
|
|
63
|
-
.strip
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
data/lib/yrby-decoder.rb
DELETED