yrby 0.4.0 → 0.6.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
@@ -1,18 +1,22 @@
1
1
  use magnus::{
2
- function, method, prelude::*, Error, ExceptionClass, RString, Ruby, TryConvert, Value,
2
+ function, method, prelude::*, Error, ExceptionClass, IntoValue, RArray, RString, Ruby,
3
+ TryConvert, Value,
3
4
  };
4
5
  use yrs::sync::{Message, SyncMessage};
5
6
  use yrs::updates::decoder::Decode;
6
7
  use yrs::updates::encoder::Encode;
7
8
  use yrs::{Doc, GetString, ReadTxn, Transact};
8
9
 
10
+ mod lexical_html;
9
11
  mod prosemirror_html;
10
12
  mod protocol;
11
13
  mod read;
14
+ mod render_rules;
12
15
  use protocol::{
13
16
  classify_message, has_pending, integrated_update, merged_doc_update, update_advances_doc,
14
17
  update_is_ready,
15
18
  };
19
+ use render_rules::{Rules, Segment};
16
20
 
17
21
  /// Wrapper around yrs Doc.
18
22
  ///
@@ -31,6 +35,7 @@ struct RbDoc(Doc);
31
35
  fn assert_thread_safe() {
32
36
  fn is_send_sync<T: Send + Sync>() {}
33
37
  is_send_sync::<Doc>();
38
+ is_send_sync::<RbLexical>();
34
39
  is_send_sync::<RbProseMirror>();
35
40
  }
36
41
 
@@ -331,53 +336,227 @@ impl RbDoc {
331
336
  }
332
337
  }
333
338
 
339
+ // ============================================================================
340
+ // Y::Lexical — schema-pinned rendering of Lexical/Lexxy documents
341
+ // ============================================================================
342
+
343
+ /// A Lexical view over a `Y::Doc`. The schema knowledge lives here rather
344
+ /// than on the schema-agnostic `Doc`: core Lexical natively, everything else
345
+ /// through the render rules compiled at construction (see `render_rules` —
346
+ /// the `Y::Lexxy` facade's rule set arrives that way). Holds a cheap clone of
347
+ /// the doc (yrs `Doc` is an Arc handle), so it reads live state.
348
+ ///
349
+ /// Thread safety matches `Y::Doc`: every method opens its own transaction
350
+ /// inside `nogvl` and holds no lock across the GVL boundary. Callback rules
351
+ /// keep that discipline: the render emits deferred segments, and the Ruby
352
+ /// layer runs the app's blocks only after the transaction has closed.
353
+ #[magnus::wrap(class = "Y::NativeLexical", free_immediately, size)]
354
+ struct RbLexical {
355
+ doc: Doc,
356
+ rules: Rules,
357
+ }
358
+
359
+ impl RbLexical {
360
+ /// `Y::NativeLexical.new(doc, rules_json)` — the Y::Lexical facade
361
+ /// compiles its `nodes:` config to the rules JSON.
362
+ fn native_new(doc: &RbDoc, rules_json: String) -> Result<Self, Error> {
363
+ Ok(RbLexical {
364
+ doc: doc.0.clone(),
365
+ rules: parse_rules(&rules_json)?,
366
+ })
367
+ }
368
+
369
+ /// Render the document's XML root (default `"root"`, Lexical's standard
370
+ /// collab root name) natively — no Node process or headless editor. The
371
+ /// native side renders core Lexical plus whatever the rules cover; with
372
+ /// the rule set `Y::Lexxy` passes, output matches Lexxy's own serializer
373
+ /// byte-for-byte on the reference fixtures (see `lexical_html.rs`). Returns nil when the root is missing or not
374
+ /// Lexical-shaped, e.g. a ProseMirror document; a String when no
375
+ /// callback rule fired; otherwise the nested segment arrays the Ruby
376
+ /// layer splices.
377
+ fn native_to_html(&self, args: &[Value]) -> Result<Value, Error> {
378
+ let name = root_name_arg(args, "root")?;
379
+ let doc = &self.doc;
380
+ let rules = &self.rules;
381
+ let segments = nogvl(move || {
382
+ let txn = doc.transact();
383
+ let fragment = txn.get_xml_fragment(name.as_str())?;
384
+ lexical_html::render_segments(&txn, &fragment, rules)
385
+ });
386
+ segments_result(segments)
387
+ }
388
+
389
+ /// The document's node types as observed facts, JSON-encoded — the
390
+ /// native half of the facade's `node_types` discovery aid. Nil when the
391
+ /// root is missing or not Lexical-shaped.
392
+ fn node_types(&self, args: &[Value]) -> Result<Value, Error> {
393
+ let name = root_name_arg(args, "root")?;
394
+ let doc = &self.doc;
395
+ let map = nogvl(move || {
396
+ let txn = doc.transact();
397
+ let fragment = txn.get_xml_fragment(name.as_str())?;
398
+ lexical_html::collect_node_types(&txn, &fragment)
399
+ });
400
+ let ruby = Ruby::get().unwrap();
401
+ match map {
402
+ None => Ok(ruby.qnil().as_value()),
403
+ Some(map) => Ok(render_rules::type_map_json(&map, |ty| {
404
+ if self.rules.nodes.contains_key(ty) {
405
+ Some("rule")
406
+ } else if lexical_html::is_builtin(ty) {
407
+ Some("builtin")
408
+ } else {
409
+ None
410
+ }
411
+ })
412
+ .into_value_with(&ruby)),
413
+ }
414
+ }
415
+ }
416
+
334
417
  // ============================================================================
335
418
  // Y::ProseMirror — schema-pinned rendering of ProseMirror/Tiptap documents
336
419
  // ============================================================================
337
420
 
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.
421
+ /// A ProseMirror view over a `Y::Doc`. The schema knowledge lives here rather
422
+ /// than on the schema-agnostic `Doc`: core ProseMirror natively, everything
423
+ /// else through the render rules compiled at construction (see
424
+ /// `render_rules` the `Y::Tiptap` facade's rule set arrives that way).
425
+ /// Holds a cheap clone of the doc (yrs `Doc` is an Arc handle), so it reads
426
+ /// live state.
342
427
  ///
343
428
  /// 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);
429
+ /// inside `nogvl` and holds no lock across the GVL boundary. Callback rules
430
+ /// keep that discipline: the render emits deferred segments, and the Ruby
431
+ /// layer runs the app's blocks only after the transaction has closed.
432
+ #[magnus::wrap(class = "Y::NativeProseMirror", free_immediately, size)]
433
+ struct RbProseMirror {
434
+ doc: Doc,
435
+ rules: Rules,
436
+ }
347
437
 
348
438
  impl RbProseMirror {
349
- /// `Y::ProseMirror.new(doc)`
350
- fn new(doc: &RbDoc) -> Self {
351
- RbProseMirror(doc.0.clone())
439
+ /// `Y::NativeProseMirror.new(doc, rules_json)` — the Y::ProseMirror facade
440
+ /// compiles its `nodes:`/`marks:` config to the rules JSON.
441
+ fn native_new(doc: &RbDoc, rules_json: String) -> Result<Self, Error> {
442
+ Ok(RbProseMirror {
443
+ doc: doc.0.clone(),
444
+ rules: parse_rules(&rules_json)?,
445
+ })
352
446
  }
353
447
 
354
448
  /// 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 || {
449
+ /// Collaboration extension uses). The native side renders core
450
+ /// ProseMirror plus whatever the rules cover; with the rule set
451
+ /// `Y::Tiptap` passes, output matches Tiptap's own `getHTML()`
452
+ /// byte-for-byte on the reference fixtures (see `prosemirror_html.rs`
453
+ /// for coverage and caveats). Returns nil when the root is missing or
454
+ /// not ProseMirror-shaped (e.g. a Lexical document); a String when no
455
+ /// callback rule fired; otherwise the nested segment arrays the Ruby
456
+ /// layer splices.
457
+ fn native_to_html(&self, args: &[Value]) -> Result<Value, Error> {
458
+ let name = root_name_arg(args, "default")?;
459
+ let doc = &self.doc;
460
+ let rules = &self.rules;
461
+ let segments = nogvl(move || {
376
462
  let txn = doc.transact();
377
463
  let fragment = txn.get_xml_fragment(name.as_str())?;
378
- prosemirror_html::render(&txn, &fragment)
379
- }))
464
+ prosemirror_html::render_segments(&txn, &fragment, rules)
465
+ });
466
+ segments_result(segments)
467
+ }
468
+
469
+ /// The document's node types as observed facts, JSON-encoded — the
470
+ /// native half of the facade's `node_types` discovery aid. Nil when the
471
+ /// root is missing or not ProseMirror-shaped.
472
+ fn node_types(&self, args: &[Value]) -> Result<Value, Error> {
473
+ let name = root_name_arg(args, "default")?;
474
+ let doc = &self.doc;
475
+ let map = nogvl(move || {
476
+ let txn = doc.transact();
477
+ let fragment = txn.get_xml_fragment(name.as_str())?;
478
+ prosemirror_html::collect_node_types(&txn, &fragment)
479
+ });
480
+ let ruby = Ruby::get().unwrap();
481
+ match map {
482
+ None => Ok(ruby.qnil().as_value()),
483
+ Some(map) => Ok(render_rules::type_map_json(&map, |ty| {
484
+ if self.rules.nodes.contains_key(ty) {
485
+ Some("rule")
486
+ } else if prosemirror_html::is_builtin(ty) {
487
+ Some("builtin")
488
+ } else {
489
+ None
490
+ }
491
+ })
492
+ .into_value_with(&ruby)),
493
+ }
494
+ }
495
+ }
496
+
497
+ /// The optional positional root-fragment name both renderers take.
498
+ fn root_name_arg(args: &[Value], default: &str) -> Result<String, Error> {
499
+ if args.len() > 1 {
500
+ let ruby = Ruby::get().unwrap();
501
+ return Err(Error::new(
502
+ ruby.exception_arg_error(),
503
+ format!(
504
+ "wrong number of arguments (given {}, expected 0..1)",
505
+ args.len()
506
+ ),
507
+ ));
508
+ }
509
+ match args.first() {
510
+ Some(arg) => TryConvert::try_convert(*arg),
511
+ None => Ok(default.to_string()),
512
+ }
513
+ }
514
+
515
+ fn parse_rules(json: &str) -> Result<Rules, Error> {
516
+ Rules::parse(json).map_err(|e| {
517
+ let ruby = Ruby::get().unwrap();
518
+ Error::new(ruby.exception_arg_error(), e)
519
+ })
520
+ }
521
+
522
+ /// A render's result as a Ruby value: nil (root missing or foreign-shaped),
523
+ /// a String when every segment is finished HTML, or nested arrays of
524
+ /// `String | [node_type, attrs_json, content, child_types]` for the Ruby
525
+ /// layer to splice.
526
+ fn segments_result(segments: Option<Vec<Segment>>) -> Result<Value, Error> {
527
+ let ruby = Ruby::get().unwrap();
528
+ match segments {
529
+ None => Ok(ruby.qnil().as_value()),
530
+ Some(segs) => match render_rules::flatten(segs) {
531
+ render_rules::Flattened::Html(html) => Ok(html.into_value_with(&ruby)),
532
+ render_rules::Flattened::Deferred(segs) => {
533
+ Ok(segments_to_ruby(&ruby, segs)?.as_value())
534
+ }
535
+ },
536
+ }
537
+ }
538
+
539
+ fn segments_to_ruby(ruby: &Ruby, segments: Vec<Segment>) -> Result<RArray, Error> {
540
+ let arr = ruby.ary_new();
541
+ for seg in segments {
542
+ match seg {
543
+ Segment::Html(s) => arr.push(s)?,
544
+ Segment::Deferred {
545
+ node_type,
546
+ attrs_json,
547
+ child_types,
548
+ content,
549
+ } => {
550
+ let entry = ruby.ary_new();
551
+ entry.push(node_type)?;
552
+ entry.push(attrs_json)?;
553
+ entry.push(segments_to_ruby(ruby, content)?)?;
554
+ entry.push(child_types)?;
555
+ arr.push(entry)?;
556
+ }
557
+ }
380
558
  }
559
+ Ok(arr)
381
560
  }
382
561
 
383
562
  // ============================================================================
@@ -460,10 +639,17 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
460
639
  "handle_sync_message",
461
640
  method!(RbDoc::handle_sync_message, 1),
462
641
  )?;
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))?;
642
+ // The native renderers are the handles the Ruby facades (Y::Lexical /
643
+ // Y::Lexxy and Y::ProseMirror / Y::Tiptap in lib/y/) hold; the Ruby
644
+ // layer marks these classes private_constant.
645
+ let lexical_class = module.define_class("NativeLexical", ruby.class_object())?;
646
+ lexical_class.define_singleton_method("new", function!(RbLexical::native_new, 2))?;
647
+ lexical_class.define_method("to_html", method!(RbLexical::native_to_html, -1))?;
648
+ lexical_class.define_method("node_types", method!(RbLexical::node_types, -1))?;
649
+ let prosemirror_class = module.define_class("NativeProseMirror", ruby.class_object())?;
650
+ prosemirror_class.define_singleton_method("new", function!(RbProseMirror::native_new, 2))?;
651
+ prosemirror_class.define_method("to_html", method!(RbProseMirror::native_to_html, -1))?;
652
+ prosemirror_class.define_method("node_types", method!(RbProseMirror::node_types, -1))?;
467
653
 
468
654
  // Stateless protocol codec, as Y module functions.
469
655
  module.define_module_function("wrap_update", function!(wrap_update, 1))?;