yrby 0.7.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7e0200b53061315ddc075bbd337a5d9ead543f99617cac94d53180242a80bbd
4
- data.tar.gz: 956ec491daabcafc73009e40df573e9c01e5b7587be9d3001d32a7b0a4092f01
3
+ metadata.gz: 80931cbbabc08011f5f8ac78dfcf92e648feaf39ef8338ba7abb77bd575bb231
4
+ data.tar.gz: 340e157cea8aeee6ab5013ca43571e74a8f01f514ee8172aad81e14c10cf55b6
5
5
  SHA512:
6
- metadata.gz: afae1336b4e184f1ecf1d878202023123cee10fa7ea47642c7f6f2b7d48eee7201f2b2198aed217b08434228904178bddaf3d46058e6da7b5b27ad7450a0ef87
7
- data.tar.gz: 48a7bd917201e4df0858a08ce0d1eab132fc160a507dc88bfcd1c390cae9a0a8b11a9132894e5f91889f946f69e4366455fb8257e0165c9f1d82da4c367547ed
6
+ metadata.gz: fe5ba9524856f360def8b53a942bc8142017cb87254344aaeab9b90d4006471d3f480db5c527e4563f031ebc25d9c504c3070d306308676e10f11127ac6695c7
7
+ data.tar.gz: ef2cf7df98eced8c39ad5059ff12d3dfbdd8fc6dbd22c949163cc074d1710add09b9c4adb9d4c4ca24fa957f1b5983578c9f38623a80b0a4172027da4505a3a9
data/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project aims
5
5
  to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.7.1] - 2026-08-19
8
+
9
+ ### Fixed
10
+
11
+ - `Y::ProseMirror` and `Y::Tiptap` keep marks shared by adjacent text
12
+ runs open across them, the way ProseMirror's own serializer does:
13
+ bold then bold-italic renders as `<strong>a<em>b</em></strong>`, not
14
+ as two sibling `<strong>` wraps. A mark whose attributes differ
15
+ between runs never merges. Found by the new cross-renderer
16
+ verification, which checks the renderers against a live Tiptap
17
+ editor, `@tiptap/html`'s static `generateHTML`, `tiptap-php`,
18
+ Lexical's `$generateHtmlFromNodes`, and Payload CMS's
19
+ `convertLexicalToHTML` on a shared corpus.
20
+
7
21
  ## [0.7.0] - 2026-08-11
8
22
 
9
23
  ### Added
data/Cargo.lock CHANGED
@@ -227,6 +227,15 @@ version = "1.3.0"
227
227
  source = "registry+https://github.com/rust-lang/crates.io-index"
228
228
  checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
229
229
 
230
+ [[package]]
231
+ name = "lexical-yjs-html"
232
+ version = "0.1.1"
233
+ dependencies = [
234
+ "serde_json",
235
+ "yjs-html-core",
236
+ "yrs",
237
+ ]
238
+
230
239
  [[package]]
231
240
  name = "libc"
232
241
  version = "0.2.180"
@@ -337,6 +346,15 @@ dependencies = [
337
346
  "unicode-ident",
338
347
  ]
339
348
 
349
+ [[package]]
350
+ name = "prosemirror-yjs-html"
351
+ version = "0.1.2"
352
+ dependencies = [
353
+ "serde_json",
354
+ "yjs-html-core",
355
+ "yrs",
356
+ ]
357
+
340
358
  [[package]]
341
359
  name = "quote"
342
360
  version = "1.0.43"
@@ -608,18 +626,26 @@ version = "0.2.1"
608
626
  source = "registry+https://github.com/rust-lang/crates.io-index"
609
627
  checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
610
628
 
629
+ [[package]]
630
+ name = "yjs-html-core"
631
+ version = "0.1.1"
632
+ dependencies = [
633
+ "serde_json",
634
+ "yrs",
635
+ ]
636
+
611
637
  [[package]]
612
638
  name = "yrby"
613
639
  version = "0.1.0"
614
640
  dependencies = [
641
+ "lexical-yjs-html",
615
642
  "magnus",
643
+ "prosemirror-yjs-html",
616
644
  "rb-sys",
617
645
  "rb-sys-env 0.1.2",
618
646
  "serde_json",
647
+ "yjs-html-core",
619
648
  "yrs",
620
- "yrs-html-core",
621
- "yrs-lexical-html",
622
- "yrs-prosemirror-html",
623
649
  ]
624
650
 
625
651
  [[package]]
@@ -640,32 +666,6 @@ dependencies = [
640
666
  "thiserror",
641
667
  ]
642
668
 
643
- [[package]]
644
- name = "yrs-html-core"
645
- version = "0.1.0"
646
- dependencies = [
647
- "serde_json",
648
- "yrs",
649
- ]
650
-
651
- [[package]]
652
- name = "yrs-lexical-html"
653
- version = "0.1.0"
654
- dependencies = [
655
- "serde_json",
656
- "yrs",
657
- "yrs-html-core",
658
- ]
659
-
660
- [[package]]
661
- name = "yrs-prosemirror-html"
662
- version = "0.1.0"
663
- dependencies = [
664
- "serde_json",
665
- "yrs",
666
- "yrs-html-core",
667
- ]
668
-
669
669
  [[package]]
670
670
  name = "zmij"
671
671
  version = "1.0.14"
data/ext/yrby/Cargo.toml CHANGED
@@ -14,9 +14,9 @@ magnus = "0.8"
14
14
  rb-sys = "0.9"
15
15
  yrs = { version = "0.27", features = ["sync"] }
16
16
  serde_json = "1.0"
17
- yrs-html-core = { path = "crates/html-core" }
18
- yrs-lexical-html = { path = "crates/lexical-html" }
19
- yrs-prosemirror-html = { path = "crates/prosemirror-html" }
17
+ yjs-html-core = { path = "crates/html-core" }
18
+ lexical-yjs-html = { path = "crates/lexical-html" }
19
+ prosemirror-yjs-html = { path = "crates/prosemirror-html" }
20
20
 
21
21
  [dev-dependencies]
22
22
  rb-sys-env = "0.1"
@@ -1,14 +1,15 @@
1
1
  [package]
2
- name = "yrs-html-core"
3
- description = "Internal core for the yrs-*-html renderer crates; depend on those instead"
2
+ name = "yjs-html-core"
3
+ description = "Internal core for the lexical-yjs-html and prosemirror-yjs-html renderer crates; depend on those instead"
4
4
  repository = "https://github.com/jpcamara/yrby"
5
5
  readme = "README.md"
6
- version = "0.1.0"
6
+ version = "0.1.1"
7
7
  edition = "2024"
8
8
  rust-version = "1.85"
9
9
  authors = ["JP Camara <johnpcamara@gmail.com>"]
10
10
  license = "MIT"
11
- publish = false
11
+ keywords = ["yjs", "crdt", "yrs", "html", "rendering"]
12
+ categories = ["text-processing", "web-programming"]
12
13
 
13
14
  [dependencies]
14
15
  yrs = { version = "0.27", features = ["sync"] }
@@ -196,11 +196,7 @@ pub fn resolve_parts<F: Fn(&str) -> Option<String>>(
196
196
  }
197
197
  }
198
198
  }
199
- if out.is_empty() {
200
- None
201
- } else {
202
- Some(out)
203
- }
199
+ if out.is_empty() { None } else { Some(out) }
204
200
  }
205
201
 
206
202
  /// An attribute reference on a node: rules say `:kind`; Lexical stores its own
@@ -384,7 +380,7 @@ fn parse_node_rule(name: &str, spec: &serde_json::Value) -> Result<NodeRule, Str
384
380
  Some(other) => {
385
381
  return Err(format!(
386
382
  "rule for {name:?}: unknown content kind {other:?} (blocks|inline|none)"
387
- ))
383
+ ));
388
384
  }
389
385
  };
390
386
  if spec
@@ -431,7 +427,7 @@ fn parse_attrs(
431
427
  Some(_) => {
432
428
  return Err(format!(
433
429
  "rule for {name:?}: attrs must be an array of [name, template] pairs"
434
- ))
430
+ ));
435
431
  }
436
432
  };
437
433
  for entry in entries {
@@ -1,16 +1,17 @@
1
1
  [package]
2
- name = "yrs-lexical-html"
2
+ name = "lexical-yjs-html"
3
3
  description = "Render Lexical-shaped yrs documents to HTML, no browser or Node required"
4
4
  repository = "https://github.com/jpcamara/yrby"
5
5
  readme = "README.md"
6
- version = "0.1.0"
6
+ version = "0.1.1"
7
7
  edition = "2024"
8
8
  rust-version = "1.85"
9
9
  authors = ["JP Camara <johnpcamara@gmail.com>"]
10
10
  license = "MIT"
11
- publish = false
11
+ keywords = ["yjs", "crdt", "lexical", "html", "yrs"]
12
+ categories = ["text-processing", "web-programming"]
12
13
 
13
14
  [dependencies]
14
15
  yrs = { version = "0.27", features = ["sync"] }
15
16
  serde_json = "1.0"
16
- yrs-html-core = { path = "../html-core" }
17
+ yjs-html-core = { path = "../html-core", version = "0.1.1" }
@@ -39,7 +39,7 @@
39
39
  //! with the unwrapped span. Case-transform format bits are never rendered
40
40
  //! (their text-transform style is outside the sanitize whitelist).
41
41
  //!
42
- //! Custom nodes: rules are registered by `__type` (see `yrs-html-core`) and
42
+ //! Custom nodes: rules are registered by `__type` (see `yjs-html-core`) and
43
43
  //! consulted before the built-in arms, so they extend the schema or override
44
44
  //! a built-in. Declarative rules render here; callback rules emit
45
45
  //! `Segment::Deferred` for the caller to fill in after the render.
@@ -50,8 +50,8 @@
50
50
  mod readme_examples {}
51
51
 
52
52
  // The full rules surface, re-exported: depend on this crate alone;
53
- // yrs-html-core is an internal implementation crate.
54
- pub use yrs_html_core::*;
53
+ // yjs-html-core is an internal implementation crate.
54
+ pub use yjs_html_core::*;
55
55
  use yrs::types::text::YChange;
56
56
  use yrs::{
57
57
  Any, GetString, Map, Out, ReadTxn, Text, Xml, XmlElementRef, XmlFragment, XmlFragmentRef,
@@ -130,7 +130,7 @@ pub fn render_segments<T: ReadTxn>(
130
130
  /// callback rules, segments always flatten.
131
131
  pub fn render<T: ReadTxn>(txn: &T, fragment: &XmlFragmentRef) -> Option<String> {
132
132
  render_segments(txn, fragment, &Rules::empty()).map(|segs| {
133
- yrs_html_core::flatten(segs)
133
+ yjs_html_core::flatten(segs)
134
134
  .into_html()
135
135
  .expect("no callback rules registered")
136
136
  })
@@ -773,11 +773,7 @@ fn lexxy_style(style: &str) -> Option<String> {
773
773
  css.push(';');
774
774
  }
775
775
  }
776
- if css.is_empty() {
777
- None
778
- } else {
779
- Some(css)
780
- }
776
+ if css.is_empty() { None } else { Some(css) }
781
777
  }
782
778
 
783
779
  /// `link` / `autolink`: Lexxy's sanitize keeps only `href` and `title`
@@ -1301,7 +1297,7 @@ mod tests {
1301
1297
  let txn = doc.transact();
1302
1298
  let frag = txn.get_xml_fragment("root").unwrap();
1303
1299
  let segs = render_segments(&txn, &frag, &rules).unwrap();
1304
- let html = yrs_html_core::flatten(segs).into_html().unwrap();
1300
+ let html = yjs_html_core::flatten(segs).into_html().unwrap();
1305
1301
  assert_eq!(
1306
1302
  html,
1307
1303
  "<p><a class=\"app-link\" href=\"https://x.example\">site</a>\
@@ -1,16 +1,17 @@
1
1
  [package]
2
- name = "yrs-prosemirror-html"
2
+ name = "prosemirror-yjs-html"
3
3
  description = "Render ProseMirror-shaped yrs documents to HTML, no browser or Node required"
4
4
  repository = "https://github.com/jpcamara/yrby"
5
5
  readme = "README.md"
6
- version = "0.1.0"
6
+ version = "0.1.2"
7
7
  edition = "2024"
8
8
  rust-version = "1.85"
9
9
  authors = ["JP Camara <johnpcamara@gmail.com>"]
10
10
  license = "MIT"
11
- publish = false
11
+ keywords = ["yjs", "crdt", "prosemirror", "tiptap", "html"]
12
+ categories = ["text-processing", "web-programming"]
12
13
 
13
14
  [dependencies]
14
15
  yrs = { version = "0.27", features = ["sync"] }
15
16
  serde_json = "1.0"
16
- yrs-html-core = { path = "../html-core" }
17
+ yjs-html-core = { path = "../html-core", version = "0.1.1" }
@@ -42,10 +42,10 @@
42
42
  mod readme_examples {}
43
43
 
44
44
  // The full rules surface, re-exported: depend on this crate alone;
45
- // yrs-html-core is an internal implementation crate.
46
- pub use yrs_html_core::*;
47
- use yrs::types::text::YChange;
45
+ // yjs-html-core is an internal implementation crate.
46
+ pub use yjs_html_core::*;
48
47
  use yrs::types::Attrs;
48
+ use yrs::types::text::YChange;
49
49
  use yrs::{
50
50
  Any, GetString, Out, ReadTxn, Text, Xml, XmlElementRef, XmlFragment, XmlFragmentRef, XmlOut,
51
51
  XmlTextRef,
@@ -105,7 +105,7 @@ pub fn render_segments<T: ReadTxn>(
105
105
  /// callback rules, segments always flatten.
106
106
  pub fn render<T: ReadTxn>(txn: &T, fragment: &XmlFragmentRef) -> Option<String> {
107
107
  render_segments(txn, fragment, &Rules::empty()).map(|segs| {
108
- yrs_html_core::flatten(segs)
108
+ yjs_html_core::flatten(segs)
109
109
  .into_html()
110
110
  .expect("no callback rules registered")
111
111
  })
@@ -605,102 +605,120 @@ fn render_rule_inline<T: ReadTxn>(
605
605
  em.push('>');
606
606
  }
607
607
 
608
- /// Emit each formatted run of a Y.XmlText.
608
+ /// Emit the formatted runs of a Y.XmlText, keeping marks shared by
609
+ /// adjacent runs open across them: `bold` then `bold italic` renders as
610
+ /// `<strong>a<em>b</em></strong>`, the way the editor's DOM serializer
611
+ /// keeps an active mark open into the next run, not as two sibling
612
+ /// `<strong>` wraps. Sharing is by identical opening tag, so a mark whose
613
+ /// attributes differ between runs (a different href, a different style)
614
+ /// never merges.
609
615
  fn render_text_runs<T: ReadTxn>(txn: &T, t: &XmlTextRef, em: &mut Emitter, rules: &Rules) {
616
+ let mut open: Vec<(String, String)> = Vec::new();
610
617
  for d in t.diff(txn, YChange::identity) {
611
618
  if let Out::Any(Any::String(s)) = &d.insert {
612
- em.push_str(&render_run(s, d.attributes.as_deref(), rules));
619
+ let stack = run_stack(d.attributes.as_deref(), rules);
620
+ let shared = open
621
+ .iter()
622
+ .zip(stack.iter())
623
+ .take_while(|(a, b)| a == b)
624
+ .count();
625
+ for (_, close) in open.split_off(shared).into_iter().rev() {
626
+ em.push_str(&close);
627
+ }
628
+ for (open_tag, _) in &stack[shared..] {
629
+ em.push_str(open_tag);
630
+ }
631
+ open = stack;
632
+ em.push_str(&escape_text(s));
613
633
  }
614
634
  }
635
+ for (_, close) in open.into_iter().rev() {
636
+ em.push_str(&close);
637
+ }
615
638
  }
616
639
 
617
- /// Wrap one text run in its marks, nesting innermost-first:
618
- /// subscript/superscript, highlight, underline, strike, italic, bold, a
619
- /// textStyle span, then link on the outside. `code` renders alone among the
620
- /// formatting marks (Tiptap's Code mark excludes them all), but a link still
621
- /// wraps it Tiptap can't produce code+link, prosemirror-schema-basic can,
622
- /// and dropping the link would lose the href.
623
- ///
624
- /// A registered mark rule claims its stored name from the built-in wraps
625
- /// (overriding it) and wraps outside everything; multiple custom marks nest
626
- /// alphabetically by name, so output is deterministic regardless of
627
- /// registration order. Overriding replaces only the markup: a claimed
628
- /// `code` still excludes the other formatting marks, and a claimed
629
- /// formatting mark stays excluded from a code run.
630
- fn render_run(text: &str, marks: Option<&Attrs>, rules: &Rules) -> String {
631
- let mut html = escape_text(text);
640
+ /// The run's mark wrappers as (open tag, close tag) pairs, outermost
641
+ /// first: registered custom marks (reverse-alphabetical, so the last name
642
+ /// alphabetically is outermost), link, the textStyle span, then bold,
643
+ /// italic, strike, underline, highlight, and innermost
644
+ /// subscript/superscriptor `code` alone in their place.
645
+ fn run_stack(marks: Option<&Attrs>, rules: &Rules) -> Vec<(String, String)> {
632
646
  let Some(marks) = marks else {
633
- return html;
647
+ return Vec::new();
634
648
  };
649
+ // Innermost first, reversed at the end.
650
+ let mut stack: Vec<(String, String)> = Vec::new();
651
+ let tag = |t: &str| (format!("<{t}>"), format!("</{t}>"));
635
652
  if has(marks, &["code"], rules) {
636
- html = wrap(html, "code");
653
+ stack.push(tag("code"));
637
654
  } else if !marks.contains_key("code") {
638
655
  // (A claimed `code` lands here too — on the run, but skipped by the
639
- // else-if: the rule replaces the wrap, via wrap_custom_marks below,
656
+ // else-if: the rule replaces the wrap, via the custom marks below,
640
657
  // and code's exclusivity over the other formatting marks holds.)
641
658
  if has(marks, &["subscript", "sub"], rules) {
642
- html = wrap(html, "sub");
659
+ stack.push(tag("sub"));
643
660
  } else if has(marks, &["superscript", "sup"], rules) {
644
- html = wrap(html, "sup");
661
+ stack.push(tag("sup"));
645
662
  }
646
663
  if has(marks, &["highlight"], rules) {
647
- html = wrap(html, "mark");
664
+ stack.push(tag("mark"));
648
665
  }
649
666
  if has(marks, &["underline", "u"], rules) {
650
- html = wrap(html, "u");
667
+ stack.push(tag("u"));
651
668
  }
652
669
  if has(marks, &["strike", "s"], rules) {
653
- html = wrap(html, "s");
670
+ stack.push(tag("s"));
654
671
  }
655
672
  if has(marks, &["italic", "em"], rules) {
656
- html = wrap(html, "em");
673
+ stack.push(tag("em"));
657
674
  }
658
675
  if has(marks, &["bold", "strong"], rules) {
659
- html = wrap(html, "strong");
676
+ stack.push(tag("strong"));
660
677
  }
661
678
  if !rules.marks.contains_key("textStyle") {
662
679
  if let Some(Any::Map(style)) = marks.get("textStyle") {
663
680
  let css = text_style_css(style);
664
681
  if !css.is_empty() {
665
- html = format!("<span style=\"{}\">{html}</span>", escape_attr(&css));
682
+ stack.push((
683
+ format!("<span style=\"{}\">", escape_attr(&css)),
684
+ "</span>".to_string(),
685
+ ));
666
686
  }
667
687
  }
668
688
  }
669
689
  }
670
690
  if !rules.marks.contains_key("link") {
671
691
  if let Some(Any::Map(link)) = marks.get("link") {
672
- html = wrap_link(html, link);
692
+ stack.push((link_open(link), "</a>".to_string()));
673
693
  }
674
694
  }
675
695
  if !rules.marks.is_empty() {
676
- html = wrap_custom_marks(html, marks, rules);
677
- }
678
- html
679
- }
680
-
681
- /// Apply the run's registered custom marks, outermost of everything. A code
682
- /// run keeps its exclusivity under overrides too: only the `code` and `link`
683
- /// claims themselves may wrap it, matching what the built-in wraps allow —
684
- /// otherwise overriding a formatting mark would change which marks render on
685
- /// a code run, not just their markup.
686
- fn wrap_custom_marks(html: String, marks: &Attrs, rules: &Rules) -> String {
687
- let code = marks.contains_key("code");
688
- let mut names: Vec<&str> = rules
689
- .marks
690
- .keys()
691
- .map(String::as_str)
692
- .filter(|name| marks.contains_key(*name))
693
- .filter(|name| !code || matches!(*name, "code" | "link"))
694
- .collect();
695
- names.sort_unstable();
696
- let mut html = html;
697
- for name in names {
698
- html = wrap_custom_mark(html, &rules.marks[name], marks.get(name));
696
+ let code = marks.contains_key("code");
697
+ let mut names: Vec<&str> = rules
698
+ .marks
699
+ .keys()
700
+ .map(String::as_str)
701
+ .filter(|name| marks.contains_key(*name))
702
+ .filter(|name| !code || matches!(*name, "code" | "link"))
703
+ .collect();
704
+ names.sort_unstable();
705
+ for name in names {
706
+ let rule = &rules.marks[name];
707
+ stack.push((
708
+ custom_mark_open(rule, marks.get(name)),
709
+ format!("</{}>", rule.tag),
710
+ ));
711
+ }
699
712
  }
700
- html
713
+ stack.reverse();
714
+ stack
701
715
  }
702
716
 
703
- fn wrap_custom_mark(inner: String, rule: &MarkRule, value: Option<&Any>) -> String {
717
+ /// The opening tag for a registered custom mark. Custom marks sit outside
718
+ /// everything; a code run keeps its exclusivity under overrides too: only
719
+ /// the `code` and `link` claims themselves may wrap it, matching what the
720
+ /// built-in wraps allow.
721
+ fn custom_mark_open(rule: &MarkRule, value: Option<&Any>) -> String {
704
722
  let mut out = String::from("<");
705
723
  out.push_str(&rule.tag);
706
724
  for (attr, parts) in &rule.attrs {
@@ -709,14 +727,10 @@ fn wrap_custom_mark(inner: String, rule: &MarkRule, value: Option<&Any>) -> Stri
709
727
  out.push_str(attr);
710
728
  out.push_str("=\"");
711
729
  out.push_str(&escape_attr(&v));
712
- out.push('"');
730
+ out.push('\"');
713
731
  }
714
732
  }
715
733
  out.push('>');
716
- out.push_str(&inner);
717
- out.push_str("</");
718
- out.push_str(&rule.tag);
719
- out.push('>');
720
734
  out
721
735
  }
722
736
 
@@ -790,7 +804,7 @@ fn hex_to_rgb(value: &str) -> Option<String> {
790
804
 
791
805
  /// `<a>` with Tiptap's attribute order (target, rel, class, href, title),
792
806
  /// skipping any that are absent or null.
793
- fn wrap_link(inner: String, link: &std::collections::HashMap<String, Any>) -> String {
807
+ fn link_open(link: &std::collections::HashMap<String, Any>) -> String {
794
808
  let mut out = String::from("<a");
795
809
  for key in ["target", "rel", "class", "href", "title"] {
796
810
  if let Some(Any::String(v)) = link.get(key) {
@@ -802,8 +816,6 @@ fn wrap_link(inner: String, link: &std::collections::HashMap<String, Any>) -> St
802
816
  }
803
817
  }
804
818
  out.push('>');
805
- out.push_str(&inner);
806
- out.push_str("</a>");
807
819
  out
808
820
  }
809
821
 
@@ -822,10 +834,6 @@ fn render_image<T: ReadTxn>(txn: &T, e: &XmlElementRef, em: &mut Emitter) {
822
834
  em.push('>');
823
835
  }
824
836
 
825
- fn wrap(inner: String, tag: &str) -> String {
826
- format!("<{tag}>{inner}</{tag}>")
827
- }
828
-
829
837
  /// A mark is present and not claimed by a registered rule for that stored
830
838
  /// name (a rule overrides the built-in wrap).
831
839
  fn has(marks: &Attrs, keys: &[&str], rules: &Rules) -> bool {
@@ -903,10 +911,44 @@ mod tests {
903
911
  a
904
912
  }
905
913
 
914
+ /// Single-run rendering for the mark unit tests: the run's full
915
+ /// open/close stack around the escaped text.
916
+ fn render_run(text: &str, marks: Option<&Attrs>, rules: &Rules) -> String {
917
+ let stack = run_stack(marks, rules);
918
+ let mut html: String = stack.iter().map(|(open, _)| open.as_str()).collect();
919
+ html.push_str(&escape_text(text));
920
+ for (_, close) in stack.iter().rev() {
921
+ html.push_str(close);
922
+ }
923
+ html
924
+ }
925
+
906
926
  fn run(text: &str, marks: Option<&Attrs>) -> String {
907
927
  render_run(text, marks, &Rules::empty())
908
928
  }
909
929
 
930
+ #[test]
931
+ fn adjacent_runs_share_their_common_outer_marks() {
932
+ use yrs::{Text, XmlFragment};
933
+ let doc = Doc::new();
934
+ let f = doc.get_or_insert_xml_fragment("default");
935
+ let mut txn = doc.transact_mut();
936
+ let p = f.insert(&mut txn, 0, XmlElementPrelim::empty("paragraph"));
937
+ let t = p.insert(&mut txn, 0, XmlTextPrelim::new(""));
938
+ let bold = marks(&["bold"]);
939
+ let bold_em = marks(&["bold", "italic"]);
940
+ t.insert_with_attributes(&mut txn, 0, "nested ", bold.clone());
941
+ t.insert_with_attributes(&mut txn, 7, "deep", bold_em);
942
+ t.insert_with_attributes(&mut txn, 11, " tail", bold);
943
+ drop(txn);
944
+ let txn = doc.transact();
945
+ assert_eq!(
946
+ render(&txn, &f).unwrap(),
947
+ "<p><strong>nested <em>deep</em> tail</strong></p>",
948
+ "shared outer marks stay open across adjacent runs"
949
+ );
950
+ }
951
+
910
952
  /// Core rendering of the captured Tiptap document, pinned as a golden
911
953
  /// (`.core.html`). The task list renders through the unknown-container
912
954
  /// fallback here (unwrapped children); the external truth — byte parity
@@ -1236,7 +1278,7 @@ mod tests {
1236
1278
  let txn = doc.transact();
1237
1279
  let segs = render_segments(&txn, &frag, &rules).unwrap();
1238
1280
  assert_eq!(
1239
- yrs_html_core::flatten(segs).into_html().unwrap(),
1281
+ yjs_html_core::flatten(segs).into_html().unwrap(),
1240
1282
  "<aside class=\"callout callout--warning\"><div class=\"para\">careful</div></aside>"
1241
1283
  );
1242
1284
  }
@@ -1294,7 +1336,7 @@ mod tests {
1294
1336
  }
1295
1337
  let txn = doc.transact();
1296
1338
  let map = collect_node_types(&txn, &frag).unwrap();
1297
- let json = yrs_html_core::type_map_json(&map, |ty| {
1339
+ let json = yjs_html_core::type_map_json(&map, |ty| {
1298
1340
  if is_builtin(ty) {
1299
1341
  Some("builtin")
1300
1342
  } else {
data/ext/yrby/src/lib.rs CHANGED
@@ -9,14 +9,14 @@ use yrs::{Doc, GetString, ReadTxn, Transact};
9
9
 
10
10
  mod protocol;
11
11
  mod read;
12
+ use lexical_yjs_html as lexical_html;
13
+ use prosemirror_yjs_html as prosemirror_html;
12
14
  use protocol::{
13
15
  classify_message, has_pending, integrated_update, merged_doc_update, update_advances_doc,
14
16
  update_is_ready,
15
17
  };
16
18
  use render_rules::{Rules, Segment};
17
- pub(crate) use yrs_html_core as render_rules;
18
- use yrs_lexical_html as lexical_html;
19
- use yrs_prosemirror_html as prosemirror_html;
19
+ pub(crate) use yjs_html_core as render_rules;
20
20
 
21
21
  /// Wrapper around yrs Doc.
22
22
  ///
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.7.0"
4
+ VERSION = "0.7.1"
5
5
  end
data/lib/y.rb CHANGED
@@ -20,5 +20,5 @@ require_relative "y/decoder"
20
20
  module Y
21
21
  # Doc, Error, and the protocol module functions are defined in the Rust
22
22
  # extension. The ActionCable integration (Y::ActionCable::Sync) lives in the
23
- # separate `yrby-actioncable` gem; require "y/action_cable".
23
+ # `yrby-rails` gem; require "y/action_cable".
24
24
  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.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - JP Camara