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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -1
- data/README.md +251 -9
- data/ext/yrby/src/lexical_html.rs +1316 -0
- data/ext/yrby/src/lib.rs +224 -38
- data/ext/yrby/src/prosemirror_html.rs +736 -267
- data/ext/yrby/src/read.rs +71 -4
- 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 +6 -1
data/ext/yrby/src/lib.rs
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
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;
|
|
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
|
|
339
|
-
///
|
|
340
|
-
///
|
|
341
|
-
///
|
|
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
|
-
|
|
346
|
-
|
|
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::
|
|
350
|
-
|
|
351
|
-
|
|
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)
|
|
356
|
-
///
|
|
357
|
-
/// `
|
|
358
|
-
///
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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::
|
|
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
|
-
//
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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))?;
|