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/lib.rs CHANGED
@@ -6,6 +6,8 @@ use yrs::updates::decoder::Decode;
6
6
  use yrs::updates::encoder::Encode;
7
7
  use yrs::{Doc, GetString, ReadTxn, Transact};
8
8
 
9
+ mod lexical_html;
10
+ mod prosemirror_html;
9
11
  mod protocol;
10
12
  mod read;
11
13
  use protocol::{
@@ -30,6 +32,8 @@ struct RbDoc(Doc);
30
32
  fn assert_thread_safe() {
31
33
  fn is_send_sync<T: Send + Sync>() {}
32
34
  is_send_sync::<Doc>();
35
+ is_send_sync::<RbLexical>();
36
+ is_send_sync::<RbProseMirror>();
33
37
  }
34
38
 
35
39
  /// Run `f` with the GVL (Global VM Lock) released, so other Ruby threads,
@@ -329,6 +333,105 @@ impl RbDoc {
329
333
  }
330
334
  }
331
335
 
336
+ // ============================================================================
337
+ // Y::Lexical — schema-pinned rendering of Lexical/Lexxy documents
338
+ // ============================================================================
339
+
340
+ /// A Lexical/Lexxy view over a `Y::Doc`. The schema knowledge (Lexxy 0.9.x
341
+ /// node set and serializer semantics) lives here rather than on the
342
+ /// schema-agnostic `Doc`. Holds a cheap clone of the doc (yrs `Doc` is an Arc
343
+ /// handle), so it reads live state.
344
+ ///
345
+ /// Thread safety matches `Y::Doc`: every method opens its own transaction
346
+ /// inside `nogvl` and holds no lock across the GVL boundary.
347
+ #[magnus::wrap(class = "Y::Lexical", free_immediately, size)]
348
+ struct RbLexical(Doc);
349
+
350
+ impl RbLexical {
351
+ /// `Y::Lexical.new(doc)`
352
+ fn new(doc: &RbDoc) -> Self {
353
+ RbLexical(doc.0.clone())
354
+ }
355
+
356
+ /// Render the document's XML root (default `"root"`, Lexical's standard
357
+ /// collab root name) to HTML, natively — no Node process or headless
358
+ /// editor. Output matches Lexxy's own serializer (the HTML a lexxy-editor
359
+ /// submits to Rails) byte-for-byte on the reference fixture; see
360
+ /// `lexical_html.rs` for the schema coverage and caveats. Returns nil when
361
+ /// the root is missing or not Lexical-shaped, e.g. a ProseMirror document.
362
+ fn to_html(&self, args: &[Value]) -> Result<Option<String>, Error> {
363
+ if args.len() > 1 {
364
+ let ruby = Ruby::get().unwrap();
365
+ return Err(Error::new(
366
+ ruby.exception_arg_error(),
367
+ format!(
368
+ "wrong number of arguments (given {}, expected 0..1)",
369
+ args.len()
370
+ ),
371
+ ));
372
+ }
373
+ let name: String = match args.first() {
374
+ Some(arg) => TryConvert::try_convert(*arg)?,
375
+ None => "root".to_string(),
376
+ };
377
+ let doc = &self.0;
378
+ Ok(nogvl(move || {
379
+ let txn = doc.transact();
380
+ let fragment = txn.get_xml_fragment(name.as_str())?;
381
+ lexical_html::render(&txn, &fragment)
382
+ }))
383
+ }
384
+ }
385
+
386
+ // ============================================================================
387
+ // Y::ProseMirror — schema-pinned rendering of ProseMirror/Tiptap documents
388
+ // ============================================================================
389
+
390
+ /// A ProseMirror/Tiptap view over a `Y::Doc`. The schema knowledge (node/mark
391
+ /// names, Tiptap's serializer semantics) lives here rather than on the
392
+ /// schema-agnostic `Doc`. Holds a cheap clone of the doc (yrs `Doc` is an Arc
393
+ /// handle), so it reads live state.
394
+ ///
395
+ /// Thread safety matches `Y::Doc`: every method opens its own transaction
396
+ /// inside `nogvl` and holds no lock across the GVL boundary.
397
+ #[magnus::wrap(class = "Y::ProseMirror", free_immediately, size)]
398
+ struct RbProseMirror(Doc);
399
+
400
+ impl RbProseMirror {
401
+ /// `Y::ProseMirror.new(doc)`
402
+ fn new(doc: &RbDoc) -> Self {
403
+ RbProseMirror(doc.0.clone())
404
+ }
405
+
406
+ /// Render an XML root (default `"default"`, the fragment name Tiptap's
407
+ /// Collaboration extension uses) to HTML. Output follows tiptap-php and
408
+ /// matches Tiptap's `getHTML()` on the reference fixture; see
409
+ /// `prosemirror_html.rs` for coverage and caveats. Returns nil when the
410
+ /// root is missing or not ProseMirror-shaped (e.g. a Lexical document).
411
+ fn to_html(&self, args: &[Value]) -> Result<Option<String>, Error> {
412
+ if args.len() > 1 {
413
+ let ruby = Ruby::get().unwrap();
414
+ return Err(Error::new(
415
+ ruby.exception_arg_error(),
416
+ format!(
417
+ "wrong number of arguments (given {}, expected 0..1)",
418
+ args.len()
419
+ ),
420
+ ));
421
+ }
422
+ let name: String = match args.first() {
423
+ Some(arg) => TryConvert::try_convert(*arg)?,
424
+ None => "default".to_string(),
425
+ };
426
+ let doc = &self.0;
427
+ Ok(nogvl(move || {
428
+ let txn = doc.transact();
429
+ let fragment = txn.get_xml_fragment(name.as_str())?;
430
+ prosemirror_html::render(&txn, &fragment)
431
+ }))
432
+ }
433
+ }
434
+
332
435
  // ============================================================================
333
436
  // Protocol codec (stateless), exposed as `Y` module functions
334
437
  // ============================================================================
@@ -409,6 +512,15 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
409
512
  "handle_sync_message",
410
513
  method!(RbDoc::handle_sync_message, 1),
411
514
  )?;
515
+ // Y::Lexical: schema-pinned Lexical/Lexxy rendering over a Doc.
516
+ let lexical_class = module.define_class("Lexical", ruby.class_object())?;
517
+ lexical_class.define_singleton_method("new", function!(RbLexical::new, 1))?;
518
+ lexical_class.define_method("to_html", method!(RbLexical::to_html, -1))?;
519
+ // Y::ProseMirror: schema-pinned ProseMirror/Tiptap rendering over a Doc.
520
+ let prosemirror_class = module.define_class("ProseMirror", ruby.class_object())?;
521
+ prosemirror_class.define_singleton_method("new", function!(RbProseMirror::new, 1))?;
522
+ prosemirror_class.define_method("to_html", method!(RbProseMirror::to_html, -1))?;
523
+
412
524
  // Stateless protocol codec, as Y module functions.
413
525
  module.define_module_function("wrap_update", function!(wrap_update, 1))?;
414
526
  module.define_module_function("message_kind", function!(message_kind, 1))?;