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.
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 (horizontal
35
- // rule, image) is an XmlElement *with* a `__type` and no extractable
36
- // text -- skip it rather than emit its `<UNDEFINED …>` serialization.
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
- _ => {} // decorator embeds we don't read for text
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Y
4
- VERSION = "0.3.1"
4
+ VERSION = "0.5.0"
5
5
  end
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.3.1
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