yrby 0.7.0 → 0.8.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 +71 -0
- data/Cargo.lock +29 -29
- data/README.md +582 -380
- data/ext/yrby/Cargo.toml +3 -3
- data/ext/yrby/crates/html-core/Cargo.toml +5 -4
- data/ext/yrby/crates/html-core/src/lib.rs +16 -20
- data/ext/yrby/crates/lexical-html/Cargo.toml +5 -4
- data/ext/yrby/crates/lexical-html/src/lib.rs +57 -48
- data/ext/yrby/crates/prosemirror-html/Cargo.toml +5 -4
- data/ext/yrby/crates/prosemirror-html/src/lib.rs +151 -104
- data/ext/yrby/src/lib.rs +30 -15
- data/ext/yrby/src/protocol.rs +17 -17
- data/ext/yrby/src/read.rs +137 -26
- data/lib/generators/yrby/install/install_generator.rb +24 -13
- data/lib/generators/yrby/install/templates/document_channel.rb +4 -7
- data/lib/generators/yrby/tables/tables_generator.rb +7 -4
- data/lib/generators/yrby/tables/templates/create_y_tables.rb +1 -1
- data/lib/y/collaborative/attribute.rb +53 -0
- data/lib/y/collaborative/helper.rb +48 -0
- data/lib/y/collaborative.rb +98 -0
- data/lib/y/lexxy.rb +2 -2
- data/lib/y/rendering.rb +15 -15
- data/lib/y/tiptap.rb +3 -3
- data/lib/y/version.rb +1 -1
- data/lib/y.rb +1 -1
- metadata +4 -9
- data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/common.rs +0 -355
- data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/dynamic.rs +0 -276
- data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/macros.rs +0 -49
- data/ext/yrby/target/debug/build/rb-sys-4407948463231c4f/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +0 -8934
- data/ext/yrby/target/debug/build/rb-sys-dbeea42737529c2d/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +0 -8934
- data/ext/yrby/target/debug/build/serde-58ea0ee887cc2602/out/private.rs +0 -6
- data/ext/yrby/target/debug/build/serde_core-41f407c21c1f205e/out/private.rs +0 -5
- data/ext/yrby/target/debug/build/thiserror-0f1416a82ff26f22/out/private.rs +0 -5
|
@@ -1,40 +1,45 @@
|
|
|
1
|
-
//!
|
|
2
|
-
//! structure
|
|
1
|
+
//! HTML for ProseMirror and Tiptap documents, rendered from the Yjs
|
|
2
|
+
//! structure the editor syncs. No Node process and no headless editor.
|
|
3
3
|
//!
|
|
4
|
-
//! The y-prosemirror binding stores a document in a Y.XmlFragment
|
|
5
|
-
//! are Y.XmlElement
|
|
6
|
-
//! attrs
|
|
7
|
-
//! marks. Node and mark names come from the editor's
|
|
8
|
-
//! both spellings
|
|
9
|
-
//! prosemirror-schema-basic snake_case (`bullet_list`,
|
|
4
|
+
//! The y-prosemirror binding stores a document in a `Y.XmlFragment`. Block
|
|
5
|
+
//! nodes are `Y.XmlElement`s: the tag is the node type and the attributes
|
|
6
|
+
//! are the node attrs. Text is `Y.XmlText`, and each run's formatting
|
|
7
|
+
//! attributes are its marks. Node and mark names come from the editor's
|
|
8
|
+
//! schema, so both spellings are accepted: Tiptap's camelCase (`bulletList`,
|
|
9
|
+
//! `bold`) and prosemirror-schema-basic's snake_case (`bullet_list`,
|
|
10
|
+
//! `strong`).
|
|
10
11
|
//!
|
|
11
|
-
//! This renders
|
|
12
|
-
//!
|
|
13
|
-
//! code blocks, bullet
|
|
14
|
-
//! and tables
|
|
15
|
-
//! `min-width` styling editor
|
|
16
|
-
//! lists, mentions, the details family
|
|
17
|
-
//! `Y::Tiptap` renderer's rule set, built on the same
|
|
18
|
-
//!
|
|
19
|
-
//! `ueberdosis/tiptap-php
|
|
20
|
-
//! own `getHTML()` byte for byte on the captured fixtures
|
|
21
|
-
//! is held at the Ruby layer
|
|
22
|
-
//!
|
|
12
|
+
//! This renders core ProseMirror: the prosemirror-schema-basic node set plus
|
|
13
|
+
//! the prosemirror-tables family. That is paragraphs, headings, blockquotes,
|
|
14
|
+
//! code blocks, bullet and ordered lists, images, hard breaks, horizontal
|
|
15
|
+
//! rules, and tables as semantic `<table><tbody>…`, without the
|
|
16
|
+
//! `<colgroup>` and `min-width` styling an editor view injects. Tiptap's
|
|
17
|
+
//! extension nodes (task lists, mentions, the details family) live in the
|
|
18
|
+
//! Ruby layer as the `Y::Tiptap` renderer's rule set, built on the same
|
|
19
|
+
//! extension API applications use. `Y::ProseMirror` is the core base class.
|
|
20
|
+
//! The output follows `ueberdosis/tiptap-php`. With `Y::Tiptap`'s rules it
|
|
21
|
+
//! matches Tiptap's own `getHTML()` byte for byte on the captured fixtures.
|
|
22
|
+
//! That guarantee is held at the Ruby layer. Where a fixture contains
|
|
23
|
+
//! Tiptap-only nodes, the tests here pin core output as goldens.
|
|
23
24
|
//!
|
|
24
|
-
//! Marks stay native on purpose
|
|
25
|
-
//! (nesting order, textStyle's CSS, code's exclusivity)
|
|
26
|
-
//!
|
|
27
|
-
//! Tiptap's
|
|
28
|
-
//! strike, underline, highlight, then subscript
|
|
29
|
-
//! the other formatting marks, so a code run is `<code>` alone
|
|
25
|
+
//! Marks stay native on purpose. Mark serialization is text-run machinery
|
|
26
|
+
//! (nesting order, textStyle's CSS, code's exclusivity) that the rule system
|
|
27
|
+
//! cannot express. The built-in set covers schema-basic's marks and
|
|
28
|
+
//! Tiptap's, nested outermost-first: link, a textStyle span, bold, italic,
|
|
29
|
+
//! strike, underline, highlight, then subscript and superscript. `code`
|
|
30
|
+
//! excludes the other formatting marks, so a code run is `<code>` alone. A
|
|
30
31
|
//! link still wraps it (see `render_run`).
|
|
31
32
|
//!
|
|
32
|
-
//!
|
|
33
|
-
//!
|
|
34
|
-
//!
|
|
35
|
-
//!
|
|
36
|
-
//!
|
|
37
|
-
//!
|
|
33
|
+
//! A node type with no rule still renders its text and child blocks, just
|
|
34
|
+
//! unwrapped.
|
|
35
|
+
//!
|
|
36
|
+
//! Applications register custom nodes and marks as rules keyed by node type
|
|
37
|
+
//! and mark name (see `render_rules`). A node rule is consulted before the
|
|
38
|
+
//! built-in arms, so it can extend the schema or replace a built-in. A mark
|
|
39
|
+
//! rule claims its mark from the built-in wraps and wraps outside
|
|
40
|
+
//! everything, link included. Declarative rules render here. Callback rules
|
|
41
|
+
//! emit `Segment::Deferred` for the caller to fill in after the render
|
|
42
|
+
//! returns.
|
|
38
43
|
|
|
39
44
|
// README examples are living code: compile-checked on every cargo test.
|
|
40
45
|
#[cfg(doctest)]
|
|
@@ -42,10 +47,10 @@
|
|
|
42
47
|
mod readme_examples {}
|
|
43
48
|
|
|
44
49
|
// The full rules surface, re-exported: depend on this crate alone;
|
|
45
|
-
//
|
|
46
|
-
pub use
|
|
47
|
-
use yrs::types::text::YChange;
|
|
50
|
+
// yjs-html-core is an internal implementation crate.
|
|
51
|
+
pub use yjs_html_core::*;
|
|
48
52
|
use yrs::types::Attrs;
|
|
53
|
+
use yrs::types::text::YChange;
|
|
49
54
|
use yrs::{
|
|
50
55
|
Any, GetString, Out, ReadTxn, Text, Xml, XmlElementRef, XmlFragment, XmlFragmentRef, XmlOut,
|
|
51
56
|
XmlTextRef,
|
|
@@ -105,7 +110,7 @@ pub fn render_segments<T: ReadTxn>(
|
|
|
105
110
|
/// callback rules, segments always flatten.
|
|
106
111
|
pub fn render<T: ReadTxn>(txn: &T, fragment: &XmlFragmentRef) -> Option<String> {
|
|
107
112
|
render_segments(txn, fragment, &Rules::empty()).map(|segs| {
|
|
108
|
-
|
|
113
|
+
yjs_html_core::flatten(segs)
|
|
109
114
|
.into_html()
|
|
110
115
|
.expect("no callback rules registered")
|
|
111
116
|
})
|
|
@@ -605,102 +610,120 @@ fn render_rule_inline<T: ReadTxn>(
|
|
|
605
610
|
em.push('>');
|
|
606
611
|
}
|
|
607
612
|
|
|
608
|
-
/// Emit
|
|
613
|
+
/// Emit the formatted runs of a Y.XmlText, keeping marks shared by
|
|
614
|
+
/// adjacent runs open across them: `bold` then `bold italic` renders as
|
|
615
|
+
/// `<strong>a<em>b</em></strong>`, the way the editor's DOM serializer
|
|
616
|
+
/// keeps an active mark open into the next run, not as two sibling
|
|
617
|
+
/// `<strong>` wraps. Sharing is by identical opening tag, so a mark whose
|
|
618
|
+
/// attributes differ between runs (a different href, a different style)
|
|
619
|
+
/// never merges.
|
|
609
620
|
fn render_text_runs<T: ReadTxn>(txn: &T, t: &XmlTextRef, em: &mut Emitter, rules: &Rules) {
|
|
621
|
+
let mut open: Vec<(String, String)> = Vec::new();
|
|
610
622
|
for d in t.diff(txn, YChange::identity) {
|
|
611
623
|
if let Out::Any(Any::String(s)) = &d.insert {
|
|
612
|
-
|
|
624
|
+
let stack = run_stack(d.attributes.as_deref(), rules);
|
|
625
|
+
let shared = open
|
|
626
|
+
.iter()
|
|
627
|
+
.zip(stack.iter())
|
|
628
|
+
.take_while(|(a, b)| a == b)
|
|
629
|
+
.count();
|
|
630
|
+
for (_, close) in open.split_off(shared).into_iter().rev() {
|
|
631
|
+
em.push_str(&close);
|
|
632
|
+
}
|
|
633
|
+
for (open_tag, _) in &stack[shared..] {
|
|
634
|
+
em.push_str(open_tag);
|
|
635
|
+
}
|
|
636
|
+
open = stack;
|
|
637
|
+
em.push_str(&escape_text(s));
|
|
613
638
|
}
|
|
614
639
|
}
|
|
640
|
+
for (_, close) in open.into_iter().rev() {
|
|
641
|
+
em.push_str(&close);
|
|
642
|
+
}
|
|
615
643
|
}
|
|
616
644
|
|
|
617
|
-
///
|
|
618
|
-
///
|
|
619
|
-
///
|
|
620
|
-
///
|
|
621
|
-
///
|
|
622
|
-
|
|
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);
|
|
645
|
+
/// The run's mark wrappers as (open tag, close tag) pairs, outermost
|
|
646
|
+
/// first: registered custom marks (reverse-alphabetical, so the last name
|
|
647
|
+
/// alphabetically is outermost), link, the textStyle span, then bold,
|
|
648
|
+
/// italic, strike, underline, highlight, and innermost
|
|
649
|
+
/// subscript/superscript — or `code` alone in their place.
|
|
650
|
+
fn run_stack(marks: Option<&Attrs>, rules: &Rules) -> Vec<(String, String)> {
|
|
632
651
|
let Some(marks) = marks else {
|
|
633
|
-
return
|
|
652
|
+
return Vec::new();
|
|
634
653
|
};
|
|
654
|
+
// Innermost first, reversed at the end.
|
|
655
|
+
let mut stack: Vec<(String, String)> = Vec::new();
|
|
656
|
+
let tag = |t: &str| (format!("<{t}>"), format!("</{t}>"));
|
|
635
657
|
if has(marks, &["code"], rules) {
|
|
636
|
-
|
|
658
|
+
stack.push(tag("code"));
|
|
637
659
|
} else if !marks.contains_key("code") {
|
|
638
660
|
// (A claimed `code` lands here too — on the run, but skipped by the
|
|
639
|
-
// else-if: the rule replaces the wrap, via
|
|
661
|
+
// else-if: the rule replaces the wrap, via the custom marks below,
|
|
640
662
|
// and code's exclusivity over the other formatting marks holds.)
|
|
641
663
|
if has(marks, &["subscript", "sub"], rules) {
|
|
642
|
-
|
|
664
|
+
stack.push(tag("sub"));
|
|
643
665
|
} else if has(marks, &["superscript", "sup"], rules) {
|
|
644
|
-
|
|
666
|
+
stack.push(tag("sup"));
|
|
645
667
|
}
|
|
646
668
|
if has(marks, &["highlight"], rules) {
|
|
647
|
-
|
|
669
|
+
stack.push(tag("mark"));
|
|
648
670
|
}
|
|
649
671
|
if has(marks, &["underline", "u"], rules) {
|
|
650
|
-
|
|
672
|
+
stack.push(tag("u"));
|
|
651
673
|
}
|
|
652
674
|
if has(marks, &["strike", "s"], rules) {
|
|
653
|
-
|
|
675
|
+
stack.push(tag("s"));
|
|
654
676
|
}
|
|
655
677
|
if has(marks, &["italic", "em"], rules) {
|
|
656
|
-
|
|
678
|
+
stack.push(tag("em"));
|
|
657
679
|
}
|
|
658
680
|
if has(marks, &["bold", "strong"], rules) {
|
|
659
|
-
|
|
681
|
+
stack.push(tag("strong"));
|
|
660
682
|
}
|
|
661
683
|
if !rules.marks.contains_key("textStyle") {
|
|
662
684
|
if let Some(Any::Map(style)) = marks.get("textStyle") {
|
|
663
685
|
let css = text_style_css(style);
|
|
664
686
|
if !css.is_empty() {
|
|
665
|
-
|
|
687
|
+
stack.push((
|
|
688
|
+
format!("<span style=\"{}\">", escape_attr(&css)),
|
|
689
|
+
"</span>".to_string(),
|
|
690
|
+
));
|
|
666
691
|
}
|
|
667
692
|
}
|
|
668
693
|
}
|
|
669
694
|
}
|
|
670
695
|
if !rules.marks.contains_key("link") {
|
|
671
696
|
if let Some(Any::Map(link)) = marks.get("link") {
|
|
672
|
-
|
|
697
|
+
stack.push((link_open(link), "</a>".to_string()));
|
|
673
698
|
}
|
|
674
699
|
}
|
|
675
700
|
if !rules.marks.is_empty() {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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));
|
|
701
|
+
let code = marks.contains_key("code");
|
|
702
|
+
let mut names: Vec<&str> = rules
|
|
703
|
+
.marks
|
|
704
|
+
.keys()
|
|
705
|
+
.map(String::as_str)
|
|
706
|
+
.filter(|name| marks.contains_key(*name))
|
|
707
|
+
.filter(|name| !code || matches!(*name, "code" | "link"))
|
|
708
|
+
.collect();
|
|
709
|
+
names.sort_unstable();
|
|
710
|
+
for name in names {
|
|
711
|
+
let rule = &rules.marks[name];
|
|
712
|
+
stack.push((
|
|
713
|
+
custom_mark_open(rule, marks.get(name)),
|
|
714
|
+
format!("</{}>", rule.tag),
|
|
715
|
+
));
|
|
716
|
+
}
|
|
699
717
|
}
|
|
700
|
-
|
|
718
|
+
stack.reverse();
|
|
719
|
+
stack
|
|
701
720
|
}
|
|
702
721
|
|
|
703
|
-
|
|
722
|
+
/// The opening tag for a registered custom mark. Custom marks sit outside
|
|
723
|
+
/// everything; a code run keeps its exclusivity under overrides too: only
|
|
724
|
+
/// the `code` and `link` claims themselves may wrap it, matching what the
|
|
725
|
+
/// built-in wraps allow.
|
|
726
|
+
fn custom_mark_open(rule: &MarkRule, value: Option<&Any>) -> String {
|
|
704
727
|
let mut out = String::from("<");
|
|
705
728
|
out.push_str(&rule.tag);
|
|
706
729
|
for (attr, parts) in &rule.attrs {
|
|
@@ -709,14 +732,10 @@ fn wrap_custom_mark(inner: String, rule: &MarkRule, value: Option<&Any>) -> Stri
|
|
|
709
732
|
out.push_str(attr);
|
|
710
733
|
out.push_str("=\"");
|
|
711
734
|
out.push_str(&escape_attr(&v));
|
|
712
|
-
out.push('"');
|
|
735
|
+
out.push('\"');
|
|
713
736
|
}
|
|
714
737
|
}
|
|
715
738
|
out.push('>');
|
|
716
|
-
out.push_str(&inner);
|
|
717
|
-
out.push_str("</");
|
|
718
|
-
out.push_str(&rule.tag);
|
|
719
|
-
out.push('>');
|
|
720
739
|
out
|
|
721
740
|
}
|
|
722
741
|
|
|
@@ -790,7 +809,7 @@ fn hex_to_rgb(value: &str) -> Option<String> {
|
|
|
790
809
|
|
|
791
810
|
/// `<a>` with Tiptap's attribute order (target, rel, class, href, title),
|
|
792
811
|
/// skipping any that are absent or null.
|
|
793
|
-
fn
|
|
812
|
+
fn link_open(link: &std::collections::HashMap<String, Any>) -> String {
|
|
794
813
|
let mut out = String::from("<a");
|
|
795
814
|
for key in ["target", "rel", "class", "href", "title"] {
|
|
796
815
|
if let Some(Any::String(v)) = link.get(key) {
|
|
@@ -802,8 +821,6 @@ fn wrap_link(inner: String, link: &std::collections::HashMap<String, Any>) -> St
|
|
|
802
821
|
}
|
|
803
822
|
}
|
|
804
823
|
out.push('>');
|
|
805
|
-
out.push_str(&inner);
|
|
806
|
-
out.push_str("</a>");
|
|
807
824
|
out
|
|
808
825
|
}
|
|
809
826
|
|
|
@@ -822,10 +839,6 @@ fn render_image<T: ReadTxn>(txn: &T, e: &XmlElementRef, em: &mut Emitter) {
|
|
|
822
839
|
em.push('>');
|
|
823
840
|
}
|
|
824
841
|
|
|
825
|
-
fn wrap(inner: String, tag: &str) -> String {
|
|
826
|
-
format!("<{tag}>{inner}</{tag}>")
|
|
827
|
-
}
|
|
828
|
-
|
|
829
842
|
/// A mark is present and not claimed by a registered rule for that stored
|
|
830
843
|
/// name (a rule overrides the built-in wrap).
|
|
831
844
|
fn has(marks: &Attrs, keys: &[&str], rules: &Rules) -> bool {
|
|
@@ -903,10 +916,44 @@ mod tests {
|
|
|
903
916
|
a
|
|
904
917
|
}
|
|
905
918
|
|
|
919
|
+
/// Single-run rendering for the mark unit tests: the run's full
|
|
920
|
+
/// open/close stack around the escaped text.
|
|
921
|
+
fn render_run(text: &str, marks: Option<&Attrs>, rules: &Rules) -> String {
|
|
922
|
+
let stack = run_stack(marks, rules);
|
|
923
|
+
let mut html: String = stack.iter().map(|(open, _)| open.as_str()).collect();
|
|
924
|
+
html.push_str(&escape_text(text));
|
|
925
|
+
for (_, close) in stack.iter().rev() {
|
|
926
|
+
html.push_str(close);
|
|
927
|
+
}
|
|
928
|
+
html
|
|
929
|
+
}
|
|
930
|
+
|
|
906
931
|
fn run(text: &str, marks: Option<&Attrs>) -> String {
|
|
907
932
|
render_run(text, marks, &Rules::empty())
|
|
908
933
|
}
|
|
909
934
|
|
|
935
|
+
#[test]
|
|
936
|
+
fn adjacent_runs_share_their_common_outer_marks() {
|
|
937
|
+
use yrs::{Text, XmlFragment};
|
|
938
|
+
let doc = Doc::new();
|
|
939
|
+
let f = doc.get_or_insert_xml_fragment("default");
|
|
940
|
+
let mut txn = doc.transact_mut();
|
|
941
|
+
let p = f.insert(&mut txn, 0, XmlElementPrelim::empty("paragraph"));
|
|
942
|
+
let t = p.insert(&mut txn, 0, XmlTextPrelim::new(""));
|
|
943
|
+
let bold = marks(&["bold"]);
|
|
944
|
+
let bold_em = marks(&["bold", "italic"]);
|
|
945
|
+
t.insert_with_attributes(&mut txn, 0, "nested ", bold.clone());
|
|
946
|
+
t.insert_with_attributes(&mut txn, 7, "deep", bold_em);
|
|
947
|
+
t.insert_with_attributes(&mut txn, 11, " tail", bold);
|
|
948
|
+
drop(txn);
|
|
949
|
+
let txn = doc.transact();
|
|
950
|
+
assert_eq!(
|
|
951
|
+
render(&txn, &f).unwrap(),
|
|
952
|
+
"<p><strong>nested <em>deep</em> tail</strong></p>",
|
|
953
|
+
"shared outer marks stay open across adjacent runs"
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
|
|
910
957
|
/// Core rendering of the captured Tiptap document, pinned as a golden
|
|
911
958
|
/// (`.core.html`). The task list renders through the unknown-container
|
|
912
959
|
/// fallback here (unwrapped children); the external truth — byte parity
|
|
@@ -1236,7 +1283,7 @@ mod tests {
|
|
|
1236
1283
|
let txn = doc.transact();
|
|
1237
1284
|
let segs = render_segments(&txn, &frag, &rules).unwrap();
|
|
1238
1285
|
assert_eq!(
|
|
1239
|
-
|
|
1286
|
+
yjs_html_core::flatten(segs).into_html().unwrap(),
|
|
1240
1287
|
"<aside class=\"callout callout--warning\"><div class=\"para\">careful</div></aside>"
|
|
1241
1288
|
);
|
|
1242
1289
|
}
|
|
@@ -1294,7 +1341,7 @@ mod tests {
|
|
|
1294
1341
|
}
|
|
1295
1342
|
let txn = doc.transact();
|
|
1296
1343
|
let map = collect_node_types(&txn, &frag).unwrap();
|
|
1297
|
-
let json =
|
|
1344
|
+
let json = yjs_html_core::type_map_json(&map, |ty| {
|
|
1298
1345
|
if is_builtin(ty) {
|
|
1299
1346
|
Some("builtin")
|
|
1300
1347
|
} 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
|
|
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
|
///
|
|
@@ -168,7 +168,7 @@ impl RbDoc {
|
|
|
168
168
|
let doc = &self.0;
|
|
169
169
|
nogvl(move || {
|
|
170
170
|
// Exactly ONE transaction per call. Opening a second while the
|
|
171
|
-
// first is still held deadlocks against a waiting writer
|
|
171
|
+
// first is still held deadlocks against a waiting writer, and
|
|
172
172
|
// inside nogvl that hang can't be interrupted.
|
|
173
173
|
let txn = doc.transact();
|
|
174
174
|
txn.get_text(name.as_str()).map(|t| t.get_string(&txn))
|
|
@@ -200,8 +200,22 @@ impl RbDoc {
|
|
|
200
200
|
})
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/// A `Y.Array` root serialized to a JSON array string (values recursive, in
|
|
204
|
+
/// array order). The counterpart to read_map for documents whose root is an
|
|
205
|
+
/// array: a board's cards, a sheet's rows. Callers parse the JSON (e.g.
|
|
206
|
+
/// `JSON.parse(doc.read_array("cards"))`). The serialization lives in
|
|
207
|
+
/// `read::array_json` (pure, Rust-tested).
|
|
208
|
+
fn read_array(&self, name: String) -> Option<String> {
|
|
209
|
+
let doc = &self.0;
|
|
210
|
+
nogvl(move || {
|
|
211
|
+
let txn = doc.transact();
|
|
212
|
+
let array = txn.get_array(name.as_str())?;
|
|
213
|
+
Some(read::array_json(&txn, &array))
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
203
217
|
/// True if the doc holds un-integrable pending structs or a pending delete
|
|
204
|
-
/// set
|
|
218
|
+
/// set: content that couldn't integrate because a causally-prior update is
|
|
205
219
|
/// missing. Such content is a recovery buffer, not document state; it heals if
|
|
206
220
|
/// the missing dependency later arrives. A pure read.
|
|
207
221
|
fn pending(&self) -> bool {
|
|
@@ -211,7 +225,7 @@ impl RbDoc {
|
|
|
211
225
|
|
|
212
226
|
/// Like `encode_state_as_update` (full state), but **gap-free**: it excludes
|
|
213
227
|
/// any pending (un-integrable) structs and pending delete set. Use this when
|
|
214
|
-
/// persisting or serving state that other peers will apply
|
|
228
|
+
/// persisting or serving state that other peers will apply. Serving pending
|
|
215
229
|
/// content poisons their sync. Non-destructive: this doc keeps its pending, so
|
|
216
230
|
/// a genuine gap still heals if its dependency arrives. (`encode_state_as_update`
|
|
217
231
|
/// stays lossless for raw-update recovery.)
|
|
@@ -337,12 +351,12 @@ impl RbDoc {
|
|
|
337
351
|
}
|
|
338
352
|
|
|
339
353
|
// ============================================================================
|
|
340
|
-
// Y::Lexical
|
|
354
|
+
// Y::Lexical: schema-pinned rendering of Lexical/Lexxy documents
|
|
341
355
|
// ============================================================================
|
|
342
356
|
|
|
343
357
|
/// A Lexical view over a `Y::Doc`. The schema knowledge lives here rather
|
|
344
358
|
/// than on the schema-agnostic `Doc`: core Lexical natively, everything else
|
|
345
|
-
/// through the render rules compiled at construction (see `render_rules
|
|
359
|
+
/// through the render rules compiled at construction (see `render_rules`;
|
|
346
360
|
/// the `Y::Lexxy` facade's rule set arrives that way). Holds a cheap clone of
|
|
347
361
|
/// the doc (yrs `Doc` is an Arc handle), so it reads live state.
|
|
348
362
|
///
|
|
@@ -357,7 +371,7 @@ struct RbLexical {
|
|
|
357
371
|
}
|
|
358
372
|
|
|
359
373
|
impl RbLexical {
|
|
360
|
-
/// `Y::NativeLexical.new(doc, rules_json)
|
|
374
|
+
/// `Y::NativeLexical.new(doc, rules_json)`; the Y::Lexical facade
|
|
361
375
|
/// compiles its `nodes:` config to the rules JSON.
|
|
362
376
|
fn native_new(doc: &RbDoc, rules_json: String) -> Result<Self, Error> {
|
|
363
377
|
Ok(RbLexical {
|
|
@@ -367,7 +381,7 @@ impl RbLexical {
|
|
|
367
381
|
}
|
|
368
382
|
|
|
369
383
|
/// Render the document's XML root (default `"root"`, Lexical's standard
|
|
370
|
-
/// collab root name) natively
|
|
384
|
+
/// collab root name) natively, with no Node process or headless editor. The
|
|
371
385
|
/// native side renders core Lexical plus whatever the rules cover; with
|
|
372
386
|
/// the rule set `Y::Lexxy` passes, output matches Lexxy's own serializer
|
|
373
387
|
/// byte-for-byte on the reference fixtures (see `lexical_html.rs`). Returns nil when the root is missing or not
|
|
@@ -386,7 +400,7 @@ impl RbLexical {
|
|
|
386
400
|
segments_result(segments)
|
|
387
401
|
}
|
|
388
402
|
|
|
389
|
-
/// The document's node types as observed facts, JSON-encoded
|
|
403
|
+
/// The document's node types as observed facts, JSON-encoded, the
|
|
390
404
|
/// native half of the facade's `node_types` discovery aid. Nil when the
|
|
391
405
|
/// root is missing or not Lexical-shaped.
|
|
392
406
|
fn node_types(&self, args: &[Value]) -> Result<Value, Error> {
|
|
@@ -415,13 +429,13 @@ impl RbLexical {
|
|
|
415
429
|
}
|
|
416
430
|
|
|
417
431
|
// ============================================================================
|
|
418
|
-
// Y::ProseMirror
|
|
432
|
+
// Y::ProseMirror: schema-pinned rendering of ProseMirror/Tiptap documents
|
|
419
433
|
// ============================================================================
|
|
420
434
|
|
|
421
435
|
/// A ProseMirror view over a `Y::Doc`. The schema knowledge lives here rather
|
|
422
436
|
/// than on the schema-agnostic `Doc`: core ProseMirror natively, everything
|
|
423
437
|
/// else through the render rules compiled at construction (see
|
|
424
|
-
/// `render_rules
|
|
438
|
+
/// `render_rules`; the `Y::Tiptap` facade's rule set arrives that way).
|
|
425
439
|
/// Holds a cheap clone of the doc (yrs `Doc` is an Arc handle), so it reads
|
|
426
440
|
/// live state.
|
|
427
441
|
///
|
|
@@ -436,7 +450,7 @@ struct RbProseMirror {
|
|
|
436
450
|
}
|
|
437
451
|
|
|
438
452
|
impl RbProseMirror {
|
|
439
|
-
/// `Y::NativeProseMirror.new(doc, rules_json)
|
|
453
|
+
/// `Y::NativeProseMirror.new(doc, rules_json)`; the Y::ProseMirror facade
|
|
440
454
|
/// compiles its `nodes:`/`marks:` config to the rules JSON.
|
|
441
455
|
fn native_new(doc: &RbDoc, rules_json: String) -> Result<Self, Error> {
|
|
442
456
|
Ok(RbProseMirror {
|
|
@@ -466,7 +480,7 @@ impl RbProseMirror {
|
|
|
466
480
|
segments_result(segments)
|
|
467
481
|
}
|
|
468
482
|
|
|
469
|
-
/// The document's node types as observed facts, JSON-encoded
|
|
483
|
+
/// The document's node types as observed facts, JSON-encoded, the
|
|
470
484
|
/// native half of the facade's `node_types` discovery aid. Nil when the
|
|
471
485
|
/// root is missing or not ProseMirror-shaped.
|
|
472
486
|
fn node_types(&self, args: &[Value]) -> Result<Value, Error> {
|
|
@@ -627,6 +641,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
627
641
|
doc_class.define_method("read_text", method!(RbDoc::read_text, 1))?;
|
|
628
642
|
doc_class.define_method("read_xml", method!(RbDoc::read_xml, 1))?;
|
|
629
643
|
doc_class.define_method("read_map", method!(RbDoc::read_map, 1))?;
|
|
644
|
+
doc_class.define_method("read_array", method!(RbDoc::read_array, 1))?;
|
|
630
645
|
doc_class.define_method("pending?", method!(RbDoc::pending, 0))?;
|
|
631
646
|
doc_class.define_method(
|
|
632
647
|
"compacted_state_update",
|