yrby 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +62 -1
- data/README.md +222 -16
- data/ext/yrby/src/lexical_html.rs +611 -288
- data/ext/yrby/src/lib.rs +206 -76
- data/ext/yrby/src/prosemirror_html.rs +736 -267
- data/ext/yrby/src/render_rules.rs +529 -0
- data/lib/y/lexxy.rb +100 -0
- data/lib/y/rendering.rb +282 -0
- data/lib/y/tiptap.rb +63 -0
- data/lib/y/version.rb +1 -1
- data/lib/y.rb +4 -0
- metadata +5 -1
data/ext/yrby/src/lib.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use magnus::{
|
|
2
|
-
function, method, prelude::*, Error, ExceptionClass,
|
|
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;
|
|
@@ -10,10 +11,12 @@ mod lexical_html;
|
|
|
10
11
|
mod prosemirror_html;
|
|
11
12
|
mod protocol;
|
|
12
13
|
mod read;
|
|
14
|
+
mod render_rules;
|
|
13
15
|
use protocol::{
|
|
14
16
|
classify_message, has_pending, integrated_update, merged_doc_update, update_advances_doc,
|
|
15
17
|
update_is_ready,
|
|
16
18
|
};
|
|
19
|
+
use render_rules::{Rules, Segment};
|
|
17
20
|
|
|
18
21
|
/// Wrapper around yrs Doc.
|
|
19
22
|
///
|
|
@@ -337,49 +340,77 @@ impl RbDoc {
|
|
|
337
340
|
// Y::Lexical — schema-pinned rendering of Lexical/Lexxy documents
|
|
338
341
|
// ============================================================================
|
|
339
342
|
|
|
340
|
-
/// A Lexical
|
|
341
|
-
///
|
|
342
|
-
///
|
|
343
|
-
///
|
|
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.
|
|
344
348
|
///
|
|
345
349
|
/// Thread safety matches `Y::Doc`: every method opens its own transaction
|
|
346
|
-
/// inside `nogvl` and holds no lock across the GVL boundary.
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
}
|
|
349
358
|
|
|
350
359
|
impl RbLexical {
|
|
351
|
-
/// `Y::
|
|
352
|
-
|
|
353
|
-
|
|
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
|
+
})
|
|
354
367
|
}
|
|
355
368
|
|
|
356
369
|
/// Render the document's XML root (default `"root"`, Lexical's standard
|
|
357
|
-
/// collab root name)
|
|
358
|
-
///
|
|
359
|
-
///
|
|
360
|
-
/// `lexical_html.rs`
|
|
361
|
-
///
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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 || {
|
|
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 || {
|
|
379
382
|
let txn = doc.transact();
|
|
380
383
|
let fragment = txn.get_xml_fragment(name.as_str())?;
|
|
381
|
-
lexical_html::
|
|
382
|
-
})
|
|
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
|
+
}
|
|
383
414
|
}
|
|
384
415
|
}
|
|
385
416
|
|
|
@@ -387,49 +418,145 @@ impl RbLexical {
|
|
|
387
418
|
// Y::ProseMirror — schema-pinned rendering of ProseMirror/Tiptap documents
|
|
388
419
|
// ============================================================================
|
|
389
420
|
|
|
390
|
-
/// A ProseMirror
|
|
391
|
-
///
|
|
392
|
-
///
|
|
393
|
-
///
|
|
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.
|
|
394
427
|
///
|
|
395
428
|
/// Thread safety matches `Y::Doc`: every method opens its own transaction
|
|
396
|
-
/// inside `nogvl` and holds no lock across the GVL boundary.
|
|
397
|
-
|
|
398
|
-
|
|
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
|
+
}
|
|
399
437
|
|
|
400
438
|
impl RbProseMirror {
|
|
401
|
-
/// `Y::
|
|
402
|
-
|
|
403
|
-
|
|
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
|
+
})
|
|
404
446
|
}
|
|
405
447
|
|
|
406
448
|
/// Render an XML root (default `"default"`, the fragment name Tiptap's
|
|
407
|
-
/// Collaboration extension uses)
|
|
408
|
-
///
|
|
409
|
-
/// `
|
|
410
|
-
///
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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 || {
|
|
462
|
+
let txn = doc.transact();
|
|
463
|
+
let fragment = txn.get_xml_fragment(name.as_str())?;
|
|
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 || {
|
|
428
476
|
let txn = doc.transact();
|
|
429
477
|
let fragment = txn.get_xml_fragment(name.as_str())?;
|
|
430
|
-
prosemirror_html::
|
|
431
|
-
})
|
|
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
|
+
}
|
|
432
558
|
}
|
|
559
|
+
Ok(arr)
|
|
433
560
|
}
|
|
434
561
|
|
|
435
562
|
// ============================================================================
|
|
@@ -512,14 +639,17 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
512
639
|
"handle_sync_message",
|
|
513
640
|
method!(RbDoc::handle_sync_message, 1),
|
|
514
641
|
)?;
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
lexical_class.
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
prosemirror_class.
|
|
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))?;
|
|
523
653
|
|
|
524
654
|
// Stateless protocol codec, as Y module functions.
|
|
525
655
|
module.define_module_function("wrap_update", function!(wrap_update, 1))?;
|