yrby 0.7.1 → 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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -0
  3. data/Cargo.lock +3 -3
  4. data/README.md +582 -380
  5. data/ext/yrby/crates/html-core/Cargo.toml +1 -1
  6. data/ext/yrby/crates/html-core/src/lib.rs +13 -13
  7. data/ext/yrby/crates/lexical-html/Cargo.toml +2 -2
  8. data/ext/yrby/crates/lexical-html/src/lib.rs +52 -39
  9. data/ext/yrby/crates/prosemirror-html/Cargo.toml +2 -2
  10. data/ext/yrby/crates/prosemirror-html/src/lib.rs +37 -32
  11. data/ext/yrby/src/lib.rs +27 -12
  12. data/ext/yrby/src/protocol.rs +17 -17
  13. data/ext/yrby/src/read.rs +137 -26
  14. data/lib/generators/yrby/install/install_generator.rb +24 -13
  15. data/lib/generators/yrby/install/templates/document_channel.rb +4 -7
  16. data/lib/generators/yrby/tables/tables_generator.rb +7 -4
  17. data/lib/generators/yrby/tables/templates/create_y_tables.rb +1 -1
  18. data/lib/y/collaborative/attribute.rb +53 -0
  19. data/lib/y/collaborative/helper.rb +48 -0
  20. data/lib/y/collaborative.rb +98 -0
  21. data/lib/y/lexxy.rb +2 -2
  22. data/lib/y/rendering.rb +15 -15
  23. data/lib/y/tiptap.rb +3 -3
  24. data/lib/y/version.rb +1 -1
  25. metadata +4 -9
  26. data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/common.rs +0 -355
  27. data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/dynamic.rs +0 -276
  28. data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/macros.rs +0 -49
  29. data/ext/yrby/target/debug/build/rb-sys-4407948463231c4f/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +0 -8934
  30. data/ext/yrby/target/debug/build/rb-sys-dbeea42737529c2d/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +0 -8934
  31. data/ext/yrby/target/debug/build/serde-58ea0ee887cc2602/out/private.rs +0 -6
  32. data/ext/yrby/target/debug/build/serde_core-41f407c21c1f205e/out/private.rs +0 -5
  33. data/ext/yrby/target/debug/build/thiserror-0f1416a82ff26f22/out/private.rs +0 -5
@@ -3,7 +3,7 @@ name = "yjs-html-core"
3
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.1"
6
+ version = "0.1.3"
7
7
  edition = "2024"
8
8
  rust-version = "1.85"
9
9
  authors = ["JP Camara <johnpcamara@gmail.com>"]
@@ -1,21 +1,21 @@
1
- //! Custom render rules and segmented output — the extensibility core shared
2
- //! by both HTML renderers.
1
+ //! The custom render rules and segmented output that both HTML renderers
2
+ //! share.
3
3
  //!
4
- //! Callers register per-node rules. Two tiers:
4
+ //! Callers register a rule per node. There are two tiers:
5
5
  //!
6
- //! - **Declarative rules** (tag, attributes, text, content slot) compile to
7
- //! [`NodeRule`]/[`MarkRule`] here and render natively, inside the document
6
+ //! - A declarative rule (tag, attributes, text, content slot) compiles to a
7
+ //! [`NodeRule`] or [`MarkRule`] and renders natively, inside the document
8
8
  //! transaction, at full speed. This covers the tiptap-php `renderHTML`
9
9
  //! shape: markup as data.
10
- //! - **Callback rules** defer to the caller. Rendering never runs app code
11
- //! while the document is locked — the renderer emits [`Segment::Deferred`]
12
- //! entries carrying the node's type, attributes (as JSON), and its
13
- //! already-rendered children, and the caller fills them in after the
14
- //! render returns. (In the Ruby gem, that caller is the app's block, run
15
- //! once the transaction has closed and the GVL is held again.)
10
+ //! - A callback rule defers to the caller. The renderer never runs
11
+ //! application code while the document is locked. It emits
12
+ //! [`Segment::Deferred`] entries with the node type, the attributes as
13
+ //! JSON, and the already-rendered children, and the caller fills them in
14
+ //! after the render returns. In the Ruby gem that caller is the app's
15
+ //! block, run once the transaction has closed and the GVL is held again.
16
16
  //!
17
- //! Rules arrive as one JSON document (see `parse`), so the same format
18
- //! serves any binding or caller.
17
+ //! Rules arrive as one JSON document (see `parse`), so one format serves
18
+ //! every binding and caller.
19
19
 
20
20
  use std::collections::{BTreeMap, BTreeSet, HashMap};
21
21
  use yrs::{Any, Out, ReadTxn, Xml};
@@ -3,7 +3,7 @@ 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.1"
6
+ version = "0.1.3"
7
7
  edition = "2024"
8
8
  rust-version = "1.85"
9
9
  authors = ["JP Camara <johnpcamara@gmail.com>"]
@@ -14,4 +14,4 @@ categories = ["text-processing", "web-programming"]
14
14
  [dependencies]
15
15
  yrs = { version = "0.27", features = ["sync"] }
16
16
  serde_json = "1.0"
17
- yjs-html-core = { path = "../html-core", version = "0.1.1" }
17
+ yjs-html-core = { path = "../html-core", version = "0.1.3" }
@@ -1,48 +1,61 @@
1
- //! Native HTML rendering of Lexical documents from the yrs collab structure —
2
- //! no Node process, no headless editor.
1
+ //! HTML for Lexical documents, rendered from the Yjs structure the editor
2
+ //! syncs. No Node process and no headless editor. A server that holds the
3
+ //! document bytes produces the markup itself.
3
4
  //!
4
- //! This renders **core Lexical**: paragraphs, headings, quotes, code, lists
5
- //! and list items, tables, horizontal rules, links, and the whole text-format
6
- //! model. Everything Lexxy-specific — its own node types (attachments,
5
+ //! This renders core Lexical: paragraphs, headings, quotes, code blocks,
6
+ //! lists and list items, tables, horizontal rules, links, and the whole
7
+ //! text-format model. Lexxy's additions live in the Ruby layer, as the
8
+ //! `Y::Lexxy` renderer's rule set: its own node types (attachments,
7
9
  //! galleries, `early_escape_code`, `horizontal_divider`) and its decorations
8
- //! of core nodes (the table figure wrapper, header-cell styling, the
9
- //! nested-list-item class) — lives in the Ruby layer as the `Y::Lexxy`
10
- //! renderer's rule set, built on the same extension API apps use
11
- //! (`Y::Lexical` is the core base class). The Lexxy byte-parity guarantee is
12
- //! held there: the Ruby fixture tests and the live headless-Chrome e2e pin
13
- //! `Y::Lexxy#to_html` against a real editor's own serialized value. The
14
- //! native tests pin core output as regression goldens (stock Lexical has no
15
- //! canonical serializer to capture against).
10
+ //! of core nodes (the figure around a table, header-cell styling, the
11
+ //! nested-list-item class). That rule set uses the same extension API
12
+ //! applications use. `Y::Lexical` is the core base class. The byte-parity
13
+ //! guarantee is held there too: the Ruby fixture tests and the
14
+ //! headless-Chrome end-to-end run pin `Y::Lexxy#to_html` against a real
15
+ //! editor's serialized value. The tests in this crate pin core output as
16
+ //! regression goldens. Stock Lexical has no canonical serializer to capture
17
+ //! from.
16
18
  //!
17
- //! Prior art: `ueberdosis/tiptap-php` renders ProseMirror JSON to HTML in pure
18
- //! PHP — a schema-pinned renderer outside the JS runtime. This works from the
19
- //! collab (Yjs) structure rather than JSON.
19
+ //! Prior art: `ueberdosis/tiptap-php` renders ProseMirror JSON to HTML in
20
+ //! plain PHP, outside any JavaScript runtime. This crate does the same job
21
+ //! from the collaborative (Yjs) structure.
20
22
  //!
21
- //! Storage model (verified against bytes captured from a live editor):
22
- //! - Blocks are `Y.XmlText` with a `__type` attribute (`paragraph`, `heading`
23
- //! (+`__tag`), `quote`, `code` (+`__language`), `list`/`listitem`,
24
- //! `table`/`tablerow`/`tablecell`, `link`/`autolink` (inline)).
25
- //! - Text runs are preceded by an embedded `Y.Map` carrying per-run metadata
26
- //! (`__type: "text" | "code-highlight" | "tab"`, `__format` bitmask).
27
- //! - `linebreak` is a bare metadata map; `tab` is a map followed by a "\t" run.
28
- //! - Decorator nodes are `Y.XmlElement`s with their fields as plain
29
- //! attributes (`horizontalrule` here; app/Lexxy decorators via rules).
23
+ //! How Lexical stores a document, checked against bytes captured from a live
24
+ //! editor:
30
25
  //!
31
- //! Text-format rendering follows the export pipeline Lexxy runs (Lexical's
32
- //! `$generateHtmlFromNodes` + sanitize), which is the only externally
33
- //! pinnable truth for formatting: inner tag `strong` (bold) / `em` (italic,
34
- //! when not bold); outer tag `code` / `mark` / `sub` / `sup`; an `<i>` wrap
35
- //! only when bold+italic combine (the `em` slot is taken); `<s>` / `<u>`
36
- //! wraps always; `<span>`s are unwrapped, so unformatted text is bare. A
37
- //! run's `__style` (highlight colors) survives on the createDOM tag,
38
- //! filtered to color/background-color; on a plain or s/u-only run it dies
39
- //! with the unwrapped span. Case-transform format bits are never rendered
40
- //! (their text-transform style is outside the sanitize whitelist).
26
+ //! - Blocks are `Y.XmlText` with a `__type` attribute: `paragraph`,
27
+ //! `heading` (plus `__tag`), `quote`, `code` (plus `__language`), `list`
28
+ //! and `listitem`, `table`, `tablerow` and `tablecell`, and the inline
29
+ //! `link` and `autolink`.
30
+ //! - Each text run is preceded by an embedded `Y.Map` with its metadata: a
31
+ //! `__type` of `text`, `code-highlight`, or `tab`, and a `__format`
32
+ //! bitmask.
33
+ //! - A `linebreak` is a bare metadata map. A `tab` is a map followed by a
34
+ //! `"\t"` run.
35
+ //! - Decorator nodes are `Y.XmlElement`s whose fields are plain attributes.
36
+ //! `horizontalrule` is handled here. Application and Lexxy decorators come
37
+ //! in through rules.
41
38
  //!
42
- //! Custom nodes: rules are registered by `__type` (see `yjs-html-core`) and
43
- //! consulted before the built-in arms, so they extend the schema or override
44
- //! a built-in. Declarative rules render here; callback rules emit
45
- //! `Segment::Deferred` for the caller to fill in after the render.
39
+ //! Text formatting follows the export pipeline Lexxy runs: Lexical's
40
+ //! `$generateHtmlFromNodes`, then sanitize. That output is the only
41
+ //! formatting truth that can be pinned from outside. The inner tag is
42
+ //! `strong` for bold, or `em` for italic without bold. The outer tag is
43
+ //! `code`, `mark`, `sub`, or `sup`. An `<i>` wraps only when bold and italic
44
+ //! combine, because the `em` slot is taken. `<s>` and `<u>` wrap whenever
45
+ //! present. `<span>`s are unwrapped, so unformatted text is bare. A run's
46
+ //! `__style` (highlight colors) survives on the createDOM tag, filtered to
47
+ //! color and background-color. On a plain run, or one with only strike or
48
+ //! underline, it goes away with the span. The case-transform format bits
49
+ //! never render; their text-transform style is outside the sanitize
50
+ //! whitelist.
51
+ //!
52
+ //! A node type with no rule still renders its text and child blocks, just
53
+ //! unwrapped.
54
+ //!
55
+ //! Custom nodes register rules by `__type` (see `yjs-html-core`). A rule is
56
+ //! consulted before the built-in arms, so it can extend the schema or
57
+ //! replace a built-in. Declarative rules render here. Callback rules emit
58
+ //! `Segment::Deferred` for the caller to fill in after the render returns.
46
59
 
47
60
  // README examples are living code: compile-checked on every cargo test.
48
61
  #[cfg(doctest)]
@@ -3,7 +3,7 @@ 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.2"
6
+ version = "0.1.4"
7
7
  edition = "2024"
8
8
  rust-version = "1.85"
9
9
  authors = ["JP Camara <johnpcamara@gmail.com>"]
@@ -14,4 +14,4 @@ categories = ["text-processing", "web-programming"]
14
14
  [dependencies]
15
15
  yrs = { version = "0.27", features = ["sync"] }
16
16
  serde_json = "1.0"
17
- yjs-html-core = { path = "../html-core", version = "0.1.1" }
17
+ yjs-html-core = { path = "../html-core", version = "0.1.3" }
@@ -1,40 +1,45 @@
1
- //! Native HTML rendering of ProseMirror/Tiptap documents from the yrs collab
2
- //! structure — no Node process, no headless editor.
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: block nodes
5
- //! are Y.XmlElement (the tag is the node type, its attributes are the node
6
- //! attrs), and text is Y.XmlText whose per-run formatting attributes are the
7
- //! marks. Node and mark names come from the editor's schema, so this accepts
8
- //! both spellings in use: Tiptap's camelCase (`bulletList`, `bold`) and the
9
- //! prosemirror-schema-basic snake_case (`bullet_list`, `strong`).
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 **core ProseMirror**: the prosemirror-schema-basic node set
12
- //! plus the prosemirror-tables family — paragraphs, headings, blockquotes,
13
- //! code blocks, bullet/ordered lists, images, hard breaks, horizontal rules,
14
- //! and tables (semantic `<table><tbody>…`, without the `<colgroup>`/
15
- //! `min-width` styling editor views inject). Tiptap's extension nodes — task
16
- //! lists, mentions, the details family — live in the Ruby layer as the
17
- //! `Y::Tiptap` renderer's rule set, built on the same extension API apps use
18
- //! (`Y::ProseMirror` is the core base class). Output follows
19
- //! `ueberdosis/tiptap-php`, and with `Y::Tiptap`'s rules it matches Tiptap's
20
- //! own `getHTML()` byte for byte on the captured fixtures — that guarantee
21
- //! is held at the Ruby layer; the native tests pin core output as goldens
22
- //! where a fixture contains Tiptap-only nodes.
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: mark serialization is text-run machinery
25
- //! (nesting order, textStyle's CSS, code's exclusivity), which the rule
26
- //! system can't express. The built-in set covers schema-basic's marks and
27
- //! Tiptap's — nesting outermost-first: link, a textStyle span, bold, italic,
28
- //! strike, underline, highlight, then subscript/superscript. `code` excludes
29
- //! the other formatting marks, so a code run is `<code>` alone — though a
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
- //! Custom nodes and marks: apps register rules by node type and mark name
33
- //! (see `render_rules`). A node rule is consulted before the built-in arms, so
34
- //! it can extend the schema or override a built-in; a mark rule claims its
35
- //! mark from the built-in wraps and wraps outside everything, link included.
36
- //! Declarative rules render here; callback rules emit `Segment::Deferred` for
37
- //! the caller to fill in after the render.
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)]
data/ext/yrby/src/lib.rs CHANGED
@@ -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 — and
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 — content that couldn't integrate because a causally-prior update is
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 — serving pending
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 — schema-pinned rendering of Lexical/Lexxy documents
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)` — the Y::Lexical facade
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 — no Node process or headless editor. The
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 — the
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 — schema-pinned rendering of ProseMirror/Tiptap documents
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` — the `Y::Tiptap` facade's rule set arrives that way).
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)` — the Y::ProseMirror facade
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 — the
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",
@@ -71,24 +71,24 @@ pub(crate) fn merged_doc_update(bytes: &[u8]) -> Result<Option<Vec<u8>>, String>
71
71
  ///
72
72
  /// This must be EXACT: the sync layer records on "ready" and resyncs on "not
73
73
  /// ready", and a parked update that slipped through would look like an
74
- /// already-applied retry downstream — acked and dropped, losing real content.
74
+ /// already-applied retry downstream: acked and dropped, losing real content.
75
75
  ///
76
76
  /// Clocks alone can't decide it. An update can satisfy every per-client clock
77
77
  /// and still fail to integrate: its items may reference other clients' blocks
78
78
  /// (origins/parents), and merged updates hide internal gaps behind Skip blocks.
79
79
  /// So the clock lower bound serves only as a cheap definitive REJECT; "ready"
80
80
  /// is decided by trial-integrating on a throwaway probe seeded with the doc's
81
- /// integrated state — ready iff nothing parks.
81
+ /// integrated state: ready iff nothing parks.
82
82
  pub(crate) fn update_is_ready(doc: &Doc, update_bytes: &[u8]) -> Result<bool, String> {
83
83
  let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
84
- // Partial order: "not covered" includes incomparable — not ready either way.
84
+ // Partial order: "not covered" includes incomparable: not ready either way.
85
85
  let lower_covered = doc.transact().state_vector() >= update.state_vector_lower();
86
86
  if !lower_covered {
87
87
  return Ok(false);
88
88
  }
89
89
  // Seed the probe with the doc's INTEGRATED state (gap-free), for two
90
90
  // reasons. A lossless seed would replant the doc's own pre-existing
91
- // pending in the probe, making has_pending true for EVERY update — the
91
+ // pending in the probe, making has_pending true for EVERY update: the
92
92
  // verdict must be about this update, not the doc's baggage. And an update
93
93
  // whose dependency exists only in that pending buffer is genuinely not
94
94
  // ready: recording it would put a gap in the durable log; a resync heals
@@ -118,13 +118,13 @@ pub(crate) fn update_is_ready(doc: &Doc, update_bytes: &[u8]) -> Result<bool, St
118
118
  /// doc), then compare the probe before and after applying the update:
119
119
  ///
120
120
  /// - **Insert/format-only updates** grow the probe's state vector, so comparing
121
- /// the state vector is enough — and cheaper than a full re-encode.
121
+ /// the state vector is enough, and cheaper than a full re-encode.
122
122
  /// - **Delete-bearing updates** don't move the state vector (a deletion tombstones
123
123
  /// an existing struct rather than adding one), so we compare the full encoded
124
124
  /// state, which carries the delete set. An already-applied pure-delete retry
125
125
  /// re-encodes byte-identically → false; a genuinely new deletion changes the
126
126
  /// delete set → true. This is exact but pays for two full encodes, so only
127
- /// delete-bearing frames — a minority — take that path.
127
+ /// delete-bearing frames, a minority, take that path.
128
128
  ///
129
129
  /// Earlier this branch was conservative: any delete-bearing update returned true
130
130
  /// (record it), which double-recorded and re-broadcast pure-delete retries the
@@ -157,7 +157,7 @@ pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool
157
157
  }
158
158
 
159
159
  // Fast path: blocks beyond the doc's state vector are content the doc
160
- // lacks — the update advances, no probe needed. The common case (a novel
160
+ // lacks: the update advances, no probe needed. The common case (a novel
161
161
  // edit) exits here; only retries and ambiguous diffs pay for the probe.
162
162
  if !has_deletes {
163
163
  let covered = doc.transact().state_vector() >= update.state_vector();
@@ -206,10 +206,10 @@ pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool
206
206
  return Ok(true);
207
207
  }
208
208
  // An unchanged state vector is ambiguous. Usually it means the doc
209
- // already had everything in this update (a retry — return false, don't
209
+ // already had everything in this update (a retry: return false, don't
210
210
  // re-record). But it can ALSO mean the update failed to integrate and
211
211
  // was stashed as pending, which doesn't move the state vector either.
212
- // That case is missing content, not a duplicate — returning false would
212
+ // That case is missing content, not a duplicate: returning false would
213
213
  // let a caller ack it and drop it. Distinguish the two by whether the
214
214
  // probe gained pending. (The sync flow screens gaps out with
215
215
  // update_is_ready before calling this; the check guards direct callers.)
@@ -240,7 +240,7 @@ pub(crate) fn has_pending(doc: &Doc) -> bool {
240
240
  /// Non-destructive: the prune happens only on the throwaway copy; `doc` keeps its
241
241
  /// pending, so a genuine gap still heals if its missing dependency later arrives.
242
242
  pub(crate) fn integrated_update(doc: &Doc, sv: &StateVector) -> Result<Vec<u8>, String> {
243
- // Pending check and encode share ONE transaction — with two, a concurrent
243
+ // Pending check and encode share ONE transaction. With two, a concurrent
244
244
  // gappy apply_update could slip between them and the encode would serve
245
245
  // the very pending this function exists to exclude.
246
246
  let full = {
@@ -548,7 +548,7 @@ mod tests {
548
548
  // it and types between C's characters, so A's delta references C's blocks as
549
549
  // origins. Returns (c_update, a_delta). On a doc missing `c_update`, the
550
550
  // per-client clock lower bound of `a_delta` is satisfied (A starts at clock
551
- // 0) but integration parks — the case a clock-only readiness check misses.
551
+ // 0) but integration parks: the case a clock-only readiness check misses.
552
552
  fn cross_client_origin_gap() -> (Vec<u8>, Vec<u8>) {
553
553
  let c = Doc::new();
554
554
  let ct = c.get_or_insert_text("t");
@@ -573,7 +573,7 @@ mod tests {
573
573
  let (c_update, a_delta) = cross_client_origin_gap();
574
574
 
575
575
  // A server that never saw C's content: the clock lower bound passes, but
576
- // the update can't integrate — it must NOT be ready (previously it was,
576
+ // the update can't integrate: it must NOT be ready (previously it was,
577
577
  // and the downstream advances? probe then acked-and-dropped it).
578
578
  let server = Doc::new();
579
579
  assert!(
@@ -617,7 +617,7 @@ mod tests {
617
617
  fn a_doc_with_legacy_pending_still_accepts_healthy_updates() {
618
618
  // Why update_is_ready seeds its probe with the INTEGRATED state: with a
619
619
  // lossless seed, the doc's own pre-existing pending would park in the
620
- // probe and every verdict would come back "not ready" — a server with
620
+ // probe and every verdict would come back "not ready": a server with
621
621
  // one legacy gap would reject every healthy keystroke forever.
622
622
  let (_first, dependent) = gap_pair();
623
623
  let doc = Doc::new();
@@ -645,7 +645,7 @@ mod tests {
645
645
  #[test]
646
646
  fn an_update_depending_only_on_pending_content_is_not_ready() {
647
647
  // The other half of the integrated-only seed: a dependency that exists
648
- // solely in the doc's pending buffer doesn't count — recording such an
648
+ // solely in the doc's pending buffer doesn't count: recording such an
649
649
  // update would put a gap in the durable log. Not ready; resync heals
650
650
  // both as one complete delta.
651
651
  let src = Doc::new();
@@ -674,7 +674,7 @@ mod tests {
674
674
  #[test]
675
675
  fn update_advances_reports_true_when_the_update_would_park() {
676
676
  // Defense in depth for callers using advances? without the ready gate: a
677
- // gappy update parks pending — that changes the doc, so it advances (it
677
+ // gappy update parks pending: that changes the doc, so it advances (it
678
678
  // must never be misread as an already-applied retry and dropped).
679
679
  let (_c_update, a_delta) = cross_client_origin_gap();
680
680
  let server = Doc::new();
@@ -896,7 +896,7 @@ mod tests {
896
896
  // for a fresh peer.
897
897
  //
898
898
  // Scope: this can't hit the original check-vs-encode race (its window
899
- // is nanoseconds; never reproduced even at 20k iterations) — that fix
899
+ // is nanoseconds; never reproduced even at 20k iterations). That fix
900
900
  // is guaranteed by using a single transaction. What this catches is
901
901
  // coarser: encoding outside the lock, or a fast path skipping the
902
902
  // pending check.
@@ -914,7 +914,7 @@ mod tests {
914
914
  let first = first.clone();
915
915
  std::thread::spawn(move || {
916
916
  while !stop.load(Ordering::Relaxed) {
917
- // Park a pending struct, then heal it, over and over — the
917
+ // Park a pending struct, then heal it, over and over: the
918
918
  // encode below keeps racing both transitions.
919
919
  doc.transact_mut()
920
920
  .apply_update(yrs::Update::decode_v1(&dependent).unwrap())