yrby 0.3.1 → 0.5.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 +35 -0
- data/README.md +61 -15
- data/ext/yrby/src/lexical_html.rs +993 -0
- data/ext/yrby/src/lib.rs +112 -0
- data/ext/yrby/src/prosemirror_html.rs +896 -0
- data/ext/yrby/src/read.rs +71 -4
- data/lib/y/version.rb +1 -1
- metadata +3 -1
data/ext/yrby/src/read.rs
CHANGED
|
@@ -31,11 +31,15 @@ pub fn xml_blocks_text<T: ReadTxn>(txn: &T, fragment: &XmlFragmentRef) -> String
|
|
|
31
31
|
XmlOut::Text(t) => walk_lexical_block(txn, &t, &mut out),
|
|
32
32
|
XmlOut::Element(e) => {
|
|
33
33
|
// ProseMirror blocks have a tag but no `__type`; get_string recurses
|
|
34
|
-
// them (tags kept, caller strips). A Lexical decorator
|
|
35
|
-
//
|
|
36
|
-
//
|
|
34
|
+
// them (tags kept, caller strips). A Lexical decorator is an
|
|
35
|
+
// XmlElement *with* a `__type`: attachments carry readable text
|
|
36
|
+
// (a mention's plain text, an upload's caption); the rest
|
|
37
|
+
// (horizontal rule) have none — skip rather than emit their
|
|
38
|
+
// `<UNDEFINED …>` serialization.
|
|
37
39
|
if e.get_attribute(txn, "__type").is_none() {
|
|
38
40
|
out.push(e.get_string(txn));
|
|
41
|
+
} else if let Some(text) = lexical_decorator_text(txn, &e) {
|
|
42
|
+
out.push(text);
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
XmlOut::Fragment(f) => out.push(f.get_string(txn)),
|
|
@@ -62,6 +66,26 @@ fn lexical_type<T: ReadTxn>(txn: &T, t: &XmlTextRef) -> String {
|
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
/// The readable text of a Lexical decorator element, if it has any: a mention
|
|
70
|
+
/// or embed attachment contributes its plain text; an upload attachment its
|
|
71
|
+
/// caption, alt text, or filename. Dividers and unknown decorators yield None.
|
|
72
|
+
fn lexical_decorator_text<T: ReadTxn>(txn: &T, e: &yrs::XmlElementRef) -> Option<String> {
|
|
73
|
+
let attr = |name: &str| match e.get_attribute(txn, name) {
|
|
74
|
+
Some(Out::Any(Any::String(s))) if !s.is_empty() => Some(s.to_string()),
|
|
75
|
+
_ => None,
|
|
76
|
+
};
|
|
77
|
+
match e.get_attribute(txn, "__type") {
|
|
78
|
+
Some(Out::Any(Any::String(ty))) => match ty.as_ref() {
|
|
79
|
+
"custom_action_text_attachment" => attr("plainText"),
|
|
80
|
+
"action_text_attachment" => attr("caption")
|
|
81
|
+
.or_else(|| attr("altText"))
|
|
82
|
+
.or_else(|| attr("fileName")),
|
|
83
|
+
_ => None,
|
|
84
|
+
},
|
|
85
|
+
_ => None,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
65
89
|
/// The `__type` of an embedded Lexical `Y.Map`. Two kinds appear inside a
|
|
66
90
|
/// block: text-node metadata (`"text"`) and node maps like the LineBreakNode
|
|
67
91
|
/// (`"linebreak"`). Structure confirmed from live-editor bytes (see the
|
|
@@ -120,7 +144,13 @@ fn walk_lexical_block<T: ReadTxn>(txn: &T, t: &XmlTextRef, out: &mut Vec<String>
|
|
|
120
144
|
}
|
|
121
145
|
}
|
|
122
146
|
}
|
|
123
|
-
|
|
147
|
+
// An inline decorator (a mention attachment) joins the line.
|
|
148
|
+
Out::YXmlElement(e) => {
|
|
149
|
+
if let Some(text) = lexical_decorator_text(txn, &e) {
|
|
150
|
+
line.push_str(&text);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
_ => {} // other embeds carry no text
|
|
124
154
|
}
|
|
125
155
|
}
|
|
126
156
|
if !line.is_empty() {
|
|
@@ -332,6 +362,43 @@ mod tests {
|
|
|
332
362
|
assert_eq!(xml_blocks_text(&txn, &frag), "foo\nbarbaz");
|
|
333
363
|
}
|
|
334
364
|
|
|
365
|
+
#[test]
|
|
366
|
+
fn lexxy_full_schema_doc_extracts_attachment_text_too() {
|
|
367
|
+
// The full-schema capture (see lexical_html.rs): attachments must now
|
|
368
|
+
// contribute readable text — a mention's plain text inline, an
|
|
369
|
+
// upload's caption as its own line — while the divider stays silent.
|
|
370
|
+
use yrs::updates::decoder::Decode;
|
|
371
|
+
use yrs::{Transact, Update};
|
|
372
|
+
let bytes = include_bytes!("fixtures/lexxy_full.bin");
|
|
373
|
+
let doc = Doc::new();
|
|
374
|
+
doc.transact_mut()
|
|
375
|
+
.apply_update(Update::decode_v1(bytes).unwrap())
|
|
376
|
+
.unwrap();
|
|
377
|
+
let txn = doc.transact();
|
|
378
|
+
let frag = txn.get_xml_fragment("root").unwrap();
|
|
379
|
+
let text = xml_blocks_text(&txn, &frag);
|
|
380
|
+
|
|
381
|
+
assert!(
|
|
382
|
+
text.contains("Mention: @Alice done."),
|
|
383
|
+
"inline mention joins its line:\n{text}"
|
|
384
|
+
);
|
|
385
|
+
assert!(
|
|
386
|
+
text.contains("The team, 2026"),
|
|
387
|
+
"upload caption becomes a line"
|
|
388
|
+
);
|
|
389
|
+
assert!(
|
|
390
|
+
!text.contains("UNDEFINED"),
|
|
391
|
+
"no decorator serialization leaks"
|
|
392
|
+
);
|
|
393
|
+
assert!(
|
|
394
|
+
!text.contains('\u{2504}'),
|
|
395
|
+
"the divider glyph is not emitted"
|
|
396
|
+
);
|
|
397
|
+
for expected in ["Heading H6", "Done item", "def hello", "after-empty"] {
|
|
398
|
+
assert!(text.contains(expected), "missing {expected:?}");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
335
402
|
#[test]
|
|
336
403
|
fn lexical_complex_doc_extracts_all_nested_text() {
|
|
337
404
|
// 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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- JP Camara
|
|
@@ -83,7 +83,9 @@ files:
|
|
|
83
83
|
- README.md
|
|
84
84
|
- ext/yrby/Cargo.toml
|
|
85
85
|
- ext/yrby/extconf.rb
|
|
86
|
+
- ext/yrby/src/lexical_html.rs
|
|
86
87
|
- ext/yrby/src/lib.rs
|
|
88
|
+
- ext/yrby/src/prosemirror_html.rs
|
|
87
89
|
- ext/yrby/src/protocol.rs
|
|
88
90
|
- ext/yrby/src/read.rs
|
|
89
91
|
- lib/y.rb
|