yrby 0.3.0 → 0.4.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/lib.rs CHANGED
@@ -6,6 +6,7 @@ use yrs::updates::decoder::Decode;
6
6
  use yrs::updates::encoder::Encode;
7
7
  use yrs::{Doc, GetString, ReadTxn, Transact};
8
8
 
9
+ mod prosemirror_html;
9
10
  mod protocol;
10
11
  mod read;
11
12
  use protocol::{
@@ -30,6 +31,7 @@ struct RbDoc(Doc);
30
31
  fn assert_thread_safe() {
31
32
  fn is_send_sync<T: Send + Sync>() {}
32
33
  is_send_sync::<Doc>();
34
+ is_send_sync::<RbProseMirror>();
33
35
  }
34
36
 
35
37
  /// Run `f` with the GVL (Global VM Lock) released, so other Ruby threads,
@@ -160,9 +162,11 @@ impl RbDoc {
160
162
  fn read_text(&self, name: String) -> Option<String> {
161
163
  let doc = &self.0;
162
164
  nogvl(move || {
163
- doc.transact()
164
- .get_text(name.as_str())
165
- .map(|t| t.get_string(&doc.transact()))
165
+ // Exactly ONE transaction per call. Opening a second while the
166
+ // first is still held deadlocks against a waiting writer — and
167
+ // inside nogvl that hang can't be interrupted.
168
+ let txn = doc.transact();
169
+ txn.get_text(name.as_str()).map(|t| t.get_string(&txn))
166
170
  })
167
171
  }
168
172
 
@@ -327,6 +331,55 @@ impl RbDoc {
327
331
  }
328
332
  }
329
333
 
334
+ // ============================================================================
335
+ // Y::ProseMirror — schema-pinned rendering of ProseMirror/Tiptap documents
336
+ // ============================================================================
337
+
338
+ /// A ProseMirror/Tiptap view over a `Y::Doc`. The schema knowledge (node/mark
339
+ /// names, Tiptap's serializer semantics) lives here rather than on the
340
+ /// schema-agnostic `Doc`. Holds a cheap clone of the doc (yrs `Doc` is an Arc
341
+ /// handle), so it reads live state.
342
+ ///
343
+ /// Thread safety matches `Y::Doc`: every method opens its own transaction
344
+ /// inside `nogvl` and holds no lock across the GVL boundary.
345
+ #[magnus::wrap(class = "Y::ProseMirror", free_immediately, size)]
346
+ struct RbProseMirror(Doc);
347
+
348
+ impl RbProseMirror {
349
+ /// `Y::ProseMirror.new(doc)`
350
+ fn new(doc: &RbDoc) -> Self {
351
+ RbProseMirror(doc.0.clone())
352
+ }
353
+
354
+ /// Render an XML root (default `"default"`, the fragment name Tiptap's
355
+ /// Collaboration extension uses) to HTML. Output follows tiptap-php and
356
+ /// matches Tiptap's `getHTML()` on the reference fixture; see
357
+ /// `prosemirror_html.rs` for coverage and caveats. Returns nil when the
358
+ /// root is missing or not ProseMirror-shaped (e.g. a Lexical document).
359
+ fn to_html(&self, args: &[Value]) -> Result<Option<String>, Error> {
360
+ if args.len() > 1 {
361
+ let ruby = Ruby::get().unwrap();
362
+ return Err(Error::new(
363
+ ruby.exception_arg_error(),
364
+ format!(
365
+ "wrong number of arguments (given {}, expected 0..1)",
366
+ args.len()
367
+ ),
368
+ ));
369
+ }
370
+ let name: String = match args.first() {
371
+ Some(arg) => TryConvert::try_convert(*arg)?,
372
+ None => "default".to_string(),
373
+ };
374
+ let doc = &self.0;
375
+ Ok(nogvl(move || {
376
+ let txn = doc.transact();
377
+ let fragment = txn.get_xml_fragment(name.as_str())?;
378
+ prosemirror_html::render(&txn, &fragment)
379
+ }))
380
+ }
381
+ }
382
+
330
383
  // ============================================================================
331
384
  // Protocol codec (stateless), exposed as `Y` module functions
332
385
  // ============================================================================
@@ -407,6 +460,11 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
407
460
  "handle_sync_message",
408
461
  method!(RbDoc::handle_sync_message, 1),
409
462
  )?;
463
+ // Y::ProseMirror: schema-pinned ProseMirror/Tiptap rendering over a Doc.
464
+ let prosemirror_class = module.define_class("ProseMirror", ruby.class_object())?;
465
+ prosemirror_class.define_singleton_method("new", function!(RbProseMirror::new, 1))?;
466
+ prosemirror_class.define_method("to_html", method!(RbProseMirror::to_html, -1))?;
467
+
410
468
  // Stateless protocol codec, as Y module functions.
411
469
  module.define_module_function("wrap_update", function!(wrap_update, 1))?;
412
470
  module.define_module_function("message_kind", function!(message_kind, 1))?;